Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8c1c158d6 | |||
| 9e6cd9d615 | |||
| 1f1128ff4a | |||
| aa01283ce1 | |||
| f81e2a8aac | |||
| 2b38e583be | |||
| 4ff1394224 | |||
| 2bbe5a3d0e | |||
| a4237c0462 | |||
| 321e5b1379 | |||
| 7c3e14c473 | |||
| aecce595e8 | |||
| cf17dedac3 | |||
| 661fdbb263 | |||
| 201abf98ce | |||
| 665965fd01 |
@@ -97,6 +97,36 @@ type WorkflowEntry = {
|
||||
|
||||
Workflow bundles (`.esm.js`) follow the same rule: export `const run` and `const descriptor`, not `export default`.
|
||||
|
||||
### Folder Module Discipline
|
||||
|
||||
Every folder under `src/` is a **module boundary**. Four rules:
|
||||
|
||||
| # | Rule | Rationale |
|
||||
|---|------|-----------|
|
||||
| 1 | **Every folder exports via `index.ts`** | Single entry point for the module |
|
||||
| 2 | **Types live in `types.ts`** | Each folder's type definitions go in `<folder>/types.ts`, not scattered across files |
|
||||
| 3 | **Single export source** | Only `index.ts` may re-export. No file may re-export from another module's internals. Cross-module imports must go through `index.ts` — never reach past it to import a specific file |
|
||||
| 4 | **`index.ts` is pure re-exports** | No type definitions, no function implementations — only `export { ... } from` statements |
|
||||
|
||||
```typescript
|
||||
// ✅ Good — import through module boundary
|
||||
import { createCasStore } from "../cas/index.js";
|
||||
import type { CasStore } from "../cas/index.js";
|
||||
|
||||
// ❌ Bad — reaching past index.ts
|
||||
import { createCasStore } from "../cas/cas.js";
|
||||
|
||||
// ❌ Bad — re-exporting from non-index file
|
||||
// in engine/engine.ts:
|
||||
export { createCasStore } from "../cas/cas.js";
|
||||
|
||||
// ❌ Bad — types defined in index.ts
|
||||
// in cas/index.ts:
|
||||
export type CasStore = { ... }; // should be in cas/types.ts
|
||||
```
|
||||
|
||||
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`, `workflow-as-agent.ts`) are not inside a folder module and follow normal rules.
|
||||
|
||||
## Naming
|
||||
|
||||
| Type | Style | Example |
|
||||
@@ -197,9 +227,8 @@ Test files (`__tests__/**`) are exempt.
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
bun run check # biome check (lint + format)
|
||||
bun run check # tsc --build + biome check
|
||||
bun run format # biome format --write
|
||||
bun run build # full build
|
||||
bun test # run tests
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createExtract, createWorkflow, END, type RoleDefinition } from "@uncaged/workflow";
|
||||
import { createWorkflow, END, type RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
type Roles = {
|
||||
@@ -32,12 +32,6 @@ const greeter: RoleDefinition<Roles["greeter"]> = {
|
||||
extractMode: "single",
|
||||
};
|
||||
|
||||
const extract = createExtract({
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
apiKey: "",
|
||||
model: "",
|
||||
});
|
||||
|
||||
export const run = createWorkflow<Roles>(
|
||||
{
|
||||
roles: { greeter },
|
||||
@@ -48,6 +42,4 @@ export const run = createWorkflow<Roles>(
|
||||
{
|
||||
agent: async (ctx) => `Hello, ${ctx.start.content}`,
|
||||
},
|
||||
extract,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ParsedAddArgv } from "../src/commands/workflow/add.js";
|
||||
import type { ParsedAddArgv } from "../src/commands/workflow/index.js";
|
||||
|
||||
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
||||
return { name, filePath, typesPath: null };
|
||||
|
||||
@@ -4,16 +4,16 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { getGlobalCasDir, getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
||||
import { cmdCasGet } from "../src/commands/cas/get.js";
|
||||
import { cmdCasList } from "../src/commands/cas/list.js";
|
||||
import { cmdCasPut } from "../src/commands/cas/put.js";
|
||||
import { cmdCasRm } from "../src/commands/cas/rm.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { cmdHistory } from "../src/commands/workflow/history.js";
|
||||
import { cmdList, formatListLines } from "../src/commands/workflow/list.js";
|
||||
import { cmdRemove } from "../src/commands/workflow/rm.js";
|
||||
import { cmdRollback } from "../src/commands/workflow/rollback.js";
|
||||
import { cmdShow } from "../src/commands/workflow/show.js";
|
||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js";
|
||||
import {
|
||||
cmdAdd,
|
||||
cmdHistory,
|
||||
cmdList,
|
||||
cmdRemove,
|
||||
cmdRollback,
|
||||
cmdShow,
|
||||
formatListLines,
|
||||
} from "../src/commands/workflow/index.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
|
||||
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} };
|
||||
@@ -402,7 +402,7 @@ export const run = async function* (input, options) {
|
||||
});
|
||||
|
||||
test("cas put/get/list/rm use global cas dir (thread id not required for storage)", async () => {
|
||||
const put = await cmdCasPut(storageRoot, "nonexistent-thread-id", "phase doc");
|
||||
const put = await cmdCasPut(storageRoot, "phase doc");
|
||||
expect(put.ok).toBe(true);
|
||||
if (!put.ok) {
|
||||
return;
|
||||
@@ -411,24 +411,24 @@ export const run = async function* (input, options) {
|
||||
const blobPath = join(getGlobalCasDir(storageRoot), `${hash}.txt`);
|
||||
expect(await readFile(blobPath, "utf8")).toBe("phase doc");
|
||||
|
||||
const got = await cmdCasGet(storageRoot, "other-thread", hash);
|
||||
const got = await cmdCasGet(storageRoot, hash);
|
||||
expect(got.ok).toBe(true);
|
||||
if (!got.ok) {
|
||||
return;
|
||||
}
|
||||
expect(got.value).toBe("phase doc");
|
||||
|
||||
const listed = await cmdCasList(storageRoot, "another-thread");
|
||||
const listed = await cmdCasList(storageRoot);
|
||||
expect(listed.ok).toBe(true);
|
||||
if (!listed.ok) {
|
||||
return;
|
||||
}
|
||||
expect(listed.value).toContain(hash);
|
||||
|
||||
const removed = await cmdCasRm(storageRoot, "rm-thread", hash);
|
||||
const removed = await cmdCasRm(storageRoot, hash);
|
||||
expect(removed.ok).toBe(true);
|
||||
|
||||
const missing = await cmdCasGet(storageRoot, "after-rm", hash);
|
||||
const missing = await cmdCasGet(storageRoot, hash);
|
||||
expect(missing.ok).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createCasStore, getContentMerklePayload, getGlobalCasDir } from "@uncaged/workflow";
|
||||
import { cmdFork } from "../src/commands/thread/fork.js";
|
||||
import { cmdRun } from "../src/commands/thread/run.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { cmdFork, cmdRun } from "../src/commands/thread/index.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/index.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
|
||||
|
||||
/** Three-role workflow that respects `input.steps` for fork/resume. */
|
||||
const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
|
||||
@@ -78,6 +78,7 @@ describe("cli fork", () => {
|
||||
prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-fork-"));
|
||||
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot;
|
||||
await ensureTestWorkflowRegistryConfig(storageRoot);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getGlobalCasDir,
|
||||
putContentMerkleNode,
|
||||
} from "@uncaged/workflow";
|
||||
import { cmdThreadRemove } from "../src/commands/thread/rm.js";
|
||||
import { cmdThreadRemove } from "../src/commands/thread/index.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||
|
||||
@@ -4,8 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { runCli } from "../src/cli-dispatch.js";
|
||||
import { cmdInitTemplate } from "../src/commands/init/template.js";
|
||||
import { cmdInitWorkspace } from "../src/commands/init/workspace.js";
|
||||
import { cmdInitTemplate, cmdInitWorkspace } from "../src/commands/init/index.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
describe("init template", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||
import { cmdInitWorkspace } from "../src/commands/init/workspace.js";
|
||||
import { cmdInitWorkspace } from "../src/commands/init/index.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
describe("init workspace", () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
LIVE_CONTENT_MAX_LINES,
|
||||
type LiveRoleRow,
|
||||
renderLiveRoleStepLines,
|
||||
} from "../src/commands/thread/live.js";
|
||||
} from "../src/commands/thread/index.js";
|
||||
import { parseLiveArgv } from "../src/live-argv.js";
|
||||
|
||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||
|
||||
@@ -5,16 +5,21 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getGlobalCasDir } from "@uncaged/workflow";
|
||||
import { cmdCasPut } from "../src/commands/cas/put.js";
|
||||
import { cmdKill, cmdPause, cmdResume } from "../src/commands/thread/control.js";
|
||||
import { cmdThreads } from "../src/commands/thread/list.js";
|
||||
import { cmdPs } from "../src/commands/thread/ps.js";
|
||||
import { cmdThreadRemove } from "../src/commands/thread/rm.js";
|
||||
import { cmdRun } from "../src/commands/thread/run.js";
|
||||
import { cmdThreadShow } from "../src/commands/thread/show.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { cmdCasPut } from "../src/commands/cas/index.js";
|
||||
import {
|
||||
cmdKill,
|
||||
cmdPause,
|
||||
cmdPs,
|
||||
cmdResume,
|
||||
cmdRun,
|
||||
cmdThreadRemove,
|
||||
cmdThreadShow,
|
||||
cmdThreads,
|
||||
} from "../src/commands/thread/index.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/index.js";
|
||||
import { pathExists, readTextFileIfExists } from "../src/fs-utils.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
|
||||
|
||||
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow";
|
||||
`;
|
||||
@@ -138,6 +143,7 @@ describe("cli thread commands", () => {
|
||||
prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-thread-"));
|
||||
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot;
|
||||
await ensureTestWorkflowRegistryConfig(storageRoot);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -232,7 +238,7 @@ describe("cli thread commands", () => {
|
||||
const runningPath = join(dirname(dataPath), `${threadId}.running`);
|
||||
await waitUntilRunningFileAbsent(runningPath, 120);
|
||||
|
||||
const put = await cmdCasPut(storageRoot, threadId, "keep-after-thread-rm");
|
||||
const put = await cmdCasPut(storageRoot, "keep-after-thread-rm");
|
||||
expect(put.ok).toBe(true);
|
||||
if (!put.ok) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Minimal valid global config so {@link executeThread} can resolve the extract scene (CLI integration tests). */
|
||||
export const TEST_WORKFLOW_REGISTRY_YAML = `config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/m
|
||||
workflows: {}
|
||||
`;
|
||||
|
||||
export async function ensureTestWorkflowRegistryConfig(storageRoot: string): Promise<void> {
|
||||
await writeFile(join(storageRoot, "workflow.yaml"), TEST_WORKFLOW_REGISTRY_YAML, "utf8");
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
import { pathExists } from "./fs-utils.js";
|
||||
|
||||
export type BundleFileSource = { kind: "text"; text: string } | { kind: "path"; path: string };
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function shouldUseColor(): boolean {
|
||||
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
||||
}
|
||||
|
||||
export function highlightLiveRole(name: string): string {
|
||||
if (!shouldUseColor()) {
|
||||
return name;
|
||||
}
|
||||
return `\x1b[1m\x1b[36m${name}\x1b[0m`;
|
||||
}
|
||||
|
||||
export function dimGreyLine(line: string): string {
|
||||
if (!shouldUseColor()) {
|
||||
return line;
|
||||
}
|
||||
return `\x1b[2m\x1b[90m${line}\x1b[0m`;
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import type { CommandEntry, DispatchFn } from "./cli-command-types.js";
|
||||
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||
import { getCommandRegistry } from "./cli-registry.js";
|
||||
import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js";
|
||||
import { createCasDispatcher, dispatchGc } from "./commands/cas/dispatch.js";
|
||||
import { createInitDispatcher } from "./commands/init/dispatch.js";
|
||||
import { createCasDispatcher, dispatchGc } from "./commands/cas/index.js";
|
||||
import { createInitDispatcher } from "./commands/init/index.js";
|
||||
import {
|
||||
createThreadDispatcher,
|
||||
dispatchFork,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
dispatchResume,
|
||||
dispatchRun,
|
||||
dispatchThreadList,
|
||||
} from "./commands/thread/dispatch.js";
|
||||
} from "./commands/thread/index.js";
|
||||
import {
|
||||
createWorkflowDispatcher,
|
||||
dispatchAdd,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
dispatchRemove,
|
||||
dispatchRollback,
|
||||
dispatchShow,
|
||||
} from "./commands/workflow/dispatch.js";
|
||||
} from "./commands/workflow/index.js";
|
||||
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js";
|
||||
|
||||
export type { CommandEntry, CommandGroup, DispatchFn } from "./cli-command-types.js";
|
||||
@@ -86,6 +86,7 @@ async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<numb
|
||||
}
|
||||
|
||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
printCliWarn('⚠ "help" is deprecated, use "skill" instead');
|
||||
const skillIdx = argv.indexOf("--skill");
|
||||
if (skillIdx !== -1) {
|
||||
return showSkillDocOrIndex(argv[skillIdx + 1]);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CommandGroup } from "./cli-command-types.js";
|
||||
import { setCommandGroupsForUsage } from "./cli-usage-context.js";
|
||||
import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/dispatch.js";
|
||||
import { INIT_SUBCOMMAND_TABLE } from "./commands/init/dispatch.js";
|
||||
import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/dispatch.js";
|
||||
import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/dispatch.js";
|
||||
import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/index.js";
|
||||
import { INIT_SUBCOMMAND_TABLE } from "./commands/init/index.js";
|
||||
import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/index.js";
|
||||
import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/index.js";
|
||||
|
||||
export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js";
|
||||
import type { CommandEntry } from "../../cli-command-types.js";
|
||||
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||
@@ -7,6 +7,7 @@ import { cmdCasGet } from "./get.js";
|
||||
import { cmdCasList } from "./list.js";
|
||||
import { cmdCasPut } from "./put.js";
|
||||
import { cmdCasRm } from "./rm.js";
|
||||
import type { CasDispatchDeps } from "./types.js";
|
||||
|
||||
function usageText(): string {
|
||||
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||
@@ -30,13 +31,12 @@ export async function dispatchGc(storageRoot: string, argv: string[]): Promise<n
|
||||
}
|
||||
|
||||
export async function dispatchCasGet(storageRoot: string, rest: string[]): Promise<number> {
|
||||
const threadId = rest[0];
|
||||
const hash = rest[1];
|
||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
||||
printCliError(`${usageText()}\n\nerror: cas get requires <thread-id> <hash>`);
|
||||
const hash = rest[0];
|
||||
if (hash === undefined || rest.length > 1) {
|
||||
printCliError(`${usageText()}\n\nerror: cas get requires <hash>`);
|
||||
return 1;
|
||||
}
|
||||
const result = await cmdCasGet(storageRoot, threadId, hash);
|
||||
const result = await cmdCasGet(storageRoot, hash);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
@@ -46,13 +46,12 @@ export async function dispatchCasGet(storageRoot: string, rest: string[]): Promi
|
||||
}
|
||||
|
||||
export async function dispatchCasPut(storageRoot: string, rest: string[]): Promise<number> {
|
||||
const threadId = rest[0];
|
||||
const content = rest[1];
|
||||
if (threadId === undefined || content === undefined || rest.length > 2) {
|
||||
printCliError(`${usageText()}\n\nerror: cas put requires <thread-id> <content>`);
|
||||
const content = rest[0];
|
||||
if (content === undefined || rest.length > 1) {
|
||||
printCliError(`${usageText()}\n\nerror: cas put requires <content>`);
|
||||
return 1;
|
||||
}
|
||||
const result = await cmdCasPut(storageRoot, threadId, content);
|
||||
const result = await cmdCasPut(storageRoot, content);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
@@ -62,12 +61,11 @@ export async function dispatchCasPut(storageRoot: string, rest: string[]): Promi
|
||||
}
|
||||
|
||||
export async function dispatchCasList(storageRoot: string, rest: string[]): Promise<number> {
|
||||
const threadId = rest[0];
|
||||
if (threadId === undefined || rest.length > 1) {
|
||||
printCliError(`${usageText()}\n\nerror: cas list requires <thread-id>`);
|
||||
if (rest.length > 0) {
|
||||
printCliError(`${usageText()}\n\nerror: cas list takes no arguments`);
|
||||
return 1;
|
||||
}
|
||||
const result = await cmdCasList(storageRoot, threadId);
|
||||
const result = await cmdCasList(storageRoot);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
@@ -79,13 +77,12 @@ export async function dispatchCasList(storageRoot: string, rest: string[]): Prom
|
||||
}
|
||||
|
||||
export async function dispatchCasRm(storageRoot: string, rest: string[]): Promise<number> {
|
||||
const threadId = rest[0];
|
||||
const hash = rest[1];
|
||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
||||
printCliError(`${usageText()}\n\nerror: cas rm requires <thread-id> <hash>`);
|
||||
const hash = rest[0];
|
||||
if (hash === undefined || rest.length > 1) {
|
||||
printCliError(`${usageText()}\n\nerror: cas rm requires <hash>`);
|
||||
return 1;
|
||||
}
|
||||
const result = await cmdCasRm(storageRoot, threadId, hash);
|
||||
const result = await cmdCasRm(storageRoot, hash);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
@@ -97,24 +94,24 @@ export async function dispatchCasRm(storageRoot: string, rest: string[]): Promis
|
||||
export const CAS_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
get: {
|
||||
handler: dispatchCasGet,
|
||||
args: "<thread-id> <hash>",
|
||||
description: "Retrieve content by hash from a thread's CAS",
|
||||
args: "<hash>",
|
||||
description: "Retrieve content by hash from CAS",
|
||||
},
|
||||
put: {
|
||||
handler: dispatchCasPut,
|
||||
args: "<thread-id> <content>",
|
||||
description: "Store content in a thread's CAS, returns hash",
|
||||
args: "<content>",
|
||||
description: "Store content in CAS, prints hash",
|
||||
},
|
||||
list: {
|
||||
handler: dispatchCasList,
|
||||
args: "<thread-id>",
|
||||
description: "List all CAS entries for a thread",
|
||||
args: "",
|
||||
description: "List all hashes in CAS",
|
||||
},
|
||||
rm: { handler: dispatchCasRm, args: "<thread-id> <hash>", description: "Remove a CAS entry" },
|
||||
rm: { handler: dispatchCasRm, args: "<hash>", description: "Remove a CAS entry by hash" },
|
||||
gc: { handler: dispatchGc, args: "", description: "Garbage-collect unreferenced CAS entries" },
|
||||
};
|
||||
|
||||
export function createCasDispatcher(deps: { dispatchGroup: DispatchGroupFn }) {
|
||||
export function createCasDispatcher(deps: CasDispatchDeps) {
|
||||
const { dispatchGroup } = deps;
|
||||
return async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/
|
||||
|
||||
export async function cmdCasGet(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export {
|
||||
CAS_SUBCOMMAND_TABLE,
|
||||
createCasDispatcher,
|
||||
dispatchCasGet,
|
||||
dispatchCasList,
|
||||
dispatchCasPut,
|
||||
dispatchCasRm,
|
||||
dispatchGc,
|
||||
} from "./dispatch.js";
|
||||
export { cmdGc } from "./gc.js";
|
||||
export { cmdCasGet } from "./get.js";
|
||||
export { cmdCasList } from "./list.js";
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasList(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
): Promise<Result<string[], string>> {
|
||||
export async function cmdCasList(storageRoot: string): Promise<Result<string[], string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const hashes = await cas.list();
|
||||
return ok(hashes);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workf
|
||||
|
||||
export async function cmdCasPut(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
content: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasRm(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<void, string>> {
|
||||
export async function cmdCasRm(storageRoot: string, hash: string): Promise<Result<void, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
await cas.delete(hash);
|
||||
return ok(undefined);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||
|
||||
export type CasDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js";
|
||||
import type { CommandEntry } from "../../cli-command-types.js";
|
||||
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||
import { cmdInitTemplate } from "./template.js";
|
||||
import type { InitDispatchDeps } from "./types.js";
|
||||
import { cmdInitWorkspace } from "./workspace.js";
|
||||
|
||||
function usageText(): string {
|
||||
@@ -52,7 +53,7 @@ export const INIT_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
},
|
||||
};
|
||||
|
||||
export function createInitDispatcher(deps: { dispatchGroup: DispatchGroupFn }) {
|
||||
export function createInitDispatcher(deps: InitDispatchDeps) {
|
||||
const { dispatchGroup } = deps;
|
||||
return async function dispatchInit(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type { CmdInitTemplateSuccess } from "./template.js";
|
||||
export {
|
||||
createInitDispatcher,
|
||||
dispatchInitTemplate,
|
||||
dispatchInitWorkspace,
|
||||
INIT_SUBCOMMAND_TABLE,
|
||||
} from "./dispatch.js";
|
||||
export { cmdInitTemplate } from "./template.js";
|
||||
export type { CmdInitWorkspaceSuccess } from "./workspace.js";
|
||||
export type { CmdInitTemplateSuccess, CmdInitWorkspaceSuccess } from "./types.js";
|
||||
export { cmdInitWorkspace } from "./workspace.js";
|
||||
|
||||
@@ -5,22 +5,15 @@ import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
|
||||
export type CmdInitTemplateSuccess = {
|
||||
templatePath: string;
|
||||
};
|
||||
|
||||
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||
if (name.length === 0) {
|
||||
return err("workspace name must not be empty");
|
||||
}
|
||||
if (name === "." || name === "..") {
|
||||
return err("invalid workspace name");
|
||||
}
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
return err("workspace name must not contain path separators");
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
import {
|
||||
templateIndexTs,
|
||||
templateModeratorTs,
|
||||
templatePackageJson,
|
||||
templateRolesTs,
|
||||
templateTsconfigJson,
|
||||
} from "./templates.js";
|
||||
import type { CmdInitTemplateSuccess } from "./types.js";
|
||||
import { validateWorkspaceSegment } from "./validate.js";
|
||||
|
||||
function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean {
|
||||
return Array.isArray(workspaces) && workspaces.includes("templates/*");
|
||||
@@ -67,108 +60,6 @@ async function findWorkflowWorkspaceRoot(startDir: string): Promise<Result<strin
|
||||
}
|
||||
}
|
||||
|
||||
function templatePackageJson(templateName: string): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: `template-${templateName}`,
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"@uncaged/workflow": "^0.1.0",
|
||||
zod: "^4.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateTsconfigJson(): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
extends: "../../tsconfig.json",
|
||||
compilerOptions: {
|
||||
rootDir: "src",
|
||||
outDir: "dist",
|
||||
},
|
||||
include: ["src/**/*.ts"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateRolesTs(): string {
|
||||
return `import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const HELLO_TEMPLATE_DESCRIPTION =
|
||||
"Minimal starter template: one greeter role, then END.";
|
||||
|
||||
export type HelloTemplateMeta = {
|
||||
greeter: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const greeterMetaSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
||||
description: "Says hello — replace with your first role.",
|
||||
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
||||
extractPrompt: "Extract the assistant's greeting as message.",
|
||||
schema: greeterMetaSchema,
|
||||
extractRefs: null,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateModeratorTs(): string {
|
||||
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
||||
|
||||
import type { HelloTemplateMeta } from "./roles.js";
|
||||
|
||||
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
||||
ctx: ModeratorContext<HelloTemplateMeta>,
|
||||
) => {
|
||||
if (ctx.steps.length === 0) {
|
||||
return "greeter";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateIndexTs(): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
||||
|
||||
import { helloTemplateModerator } from "./moderator.js";
|
||||
import {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
|
||||
export {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
export { helloTemplateModerator } from "./moderator.js";
|
||||
|
||||
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
||||
description: HELLO_TEMPLATE_DESCRIPTION,
|
||||
roles: {
|
||||
greeter: greeterRole,
|
||||
},
|
||||
moderator: helloTemplateModerator,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function cmdInitTemplate(
|
||||
startDir: string,
|
||||
templateName: string,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
export function templatePackageJson(templateName: string): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: `template-${templateName}`,
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"@uncaged/workflow": "^0.1.0",
|
||||
zod: "^4.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export function templateTsconfigJson(): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
extends: "../../tsconfig.json",
|
||||
compilerOptions: {
|
||||
rootDir: "src",
|
||||
outDir: "dist",
|
||||
},
|
||||
include: ["src/**/*.ts"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export function templateRolesTs(): string {
|
||||
return `import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const HELLO_TEMPLATE_DESCRIPTION =
|
||||
"Minimal starter template: one greeter role, then END.";
|
||||
|
||||
export type HelloTemplateMeta = {
|
||||
greeter: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const greeterMetaSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
||||
description: "Says hello — replace with your first role.",
|
||||
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
||||
extractPrompt: "Extract the assistant's greeting as message.",
|
||||
schema: greeterMetaSchema,
|
||||
extractRefs: null,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
export function templateModeratorTs(): string {
|
||||
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
||||
|
||||
import type { HelloTemplateMeta } from "./roles.js";
|
||||
|
||||
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
||||
ctx: ModeratorContext<HelloTemplateMeta>,
|
||||
) => {
|
||||
if (ctx.steps.length === 0) {
|
||||
return "greeter";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
export function templateIndexTs(): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
||||
|
||||
import { helloTemplateModerator } from "./moderator.js";
|
||||
import {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
|
||||
export {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
export { helloTemplateModerator } from "./moderator.js";
|
||||
|
||||
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
||||
description: HELLO_TEMPLATE_DESCRIPTION,
|
||||
roles: {
|
||||
greeter: greeterRole,
|
||||
},
|
||||
moderator: helloTemplateModerator,
|
||||
};
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||
|
||||
export type CmdInitTemplateSuccess = {
|
||||
templatePath: string;
|
||||
};
|
||||
|
||||
export type CmdInitWorkspaceSuccess = {
|
||||
rootPath: string;
|
||||
};
|
||||
|
||||
export type InitDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
/** Validates a single path segment for workspace / template names (no separators, not `.` / `..`). */
|
||||
export function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||
if (name.length === 0) {
|
||||
return err("workspace name must not be empty");
|
||||
}
|
||||
if (name === "." || name === "..") {
|
||||
return err("invalid workspace name");
|
||||
}
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
return err("workspace name must not contain path separators");
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -4,23 +4,8 @@ import { join } from "node:path";
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
|
||||
export type CmdInitWorkspaceSuccess = {
|
||||
rootPath: string;
|
||||
};
|
||||
|
||||
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||
if (name.length === 0) {
|
||||
return err("workspace name must not be empty");
|
||||
}
|
||||
if (name === "." || name === "..") {
|
||||
return err("invalid workspace name");
|
||||
}
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
return err("workspace name must not contain path separators");
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
import type { CmdInitWorkspaceSuccess } from "./types.js";
|
||||
import { validateWorkspaceSegment } from "./validate.js";
|
||||
|
||||
function rootPackageJson(workspaceName: string): string {
|
||||
return `${JSON.stringify(
|
||||
@@ -122,7 +107,7 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下
|
||||
2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`extractPrompt\` / \`description\`。
|
||||
3. **编写 Moderator**:根据 \`ctx.steps\` 与业务状态返回下一个角色名或 \`END\`。
|
||||
4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / moderator 导出)。
|
||||
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding, extract)\`(或项目约定的封装)绑定 **AgentFn** / **ExtractFn**。
|
||||
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowFnOptions\`。
|
||||
6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。
|
||||
|
||||
## 4. 编码规范
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js";
|
||||
import type { CommandEntry } from "../../cli-command-types.js";
|
||||
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||
import { parseLiveArgv } from "../../live-argv.js";
|
||||
import { parseRunArgv } from "../../run-argv.js";
|
||||
import { cmdKill, cmdPause, cmdResume } from "./control.js";
|
||||
import { cmdFork, parseForkArgv } from "./fork.js";
|
||||
import { cmdFork } from "./fork.js";
|
||||
import { parseForkArgv } from "./fork-argv.js";
|
||||
import { cmdThreads } from "./list.js";
|
||||
import { cmdLive } from "./live.js";
|
||||
import { cmdPs } from "./ps.js";
|
||||
import { cmdThreadRemove } from "./rm.js";
|
||||
import { cmdRun } from "./run.js";
|
||||
import { cmdThreadShow } from "./show.js";
|
||||
import type { ThreadDispatchDeps } from "./types.js";
|
||||
|
||||
function usageText(): string {
|
||||
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||
@@ -190,7 +192,7 @@ export const THREAD_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
resume: { handler: dispatchResume, args: "<thread-id>", description: "Resume a paused thread" },
|
||||
};
|
||||
|
||||
export function createThreadDispatcher(deps: { dispatchGroup: DispatchGroupFn }) {
|
||||
export function createThreadDispatcher(deps: ThreadDispatchDeps) {
|
||||
const { dispatchGroup } = deps;
|
||||
return async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("thread", THREAD_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import type { ParsedForkArgv } from "./types.js";
|
||||
|
||||
export function parseForkArgv(argv: string[]): Result<ParsedForkArgv, string> {
|
||||
if (argv.length === 0) {
|
||||
return err("fork requires <thread-id>");
|
||||
}
|
||||
const threadId = argv[0];
|
||||
if (threadId === undefined || threadId === "") {
|
||||
return err("fork requires <thread-id>");
|
||||
}
|
||||
let fromRole: string | null = null;
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--from-role") {
|
||||
const r = argv[i + 1];
|
||||
if (r === undefined || r === "") {
|
||||
return err("--from-role requires a role name");
|
||||
}
|
||||
fromRole = r;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
return err(`unexpected argument: ${a}`);
|
||||
}
|
||||
return ok({ threadId, fromRole });
|
||||
}
|
||||
@@ -6,33 +6,6 @@ import { pathExists, readTextFileIfExists } from "../../fs-utils.js";
|
||||
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
|
||||
|
||||
export function parseForkArgv(
|
||||
argv: string[],
|
||||
): Result<{ threadId: string; fromRole: string | null }, string> {
|
||||
if (argv.length === 0) {
|
||||
return err("fork requires <thread-id>");
|
||||
}
|
||||
const threadId = argv[0];
|
||||
if (threadId === undefined || threadId === "") {
|
||||
return err("fork requires <thread-id>");
|
||||
}
|
||||
let fromRole: string | null = null;
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--from-role") {
|
||||
const r = argv[i + 1];
|
||||
if (r === undefined || r === "") {
|
||||
return err("--from-role requires a role name");
|
||||
}
|
||||
fromRole = r;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
return err(`unexpected argument: ${a}`);
|
||||
}
|
||||
return ok({ threadId, fromRole });
|
||||
}
|
||||
|
||||
export async function cmdFork(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
export { cmdKill, cmdPause, cmdResume } from "./control.js";
|
||||
export { cmdFork, parseForkArgv } from "./fork.js";
|
||||
export {
|
||||
createThreadDispatcher,
|
||||
dispatchFork,
|
||||
dispatchKill,
|
||||
dispatchLive,
|
||||
dispatchPause,
|
||||
dispatchPs,
|
||||
dispatchResume,
|
||||
dispatchRun,
|
||||
dispatchThreadList,
|
||||
dispatchThreadRm,
|
||||
dispatchThreadShow,
|
||||
THREAD_SUBCOMMAND_TABLE,
|
||||
} from "./dispatch.js";
|
||||
export { cmdFork } from "./fork.js";
|
||||
export { parseForkArgv } from "./fork-argv.js";
|
||||
export { cmdThreads } from "./list.js";
|
||||
export type { LiveRoleRow } from "./live.js";
|
||||
export {
|
||||
cmdLive,
|
||||
formatLiveDebugLine,
|
||||
@@ -13,3 +27,4 @@ export { cmdPs } from "./ps.js";
|
||||
export { cmdThreadRemove } from "./rm.js";
|
||||
export { cmdRun } from "./run.js";
|
||||
export { cmdThreadShow } from "./show.js";
|
||||
export type { LiveRoleRow } from "./types.js";
|
||||
|
||||
@@ -12,20 +12,15 @@ import {
|
||||
type WorkflowCompletion,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { dimGreyLine, highlightLiveRole } from "../../cli-color.js";
|
||||
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
import type { ParsedLiveArgv } from "../../live-argv.js";
|
||||
import { findLatestThreadDataPath, resolveThreadDataPath } from "../../thread-scan.js";
|
||||
import type { LiveRoleRow } from "./types.js";
|
||||
|
||||
export const LIVE_CONTENT_MAX_LINES = 10;
|
||||
|
||||
export type LiveRoleRow = {
|
||||
role: string;
|
||||
content: string;
|
||||
meta: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export function formatLiveTimeLabel(timestampMs: number): string {
|
||||
const d = new Date(timestampMs);
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
@@ -34,24 +29,6 @@ export function formatLiveTimeLabel(timestampMs: number): string {
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function shouldUseColor(): boolean {
|
||||
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
||||
}
|
||||
|
||||
function highlightLiveRole(name: string): string {
|
||||
if (!shouldUseColor()) {
|
||||
return name;
|
||||
}
|
||||
return `\x1b[1m\x1b[36m${name}\x1b[0m`;
|
||||
}
|
||||
|
||||
function dimGreyLine(line: string): string {
|
||||
if (!shouldUseColor()) {
|
||||
return line;
|
||||
}
|
||||
return `\x1b[2m\x1b[90m${line}\x1b[0m`;
|
||||
}
|
||||
|
||||
export function formatLiveDebugLine(timestampMs: number, tag: string, message: string): string {
|
||||
const label = `[${formatLiveTimeLabel(timestampMs)}] [${tag}] ${message.replace(/\n/g, " ")}`;
|
||||
return dimGreyLine(label);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||
|
||||
export type LiveRoleRow = {
|
||||
role: string;
|
||||
content: string;
|
||||
meta: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ParsedForkArgv = {
|
||||
threadId: string;
|
||||
fromRole: string | null;
|
||||
};
|
||||
|
||||
export type ThreadDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import type { ParsedAddArgv } from "./types.js";
|
||||
|
||||
type ParsedLongFlag = { advance: 2; kind: "types"; value: string };
|
||||
|
||||
function tryParseAddLongFlag(argv: string[], index: number): Result<ParsedLongFlag | null, string> {
|
||||
const tok = argv[index];
|
||||
if (tok !== "--types") {
|
||||
return ok(null);
|
||||
}
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
return err("missing value for --types");
|
||||
}
|
||||
return ok({ advance: 2, kind: "types", value });
|
||||
}
|
||||
|
||||
type PositionalSlots = {
|
||||
name: string | undefined;
|
||||
filePath: string | undefined;
|
||||
};
|
||||
|
||||
function assignPositional(tok: string, slots: PositionalSlots): Result<void, string> {
|
||||
if (slots.name === undefined) {
|
||||
slots.name = tok;
|
||||
return ok(undefined);
|
||||
}
|
||||
if (slots.filePath === undefined) {
|
||||
slots.filePath = tok;
|
||||
return ok(undefined);
|
||||
}
|
||||
return err("too many arguments");
|
||||
}
|
||||
|
||||
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
||||
const slots: PositionalSlots = { name: undefined, filePath: undefined };
|
||||
let typesPath: string | null = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < argv.length) {
|
||||
const flag = tryParseAddLongFlag(argv, i);
|
||||
if (!flag.ok) {
|
||||
return flag;
|
||||
}
|
||||
if (flag.value !== null) {
|
||||
typesPath = flag.value.value;
|
||||
i += flag.value.advance;
|
||||
continue;
|
||||
}
|
||||
|
||||
const tok = argv[i];
|
||||
if (tok?.startsWith("--")) {
|
||||
return err(`unknown add flag: ${tok}`);
|
||||
}
|
||||
if (tok === undefined) {
|
||||
break;
|
||||
}
|
||||
const placed = assignPositional(tok, slots);
|
||||
if (!placed.ok) {
|
||||
return placed;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const { name, filePath } = slots;
|
||||
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
||||
return err("add requires <name> <file>");
|
||||
}
|
||||
|
||||
return ok({ name, filePath, typesPath });
|
||||
}
|
||||
@@ -17,17 +17,7 @@ import {
|
||||
import { storeWorkflowBundleArtifacts } from "../../bundle-store.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export type ParsedAddArgv = {
|
||||
name: string;
|
||||
filePath: string;
|
||||
/** Override path to `.d.ts` when adding a bundle. */
|
||||
typesPath: string | null;
|
||||
};
|
||||
|
||||
export type CmdAddSuccess = {
|
||||
hash: string;
|
||||
warnings: ReadonlyArray<string>;
|
||||
};
|
||||
import type { CmdAddSuccess, ParsedAddArgv } from "./types.js";
|
||||
|
||||
function isEsmBundle(path: string): boolean {
|
||||
return path.endsWith(".esm.js");
|
||||
@@ -37,75 +27,6 @@ function defaultTypesPath(bundlePath: string): string {
|
||||
return bundlePath.replace(/\.esm\.js$/i, ".d.ts");
|
||||
}
|
||||
|
||||
type ParsedLongFlag = { advance: 2; kind: "types"; value: string };
|
||||
|
||||
function tryParseAddLongFlag(argv: string[], index: number): Result<ParsedLongFlag | null, string> {
|
||||
const tok = argv[index];
|
||||
if (tok !== "--types") {
|
||||
return ok(null);
|
||||
}
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
return err("missing value for --types");
|
||||
}
|
||||
return ok({ advance: 2, kind: "types", value });
|
||||
}
|
||||
|
||||
type PositionalSlots = {
|
||||
name: string | undefined;
|
||||
filePath: string | undefined;
|
||||
};
|
||||
|
||||
function assignPositional(tok: string, slots: PositionalSlots): Result<void, string> {
|
||||
if (slots.name === undefined) {
|
||||
slots.name = tok;
|
||||
return ok(undefined);
|
||||
}
|
||||
if (slots.filePath === undefined) {
|
||||
slots.filePath = tok;
|
||||
return ok(undefined);
|
||||
}
|
||||
return err("too many arguments");
|
||||
}
|
||||
|
||||
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
||||
const slots: PositionalSlots = { name: undefined, filePath: undefined };
|
||||
let typesPath: string | null = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < argv.length) {
|
||||
const flag = tryParseAddLongFlag(argv, i);
|
||||
if (!flag.ok) {
|
||||
return flag;
|
||||
}
|
||||
if (flag.value !== null) {
|
||||
typesPath = flag.value.value;
|
||||
i += flag.value.advance;
|
||||
continue;
|
||||
}
|
||||
|
||||
const tok = argv[i];
|
||||
if (tok?.startsWith("--")) {
|
||||
return err(`unknown add flag: ${tok}`);
|
||||
}
|
||||
if (tok === undefined) {
|
||||
break;
|
||||
}
|
||||
const placed = assignPositional(tok, slots);
|
||||
if (!placed.ok) {
|
||||
return placed;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const { name, filePath } = slots;
|
||||
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
||||
return err("add requires <name> <file>");
|
||||
}
|
||||
|
||||
return ok({ name, filePath, typesPath });
|
||||
}
|
||||
|
||||
async function registerHash(
|
||||
storageRoot: string,
|
||||
name: string,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js";
|
||||
import type { CommandEntry } from "../../cli-command-types.js";
|
||||
import { printCliError, printCliLine, printCliWarn } from "../../cli-output.js";
|
||||
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||
import { cmdAdd, formatAddSuccess, parseAddArgv } from "./add.js";
|
||||
import { cmdAdd, formatAddSuccess } from "./add.js";
|
||||
import { parseAddArgv } from "./add-argv.js";
|
||||
import { cmdHistory } from "./history.js";
|
||||
import { cmdList, formatListLines } from "./list.js";
|
||||
import { cmdRemove } from "./rm.js";
|
||||
import { cmdRollback } from "./rollback.js";
|
||||
import { cmdShow, formatShowYaml } from "./show.js";
|
||||
import type { WorkflowDispatchDeps } from "./types.js";
|
||||
|
||||
function usageText(): string {
|
||||
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||
@@ -139,11 +141,6 @@ export const WORKFLOW_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
},
|
||||
};
|
||||
|
||||
type WorkflowDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
printDeprecation: (oldCmd: string, newCmd: string) => void;
|
||||
};
|
||||
|
||||
export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) {
|
||||
const { dispatchGroup, printDeprecation } = deps;
|
||||
return async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
export type { CmdAddSuccess, ParsedAddArgv } from "./add.js";
|
||||
export { cmdAdd, formatAddSuccess, parseAddArgv } from "./add.js";
|
||||
export { cmdAdd, formatAddSuccess } from "./add.js";
|
||||
export { parseAddArgv } from "./add-argv.js";
|
||||
export {
|
||||
createWorkflowDispatcher,
|
||||
dispatchAdd,
|
||||
dispatchHistory,
|
||||
dispatchList,
|
||||
dispatchRemove,
|
||||
dispatchRollback,
|
||||
dispatchShow,
|
||||
WORKFLOW_SUBCOMMAND_TABLE,
|
||||
} from "./dispatch.js";
|
||||
export { cmdHistory } from "./history.js";
|
||||
export { cmdList, formatListLines } from "./list.js";
|
||||
export { cmdRemove } from "./rm.js";
|
||||
export { cmdRollback } from "./rollback.js";
|
||||
export { cmdShow, formatShowYaml } from "./show.js";
|
||||
export type { CmdAddSuccess, ParsedAddArgv } from "./types.js";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||
|
||||
export type ParsedAddArgv = {
|
||||
name: string;
|
||||
filePath: string;
|
||||
/** Override path to `.d.ts` when adding a bundle. */
|
||||
typesPath: string | null;
|
||||
};
|
||||
|
||||
export type CmdAddSuccess = {
|
||||
hash: string;
|
||||
warnings: ReadonlyArray<string>;
|
||||
};
|
||||
|
||||
export type WorkflowDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
printDeprecation: (oldCmd: string, newCmd: string) => void;
|
||||
};
|
||||
@@ -126,13 +126,13 @@ uncaged-workflow thread list
|
||||
|
||||
## CAS (Content-Addressable Storage)
|
||||
|
||||
Store and retrieve content by hash, scoped to the current thread.
|
||||
Store and retrieve content by hash in workflow storage (global CAS directory).
|
||||
|
||||
| Operation | Command |
|
||||
|-----------|---------|
|
||||
| **Store** | \`uncaged-workflow cas put <THREAD_ID> '<content>'\` → prints hash |
|
||||
| **Read** | \`uncaged-workflow cas get <THREAD_ID> <HASH>\` → prints content |
|
||||
| **List** | \`uncaged-workflow cas list <THREAD_ID>\` |
|
||||
| **Store** | \`uncaged-workflow cas put '<content>'\` → prints hash |
|
||||
| **Read** | \`uncaged-workflow cas get <HASH>\` → prints content |
|
||||
| **List** | \`uncaged-workflow cas list\` |
|
||||
|
||||
CAS is the **only** supported way to persist structured data (phase plans, review notes, etc.) within a thread. Do not use temp files.
|
||||
|
||||
|
||||
@@ -3,6 +3,23 @@ import { join } from "node:path";
|
||||
|
||||
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
|
||||
|
||||
function parseFirstJsonLineObject(text: string): Record<string, unknown> | null {
|
||||
const firstLine = text.split("\n")[0];
|
||||
if (firstLine === undefined || firstLine.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(firstLine) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type RunningThreadRow = {
|
||||
threadId: string;
|
||||
hash: string;
|
||||
@@ -20,20 +37,11 @@ async function readThreadStartTimestampMs(dataPath: string): Promise<number | nu
|
||||
if (text === null) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = text.split("\n")[0];
|
||||
if (firstLine === undefined || firstLine.trim() === "") {
|
||||
const parsed = parseFirstJsonLineObject(text);
|
||||
if (parsed === null) {
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(firstLine) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
const ts = (parsed as Record<string, unknown>).timestamp;
|
||||
const ts = parsed.timestamp;
|
||||
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
||||
}
|
||||
|
||||
@@ -42,20 +50,11 @@ async function readWorkflowNameFromDataJsonl(dataPath: string): Promise<string |
|
||||
if (text === null) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = text.split("\n")[0];
|
||||
if (firstLine === undefined || firstLine.trim() === "") {
|
||||
const parsed = parseFirstJsonLineObject(text);
|
||||
if (parsed === null) {
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(firstLine) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
const name = (parsed as Record<string, unknown>).name;
|
||||
const name = parsed.name;
|
||||
return typeof name === "string" ? name : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
type AgentBinding,
|
||||
createWorkflow,
|
||||
type ExtractFn,
|
||||
type LlmProvider,
|
||||
type WorkflowDefinition,
|
||||
type WorkflowFn,
|
||||
} from "@uncaged/workflow";
|
||||
@@ -43,10 +41,6 @@ export const developWorkflowDefinition: WorkflowDefinition<DevelopMeta> = {
|
||||
moderator: developModerator,
|
||||
};
|
||||
|
||||
export function createDevelopRun(
|
||||
binding: AgentBinding,
|
||||
extract: ExtractFn,
|
||||
llmProvider: LlmProvider | null,
|
||||
): WorkflowFn {
|
||||
return createWorkflow(developWorkflowDefinition, binding, extract, llmProvider);
|
||||
export function createDevelopRun(binding: AgentBinding): WorkflowFn {
|
||||
return createWorkflow(developWorkflowDefinition, binding);
|
||||
}
|
||||
|
||||
@@ -250,17 +250,20 @@ describe("createSolveIssueRun", () => {
|
||||
const cas = createCasStore(casDir);
|
||||
|
||||
// Override developer so the test does not spin up a child workflow.
|
||||
const run = createSolveIssueRun(
|
||||
{
|
||||
agent: async () => "",
|
||||
overrides: { developer: async () => "stub-root-hash" },
|
||||
},
|
||||
stubExtract,
|
||||
stubLlmProvider,
|
||||
);
|
||||
const run = createSolveIssueRun({
|
||||
agent: async () => "",
|
||||
overrides: { developer: async () => "stub-root-hash" },
|
||||
});
|
||||
const gen = run(
|
||||
{ prompt: "task", steps: [] },
|
||||
{ threadId: "01TEST000000000000000000TR", maxRounds: 20, depth: 0, cas },
|
||||
{
|
||||
threadId: "01TEST000000000000000000TR",
|
||||
maxRounds: 20,
|
||||
depth: 0,
|
||||
cas,
|
||||
extract: stubExtract,
|
||||
llmProvider: stubLlmProvider,
|
||||
},
|
||||
);
|
||||
const first = await gen.next();
|
||||
expect(first.done).toBe(false);
|
||||
@@ -294,33 +297,36 @@ describe("createSolveIssueRun", () => {
|
||||
const cas = createCasStore(casDir);
|
||||
|
||||
const calls: string[] = [];
|
||||
const run = createSolveIssueRun(
|
||||
{
|
||||
agent: async () => {
|
||||
calls.push("default");
|
||||
const run = createSolveIssueRun({
|
||||
agent: async () => {
|
||||
calls.push("default");
|
||||
return "";
|
||||
},
|
||||
overrides: {
|
||||
preparer: async () => {
|
||||
calls.push("preparer");
|
||||
return "";
|
||||
},
|
||||
overrides: {
|
||||
preparer: async () => {
|
||||
calls.push("preparer");
|
||||
return "";
|
||||
},
|
||||
developer: async () => {
|
||||
calls.push("developer");
|
||||
return "stub-root-hash";
|
||||
},
|
||||
submitter: async () => {
|
||||
calls.push("submitter");
|
||||
return "";
|
||||
},
|
||||
developer: async () => {
|
||||
calls.push("developer");
|
||||
return "stub-root-hash";
|
||||
},
|
||||
submitter: async () => {
|
||||
calls.push("submitter");
|
||||
return "";
|
||||
},
|
||||
},
|
||||
stubExtract,
|
||||
stubLlmProvider,
|
||||
);
|
||||
});
|
||||
const gen = run(
|
||||
{ prompt: "task", steps: [] },
|
||||
{ threadId: "01TEST000000000000000000TR", maxRounds: 20, depth: 0, cas },
|
||||
{
|
||||
threadId: "01TEST000000000000000000TR",
|
||||
maxRounds: 20,
|
||||
depth: 0,
|
||||
cas,
|
||||
extract: stubExtract,
|
||||
llmProvider: stubLlmProvider,
|
||||
},
|
||||
);
|
||||
await gen.next();
|
||||
expect(calls).toEqual(["preparer"]);
|
||||
@@ -353,22 +359,25 @@ describe("createSolveIssueRun", () => {
|
||||
const cas = createCasStore(casDir);
|
||||
|
||||
let developerInvocations = 0;
|
||||
const run = createSolveIssueRun(
|
||||
{
|
||||
agent: async () => "",
|
||||
overrides: {
|
||||
developer: async () => {
|
||||
developerInvocations += 1;
|
||||
return "stub-root-hash";
|
||||
},
|
||||
const run = createSolveIssueRun({
|
||||
agent: async () => "",
|
||||
overrides: {
|
||||
developer: async () => {
|
||||
developerInvocations += 1;
|
||||
return "stub-root-hash";
|
||||
},
|
||||
},
|
||||
stubExtract,
|
||||
stubLlmProvider,
|
||||
);
|
||||
});
|
||||
const gen = run(
|
||||
{ prompt: "task", steps: [] },
|
||||
{ threadId: "01TEST000000000000000000TR", maxRounds: 20, depth: 0, cas },
|
||||
{
|
||||
threadId: "01TEST000000000000000000TR",
|
||||
maxRounds: 20,
|
||||
depth: 0,
|
||||
cas,
|
||||
extract: stubExtract,
|
||||
llmProvider: stubLlmProvider,
|
||||
},
|
||||
);
|
||||
// preparer
|
||||
await gen.next();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
type AgentBinding,
|
||||
createWorkflow,
|
||||
type ExtractFn,
|
||||
type LlmProvider,
|
||||
type WorkflowDefinition,
|
||||
type WorkflowFn,
|
||||
workflowAsAgent,
|
||||
@@ -46,11 +44,7 @@ export const solveIssueWorkflowDefinition: WorkflowDefinition<SolveIssueMeta> =
|
||||
* {@link workflowAsAgent}; if the caller supplies their own `developer` override in
|
||||
* `binding.overrides`, it takes precedence so tests and custom hosts can stub it.
|
||||
*/
|
||||
export function createSolveIssueRun(
|
||||
binding: AgentBinding,
|
||||
extract: ExtractFn,
|
||||
llmProvider: LlmProvider | null,
|
||||
): WorkflowFn {
|
||||
export function createSolveIssueRun(binding: AgentBinding): WorkflowFn {
|
||||
const developerOverride = binding.overrides?.developer ?? workflowAsAgent("develop");
|
||||
const mergedBinding: AgentBinding = {
|
||||
agent: binding.agent,
|
||||
@@ -59,5 +53,5 @@ export function createSolveIssueRun(
|
||||
developer: developerOverride,
|
||||
},
|
||||
};
|
||||
return createWorkflow(solveIssueWorkflowDefinition, mergedBinding, extract, llmProvider);
|
||||
return createWorkflow(solveIssueWorkflowDefinition, mergedBinding);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
decodeCrockfordToUint64,
|
||||
encodeCrockfordBase32Bits,
|
||||
encodeUint64AsCrockford,
|
||||
} from "../src/base32.js";
|
||||
} from "../src/util/base32.js";
|
||||
|
||||
describe("Crockford Base32", () => {
|
||||
test("roundtrip 64-bit hash encoding", () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { buildDescriptor } from "../src/build-descriptor.js";
|
||||
import { buildDescriptor } from "../src/bundle/build-descriptor.js";
|
||||
import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js";
|
||||
|
||||
describe("buildDescriptor", () => {
|
||||
test("produces a descriptor that validates and includes JSON schemas per role", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { validateWorkflowBundle } from "../src/bundle-validator.js";
|
||||
import { validateWorkflowBundle } from "../src/bundle/bundle-validator.js";
|
||||
|
||||
const minimalDescriptor = `export const descriptor = { description: "x", roles: {} };
|
||||
`;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createCasStore, createThreadCas } from "../src/cas.js";
|
||||
import { hashString } from "../src/hash.js";
|
||||
import { createCasStore, createThreadCas } from "../src/cas/cas.js";
|
||||
import { hashString } from "../src/cas/hash.js";
|
||||
|
||||
describe("cas module exports", () => {
|
||||
test("createThreadCas is a deprecated alias of createCasStore", () => {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { createWorkflow } from "../src/create-workflow.js";
|
||||
import { executeThread } from "../src/engine.js";
|
||||
import { createExtract } from "../src/extract-fn.js";
|
||||
import { createLogger } from "../src/logger.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import {
|
||||
createContentMerkleNode,
|
||||
getContentMerklePayload,
|
||||
parseMerkleNode,
|
||||
serializeMerkleNode,
|
||||
} from "../src/merkle.js";
|
||||
import { END, type LlmProvider } from "../src/types.js";
|
||||
} from "../src/cas/merkle.js";
|
||||
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||
import { executeThread } from "../src/engine/engine.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
|
||||
const plannerMetaSchema = z.object({
|
||||
plan: z.string(),
|
||||
@@ -82,11 +81,20 @@ function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unkno
|
||||
};
|
||||
}
|
||||
|
||||
const demoExtract = createExtract({
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
apiKey: "test",
|
||||
model: "test",
|
||||
});
|
||||
const EXTRACT_REGISTRY_YAML = `config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/model
|
||||
workflows: {}
|
||||
`;
|
||||
|
||||
async function writeExtractRegistryConfig(storageRoot: string): Promise<void> {
|
||||
await writeFile(join(storageRoot, "workflow.yaml"), EXTRACT_REGISTRY_YAML, "utf8");
|
||||
}
|
||||
|
||||
const demoWorkflow = createWorkflow<DemoMeta>(
|
||||
{
|
||||
@@ -125,8 +133,6 @@ const demoWorkflow = createWorkflow<DemoMeta>(
|
||||
coder: async () => "code-body",
|
||||
},
|
||||
},
|
||||
demoExtract,
|
||||
null,
|
||||
);
|
||||
|
||||
describe("executeThread", () => {
|
||||
@@ -150,6 +156,7 @@ describe("executeThread", () => {
|
||||
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
|
||||
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
|
||||
await mkdir(join(root, "logs", hash), { recursive: true });
|
||||
await writeExtractRegistryConfig(root);
|
||||
const cas = createCasStore(join(root, "cas"));
|
||||
|
||||
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
|
||||
@@ -166,6 +173,7 @@ describe("executeThread", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
@@ -258,6 +266,7 @@ describe("executeThread", () => {
|
||||
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
|
||||
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
|
||||
await mkdir(join(root, "logs", hash), { recursive: true });
|
||||
await writeExtractRegistryConfig(root);
|
||||
const cas = createCasStore(join(root, "cas"));
|
||||
const plannerHash = await cas.put(serializeMerkleNode(createContentMerkleNode("plan-body")));
|
||||
|
||||
@@ -295,6 +304,7 @@ describe("executeThread", () => {
|
||||
timestamp: histTs,
|
||||
},
|
||||
],
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
@@ -354,6 +364,7 @@ describe("executeThread", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
@@ -391,6 +402,7 @@ describe("executeThread", () => {
|
||||
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
|
||||
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
|
||||
await mkdir(join(root, "logs", hash), { recursive: true });
|
||||
await writeExtractRegistryConfig(root);
|
||||
const cas = createCasStore(join(root, "cas"));
|
||||
|
||||
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
|
||||
@@ -407,6 +419,7 @@ describe("executeThread", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
@@ -549,9 +562,6 @@ describe("executeThread", () => {
|
||||
{ preconnect: origFetch.preconnect.bind(origFetch) },
|
||||
) as typeof fetch;
|
||||
|
||||
const llm: LlmProvider = { baseUrl: "http://127.0.0.1:9", apiKey: "test", model: "test" };
|
||||
const extractFn = createExtract(llm);
|
||||
|
||||
const dagWorkflow = createWorkflow<DagDemoMeta>(
|
||||
{
|
||||
roles: {
|
||||
@@ -568,8 +578,6 @@ describe("executeThread", () => {
|
||||
moderator: (ctx) => (ctx.steps.length === 0 ? "walker" : END),
|
||||
},
|
||||
{ agent: async () => dagRootHash },
|
||||
extractFn,
|
||||
llm,
|
||||
);
|
||||
|
||||
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
||||
@@ -577,6 +585,7 @@ describe("executeThread", () => {
|
||||
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
|
||||
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
|
||||
await mkdir(join(root, "logs", hash), { recursive: true });
|
||||
await writeExtractRegistryConfig(root);
|
||||
|
||||
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
|
||||
const ac = new AbortController();
|
||||
@@ -592,6 +601,7 @@ describe("executeThread", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { getExtractProvider } from "../src/extract-provider.js";
|
||||
|
||||
describe("getExtractProvider", () => {
|
||||
test("returns provider when config.extract is present", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "wf-ext-prov-ok-"));
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "workflow.yaml"),
|
||||
`config:
|
||||
maxDepth: 3
|
||||
extract:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
model: qwen-plus
|
||||
apiKey: literal-key
|
||||
workflows: {}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const r = await getExtractProvider(root);
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.value.baseUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1");
|
||||
expect(r.value.model).toBe("qwen-plus");
|
||||
expect(r.value.apiKey).toBe("literal-key");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("errs when registry has no config section", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "wf-ext-prov-missing-"));
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(join(root, "workflow.yaml"), "workflows: {}\n", "utf8");
|
||||
const r = await getExtractProvider(root);
|
||||
expect(r.ok).toBe(false);
|
||||
if (r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.error).toContain("no global config");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolves apiKey from env at registry read time", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "wf-ext-prov-env-"));
|
||||
const prev = process.env.WF_GET_EXTRACT_PROVIDER_KEY;
|
||||
process.env.WF_GET_EXTRACT_PROVIDER_KEY = "resolved-secret";
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "workflow.yaml"),
|
||||
`config:
|
||||
maxDepth: 1
|
||||
extract:
|
||||
baseUrl: https://example.com
|
||||
model: m
|
||||
apiKey: env:WF_GET_EXTRACT_PROVIDER_KEY
|
||||
workflows: {}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const r = await getExtractProvider(root);
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.value.apiKey).toBe("resolved-secret");
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.WF_GET_EXTRACT_PROVIDER_KEY;
|
||||
} else {
|
||||
process.env.WF_GET_EXTRACT_PROVIDER_KEY = prev;
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
buildForkPlan,
|
||||
parseThreadDataJsonl,
|
||||
selectForkHistoricalSteps,
|
||||
} from "../src/fork-thread.js";
|
||||
} from "../src/engine/fork-thread.js";
|
||||
|
||||
const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100}
|
||||
{"role":"planner","contentHash":"HP0000000000000000000001","meta":{},"refs":[],"timestamp":101}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { decodeCrockfordToUint64 } from "../src/base32.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||
import { decodeCrockfordToUint64 } from "../src/util/base32.js";
|
||||
|
||||
describe("hashWorkflowBundleBytes", () => {
|
||||
test("matches XXH64 reference for empty input", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createLogger } from "../src/logger.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
|
||||
describe("createLogger", () => {
|
||||
test("writes JSONL records to a file sink", async () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { createContentMerkleNode, parseMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
||||
import {
|
||||
createContentMerkleNode,
|
||||
parseMerkleNode,
|
||||
serializeMerkleNode,
|
||||
} from "../src/cas/merkle.js";
|
||||
|
||||
describe("merkle", () => {
|
||||
test("content node roundtrips through YAML", () => {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
||||
import { reactExtract } from "../src/react-extract.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js";
|
||||
import { reactExtract } from "../src/extract/react-extract.js";
|
||||
import type { LlmProvider } from "../src/types.js";
|
||||
|
||||
const metaSchema = z.object({ seen: z.string() });
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { createWorkflow } from "../src/create-workflow.js";
|
||||
import { executeThread } from "../src/engine.js";
|
||||
import { createExtract } from "../src/extract-fn.js";
|
||||
import { buildForkPlan, parseThreadDataJsonl } from "../src/fork-thread.js";
|
||||
import { createLogger } from "../src/logger.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||
import { executeThread } from "../src/engine/engine.js";
|
||||
import { buildForkPlan, parseThreadDataJsonl } from "../src/engine/fork-thread.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
|
||||
const phaseSchema = z.object({
|
||||
hash: z.string(),
|
||||
@@ -76,11 +75,16 @@ function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unkno
|
||||
};
|
||||
}
|
||||
|
||||
const refsDemoExtract = createExtract({
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
apiKey: "test",
|
||||
model: "test",
|
||||
});
|
||||
const EXTRACT_REGISTRY_YAML = `config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/model
|
||||
workflows: {}
|
||||
`;
|
||||
|
||||
const refsDemoWorkflow = createWorkflow<RefsDemoMeta>(
|
||||
{
|
||||
@@ -99,8 +103,6 @@ const refsDemoWorkflow = createWorkflow<RefsDemoMeta>(
|
||||
{
|
||||
agent: async () => "plan-output",
|
||||
},
|
||||
refsDemoExtract,
|
||||
null,
|
||||
);
|
||||
|
||||
describe("RoleStep refs tracking", () => {
|
||||
@@ -142,6 +144,7 @@ describe("RoleStep refs tracking", () => {
|
||||
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
|
||||
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
|
||||
await mkdir(join(root, "logs", hash), { recursive: true });
|
||||
await writeFile(join(root, "workflow.yaml"), EXTRACT_REGISTRY_YAML, "utf8");
|
||||
const cas = createCasStore(join(root, "cas"));
|
||||
|
||||
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
|
||||
@@ -158,6 +161,7 @@ describe("RoleStep refs tracking", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
rollbackWorkflowToHistoryHash,
|
||||
unregisterWorkflow,
|
||||
writeWorkflowRegistry,
|
||||
} from "../src/registry.js";
|
||||
} from "../src/registry/registry.js";
|
||||
|
||||
describe("workflow registry", () => {
|
||||
test("roundtrips through workflow.yaml", async () => {
|
||||
@@ -105,10 +105,13 @@ describe("workflow registry", () => {
|
||||
const yaml = `
|
||||
config:
|
||||
maxDepth: 3
|
||||
extract:
|
||||
baseUrl: https://example.com/v1
|
||||
model: qwen-plus
|
||||
apiKey: secret-key
|
||||
providers:
|
||||
dashscope:
|
||||
baseUrl: https://example.com/v1
|
||||
apiKey: secret-key
|
||||
models:
|
||||
default: dashscope/qwen-turbo
|
||||
extract: dashscope/qwen-plus
|
||||
workflows:
|
||||
solve-issue:
|
||||
hash: SPVR4BDMSGC1W
|
||||
@@ -125,9 +128,10 @@ workflows:
|
||||
return;
|
||||
}
|
||||
expect(r.value.config.maxDepth).toBe(3);
|
||||
expect(r.value.config.extract.baseUrl).toBe("https://example.com/v1");
|
||||
expect(r.value.config.extract.model).toBe("qwen-plus");
|
||||
expect(r.value.config.extract.apiKey).toBe("secret-key");
|
||||
expect(r.value.config.providers.dashscope?.baseUrl).toBe("https://example.com/v1");
|
||||
expect(r.value.config.providers.dashscope?.apiKey).toBe("secret-key");
|
||||
expect(r.value.config.models.extract).toBe("dashscope/qwen-plus");
|
||||
expect(r.value.config.models.default).toBe("dashscope/qwen-turbo");
|
||||
});
|
||||
|
||||
test("parses config apiKey env: prefix from process.env", () => {
|
||||
@@ -137,10 +141,13 @@ workflows:
|
||||
const yaml = `
|
||||
config:
|
||||
maxDepth: 1
|
||||
extract:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
model: qwen-plus
|
||||
apiKey: env:WF_REGISTRY_TEST_API_KEY
|
||||
providers:
|
||||
dashscope:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
apiKey: env:WF_REGISTRY_TEST_API_KEY
|
||||
models:
|
||||
default: dashscope/qwen-plus
|
||||
extract: dashscope/qwen-plus
|
||||
workflows: {}
|
||||
`;
|
||||
const r = parseWorkflowRegistryYaml(yaml);
|
||||
@@ -148,7 +155,7 @@ workflows: {}
|
||||
if (!r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.value.config?.extract.apiKey).toBe("from-env");
|
||||
expect(r.value.config?.providers.dashscope?.apiKey).toBe("from-env");
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.WF_REGISTRY_TEST_API_KEY;
|
||||
@@ -165,10 +172,12 @@ workflows: {}
|
||||
const yaml = `
|
||||
config:
|
||||
maxDepth: 1
|
||||
extract:
|
||||
baseUrl: https://example.com
|
||||
model: m
|
||||
apiKey: env:WF_REGISTRY_TEST_API_KEY_UNSET
|
||||
providers:
|
||||
p:
|
||||
baseUrl: https://example.com
|
||||
apiKey: env:WF_REGISTRY_TEST_API_KEY_UNSET
|
||||
models:
|
||||
default: p/m
|
||||
workflows: {}
|
||||
`;
|
||||
const r = parseWorkflowRegistryYaml(yaml);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { resolveModel } from "../src/config/resolve-model.js";
|
||||
import type { WorkflowConfig } from "../src/registry/index.js";
|
||||
|
||||
function sampleConfig(): WorkflowConfig {
|
||||
return {
|
||||
maxDepth: 3,
|
||||
providers: {
|
||||
dashscope: {
|
||||
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: "secret",
|
||||
},
|
||||
other: {
|
||||
baseUrl: "https://other.example/v1",
|
||||
apiKey: "k2",
|
||||
},
|
||||
},
|
||||
models: {
|
||||
default: "dashscope/qwen-plus",
|
||||
extract: "other/foo/bar-model",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveModel", () => {
|
||||
test("uses explicit scene mapping", () => {
|
||||
const config = sampleConfig();
|
||||
const r = resolveModel(config, "extract");
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.value.baseUrl).toBe("https://other.example/v1");
|
||||
expect(r.value.apiKey).toBe("k2");
|
||||
expect(r.value.model).toBe("foo/bar-model");
|
||||
});
|
||||
|
||||
test("falls back to models.default when scene is missing", () => {
|
||||
const config = sampleConfig();
|
||||
const r = resolveModel(config, "unknown-scene");
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.value.model).toBe("qwen-plus");
|
||||
expect(r.value.baseUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1");
|
||||
});
|
||||
|
||||
test("errs when scene missing and no default", () => {
|
||||
const config: WorkflowConfig = {
|
||||
maxDepth: 1,
|
||||
providers: {
|
||||
p: { baseUrl: "https://x", apiKey: "k" },
|
||||
},
|
||||
models: {
|
||||
extract: "p/m",
|
||||
},
|
||||
};
|
||||
const r = resolveModel(config, "other");
|
||||
expect(r.ok).toBe(false);
|
||||
if (r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.error).toContain("no model mapping");
|
||||
expect(r.error).toContain("default");
|
||||
});
|
||||
|
||||
test("errs when provider is unknown", () => {
|
||||
const config: WorkflowConfig = {
|
||||
maxDepth: 1,
|
||||
providers: {
|
||||
p: { baseUrl: "https://x", apiKey: "k" },
|
||||
},
|
||||
models: {
|
||||
default: "missing/m",
|
||||
},
|
||||
};
|
||||
const r = resolveModel(config, "any");
|
||||
expect(r.ok).toBe(false);
|
||||
if (r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.error).toContain("unknown provider");
|
||||
});
|
||||
|
||||
test("errs on invalid model reference shape", () => {
|
||||
const config: WorkflowConfig = {
|
||||
maxDepth: 1,
|
||||
providers: {
|
||||
p: { baseUrl: "https://x", apiKey: "k" },
|
||||
},
|
||||
models: {
|
||||
default: "no-slash-model",
|
||||
},
|
||||
};
|
||||
const r = resolveModel(config, "x");
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { err, ok } from "../src/result.js";
|
||||
import { err, ok } from "../src/util/result.js";
|
||||
|
||||
describe("result helpers", () => {
|
||||
test("ok wraps value", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "../src/storage-root.js";
|
||||
import { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "../src/util/storage-root.js";
|
||||
|
||||
describe("getGlobalCasDir", () => {
|
||||
test("joins cas segment under explicit storage root", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { createThreadPauseGate } from "../src/thread-pause-gate.js";
|
||||
import { createThreadPauseGate } from "../src/engine/thread-pause-gate.js";
|
||||
|
||||
describe("createThreadPauseGate", () => {
|
||||
test("pause blocks awaitAfterYield until resume", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { decodeCrockfordBase32Bits } from "../src/base32.js";
|
||||
import { generateUlid } from "../src/ulid.js";
|
||||
import { decodeCrockfordBase32Bits } from "../src/util/base32.js";
|
||||
import { generateUlid } from "../src/util/ulid.js";
|
||||
|
||||
describe("generateUlid", () => {
|
||||
test("length and decodable Crockford payload", () => {
|
||||
|
||||
@@ -5,9 +5,20 @@ import { createConnection } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
||||
import { getWorkerHostScriptPath } from "../src/worker-entry-path.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js";
|
||||
import { getWorkerHostScriptPath } from "../src/engine/worker-entry-path.js";
|
||||
|
||||
const WORKER_REGISTRY_YAML = `config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/model
|
||||
workflows: {}
|
||||
`;
|
||||
|
||||
const bundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
|
||||
|
||||
@@ -89,6 +100,7 @@ describe("worker process", () => {
|
||||
try {
|
||||
const hash = "C9NMV6V2TQT81";
|
||||
await mkdir(join(root, "bundles"), { recursive: true });
|
||||
await writeFile(join(root, "workflow.yaml"), WORKER_REGISTRY_YAML, "utf8");
|
||||
const bundlePath = join(root, "bundles", `${hash}.esm.js`);
|
||||
await writeFile(bundlePath, bundleSource, "utf8");
|
||||
|
||||
@@ -136,6 +148,7 @@ describe("worker process", () => {
|
||||
try {
|
||||
const hash = "C9NMV6V2TQT81";
|
||||
await mkdir(join(root, "bundles"), { recursive: true });
|
||||
await writeFile(join(root, "workflow.yaml"), WORKER_REGISTRY_YAML, "utf8");
|
||||
const bundlePath = join(root, "bundles", `${hash}.esm.js`);
|
||||
await writeFile(bundlePath, bundleSource, "utf8");
|
||||
|
||||
|
||||
@@ -4,19 +4,18 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { createWorkflow } from "../src/create-workflow.js";
|
||||
import { executeThread } from "../src/engine.js";
|
||||
import { createExtract } from "../src/extract-fn.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
||||
import { createLogger } from "../src/logger.js";
|
||||
import { getContentMerklePayload, parseMerkleNode } from "../src/merkle.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||
import { getContentMerklePayload, parseMerkleNode } from "../src/cas/merkle.js";
|
||||
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||
import { executeThread } from "../src/engine/engine.js";
|
||||
import {
|
||||
readWorkflowRegistry,
|
||||
registerWorkflowVersion,
|
||||
writeWorkflowRegistry,
|
||||
} from "../src/registry.js";
|
||||
} from "../src/registry/registry.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
||||
|
||||
const callerMetaSchema = z.object({ done: z.literal(true) });
|
||||
@@ -76,11 +75,16 @@ function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unkno
|
||||
};
|
||||
}
|
||||
|
||||
const parentExtract = createExtract({
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
apiKey: "test",
|
||||
model: "test",
|
||||
});
|
||||
const PARENT_REGISTRY_WITH_CONFIG = `config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/m
|
||||
workflows: {}
|
||||
`;
|
||||
|
||||
const childBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
|
||||
|
||||
@@ -131,6 +135,8 @@ describe("workflowAsAgent integration", () => {
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), "wf-waa-int-"));
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(join(root, "workflow.yaml"), PARENT_REGISTRY_WITH_CONFIG, "utf8");
|
||||
const { hash: childHash } = await installChildWorkflow(root);
|
||||
|
||||
const parentWorkflow = createWorkflow<ParentMeta>(
|
||||
@@ -148,8 +154,6 @@ describe("workflowAsAgent integration", () => {
|
||||
moderator: (ctx) => (ctx.steps.length === 0 ? "caller" : END),
|
||||
},
|
||||
{ agent: workflowAsAgent("child-wf", { storageRoot: root }) },
|
||||
parentExtract,
|
||||
null,
|
||||
);
|
||||
|
||||
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
||||
@@ -173,6 +177,7 @@ describe("workflowAsAgent integration", () => {
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: null,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot: root,
|
||||
},
|
||||
{ threadId, hash: parentHash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas },
|
||||
logger,
|
||||
|
||||
@@ -3,14 +3,14 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createCasStore } from "../src/cas.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
||||
import { parseMerkleNode } from "../src/merkle.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||
import { parseMerkleNode } from "../src/cas/merkle.js";
|
||||
import {
|
||||
readWorkflowRegistry,
|
||||
registerWorkflowVersion,
|
||||
writeWorkflowRegistry,
|
||||
} from "../src/registry.js";
|
||||
} from "../src/registry/registry.js";
|
||||
import { type AgentContext, START } from "../src/types.js";
|
||||
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
||||
|
||||
@@ -93,6 +93,21 @@ describe("workflowAsAgent", () => {
|
||||
test("runs registered workflow and returns child thread root CAS hash", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "wf-waa-ok-"));
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "workflow.yaml"),
|
||||
`config:
|
||||
maxDepth: 3
|
||||
providers:
|
||||
stub:
|
||||
baseUrl: http://127.0.0.1:9
|
||||
apiKey: test
|
||||
models:
|
||||
default: stub/m
|
||||
workflows: {}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
await installChildWorkflow(root);
|
||||
const agent = workflowAsAgent("child-wf", { storageRoot: root });
|
||||
const out = await agent(
|
||||
@@ -140,10 +155,15 @@ describe("workflowAsAgent", () => {
|
||||
...reg.value,
|
||||
config: {
|
||||
maxDepth: 2,
|
||||
extract: {
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
model: "m",
|
||||
apiKey: "k",
|
||||
providers: {
|
||||
local: {
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
apiKey: "k",
|
||||
},
|
||||
},
|
||||
models: {
|
||||
default: "local/m",
|
||||
extract: "local/m",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js";
|
||||
import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js";
|
||||
|
||||
describe("validateWorkflowDescriptor", () => {
|
||||
// 1. Valid minimal descriptor
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import type { RoleMeta, WorkflowDefinition } from "./types.js";
|
||||
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./workflow-descriptor.js";
|
||||
import type { RoleMeta, WorkflowDefinition } from "../types.js";
|
||||
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./types.js";
|
||||
|
||||
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
|
||||
const { $schema: _drop, ...rest } = json;
|
||||
+5
-11
@@ -10,6 +10,11 @@ import type {
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
} from "acorn";
|
||||
import * as acorn from "acorn";
|
||||
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
import type { WorkflowBundleValidationInput } from "./types.js";
|
||||
|
||||
/** Acorn Node with index-access for property traversal. */
|
||||
type AcornNode = Node & { [key: string]: unknown };
|
||||
@@ -22,17 +27,6 @@ function narrowNode<T extends Node>(node: Node): T {
|
||||
return node as unknown as T;
|
||||
}
|
||||
|
||||
import * as acorn from "acorn";
|
||||
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
export type WorkflowBundleValidationInput = {
|
||||
/** Absolute or relative path (used for `.esm.js` suffix checks). */
|
||||
filePath: string;
|
||||
/** UTF-8 source of the bundle. */
|
||||
source: string;
|
||||
};
|
||||
|
||||
function endsWithEsmJs(path: string): boolean {
|
||||
return path.endsWith(".esm.js");
|
||||
}
|
||||
+2
-2
@@ -2,9 +2,9 @@ import { mkdir, readlink, symlink, unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/** This module lives in `@uncaged/workflow/src`; parent dir is the package root. */
|
||||
/** This module lives in `@uncaged/workflow/src/bundle`; grandparent dir is the package root. */
|
||||
function installedWorkflowPackageDir(): string {
|
||||
return fileURLToPath(new URL("..", import.meta.url));
|
||||
return fileURLToPath(new URL("../..", import.meta.url));
|
||||
}
|
||||
|
||||
/**
|
||||
+3
-13
@@ -1,20 +1,10 @@
|
||||
import type { WorkflowFn } from "../types.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
import { importWorkflowBundleModule } from "./bundle-import-env.js";
|
||||
import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import type { WorkflowFn } from "./types.js";
|
||||
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
||||
import type { ExtractBundleExportsOptions, ExtractedBundleExports } from "./types.js";
|
||||
import { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
||||
|
||||
export type ExtractedBundleExports = {
|
||||
run: WorkflowFn;
|
||||
descriptor: WorkflowDescriptor;
|
||||
};
|
||||
|
||||
export type ExtractBundleExportsOptions = {
|
||||
/** When set, ensures `node_modules/@uncaged/workflow` exists under this root before import. */
|
||||
storageRoot: string | null;
|
||||
};
|
||||
|
||||
/** Load a workflow `.esm.js` bundle and read its named exports (`run`, `descriptor`). */
|
||||
export async function extractBundleExports(
|
||||
bundlePath: string,
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { stringify } from "yaml";
|
||||
|
||||
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
||||
import type { WorkflowDescriptor } from "./types.js";
|
||||
|
||||
/** Serialize a validated workflow descriptor to YAML for storage next to the bundle. */
|
||||
export function stringifyWorkflowDescriptor(descriptor: WorkflowDescriptor): string {
|
||||
@@ -0,0 +1,15 @@
|
||||
export { buildDescriptor } from "./build-descriptor.js";
|
||||
export { importWorkflowBundleModule } from "./bundle-import-env.js";
|
||||
export { validateWorkflowBundle } from "./bundle-validator.js";
|
||||
export { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
||||
export { extractBundleExports } from "./extract-bundle-exports.js";
|
||||
export { stringifyWorkflowDescriptor } from "./generate-descriptor.js";
|
||||
export type {
|
||||
ExtractBundleExportsOptions,
|
||||
ExtractedBundleExports,
|
||||
WorkflowBundleValidationInput,
|
||||
WorkflowDescriptor,
|
||||
WorkflowRoleDescriptor,
|
||||
WorkflowRoleSchema,
|
||||
} from "./types.js";
|
||||
export { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { WorkflowFn } from "../types.js";
|
||||
|
||||
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
||||
export type WorkflowRoleSchema = Record<string, unknown>;
|
||||
|
||||
export type WorkflowRoleDescriptor = {
|
||||
description: string;
|
||||
schema: WorkflowRoleSchema;
|
||||
};
|
||||
|
||||
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
||||
export type WorkflowDescriptor = {
|
||||
description: string;
|
||||
roles: Record<string, WorkflowRoleDescriptor>;
|
||||
};
|
||||
|
||||
export type WorkflowBundleValidationInput = {
|
||||
/** Absolute or relative path (used for `.esm.js` suffix checks). */
|
||||
filePath: string;
|
||||
/** UTF-8 source of the bundle. */
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type ExtractedBundleExports = {
|
||||
run: WorkflowFn;
|
||||
descriptor: WorkflowDescriptor;
|
||||
};
|
||||
|
||||
export type ExtractBundleExportsOptions = {
|
||||
/** When set, ensures `node_modules/@uncaged/workflow` exists under this root before import. */
|
||||
storageRoot: string | null;
|
||||
};
|
||||
+2
-14
@@ -1,18 +1,6 @@
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
||||
export type WorkflowRoleSchema = Record<string, unknown>;
|
||||
|
||||
export type WorkflowRoleDescriptor = {
|
||||
description: string;
|
||||
schema: WorkflowRoleSchema;
|
||||
};
|
||||
|
||||
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
||||
export type WorkflowDescriptor = {
|
||||
description: string;
|
||||
roles: Record<string, WorkflowRoleDescriptor>;
|
||||
};
|
||||
import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js";
|
||||
|
||||
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
@@ -2,13 +2,7 @@ import { mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/pro
|
||||
import { join } from "node:path";
|
||||
|
||||
import { hashString } from "./hash.js";
|
||||
|
||||
export type CasStore = {
|
||||
put(content: string): Promise<string>;
|
||||
get(hash: string): Promise<string | null>;
|
||||
delete(hash: string): Promise<void>;
|
||||
list(): Promise<string[]>;
|
||||
};
|
||||
import type { CasStore } from "./types.js";
|
||||
|
||||
export function createCasStore(casDir: string): CasStore {
|
||||
async function ensureDir(): Promise<void> {
|
||||
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
|
||||
|
||||
import XXH from "xxhashjs";
|
||||
|
||||
import { encodeUint64AsCrockford } from "./base32.js";
|
||||
import { encodeUint64AsCrockford } from "../util/index.js";
|
||||
|
||||
function digestToUint64(digest: { toString(radix?: number): string }): bigint {
|
||||
const hex = digest.toString(16).padStart(16, "0");
|
||||
@@ -0,0 +1,18 @@
|
||||
export { createCasStore, createThreadCas } from "./cas.js";
|
||||
export { hashString, hashWorkflowBundleBytes } from "./hash.js";
|
||||
export {
|
||||
createContentMerkleNode,
|
||||
getContentMerklePayload,
|
||||
parseMerkleNode,
|
||||
putContentMerkleNode,
|
||||
putStepMerkleNode,
|
||||
putThreadMerkleNode,
|
||||
serializeMerkleNode,
|
||||
} from "./merkle.js";
|
||||
export type {
|
||||
CasStore,
|
||||
MerkleNode,
|
||||
MerkleNodeType,
|
||||
StepMerklePayload,
|
||||
ThreadMerklePayload,
|
||||
} from "./types.js";
|
||||
@@ -1,14 +1,6 @@
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
import type { CasStore } from "./cas.js";
|
||||
|
||||
export type MerkleNodeType = "content" | "step" | "thread";
|
||||
|
||||
export type MerkleNode = {
|
||||
type: MerkleNodeType;
|
||||
payload: string | Record<string, unknown>;
|
||||
children: string[];
|
||||
};
|
||||
import type { CasStore, MerkleNode, StepMerklePayload, ThreadMerklePayload } from "./types.js";
|
||||
|
||||
export function serializeMerkleNode(node: MerkleNode): string {
|
||||
return stringify(
|
||||
@@ -53,20 +45,6 @@ export function createContentMerkleNode(payload: string): MerkleNode {
|
||||
return { type: "content", payload, children: [] };
|
||||
}
|
||||
|
||||
export type StepMerklePayload = {
|
||||
role: string;
|
||||
meta: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ThreadMerklePayload = {
|
||||
workflow: string;
|
||||
threadId: string;
|
||||
result: {
|
||||
returnCode: number;
|
||||
summary: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Serializes a step Merkle node (role + meta + content child) and stores it in CAS. */
|
||||
export async function putStepMerkleNode(
|
||||
store: CasStore,
|
||||
@@ -0,0 +1,28 @@
|
||||
export type CasStore = {
|
||||
put(content: string): Promise<string>;
|
||||
get(hash: string): Promise<string | null>;
|
||||
delete(hash: string): Promise<void>;
|
||||
list(): Promise<string[]>;
|
||||
};
|
||||
|
||||
export type MerkleNodeType = "content" | "step" | "thread";
|
||||
|
||||
export type MerkleNode = {
|
||||
type: MerkleNodeType;
|
||||
payload: string | Record<string, unknown>;
|
||||
children: string[];
|
||||
};
|
||||
|
||||
export type StepMerklePayload = {
|
||||
role: string;
|
||||
meta: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ThreadMerklePayload = {
|
||||
workflow: string;
|
||||
threadId: string;
|
||||
result: {
|
||||
returnCode: number;
|
||||
summary: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { resolveModel } from "./resolve-model.js";
|
||||
export { splitProviderModelRef } from "./split-provider-model-ref.js";
|
||||
export type { ProviderConfig, ResolvedModel } from "./types.js";
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { WorkflowConfig } from "../registry/index.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
import { splitProviderModelRef } from "./split-provider-model-ref.js";
|
||||
import type { ResolvedModel } from "./types.js";
|
||||
|
||||
/** Resolves scene → provider endpoint + model using {@link WorkflowConfig.providers} and {@link WorkflowConfig.models}. */
|
||||
export function resolveModel(config: WorkflowConfig, scene: string): Result<ResolvedModel, string> {
|
||||
const models = config.models;
|
||||
let ref = models[scene] ?? null;
|
||||
if (ref === null) {
|
||||
ref = models.default ?? null;
|
||||
}
|
||||
if (ref === null) {
|
||||
return err(`no model mapping for scene "${scene}" and no models.default fallback`);
|
||||
}
|
||||
const split = splitProviderModelRef(ref);
|
||||
if (!split.ok) {
|
||||
return split;
|
||||
}
|
||||
const { providerName, modelName } = split.value;
|
||||
const provider = config.providers[providerName] ?? null;
|
||||
if (provider === null) {
|
||||
return err(`unknown provider "${providerName}" referenced by scene "${scene}"`);
|
||||
}
|
||||
return ok({
|
||||
baseUrl: provider.baseUrl,
|
||||
apiKey: provider.apiKey,
|
||||
model: modelName,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
/** Parses `providerName/modelName` references used in {@link WorkflowConfig.models}. */
|
||||
export function splitProviderModelRef(
|
||||
ref: string,
|
||||
): Result<{ providerName: string; modelName: string }, string> {
|
||||
const idx = ref.indexOf("/");
|
||||
if (idx <= 0 || idx === ref.length - 1) {
|
||||
return err(`invalid model reference "${ref}": expected providerName/modelName`);
|
||||
}
|
||||
const providerName = ref.slice(0, idx);
|
||||
const modelName = ref.slice(idx + 1);
|
||||
if (providerName === "" || modelName === "") {
|
||||
return err(`invalid model reference "${ref}": expected providerName/modelName`);
|
||||
}
|
||||
return ok({ providerName, modelName });
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type ProviderConfig = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type ResolvedModel = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
};
|
||||
+14
-23
@@ -1,14 +1,10 @@
|
||||
import type { CasStore } from "./cas.js";
|
||||
import { buildExtractUserContent, type ExtractFn } from "./extract-fn.js";
|
||||
import { putContentMerkleNode } from "./merkle.js";
|
||||
import { reactExtract } from "./react-extract.js";
|
||||
import { mergeRefsWithContentHash } from "./refs-field.js";
|
||||
import { putContentMerkleNode } from "../cas/index.js";
|
||||
import { buildExtractUserContent, reactExtract } from "../extract/index.js";
|
||||
import {
|
||||
type AgentBinding,
|
||||
type AgentContext,
|
||||
END,
|
||||
type ExtractContext,
|
||||
type LlmProvider,
|
||||
type ModeratorContext,
|
||||
type RoleDefinition,
|
||||
type RoleMeta,
|
||||
@@ -20,7 +16,8 @@ import {
|
||||
type WorkflowDefinition,
|
||||
type WorkflowFn,
|
||||
type WorkflowFnOptions,
|
||||
} from "./types.js";
|
||||
} from "../types.js";
|
||||
import { mergeRefsWithContentHash } from "../util/index.js";
|
||||
|
||||
function isRoleNext<M extends RoleMeta>(
|
||||
next: (keyof M & string) | typeof END,
|
||||
@@ -42,14 +39,12 @@ function resolveExtractedRefs(
|
||||
async function resolveRoleMeta<M extends RoleMeta>(
|
||||
roleDef: RoleDefinition<Record<string, unknown>>,
|
||||
extractCtx: ExtractContext<M>,
|
||||
extract: ExtractFn,
|
||||
llmProvider: LlmProvider | null,
|
||||
cas: CasStore,
|
||||
options: WorkflowFnOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (roleDef.extractMode === "react") {
|
||||
if (llmProvider === null) {
|
||||
if (options.llmProvider === null) {
|
||||
throw new Error(
|
||||
'createWorkflow: llmProvider is required when a role uses extractMode "react"',
|
||||
'createWorkflow: WorkflowFnOptions.llmProvider is required when a role uses extractMode "react"',
|
||||
);
|
||||
}
|
||||
const text = await buildExtractUserContent(
|
||||
@@ -59,15 +54,15 @@ async function resolveRoleMeta<M extends RoleMeta>(
|
||||
const reactResult = await reactExtract({
|
||||
text,
|
||||
schema: roleDef.schema,
|
||||
provider: llmProvider,
|
||||
cas,
|
||||
provider: options.llmProvider,
|
||||
cas: options.cas,
|
||||
});
|
||||
if (!reactResult.ok) {
|
||||
throw new Error(`react extract failed: ${reactResult.error}`);
|
||||
}
|
||||
return reactResult.value as Record<string, unknown>;
|
||||
}
|
||||
return (await extract(
|
||||
return (await options.extract(
|
||||
roleDef.schema,
|
||||
roleDef.extractPrompt,
|
||||
extractCtx as unknown as ExtractContext,
|
||||
@@ -75,15 +70,13 @@ async function resolveRoleMeta<M extends RoleMeta>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds pure role definitions + moderator to runtime agents and structured extraction.
|
||||
* Assign with `export const run = createWorkflow(def, binding, extract, llmProvider)`.
|
||||
* Pass the same {@link LlmProvider} as {@link createExtract} when any role uses `extractMode: "react"`.
|
||||
* Binds pure role definitions + moderator to runtime agents.
|
||||
* Assign with `export const run = createWorkflow(def, binding)`.
|
||||
* The engine supplies {@link WorkflowFnOptions.extract} and {@link WorkflowFnOptions.llmProvider} from workflow.yaml.
|
||||
*/
|
||||
export function createWorkflow<M extends RoleMeta>(
|
||||
def: Pick<WorkflowDefinition<M>, "roles" | "moderator">,
|
||||
binding: AgentBinding,
|
||||
extract: ExtractFn,
|
||||
llmProvider: LlmProvider | null,
|
||||
): WorkflowFn {
|
||||
return async function* workflowLoop(
|
||||
input: ThreadInput,
|
||||
@@ -150,9 +143,7 @@ export function createWorkflow<M extends RoleMeta>(
|
||||
const meta = await resolveRoleMeta(
|
||||
roleDef as unknown as RoleDefinition<Record<string, unknown>>,
|
||||
extractCtx,
|
||||
extract,
|
||||
llmProvider,
|
||||
options.cas,
|
||||
options,
|
||||
);
|
||||
|
||||
const contentHash = await putContentMerkleNode(options.cas, raw);
|
||||
@@ -1,50 +1,52 @@
|
||||
import { appendFile, mkdir } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import type { CasStore } from "./cas.js";
|
||||
import type { LogFn } from "./logger.js";
|
||||
import { getContentMerklePayload, putStepMerkleNode, putThreadMerkleNode } from "./merkle.js";
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import {
|
||||
type CasStore,
|
||||
getContentMerklePayload,
|
||||
putStepMerkleNode,
|
||||
putThreadMerkleNode,
|
||||
} from "../cas/index.js";
|
||||
import { resolveModel } from "../config/index.js";
|
||||
import { createExtract } from "../extract/index.js";
|
||||
import { readWorkflowRegistry } from "../registry/index.js";
|
||||
import type {
|
||||
LlmProvider,
|
||||
ThreadInput,
|
||||
WorkflowCompletion,
|
||||
WorkflowFn,
|
||||
WorkflowFnOptions,
|
||||
WorkflowResult,
|
||||
} from "./types.js";
|
||||
} from "../types.js";
|
||||
import { err, type LogFn, normalizeRefsField, ok, type Result } from "../util/index.js";
|
||||
|
||||
export type ExecuteThreadIo = {
|
||||
threadId: string;
|
||||
hash: string;
|
||||
dataJsonlPath: string;
|
||||
infoJsonlPath: string;
|
||||
cas: CasStore;
|
||||
};
|
||||
import type { ExecuteThreadIo, ExecuteThreadOptions } from "./types.js";
|
||||
|
||||
/** One persisted role line in `.data.jsonl` (engine adds these for fork replay before running the generator). */
|
||||
export type PrefilledDiskStep = {
|
||||
role: string;
|
||||
contentHash: string;
|
||||
meta: Record<string, unknown>;
|
||||
refs: string[];
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ExecuteThreadOptions = {
|
||||
maxRounds: number;
|
||||
/** Passed to the bundle as `WorkflowFnOptions.depth`. */
|
||||
depth: number;
|
||||
signal: AbortSignal;
|
||||
/** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */
|
||||
awaitAfterEachYield: () => Promise<void>;
|
||||
/** When non-null, written into the start record so tooling can trace lineage. */
|
||||
forkSourceThreadId: string | null;
|
||||
/**
|
||||
* Written to `.data.jsonl` immediately after the start record, before the generator runs.
|
||||
* Must match `input.steps` length and order when present.
|
||||
*/
|
||||
prefilledDiskSteps: PrefilledDiskStep[] | null;
|
||||
};
|
||||
async function resolveExtractRuntime(
|
||||
storageRoot: string,
|
||||
): Promise<
|
||||
Result<{ extract: ReturnType<typeof createExtract>; llmProvider: LlmProvider }, string>
|
||||
> {
|
||||
const reg = await readWorkflowRegistry(storageRoot);
|
||||
if (!reg.ok) {
|
||||
return err(reg.error.message);
|
||||
}
|
||||
const cfg = reg.value.config;
|
||||
if (cfg === null) {
|
||||
return err("workflow registry has no global config section");
|
||||
}
|
||||
const resolved = resolveModel(cfg, "extract");
|
||||
if (!resolved.ok) {
|
||||
return resolved;
|
||||
}
|
||||
const ex = resolved.value;
|
||||
const llmProvider: LlmProvider = {
|
||||
baseUrl: ex.baseUrl,
|
||||
apiKey: ex.apiKey,
|
||||
model: ex.model,
|
||||
};
|
||||
return ok({ extract: createExtract(llmProvider), llmProvider });
|
||||
}
|
||||
|
||||
async function appendDataLine(path: string, record: unknown): Promise<void> {
|
||||
const line = `${JSON.stringify(record)}\n`;
|
||||
@@ -278,11 +280,18 @@ export async function executeThread(
|
||||
});
|
||||
}
|
||||
|
||||
const extractRuntime = await resolveExtractRuntime(options.storageRoot);
|
||||
if (!extractRuntime.ok) {
|
||||
throw new Error(extractRuntime.error);
|
||||
}
|
||||
|
||||
const bundleOptions: WorkflowFnOptions = {
|
||||
threadId: io.threadId,
|
||||
maxRounds: options.maxRounds,
|
||||
depth: options.depth,
|
||||
cas: io.cas,
|
||||
extract: extractRuntime.value.extract,
|
||||
llmProvider: extractRuntime.value.llmProvider,
|
||||
};
|
||||
|
||||
return await driveWorkflowGenerator({
|
||||
@@ -1,18 +1,7 @@
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import type { RoleOutput, WorkflowCompletion } from "./types.js";
|
||||
import type { WorkflowCompletion } from "../types.js";
|
||||
import { err, normalizeRefsField, ok, type Result } from "../util/index.js";
|
||||
|
||||
/** Role steps replayed from `.data.jsonl`, including persisted timestamps. */
|
||||
export type ForkHistoricalStep = RoleOutput & { timestamp: number };
|
||||
|
||||
export type ParsedThreadStartRecord = {
|
||||
workflowName: string;
|
||||
hash: string;
|
||||
threadId: string;
|
||||
prompt: string;
|
||||
maxRounds: number;
|
||||
depth: number;
|
||||
};
|
||||
import type { ForkHistoricalStep, ForkPlan, ParsedThreadStartRecord } from "./types.js";
|
||||
|
||||
/** Recognizes a persisted workflow completion line (no `role`; has numeric `returnCode` and string `summary`). Omits `rootHash` when absent. */
|
||||
export function tryParseWorkflowResultRecord(
|
||||
@@ -228,15 +217,6 @@ export function selectForkHistoricalSteps(
|
||||
return ok(roleSteps.slice(0, idx + 1));
|
||||
}
|
||||
|
||||
export type ForkPlan = {
|
||||
workflowName: string;
|
||||
hash: string;
|
||||
sourceThreadId: string;
|
||||
prompt: string;
|
||||
runOptions: { maxRounds: number; depth: number };
|
||||
historicalSteps: ForkHistoricalStep[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Read `.data.jsonl` text and compute fork payload for the worker `run` command.
|
||||
*/
|
||||
@@ -1,17 +1,9 @@
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { type CasStore, createCasStore } from "./cas.js";
|
||||
import { type CasStore, createCasStore } from "../cas/index.js";
|
||||
import { err, getGlobalCasDir, ok, type Result } from "../util/index.js";
|
||||
import { parseThreadDataJsonl } from "./fork-thread.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { getGlobalCasDir } from "./storage-root.js";
|
||||
|
||||
export type GcResult = {
|
||||
scannedThreads: number;
|
||||
activeRefs: number;
|
||||
deletedEntries: number;
|
||||
deletedHashes: string[];
|
||||
};
|
||||
import type { GcResult } from "./types.js";
|
||||
|
||||
async function listThreadDataJsonlPaths(storageRoot: string): Promise<Result<string[], string>> {
|
||||
const logsRoot = join(storageRoot, "logs");
|
||||
@@ -0,0 +1,22 @@
|
||||
export { createWorkflow } from "./create-workflow.js";
|
||||
export { executeThread } from "./engine.js";
|
||||
export {
|
||||
buildForkPlan,
|
||||
parseThreadDataJsonl,
|
||||
selectForkHistoricalSteps,
|
||||
tryParseRoleStepRecord,
|
||||
tryParseWorkflowResultRecord,
|
||||
} from "./fork-thread.js";
|
||||
export { garbageCollectCas } from "./gc.js";
|
||||
export { createThreadPauseGate } from "./thread-pause-gate.js";
|
||||
export type {
|
||||
ExecuteThreadIo,
|
||||
ExecuteThreadOptions,
|
||||
ForkHistoricalStep,
|
||||
ForkPlan,
|
||||
GcResult,
|
||||
ParsedThreadStartRecord,
|
||||
PrefilledDiskStep,
|
||||
ThreadPauseGate,
|
||||
} from "./types.js";
|
||||
export { getWorkerHostScriptPath } from "./worker-entry-path.js";
|
||||
+2
-7
@@ -1,11 +1,6 @@
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
export type ThreadPauseGate = {
|
||||
awaitAfterYield: () => Promise<void>;
|
||||
pause: () => Result<void, string>;
|
||||
resume: () => Result<void, string>;
|
||||
isPaused: () => boolean;
|
||||
};
|
||||
import type { ThreadPauseGate } from "./types.js";
|
||||
|
||||
/**
|
||||
* Pause/resume gate for workflow threads: after each generator yield the engine awaits
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { CasStore } from "../cas/index.js";
|
||||
import type { RoleOutput } from "../types.js";
|
||||
import type { Result } from "../util/index.js";
|
||||
|
||||
export type ExecuteThreadIo = {
|
||||
threadId: string;
|
||||
hash: string;
|
||||
dataJsonlPath: string;
|
||||
infoJsonlPath: string;
|
||||
cas: CasStore;
|
||||
};
|
||||
|
||||
/** One persisted role line in `.data.jsonl` (engine adds these for fork replay before running the generator). */
|
||||
export type PrefilledDiskStep = {
|
||||
role: string;
|
||||
contentHash: string;
|
||||
meta: Record<string, unknown>;
|
||||
refs: string[];
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ExecuteThreadOptions = {
|
||||
maxRounds: number;
|
||||
/** Passed to the bundle as `WorkflowFnOptions.depth`. */
|
||||
depth: number;
|
||||
signal: AbortSignal;
|
||||
/** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */
|
||||
awaitAfterEachYield: () => Promise<void>;
|
||||
/** When non-null, written into the start record so tooling can trace lineage. */
|
||||
forkSourceThreadId: string | null;
|
||||
/**
|
||||
* Written to `.data.jsonl` immediately after the start record, before the generator runs.
|
||||
* Must match `input.steps` length and order when present.
|
||||
*/
|
||||
prefilledDiskSteps: PrefilledDiskStep[] | null;
|
||||
/** Workspace root containing `workflow.yaml`; used to resolve the `extract` scene for meta extraction. */
|
||||
storageRoot: string;
|
||||
};
|
||||
|
||||
/** Role steps replayed from `.data.jsonl`, including persisted timestamps. */
|
||||
export type ForkHistoricalStep = RoleOutput & { timestamp: number };
|
||||
|
||||
export type ParsedThreadStartRecord = {
|
||||
workflowName: string;
|
||||
hash: string;
|
||||
threadId: string;
|
||||
prompt: string;
|
||||
maxRounds: number;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
export type ForkPlan = {
|
||||
workflowName: string;
|
||||
hash: string;
|
||||
sourceThreadId: string;
|
||||
prompt: string;
|
||||
runOptions: { maxRounds: number; depth: number };
|
||||
historicalSteps: ForkHistoricalStep[];
|
||||
};
|
||||
|
||||
export type GcResult = {
|
||||
scannedThreads: number;
|
||||
activeRefs: number;
|
||||
deletedEntries: number;
|
||||
deletedHashes: string[];
|
||||
};
|
||||
|
||||
export type ThreadPauseGate = {
|
||||
awaitAfterYield: () => Promise<void>;
|
||||
pause: () => Result<void, string>;
|
||||
resume: () => Result<void, string>;
|
||||
isPaused: () => boolean;
|
||||
};
|
||||
@@ -1,17 +1,20 @@
|
||||
import { appendFile, mkdir, unlink, writeFile } from "node:fs/promises";
|
||||
import { createServer, type Socket } from "node:net";
|
||||
import { dirname, join } from "node:path";
|
||||
import { importWorkflowBundleModule } from "./bundle-import-env.js";
|
||||
import { createCasStore } from "./cas.js";
|
||||
import type { PrefilledDiskStep } from "./engine.js";
|
||||
import { type ExecuteThreadIo, executeThread } from "./engine.js";
|
||||
import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { getGlobalCasDir } from "./storage-root.js";
|
||||
import { createThreadPauseGate, type ThreadPauseGate } from "./thread-pause-gate.js";
|
||||
import type { RoleOutput, WorkflowFn, WorkflowResult } from "./types.js";
|
||||
import { ensureUncagedWorkflowSymlink, importWorkflowBundleModule } from "../bundle/index.js";
|
||||
import { createCasStore } from "../cas/index.js";
|
||||
import type { RoleOutput, WorkflowFn, WorkflowResult } from "../types.js";
|
||||
import {
|
||||
createLogger,
|
||||
err,
|
||||
getGlobalCasDir,
|
||||
normalizeRefsField,
|
||||
ok,
|
||||
type Result,
|
||||
} from "../util/index.js";
|
||||
import { executeThread } from "./engine.js";
|
||||
import { createThreadPauseGate } from "./thread-pause-gate.js";
|
||||
import type { ExecuteThreadIo, PrefilledDiskStep, ThreadPauseGate } from "./types.js";
|
||||
|
||||
const bootLog = createLogger({ sink: { kind: "stderr" } });
|
||||
|
||||
@@ -414,6 +417,7 @@ async function main(): Promise<void> {
|
||||
awaitAfterEachYield: () => pauseGate.awaitAfterYield(),
|
||||
forkSourceThreadId: cmd.forkSourceThreadId,
|
||||
prefilledDiskSteps,
|
||||
storageRoot,
|
||||
},
|
||||
io,
|
||||
logger,
|
||||
@@ -1,35 +0,0 @@
|
||||
import { readWorkflowRegistry } from "./registry.js";
|
||||
import type { WorkflowConfig } from "./registry-types.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { getDefaultWorkflowStorageRoot } from "./storage-root.js";
|
||||
import type { LlmProvider } from "./types.js";
|
||||
|
||||
const DEFAULT_WORKFLOW_AS_AGENT_MAX_DEPTH = 3;
|
||||
|
||||
export function getWorkflowAsAgentMaxDepth(config: WorkflowConfig | null): number {
|
||||
if (config === null) {
|
||||
return DEFAULT_WORKFLOW_AS_AGENT_MAX_DEPTH;
|
||||
}
|
||||
return config.maxDepth;
|
||||
}
|
||||
|
||||
/** Loads `config.extract` from workflow.yaml (apiKey already resolved at registry parse time). */
|
||||
export async function getExtractProvider(
|
||||
storageRoot: string | undefined,
|
||||
): Promise<Result<LlmProvider, string>> {
|
||||
const root = storageRoot ?? getDefaultWorkflowStorageRoot();
|
||||
const regResult = await readWorkflowRegistry(root);
|
||||
if (!regResult.ok) {
|
||||
return err(regResult.error.message);
|
||||
}
|
||||
const cfg = regResult.value.config;
|
||||
if (cfg === null) {
|
||||
return err("workflow registry has no global config section");
|
||||
}
|
||||
const ex = cfg.extract;
|
||||
return ok({
|
||||
baseUrl: ex.baseUrl,
|
||||
apiKey: ex.apiKey,
|
||||
model: ex.model,
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import type * as z from "zod/v4";
|
||||
|
||||
import { getContentMerklePayload } from "../cas/index.js";
|
||||
import type { ExtractContext, LlmProvider } from "../types.js";
|
||||
import { llmExtractWithRetry } from "./llm-extract.js";
|
||||
import { getContentMerklePayload } from "./merkle.js";
|
||||
import type { ExtractContext, LlmProvider } from "./types.js";
|
||||
|
||||
export type ExtractFn = <T extends Record<string, unknown>>(
|
||||
schema: z.ZodType<T>,
|
||||
prompt: string,
|
||||
ctx: ExtractContext,
|
||||
) => Promise<T>;
|
||||
import type { ExtractFn } from "./types.js";
|
||||
|
||||
/** Builds the user-side extraction prompt (thread + agent output + instruction). */
|
||||
export async function buildExtractUserContent(
|
||||
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
buildExtractUserContent,
|
||||
createExtract,
|
||||
} from "./extract-fn.js";
|
||||
export {
|
||||
extractFunctionToolFromZodSchema,
|
||||
llmErrorToCause,
|
||||
llmExtract,
|
||||
llmExtractWithRetry,
|
||||
} from "./llm-extract.js";
|
||||
export { reactExtract } from "./react-extract.js";
|
||||
export type {
|
||||
ExtractFn,
|
||||
LlmError,
|
||||
LlmExtractArgs,
|
||||
ReactExtractArgs,
|
||||
} from "./types.js";
|
||||
+2
-15
@@ -1,21 +1,8 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import type { LlmProvider } from "./types.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
export type LlmExtractArgs<T> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
provider: LlmProvider;
|
||||
};
|
||||
|
||||
export type LlmError =
|
||||
| { kind: "http_error"; status: number; body: string }
|
||||
| { kind: "invalid_response_json"; message: string }
|
||||
| { kind: "no_tool_call"; preview: string }
|
||||
| { kind: "tool_arguments_invalid_json"; message: string }
|
||||
| { kind: "schema_validation_failed"; message: string }
|
||||
| { kind: "network_error"; message: string };
|
||||
import type { LlmError, LlmExtractArgs } from "./types.js";
|
||||
|
||||
function chatCompletionsUrl(baseUrl: string): string {
|
||||
const trimmed = baseUrl.replace(/\/+$/, "");
|
||||
+5
-10
@@ -1,16 +1,11 @@
|
||||
import type * as z from "zod/v4";
|
||||
|
||||
import type { CasStore } from "./cas.js";
|
||||
import { extractFunctionToolFromZodSchema } from "./llm-extract.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import type { LlmProvider } from "./types.js";
|
||||
import type { CasStore } from "../cas/index.js";
|
||||
import type { LlmProvider } from "../types.js";
|
||||
import { err, ok, type Result } from "../util/index.js";
|
||||
|
||||
export type ReactExtractArgs<T extends Record<string, unknown>> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
provider: LlmProvider;
|
||||
cas: CasStore;
|
||||
};
|
||||
import { extractFunctionToolFromZodSchema } from "./llm-extract.js";
|
||||
import type { ReactExtractArgs } from "./types.js";
|
||||
|
||||
const MAX_REACT_ROUNDS = 10;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type * as z from "zod/v4";
|
||||
|
||||
import type { CasStore } from "../cas/index.js";
|
||||
import type { ExtractContext, LlmProvider } from "../types.js";
|
||||
|
||||
export type ExtractFn = <T extends Record<string, unknown>>(
|
||||
schema: z.ZodType<T>,
|
||||
prompt: string,
|
||||
ctx: ExtractContext,
|
||||
) => Promise<T>;
|
||||
|
||||
export type ReactExtractArgs<T extends Record<string, unknown>> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
provider: LlmProvider;
|
||||
cas: CasStore;
|
||||
};
|
||||
|
||||
export type LlmExtractArgs<T> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
provider: LlmProvider;
|
||||
};
|
||||
|
||||
export type LlmError =
|
||||
| { kind: "http_error"; status: number; body: string }
|
||||
| { kind: "invalid_response_json"; message: string }
|
||||
| { kind: "no_tool_call"; preview: string }
|
||||
| { kind: "tool_arguments_invalid_json"; message: string }
|
||||
| { kind: "schema_validation_failed"; message: string }
|
||||
| { kind: "network_error"; message: string };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user