Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 520b17b351 | |||
| 085cdcd3f4 | |||
| a8c1c158d6 | |||
| 83649fd836 | |||
| a5c09adae6 | |||
| 9e6cd9d615 |
@@ -48,7 +48,7 @@ uncaged-workflow run solve-issue --prompt "Fix bug #42"
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
uncaged-workflow help # Show all commands
|
||||
uncaged-workflow # Print full command usage (exits with status 1)
|
||||
uncaged-workflow workflow list # List registered workflows
|
||||
uncaged-workflow run <name> # Start a workflow thread
|
||||
uncaged-workflow thread list # List all threads
|
||||
@@ -56,7 +56,7 @@ uncaged-workflow thread show <id> # Inspect a thread
|
||||
uncaged-workflow skill # Agent-consumable reference docs
|
||||
```
|
||||
|
||||
See `uncaged-workflow help` for the full command reference.
|
||||
Run `uncaged-workflow` with no arguments to print usage, or `uncaged-workflow skill cli` for the full CLI skill reference.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# @uncaged/cli-workflow
|
||||
|
||||
Command-line interface for the Uncaged workflow engine (`uncaged-workflow`).
|
||||
|
||||
The CLI reads and writes the workflow registry, starts and inspects threads, manages CAS blobs, and prints agent-oriented reference docs via `skill`. It uses the same storage layout as `@uncaged/workflow` (default `~/.uncaged/workflow`).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/cli-workflow
|
||||
```
|
||||
|
||||
In this monorepo: `"@uncaged/cli-workflow": "workspace:*"`. Depends on `"@uncaged/workflow": "workspace:*"`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uncaged-workflow workflow list
|
||||
uncaged-workflow run <name> --prompt "Your task"
|
||||
uncaged-workflow thread show <id>
|
||||
uncaged-workflow skill
|
||||
```
|
||||
|
||||
Invoking the CLI with no command (or from this repo: `bun packages/cli-workflow/src/cli.ts`) prints:
|
||||
|
||||
```
|
||||
uncaged-workflow — workflow engine CLI
|
||||
|
||||
Workflow registry:
|
||||
workflow add <name> <file.esm.js> [--types <path>] Register a workflow bundle in the registry
|
||||
workflow list List all registered workflows
|
||||
workflow show <name> Show details of a registered workflow
|
||||
workflow rm <name> Remove a workflow from the registry
|
||||
workflow history <name> Show version history of a workflow
|
||||
workflow rollback <name> [hash] Rollback a workflow to a previous version
|
||||
|
||||
Thread execution:
|
||||
thread run <name> [--prompt <text>] [--max-rounds N] Start a new thread executing a workflow
|
||||
thread list [name] List threads, optionally filtered by workflow name
|
||||
thread show <id> Show thread details and state
|
||||
thread rm <id> Remove a thread
|
||||
thread fork <thread-id> [--from-role <role>] Fork a thread, optionally from a specific role
|
||||
thread ps List running threads
|
||||
thread kill <thread-id> Kill a running thread
|
||||
thread live <thread-id> | --latest [--debug] [--role <name>] Attach to a thread and stream output live
|
||||
thread pause <thread-id> Pause a running thread
|
||||
thread resume <thread-id> Resume a paused thread
|
||||
|
||||
Content-addressable storage:
|
||||
cas get <hash> Retrieve content by hash from CAS
|
||||
cas put <content> Store content in CAS, prints hash
|
||||
cas list List all hashes in CAS
|
||||
cas rm <hash> Remove a CAS entry by hash
|
||||
cas gc Garbage-collect unreferenced CAS entries
|
||||
|
||||
Development:
|
||||
init workspace <name> Initialize a new workflow workspace
|
||||
init template <name> Initialize a new workflow template
|
||||
|
||||
Shortcuts:
|
||||
run <name> [...] → thread run
|
||||
live <id> [...] → thread live
|
||||
|
||||
Reference:
|
||||
skill [topic] Agent-consumable docs (cli, develop, author)
|
||||
|
||||
Use <command> --help for subcommand details.
|
||||
|
||||
Environment variables:
|
||||
WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)
|
||||
UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
This package is bin-only; programmatic use is via `@uncaged/workflow`. Entry: `src/cli.ts` → `runCli` in `src/cli-dispatch.js`.
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -77,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 () => {
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||
import {
|
||||
formatSkillDoc,
|
||||
formatSkillIndex,
|
||||
formatSkillTopic,
|
||||
getSkillTopics,
|
||||
} from "../src/skill.js";
|
||||
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "../src/skill.js";
|
||||
|
||||
const STORAGE_ROOT = "/tmp/help-test-storage";
|
||||
|
||||
describe("help command", () => {
|
||||
test("help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("no args prints usage (not red) and returns 1", async () => {
|
||||
describe("runCli usage", () => {
|
||||
test("no args prints usage and returns 1", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, []);
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
@@ -70,13 +60,6 @@ describe("--help flag on groups", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy help --skill compat", () => {
|
||||
test("help --skill still works (lists topics)", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSkillTopics", () => {
|
||||
test("returns all topics", () => {
|
||||
const topics = getSkillTopics();
|
||||
@@ -128,8 +111,13 @@ describe("formatCliUsage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('cli') — legacy formatSkillDoc", () => {
|
||||
const doc = formatSkillDoc();
|
||||
const cliSkillDoc = formatSkillTopic("cli");
|
||||
if (cliSkillDoc === null) {
|
||||
throw new Error("BUG: cli skill topic missing");
|
||||
}
|
||||
|
||||
describe("formatSkillTopic('cli')", () => {
|
||||
const doc = cliSkillDoc;
|
||||
|
||||
test("contains title", () => {
|
||||
expect(doc).toContain("# uncaged-workflow CLI Reference");
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
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";
|
||||
`;
|
||||
@@ -142,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 () => {
|
||||
|
||||
@@ -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,29 +1,11 @@
|
||||
import type { CommandEntry, DispatchFn } from "./cli-command-types.js";
|
||||
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||
import { printCliError, printCliLine } from "./cli-output.js";
|
||||
import { getCommandRegistry } from "./cli-registry.js";
|
||||
import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js";
|
||||
import { createCasDispatcher, dispatchGc } from "./commands/cas/index.js";
|
||||
import { createCasDispatcher } from "./commands/cas/index.js";
|
||||
import { createInitDispatcher } from "./commands/init/index.js";
|
||||
import {
|
||||
createThreadDispatcher,
|
||||
dispatchFork,
|
||||
dispatchKill,
|
||||
dispatchLive,
|
||||
dispatchPause,
|
||||
dispatchPs,
|
||||
dispatchResume,
|
||||
dispatchRun,
|
||||
dispatchThreadList,
|
||||
} from "./commands/thread/index.js";
|
||||
import {
|
||||
createWorkflowDispatcher,
|
||||
dispatchAdd,
|
||||
dispatchHistory,
|
||||
dispatchList,
|
||||
dispatchRemove,
|
||||
dispatchRollback,
|
||||
dispatchShow,
|
||||
} from "./commands/workflow/index.js";
|
||||
import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js";
|
||||
import { createWorkflowDispatcher } from "./commands/workflow/index.js";
|
||||
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js";
|
||||
|
||||
export type { CommandEntry, CommandGroup, DispatchFn } from "./cli-command-types.js";
|
||||
@@ -54,15 +36,11 @@ function dispatchGroup(
|
||||
return entry.handler(storageRoot, argv.slice(1));
|
||||
}
|
||||
|
||||
function printDeprecation(oldCmd: string, newCmd: string): void {
|
||||
printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`);
|
||||
}
|
||||
|
||||
export function formatCliUsage(): string {
|
||||
return formatCliUsageWithGroups(getCommandRegistry(), getSkillTopics());
|
||||
}
|
||||
|
||||
const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup, printDeprecation });
|
||||
const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup });
|
||||
const dispatchThread = createThreadDispatcher({ dispatchGroup });
|
||||
const dispatchCas = createCasDispatcher({ dispatchGroup });
|
||||
const dispatchInit = createInitDispatcher({ dispatchGroup });
|
||||
@@ -85,43 +63,16 @@ async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<numb
|
||||
return showSkillDocOrIndex(argv[0]);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
printCliLine(formatCliUsage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
workflow: dispatchWorkflow,
|
||||
thread: dispatchThread,
|
||||
cas: dispatchCas,
|
||||
init: dispatchInit,
|
||||
help: dispatchHelp,
|
||||
skill: dispatchSkill,
|
||||
run: dispatchRun,
|
||||
live: dispatchLive,
|
||||
};
|
||||
|
||||
const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }> = {
|
||||
add: { newCmd: "workflow add", handler: dispatchAdd },
|
||||
list: { newCmd: "workflow list", handler: dispatchList },
|
||||
show: { newCmd: "workflow show", handler: dispatchShow },
|
||||
remove: { newCmd: "workflow rm", handler: dispatchRemove },
|
||||
ps: { newCmd: "thread ps", handler: dispatchPs },
|
||||
kill: { newCmd: "thread kill", handler: dispatchKill },
|
||||
pause: { newCmd: "thread pause", handler: dispatchPause },
|
||||
resume: { newCmd: "thread resume", handler: dispatchResume },
|
||||
threads: { newCmd: "thread list", handler: dispatchThreadList },
|
||||
fork: { newCmd: "thread fork", handler: dispatchFork },
|
||||
gc: { newCmd: "cas gc", handler: dispatchGc },
|
||||
history: { newCmd: "workflow history", handler: dispatchHistory },
|
||||
rollback: { newCmd: "workflow rollback", handler: dispatchRollback },
|
||||
};
|
||||
|
||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||
if (argv.length === 0) {
|
||||
printCliLine(formatCliUsage());
|
||||
@@ -139,12 +90,6 @@ export async function runCli(storageRoot: string, argv: string[]): Promise<numbe
|
||||
return dispatch(storageRoot, rest);
|
||||
}
|
||||
|
||||
const deprecated = DEPRECATED_ALIASES[command];
|
||||
if (deprecated !== undefined) {
|
||||
printDeprecation(command, deprecated.newCmd);
|
||||
return deprecated.handler(storageRoot, rest);
|
||||
}
|
||||
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown command ${command}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -107,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. 编码规范
|
||||
|
||||
@@ -142,7 +142,7 @@ export const WORKFLOW_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
};
|
||||
|
||||
export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) {
|
||||
const { dispatchGroup, printDeprecation } = deps;
|
||||
const { dispatchGroup } = deps;
|
||||
return async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
if (result !== null) {
|
||||
@@ -150,7 +150,6 @@ export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) {
|
||||
}
|
||||
const sub = argv[0];
|
||||
if (sub === "remove") {
|
||||
printDeprecation("workflow remove", "workflow rm");
|
||||
return dispatchRemove(storageRoot, argv.slice(1));
|
||||
}
|
||||
printCliError(`${usageText()}\n\nerror: unknown workflow subcommand: ${sub}`);
|
||||
|
||||
@@ -14,5 +14,4 @@ export type CmdAddSuccess = {
|
||||
|
||||
export type WorkflowDispatchDeps = {
|
||||
dispatchGroup: DispatchGroupFn;
|
||||
printDeprecation: (oldCmd: string, newCmd: string) => void;
|
||||
};
|
||||
|
||||
@@ -229,10 +229,3 @@ uncaged-workflow live --latest
|
||||
Bundles are immutable and identified by XXH64 hash. Re-registering a workflow with a new bundle creates a new version. Use \`workflow history\` and \`workflow rollback\` to manage versions.
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Legacy compat ──────────────────────────────────────────────────────
|
||||
|
||||
/** @deprecated Use formatSkillTopic("cli") instead */
|
||||
export function formatSkillDoc(): string {
|
||||
return formatSkillCli();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# @uncaged/workflow-agent-cursor
|
||||
|
||||
`AgentFn` adapter that runs the `cursor-agent` CLI against a workspace path derived from the thread.
|
||||
|
||||
The agent builds a full prompt (system + task + step history via `@uncaged/workflow-util-agent`), extracts the absolute workspace path with your `extract` + Zod schema, then spawns `cursor-agent` with `--workspace`, model, and non-interactive flags.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-agent-cursor @uncaged/workflow @uncaged/workflow-util-agent zod
|
||||
```
|
||||
|
||||
In this monorepo: `"@uncaged/workflow-agent-cursor": "workspace:*"` plus `workspace:*` for `@uncaged/workflow` and `@uncaged/workflow-util-agent`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
||||
|
||||
const agent = createCursorAgent({
|
||||
model: null, // null → "auto"
|
||||
timeout: 0, // ms; 0 = no limit (spawnCli timeout disabled)
|
||||
extract: myExtractFn,
|
||||
});
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createCursorAgent(config)` | Returns `AgentFn` that runs `cursor-agent` with `buildAgentPrompt(ctx)` |
|
||||
| `CursorAgentConfig` | `model`, `timeout`, `extract` (must supply workspace path) |
|
||||
| `validateCursorAgentConfig` | Config validation result |
|
||||
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
|
||||
|
||||
Requires `cursor-agent` on `PATH` at runtime.
|
||||
@@ -0,0 +1,35 @@
|
||||
# @uncaged/workflow-agent-hermes
|
||||
|
||||
`AgentFn` adapter that runs the `hermes` CLI in non-interactive `chat` mode (Nerve-style flags: `-q`, `--yolo`, `--quiet`, bounded `--max-turns`).
|
||||
|
||||
The agent composes the same thread-aware prompt as other CLI-backed agents via `buildAgentPrompt` from `@uncaged/workflow-util-agent`, then spawns `hermes` and returns stdout on success.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-agent-hermes @uncaged/workflow @uncaged/workflow-util-agent
|
||||
```
|
||||
|
||||
In this monorepo: use `workspace:*` for all three `@uncaged/*` packages.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
||||
|
||||
const agent = createHermesAgent({
|
||||
model: "your-model", // or null to omit --model
|
||||
timeout: 600_000, // ms, or null for no timeout
|
||||
});
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createHermesAgent(config)` | Returns `AgentFn` wrapping `hermes chat -q ...` |
|
||||
| `HermesAgentConfig` | `model`, `timeout` |
|
||||
| `validateHermesAgentConfig` | Config validation result |
|
||||
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
|
||||
|
||||
Requires `hermes` on `PATH` at runtime.
|
||||
@@ -0,0 +1,34 @@
|
||||
# @uncaged/workflow-agent-llm
|
||||
|
||||
`AgentFn` adapter that calls an OpenAI-compatible `POST /chat/completions` endpoint using `@uncaged/workflow`’s `LlmProvider` (base URL, API key, model).
|
||||
|
||||
Single-turn: system text is the current role’s `systemPrompt`, user text is the thread’s initial prompt (`ctx.start.content`). Errors from HTTP, JSON, or empty choices are thrown as `Error` with a JSON payload string.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-agent-llm @uncaged/workflow
|
||||
```
|
||||
|
||||
In this monorepo: `"@uncaged/workflow-agent-llm": "workspace:*"`, `"@uncaged/workflow": "workspace:*"`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createLlmAdapter } from "@uncaged/workflow-agent-llm";
|
||||
|
||||
const agent = createLlmAdapter({
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: "gpt-4.1-mini",
|
||||
});
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createLlmAdapter(provider)` | `LlmProvider` → `AgentFn` |
|
||||
| `chatCompletionText({ provider, messages })` | Low-level `Result<string, LlmChatError>` helper |
|
||||
| `LlmMessage` | `{ role: "system" \| "user" \| "assistant"; content: string }` |
|
||||
| `LlmChatError` | Discriminated error kinds for failed completions |
|
||||
@@ -0,0 +1,53 @@
|
||||
# @uncaged/workflow-template-develop
|
||||
|
||||
Reference **develop** workflow template: plan phases, implement in a loop, review, test, then commit.
|
||||
|
||||
Export a `WorkflowDefinition` and `createDevelopRun` so a host can bind agents/LLM and run the same graph the bundled `.esm.js` would use. Use `buildDevelopDescriptor()` when assembling `descriptor` metadata for a bundle.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-template-develop @uncaged/workflow zod
|
||||
```
|
||||
|
||||
In this monorepo: `workspace:*` for `@uncaged/workflow-template-develop` and `@uncaged/workflow`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createDevelopRun, developWorkflowDefinition } from "@uncaged/workflow-template-develop";
|
||||
|
||||
const run = createDevelopRun(binding, extract, llmProvider);
|
||||
// run(...) executes the develop moderator graph with your AgentBinding
|
||||
```
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Purpose |
|
||||
|------|---------|
|
||||
| **planner** | Break work into ordered phases (hashes) |
|
||||
| **coder** | Implement current phase; repeats until phases complete or limits hit |
|
||||
| **reviewer** | Code review gate (`approved` vs send back to coder) |
|
||||
| **tester** | Verify via tests/build/lint (`passed` vs send back to coder) |
|
||||
| **committer** | Final commit step |
|
||||
|
||||
Also exported: role factories/meta schemas (`plannerRole`, `coderRole`, …), `DevelopMeta`, `developRoles`.
|
||||
|
||||
## Moderator flow
|
||||
|
||||
1. **Start** → `planner`
|
||||
2. After **planner** → `coder`
|
||||
3. After **coder** → if all planned phases are done (or last phase completed) → `reviewer`; else `coder` again, until `maxRounds` then `END`
|
||||
4. After **reviewer** → if approved → `tester`; else `coder` (or `END` if out of rounds)
|
||||
5. After **tester** → if passed → `committer`; else `coder` (or `END` if out of rounds)
|
||||
6. After **committer** → `END`
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createDevelopRun` | `createWorkflow(developWorkflowDefinition, …)` factory |
|
||||
| `developWorkflowDefinition` | `description`, `roles`, `developModerator` |
|
||||
| `developModerator` | `Moderator<DevelopMeta>` |
|
||||
| `buildDevelopDescriptor` | `buildDescriptor({ … })` for bundle metadata |
|
||||
| `DEVELOP_WORKFLOW_DESCRIPTION` | Human-readable one-liner |
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and m
|
||||
|
||||
## Reading phase details
|
||||
|
||||
Each planner phase has a content-hash and title. Read full details with \`uncaged-workflow cas get <THREAD_ID> <HASH>\`.
|
||||
Each planner phase has a content-hash and title. Read full details with \`uncaged-workflow cas get <HASH>\`.
|
||||
|
||||
The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and m
|
||||
|
||||
## Storing phase details — MANDATORY
|
||||
|
||||
For each phase, store its full detail text in CAS via \`uncaged-workflow cas put <THREAD_ID> '<content>'\`. The command prints a content-hash — use that as the phase identifier.
|
||||
For each phase, store its full detail text in CAS via \`uncaged-workflow cas put '<content>'\`. The command prints a content-hash — use that as the phase identifier.
|
||||
|
||||
The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# @uncaged/workflow-template-solve-issue
|
||||
|
||||
Reference **solve-issue** workflow template: prepare a repo, delegate implementation to the **develop** workflow, then submit (e.g. open a PR).
|
||||
|
||||
`createSolveIssueRun` wires the `developer` role to `workflowAsAgent("develop")` by default; `binding.overrides.developer` wins if you pass one (for tests or custom hosts).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-template-solve-issue @uncaged/workflow zod
|
||||
```
|
||||
|
||||
In this monorepo: `workspace:*` for this package and `@uncaged/workflow`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createSolveIssueRun, solveIssueWorkflowDefinition } from "@uncaged/workflow-template-solve-issue";
|
||||
|
||||
const run = createSolveIssueRun(binding, extract, llmProvider);
|
||||
```
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Purpose |
|
||||
|------|---------|
|
||||
| **preparer** | Set up context / repo state for the issue |
|
||||
| **developer** | Implementation; default runs the registered `develop` workflow as a sub-agent |
|
||||
| **submitter** | Finalize and submit the outcome (e.g. PR) |
|
||||
|
||||
Also exported: `preparerRole`, `developerRole`, `submitterRole` and their Zod meta schemas, `SolveIssueMeta`, `solveIssueRoles`.
|
||||
|
||||
## Moderator flow
|
||||
|
||||
1. **Start** → `preparer`
|
||||
2. After **preparer** → `developer`
|
||||
3. After **developer** → `submitter`
|
||||
4. After **submitter** → `END`
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createSolveIssueRun` | Merges `developer` override with `workflowAsAgent("develop")`, then `createWorkflow` |
|
||||
| `solveIssueWorkflowDefinition` | `description`, `roles`, `solveIssueModerator` |
|
||||
| `solveIssueModerator` | Linear `Moderator<SolveIssueMeta>` |
|
||||
| `buildSolveIssueDescriptor` | Descriptor helper for bundles |
|
||||
| `SOLVE_ISSUE_WORKFLOW_DESCRIPTION` | Human-readable one-liner |
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# @uncaged/workflow-util-agent
|
||||
|
||||
Shared helpers for CLI-backed workflow agents: assemble prompts from thread context and spawn subprocesses with timeouts.
|
||||
|
||||
Used by `@uncaged/workflow-agent-cursor` and `@uncaged/workflow-agent-hermes`. Depends on `@uncaged/workflow` for CAS reads (`getContentMerklePayload`) and `Result` typing.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow-util-agent @uncaged/workflow
|
||||
```
|
||||
|
||||
In this monorepo: `workspace:*` for both packages.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { buildAgentPrompt, spawnCli } from "@uncaged/workflow-util-agent";
|
||||
|
||||
const prompt = await buildAgentPrompt(agentContext);
|
||||
const result = await spawnCli("my-cli", ["--json"], { cwd: "/tmp", timeoutMs: 60_000 });
|
||||
if (!result.ok) { /* handle SpawnCliError */ }
|
||||
const stdout = result.value;
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `buildAgentPrompt(ctx)` | System prompt + task + prior step summaries + latest body from CAS; appends `uncaged-workflow thread <id>` tool hint |
|
||||
| `spawnCli(cmd, args, { cwd, timeoutMs })` | `Promise<Result<string, SpawnCliError>>`; captures stdout, non-zero exit and spawn failures as `err` |
|
||||
| `SpawnCliConfig` | `cwd: string \| null`, `timeoutMs: number \| null` |
|
||||
| `SpawnCliError` | `non_zero_exit` \| `timeout` \| `spawn_failed` |
|
||||
| `SpawnCliResult` | Alias for `Result<string, SpawnCliError>` |
|
||||
@@ -0,0 +1,36 @@
|
||||
# @uncaged/workflow
|
||||
|
||||
Core workflow engine: registry, CAS, thread execution, bundle validation, and role/workflow types.
|
||||
|
||||
This package implements the three-phase engine loop that runs single-file ESM workflow bundles (each exports `run` and `descriptor`). It persists threads under `~/.uncaged/workflow/` by default and hashes bundles with XXH64 (Crockford Base32). See the repo root [README](../../README.md) for workflow, bundle, thread, role, and registry concepts.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @uncaged/workflow zod
|
||||
```
|
||||
|
||||
In this monorepo, depend with `"@uncaged/workflow": "workspace:*"`. `zod` is a peer dependency (used by bundle/shape validation at the integration boundary).
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createWorkflow, readWorkflowRegistry, executeThread } from "@uncaged/workflow";
|
||||
// Wire a WorkflowDefinition + AgentBinding + extract + optional LlmProvider into createWorkflow,
|
||||
// then run the returned WorkflowFn inside your host (or use executeThread for disk-backed runs).
|
||||
```
|
||||
|
||||
## API overview
|
||||
|
||||
| Area | Exports (representative) |
|
||||
|------|--------------------------|
|
||||
| **Types** | `WorkflowDefinition`, `WorkflowFn`, `AgentFn`, `AgentBinding`, `Moderator`, `RoleDefinition`, `ThreadContext`, `LlmProvider`, `Result` shape via `ok` / `err`, `START` / `END` |
|
||||
| **Bundle** | `buildDescriptor`, `extractBundleExports`, `validateWorkflowBundle`, `validateWorkflowDescriptor`, `WorkflowDescriptor`, `WorkflowRoleDescriptor` |
|
||||
| **Registry** | `readWorkflowRegistry`, `writeWorkflowRegistry`, `registerWorkflowVersion`, `workflowRegistryPath`, YAML helpers |
|
||||
| **CAS** | `createCasStore`, Merkle helpers (`putStepMerkleNode`, `getContentMerklePayload`, …), `hashWorkflowBundleBytes` |
|
||||
| **Engine** | `createWorkflow`, `executeThread`, `parseThreadDataJsonl`, fork helpers, `garbageCollectCas` |
|
||||
| **Extract / LLM tools** | `llmExtract`, `reactExtract`, `createExtract`, `getExtractProvider` |
|
||||
| **Agent bridge** | `workflowAsAgent` — expose a registered workflow as an agent-backed role |
|
||||
| **Utilities** | `createLogger`, ULID / Crockford Base32 codecs, `getDefaultWorkflowStorageRoot`, paths |
|
||||
|
||||
Full surface is re-exported from `src/index.ts`.
|
||||
@@ -3,15 +3,9 @@ import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createCasStore, createThreadCas } from "../src/cas/cas.js";
|
||||
import { createCasStore } from "../src/cas/cas.js";
|
||||
import { hashString } from "../src/cas/hash.js";
|
||||
|
||||
describe("cas module exports", () => {
|
||||
test("createThreadCas is a deprecated alias of createCasStore", () => {
|
||||
expect(createThreadCas).toBe(createCasStore);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createCasStore", () => {
|
||||
let casDir: string;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
} from "../src/cas/merkle.js";
|
||||
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||
import { executeThread } from "../src/engine/engine.js";
|
||||
import { createExtract } from "../src/extract/extract-fn.js";
|
||||
import { END, type LlmProvider } from "../src/types.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
|
||||
const plannerMetaSchema = z.object({
|
||||
@@ -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,93 +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.models.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
|
||||
providers:
|
||||
dashscope:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
apiKey: literal-key
|
||||
models:
|
||||
default: dashscope/qwen-turbo
|
||||
extract: dashscope/qwen-plus
|
||||
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
|
||||
providers:
|
||||
p:
|
||||
baseUrl: https://example.com
|
||||
apiKey: env:WF_GET_EXTRACT_PROVIDER_KEY
|
||||
models:
|
||||
default: p/other-model
|
||||
extract: p/m
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
@@ -8,7 +8,6 @@ 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 { createExtract } from "../src/extract/extract-fn.js";
|
||||
import { END } from "../src/types.js";
|
||||
import { createLogger } from "../src/util/logger.js";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,17 @@ 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";
|
||||
|
||||
export const descriptor = {
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 { createExtract } from "../src/extract/extract-fn.js";
|
||||
import {
|
||||
readWorkflowRegistry,
|
||||
registerWorkflowVersion,
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -62,6 +62,3 @@ export function createCasStore(casDir: string): CasStore {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link createCasStore} — CAS is global, not per-thread. */
|
||||
export const createThreadCas = createCasStore;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { createCasStore, createThreadCas } from "./cas.js";
|
||||
export { createCasStore } from "./cas.js";
|
||||
export { hashString, hashWorkflowBundleBytes } from "./hash.js";
|
||||
export {
|
||||
createContentMerkleNode,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { CasStore } from "../cas/index.js";
|
||||
import { putContentMerkleNode } from "../cas/index.js";
|
||||
import { buildExtractUserContent, type ExtractFn, reactExtract } from "../extract/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,
|
||||
@@ -41,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(
|
||||
@@ -58,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,
|
||||
@@ -74,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,
|
||||
@@ -149,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);
|
||||
|
||||
@@ -7,17 +7,47 @@ import {
|
||||
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";
|
||||
import { type LogFn, normalizeRefsField } from "../util/index.js";
|
||||
import { err, type LogFn, normalizeRefsField, ok, type Result } from "../util/index.js";
|
||||
|
||||
import type { ExecuteThreadIo, ExecuteThreadOptions } from "./types.js";
|
||||
|
||||
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`;
|
||||
await appendFile(path, line, "utf8");
|
||||
@@ -250,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({
|
||||
|
||||
@@ -33,6 +33,8 @@ export type ExecuteThreadOptions = {
|
||||
* 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. */
|
||||
|
||||
@@ -417,6 +417,7 @@ async function main(): Promise<void> {
|
||||
awaitAfterEachYield: () => pauseGate.awaitAfterYield(),
|
||||
forkSourceThreadId: cmd.forkSourceThreadId,
|
||||
prefilledDiskSteps,
|
||||
storageRoot,
|
||||
},
|
||||
io,
|
||||
logger,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { resolveModel } from "./config/index.js";
|
||||
import type { WorkflowConfig } from "./registry/index.js";
|
||||
import { readWorkflowRegistry } from "./registry/index.js";
|
||||
import type { LlmProvider } from "./types.js";
|
||||
import { err, getDefaultWorkflowStorageRoot, ok, type Result } from "./util/index.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 the LLM provider for scene `extract` from workflow.yaml (`config.models` + `config.providers`; apiKey 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 resolved = resolveModel(cfg, "extract");
|
||||
if (!resolved.ok) {
|
||||
return resolved;
|
||||
}
|
||||
const ex = resolved.value;
|
||||
return ok({
|
||||
baseUrl: ex.baseUrl,
|
||||
apiKey: ex.apiKey,
|
||||
model: ex.model,
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
type CasStore,
|
||||
createCasStore,
|
||||
createContentMerkleNode,
|
||||
createThreadCas,
|
||||
getContentMerklePayload,
|
||||
hashString,
|
||||
hashWorkflowBundleBytes,
|
||||
@@ -63,7 +62,6 @@ export {
|
||||
type ReactExtractArgs,
|
||||
reactExtract,
|
||||
} from "./extract/index.js";
|
||||
export { getExtractProvider } from "./extract-provider.js";
|
||||
export {
|
||||
getRegisteredWorkflow,
|
||||
listRegisteredWorkflowNames,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as z from "zod/v4";
|
||||
|
||||
import type { CasStore } from "./cas/index.js";
|
||||
import type { ExtractFn } from "./extract/types.js";
|
||||
|
||||
/** Sentinel values for automaton control flow. */
|
||||
export const START = "__start__" as const;
|
||||
@@ -54,6 +55,10 @@ export type WorkflowFnOptions = {
|
||||
depth: number;
|
||||
/** Global CAS store for Merkle content blobs (role step bodies). */
|
||||
cas: CasStore;
|
||||
/** Structured meta extraction; resolved from workflow.yaml `extract` scene by the engine. */
|
||||
extract: ExtractFn;
|
||||
/** Provider for `extractMode: "react"` roles; same backing config as `extract`. */
|
||||
llmProvider: LlmProvider | null;
|
||||
};
|
||||
|
||||
/** Bundle contract — named export `run` is a function returning an AsyncGenerator. */
|
||||
|
||||
@@ -4,7 +4,7 @@ import { extractBundleExports } from "./bundle/index.js";
|
||||
import { createCasStore } from "./cas/index.js";
|
||||
import type { ExecuteThreadIo } from "./engine/index.js";
|
||||
import { executeThread } from "./engine/index.js";
|
||||
import { getWorkflowAsAgentMaxDepth } from "./extract-provider.js";
|
||||
import type { WorkflowConfig } from "./registry/index.js";
|
||||
import { getRegisteredWorkflow, readWorkflowRegistry } from "./registry/index.js";
|
||||
import type { AgentContext, AgentFn, ThreadInput } from "./types.js";
|
||||
import {
|
||||
@@ -14,6 +14,15 @@ import {
|
||||
getGlobalCasDir,
|
||||
} from "./util/index.js";
|
||||
|
||||
const DEFAULT_WORKFLOW_AS_AGENT_MAX_DEPTH = 3;
|
||||
|
||||
function workflowAsAgentMaxDepth(config: WorkflowConfig | null): number {
|
||||
if (config === null) {
|
||||
return DEFAULT_WORKFLOW_AS_AGENT_MAX_DEPTH;
|
||||
}
|
||||
return config.maxDepth;
|
||||
}
|
||||
|
||||
export type WorkflowAsAgentOptions = {
|
||||
/** When `null`, uses `getDefaultWorkflowStorageRoot()`. */
|
||||
storageRoot: string | null;
|
||||
@@ -44,7 +53,7 @@ export function workflowAsAgent(
|
||||
return `ERROR: failed to read workflow registry: ${registryResult.error.message}`;
|
||||
}
|
||||
|
||||
const maxDepth = getWorkflowAsAgentMaxDepth(registryResult.value.config);
|
||||
const maxDepth = workflowAsAgentMaxDepth(registryResult.value.config);
|
||||
if (nextDepth > maxDepth) {
|
||||
return `ERROR: workflow-as-agent depth limit exceeded (max ${maxDepth})`;
|
||||
}
|
||||
@@ -92,6 +101,7 @@ export function workflowAsAgent(
|
||||
awaitAfterEachYield: async () => {},
|
||||
forkSourceThreadId: ctx.threadId,
|
||||
prefilledDiskSteps: null,
|
||||
storageRoot,
|
||||
},
|
||||
io,
|
||||
logger,
|
||||
|
||||
Reference in New Issue
Block a user