refactor!: remove deprecated Agent types, introduce Adapter-first API
BREAKING CHANGES:
- Remove AgentFn, AgentFnResult, AgentBinding from workflow-protocol
- Remove wrapAgentAsAdapter from workflow-util-agent
- workflowAsAgent → workflowAdapter (old name kept as deprecated re-export)
New APIs:
- createTextAdapter(producer) — bridges text-producing functions to AdapterFn
- TextProducerFn, TextAdapterResult types
- workflowAdapter() — direct AdapterFn for child workflow delegation
All agent packages (cursor, hermes, llm) now return AdapterFn directly,
no wrapping needed. Bundle entries simplified accordingly.
小橘 🍊(NEKO Team)
This commit is contained in:
@@ -91,7 +91,7 @@ describe("init workspace", () => {
|
|||||||
"RoleDefinition",
|
"RoleDefinition",
|
||||||
"WorkflowDefinition",
|
"WorkflowDefinition",
|
||||||
"ModeratorTable",
|
"ModeratorTable",
|
||||||
"AgentFn",
|
"AdapterFn",
|
||||||
"ExtractFn",
|
"ExtractFn",
|
||||||
"RoleMeta",
|
"RoleMeta",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function agentsMd(): string {
|
|||||||
|------|----------------|------|
|
|------|----------------|------|
|
||||||
| **Workspace** | 仓库根(\`package.json\` 含 \`workspaces: ["templates/*", "workflows"]\`) | Bun monorepo:统一管理本地模板包与 workflow 实例 |
|
| **Workspace** | 仓库根(\`package.json\` 含 \`workspaces: ["templates/*", "workflows"]\`) | Bun monorepo:统一管理本地模板包与 workflow 实例 |
|
||||||
| **Template** | \`templates/<name>/\`(如 \`src/roles.ts\`、\`src/moderator.ts\`、\`src/index.ts\`) | 纯数据:**WorkflowDefinition**(各 **RoleDefinition** + **ModeratorTable**),**不绑定**具体 Agent |
|
| **Template** | \`templates/<name>/\`(如 \`src/roles.ts\`、\`src/moderator.ts\`、\`src/index.ts\`) | 纯数据:**WorkflowDefinition**(各 **RoleDefinition** + **ModeratorTable**),**不绑定**具体 Agent |
|
||||||
| **Workflow instance** | \`workflows/\`(或单独包) | 把模板与运行时 **AgentFn** / **ExtractFn** 组合,产出可注册的 **单文件 ESM bundle**(\`run\` + \`descriptor\` 命名导出) |
|
| **Workflow instance** | \`workflows/\`(或单独包) | 把模板与运行时 **AdapterFn** / **ExtractFn** 组合,产出可注册的 **单文件 ESM bundle**(\`run\` + \`descriptor\` 命名导出) |
|
||||||
|
|
||||||
Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下放绑定与打包入口。
|
Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下放绑定与打包入口。
|
||||||
|
|
||||||
@@ -100,10 +100,10 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下
|
|||||||
- **RoleDefinition<Meta>**:纯数据——\`description\`、\`systemPrompt\`、\`schema\`(Zod v4)。不含执行逻辑。
|
- **RoleDefinition<Meta>**:纯数据——\`description\`、\`systemPrompt\`、\`schema\`(Zod v4)。不含执行逻辑。
|
||||||
- **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **ModeratorTable**(声明式路由表)。
|
- **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **ModeratorTable**(声明式路由表)。
|
||||||
- **ModeratorTable**:从 \`START\` 与各角色名映射到有序 transition 列表(条件 + 下一角色或 \`END\`);可序列化,供描述符提取 **graph**。
|
- **ModeratorTable**:从 \`START\` 与各角色名映射到有序 transition 列表(条件 + 下一角色或 \`END\`);可序列化,供描述符提取 **graph**。
|
||||||
- **AgentFn**:\`(ctx: AgentContext) => Promise<string>\`,原始文本输出;从上下文读取当前角色的 \`systemPrompt\`。
|
- **AdapterFn**:接收系统提示词与 Zod schema,返回角色执行函数(RoleFn)。
|
||||||
- **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Agent 都可使用)。
|
- **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Adapter 都可使用)。
|
||||||
|
|
||||||
引擎循环简述:按 **ModeratorTable** 选下一角色 → **Agent** 产出文本 → **Extract** 写入 **meta** → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。
|
引擎循环简述:按 **ModeratorTable** 选下一角色 → **Adapter** 产出 typed meta → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。
|
||||||
|
|
||||||
## 3. 开发流程
|
## 3. 开发流程
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下
|
|||||||
2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\`。
|
2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\`。
|
||||||
3. **编写 ModeratorTable**:为 \`START\` 与各角色声明 transition(\`FALLBACK\` 或命名条件 + \`check\`)。
|
3. **编写 ModeratorTable**:为 \`START\` 与各角色声明 transition(\`FALLBACK\` 或命名条件 + \`check\`)。
|
||||||
4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / table 导出)。
|
4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / table 导出)。
|
||||||
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`。
|
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AdapterFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`。
|
||||||
6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。
|
6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。
|
||||||
|
|
||||||
## 4. 编码规范
|
## 4. 编码规范
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe("validateCursorAgentConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createCursorAgent", () => {
|
describe("createCursorAgent", () => {
|
||||||
test("returns an AgentFn with explicit workspace", () => {
|
test("returns an AdapterFn with explicit workspace", () => {
|
||||||
const agent = createCursorAgent({
|
const agent = createCursorAgent({
|
||||||
command: "/usr/local/bin/cursor-agent",
|
command: "/usr/local/bin/cursor-agent",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -90,7 +90,7 @@ describe("createCursorAgent", () => {
|
|||||||
expect(typeof agent).toBe("function");
|
expect(typeof agent).toBe("function");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns an AgentFn with null workspace and llmProvider", () => {
|
test("returns an AdapterFn with null workspace and llmProvider", () => {
|
||||||
const agent = createCursorAgent({
|
const agent = createCursorAgent({
|
||||||
command: "/usr/local/bin/cursor-agent",
|
command: "/usr/local/bin/cursor-agent",
|
||||||
model: null,
|
model: null,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { AgentFn } from "@uncaged/workflow-runtime";
|
import type { AdapterFn } from "@uncaged/workflow-runtime";
|
||||||
import { createLogger } from "@uncaged/workflow-util";
|
import { createLogger } from "@uncaged/workflow-util";
|
||||||
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
import {
|
||||||
|
buildThreadInput,
|
||||||
|
createTextAdapter,
|
||||||
|
type SpawnCliError,
|
||||||
|
spawnCli,
|
||||||
|
} from "@uncaged/workflow-util-agent";
|
||||||
|
|
||||||
import { extractWorkspacePath } from "./extract-workspace.js";
|
import { extractWorkspacePath } from "./extract-workspace.js";
|
||||||
import type { CursorAgentConfig } from "./types.js";
|
import type { CursorAgentConfig } from "./types.js";
|
||||||
@@ -29,12 +34,12 @@ function resolveCursorModel(model: string | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Runs `cursor-agent` with workspace from config or extracted from context via LLM. */
|
/** Runs `cursor-agent` with workspace from config or extracted from context via LLM. */
|
||||||
export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
export function createCursorAgent(config: CursorAgentConfig): AdapterFn {
|
||||||
const modelFlag = resolveCursorModel(config.model);
|
const modelFlag = resolveCursorModel(config.model);
|
||||||
const timeoutMs = config.timeout > 0 ? config.timeout : null;
|
const timeoutMs = config.timeout > 0 ? config.timeout : null;
|
||||||
const logger = createLogger({ sink: { kind: "stderr" } });
|
const logger = createLogger({ sink: { kind: "stderr" } });
|
||||||
|
|
||||||
return async (ctx) => {
|
return createTextAdapter(async (ctx, prompt) => {
|
||||||
const validated = validateCursorAgentConfig(config);
|
const validated = validateCursorAgentConfig(config);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
throw new Error(validated.error);
|
throw new Error(validated.error);
|
||||||
@@ -48,7 +53,8 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
|||||||
if (config.llmProvider === null) {
|
if (config.llmProvider === null) {
|
||||||
throw new Error("cursor-agent: llmProvider is required when workspace is null");
|
throw new Error("cursor-agent: llmProvider is required when workspace is null");
|
||||||
}
|
}
|
||||||
const extracted = await extractWorkspacePath(ctx, config.llmProvider, logger);
|
const agentCtx = { ...ctx, currentRole: { name: "cursor", systemPrompt: prompt } };
|
||||||
|
const extracted = await extractWorkspacePath(agentCtx, config.llmProvider, logger);
|
||||||
if (extracted === null) {
|
if (extracted === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"cursor-agent: failed to extract workspace path from context. Provide an explicit workspace or ensure previous steps include a repoPath.",
|
"cursor-agent: failed to extract workspace path from context. Provide an explicit workspace or ensure previous steps include a repoPath.",
|
||||||
@@ -58,7 +64,8 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger("R5HN3YKQ", `cursor-agent workspace: ${workspace}`);
|
logger("R5HN3YKQ", `cursor-agent workspace: ${workspace}`);
|
||||||
const fullPrompt = await buildAgentPrompt(ctx);
|
const threadInput = await buildThreadInput(ctx);
|
||||||
|
const fullPrompt = `${prompt}\n\n${threadInput}`;
|
||||||
const args = [
|
const args = [
|
||||||
"-p",
|
"-p",
|
||||||
fullPrompt,
|
fullPrompt,
|
||||||
@@ -79,5 +86,5 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
|||||||
throwCursorSpawnError(run.error);
|
throwCursorSpawnError(run.error);
|
||||||
}
|
}
|
||||||
return run.value;
|
return run.value;
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe("validateHermesAgentConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createHermesAgent", () => {
|
describe("createHermesAgent", () => {
|
||||||
test("returns an AgentFn even with invalid config (validation deferred to call)", () => {
|
test("returns an AdapterFn even with invalid config (validation deferred to call)", () => {
|
||||||
const agent = createHermesAgent({
|
const agent = createHermesAgent({
|
||||||
command: "/usr/local/bin/hermes",
|
command: "/usr/local/bin/hermes",
|
||||||
model: null,
|
model: null,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { AgentFn } from "@uncaged/workflow-runtime";
|
import type { AdapterFn } from "@uncaged/workflow-runtime";
|
||||||
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
import {
|
||||||
|
buildThreadInput,
|
||||||
|
createTextAdapter,
|
||||||
|
type SpawnCliError,
|
||||||
|
spawnCli,
|
||||||
|
} from "@uncaged/workflow-util-agent";
|
||||||
|
|
||||||
import type { HermesAgentConfig } from "./types.js";
|
import type { HermesAgentConfig } from "./types.js";
|
||||||
import { validateHermesAgentConfig } from "./validate-config.js";
|
import { validateHermesAgentConfig } from "./validate-config.js";
|
||||||
@@ -25,16 +30,17 @@ function throwHermesSpawnError(error: SpawnCliError): never {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Runs `hermes chat` non-interactively with the Nerve-style argv contract (`-q`, `--yolo`, `--quiet`). */
|
/** Runs `hermes chat` non-interactively with the Nerve-style argv contract (`-q`, `--yolo`, `--quiet`). */
|
||||||
export function createHermesAgent(config: HermesAgentConfig): AgentFn {
|
export function createHermesAgent(config: HermesAgentConfig): AdapterFn {
|
||||||
const timeoutMs = config.timeout;
|
const timeoutMs = config.timeout;
|
||||||
|
|
||||||
return async (ctx) => {
|
return createTextAdapter(async (ctx, prompt) => {
|
||||||
const validated = validateHermesAgentConfig(config);
|
const validated = validateHermesAgentConfig(config);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
throw new Error(validated.error);
|
throw new Error(validated.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrompt = await buildAgentPrompt(ctx);
|
const threadInput = await buildThreadInput(ctx);
|
||||||
|
const fullPrompt = `${prompt}\n\n${threadInput}`;
|
||||||
const args = [
|
const args = [
|
||||||
"chat",
|
"chat",
|
||||||
"-q",
|
"-q",
|
||||||
@@ -55,5 +61,5 @@ export function createHermesAgent(config: HermesAgentConfig): AgentFn {
|
|||||||
throwHermesSpawnError(run.error);
|
throwHermesSpawnError(run.error);
|
||||||
}
|
}
|
||||||
return run.value;
|
return run.value;
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type AgentContext, START } from "@uncaged/workflow-runtime";
|
import {
|
||||||
|
type CasStore,
|
||||||
|
type ExtractFn,
|
||||||
|
START,
|
||||||
|
type ThreadContext,
|
||||||
|
type WorkflowRuntime,
|
||||||
|
} from "@uncaged/workflow-runtime";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
import { createLlmAdapter } from "../src/create-llm-adapter.js";
|
import { createLlmAdapter } from "../src/create-llm-adapter.js";
|
||||||
|
|
||||||
function makeCtx(userContent: string): AgentContext {
|
function makeCtx(userContent: string): ThreadContext {
|
||||||
return {
|
return {
|
||||||
start: {
|
start: {
|
||||||
role: START,
|
role: START,
|
||||||
@@ -16,14 +23,34 @@ function makeCtx(userContent: string): AgentContext {
|
|||||||
bundleHash: "TESTHASH00001",
|
bundleHash: "TESTHASH00001",
|
||||||
steps: [],
|
steps: [],
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: "planner", systemPrompt: "system instructions" },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testSchema = z.object({ summary: z.string() });
|
||||||
|
|
||||||
|
function makeRuntime(): WorkflowRuntime {
|
||||||
|
let stored = "";
|
||||||
|
const cas: CasStore = {
|
||||||
|
put: async (content: string) => {
|
||||||
|
stored = content;
|
||||||
|
return "HASH001";
|
||||||
|
},
|
||||||
|
get: async () => stored,
|
||||||
|
delete: async () => {},
|
||||||
|
list: async () => [],
|
||||||
|
};
|
||||||
|
const extract: ExtractFn = async (_schema, _contentHash) => ({
|
||||||
|
meta: { summary: "extracted" },
|
||||||
|
contentPayload: stored,
|
||||||
|
refs: [],
|
||||||
|
});
|
||||||
|
return { cas, extract };
|
||||||
|
}
|
||||||
|
|
||||||
describe("createLlmAdapter", () => {
|
describe("createLlmAdapter", () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
test("posts system + user (start.content) and returns assistant text", async () => {
|
test("posts system + user (start.content) and returns typed meta with childThread: null", async () => {
|
||||||
globalThis.fetch = (() =>
|
globalThis.fetch = (() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
new Response(JSON.stringify({ choices: [{ message: { content: "model reply" } }] }), {
|
new Response(JSON.stringify({ choices: [{ message: { content: "model reply" } }] }), {
|
||||||
@@ -34,11 +61,13 @@ describe("createLlmAdapter", () => {
|
|||||||
|
|
||||||
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
||||||
const adapter = createLlmAdapter(provider);
|
const adapter = createLlmAdapter(provider);
|
||||||
const out = await adapter(makeCtx("trigger text"));
|
const roleFn = adapter("system instructions", testSchema);
|
||||||
|
const result = await roleFn(makeCtx("trigger text"), makeRuntime());
|
||||||
|
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
|
|
||||||
expect(out).toBe("model reply");
|
expect(result.meta).toEqual({ summary: "extracted" });
|
||||||
|
expect(result.childThread).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws on non-ok fetch response", async () => {
|
test("throws on non-ok fetch response", async () => {
|
||||||
@@ -52,8 +81,9 @@ describe("createLlmAdapter", () => {
|
|||||||
|
|
||||||
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
||||||
const adapter = createLlmAdapter(provider);
|
const adapter = createLlmAdapter(provider);
|
||||||
|
const roleFn = adapter("system", testSchema);
|
||||||
|
|
||||||
await expect(adapter(makeCtx("hi"))).rejects.toThrow("llm:");
|
await expect(roleFn(makeCtx("hi"), makeRuntime())).rejects.toThrow("llm:");
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,8 +92,9 @@ describe("createLlmAdapter", () => {
|
|||||||
|
|
||||||
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
|
||||||
const adapter = createLlmAdapter(provider);
|
const adapter = createLlmAdapter(provider);
|
||||||
|
const roleFn = adapter("system", testSchema);
|
||||||
|
|
||||||
await expect(adapter(makeCtx("hi"))).rejects.toThrow();
|
await expect(roleFn(makeCtx("hi"), makeRuntime())).rejects.toThrow();
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/workflow-runtime": "workspace:*"
|
"@uncaged/workflow-runtime": "workspace:*",
|
||||||
|
"@uncaged/workflow-util-agent": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import { type AdapterFn, err, type LlmProvider, ok, type Result } from "@uncaged/workflow-runtime";
|
||||||
type AgentContext,
|
import { createTextAdapter } from "@uncaged/workflow-util-agent";
|
||||||
type AgentFn,
|
|
||||||
err,
|
|
||||||
type LlmProvider,
|
|
||||||
ok,
|
|
||||||
type Result,
|
|
||||||
} from "@uncaged/workflow-runtime";
|
|
||||||
|
|
||||||
/** OpenAI chat completion message shape (passed to `/chat/completions`). */
|
/** OpenAI chat completion message shape (passed to `/chat/completions`). */
|
||||||
export type LlmMessage = { role: "system" | "user" | "assistant"; content: string };
|
export type LlmMessage = { role: "system" | "user" | "assistant"; content: string };
|
||||||
@@ -97,13 +91,13 @@ export async function chatCompletionText(options: {
|
|||||||
return parseAssistantText(res.value);
|
return parseAssistantText(res.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single-turn chat adapter: system prompt comes from {@link AgentContext.currentRole}. */
|
/** Single-turn chat adapter: system prompt is passed by the workflow engine. */
|
||||||
export function createLlmAdapter(provider: LlmProvider): AgentFn {
|
export function createLlmAdapter(provider: LlmProvider): AdapterFn {
|
||||||
return async (ctx: AgentContext) => {
|
return createTextAdapter(async (ctx, prompt) => {
|
||||||
const result = await chatCompletionText({
|
const result = await chatCompletionText({
|
||||||
provider,
|
provider,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: ctx.currentRole.systemPrompt },
|
{ role: "system", content: prompt },
|
||||||
{ role: "user", content: ctx.start.content },
|
{ role: "user", content: ctx.start.content },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -111,5 +105,5 @@ export function createLlmAdapter(provider: LlmProvider): AgentFn {
|
|||||||
throw new Error(`llm: ${formatLlmChatError(result.error)}`);
|
throw new Error(`llm: ${formatLlmChatError(result.error)}`);
|
||||||
}
|
}
|
||||||
return result.value;
|
return result.value;
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"composite": true
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"references": [{ "path": "../workflow-runtime" }]
|
"references": [{ "path": "../workflow-runtime" }, { "path": "../workflow-util-agent" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,7 @@ export {
|
|||||||
llmErrorToCause,
|
llmErrorToCause,
|
||||||
llmExtract,
|
llmExtract,
|
||||||
} from "./extract/index.js";
|
} from "./extract/index.js";
|
||||||
|
export { type WorkflowAdapterOptions, workflowAdapter } from "./workflow-adapter.js";
|
||||||
|
|
||||||
|
/** @deprecated Use {@link workflowAdapter} instead. */
|
||||||
export { type WorkflowAsAgentOptions, workflowAsAgent } from "./workflow-as-agent.js";
|
export { type WorkflowAsAgentOptions, workflowAsAgent } from "./workflow-as-agent.js";
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import { createCasStore, putContentNodeWithRefs } from "@uncaged/workflow-cas";
|
||||||
|
import type { WorkflowConfig } from "@uncaged/workflow-register";
|
||||||
|
import {
|
||||||
|
extractBundleExports,
|
||||||
|
getRegisteredWorkflow,
|
||||||
|
readWorkflowRegistry,
|
||||||
|
} from "@uncaged/workflow-register";
|
||||||
|
import type {
|
||||||
|
AdapterFn,
|
||||||
|
RoleResult,
|
||||||
|
ThreadContext,
|
||||||
|
WorkflowFn,
|
||||||
|
WorkflowRuntime,
|
||||||
|
} from "@uncaged/workflow-runtime";
|
||||||
|
import {
|
||||||
|
createLogger,
|
||||||
|
generateUlid,
|
||||||
|
getDefaultWorkflowStorageRoot,
|
||||||
|
getGlobalCasDir,
|
||||||
|
} from "@uncaged/workflow-util";
|
||||||
|
import type * as z from "zod/v4";
|
||||||
|
import type { ExecuteThreadIo } from "./engine/index.js";
|
||||||
|
import { executeThread, getBundleDir, readThreadsIndex } from "./engine/index.js";
|
||||||
|
|
||||||
|
const DEFAULT_WORKFLOW_ADAPTER_MAX_DEPTH = 3;
|
||||||
|
|
||||||
|
function workflowAdapterMaxDepth(config: WorkflowConfig | null): number {
|
||||||
|
return config === null ? DEFAULT_WORKFLOW_ADAPTER_MAX_DEPTH : config.maxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowAdapterOptions = {
|
||||||
|
/** When `null`, uses `getDefaultWorkflowStorageRoot()`. */
|
||||||
|
storageRoot: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveStorageRoot(options: WorkflowAdapterOptions | null): string {
|
||||||
|
if (options !== null && options.storageRoot !== null) {
|
||||||
|
return options.storageRoot;
|
||||||
|
}
|
||||||
|
return getDefaultWorkflowStorageRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readParentHeadState(
|
||||||
|
storageRoot: string,
|
||||||
|
ctx: ThreadContext,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const bundleDir = getBundleDir(storageRoot, ctx.bundleHash);
|
||||||
|
const index = await readThreadsIndex(bundleDir);
|
||||||
|
const entry = index[ctx.threadId] ?? null;
|
||||||
|
return entry !== null ? entry.head : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the workflow bundle and validate depth limits. */
|
||||||
|
async function resolveWorkflowBundle(workflowName: string, storageRoot: string, nextDepth: number) {
|
||||||
|
const registryResult = await readWorkflowRegistry(storageRoot);
|
||||||
|
if (!registryResult.ok) {
|
||||||
|
throw new Error(`failed to read workflow registry: ${registryResult.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDepth = workflowAdapterMaxDepth(registryResult.value.config);
|
||||||
|
if (nextDepth > maxDepth) {
|
||||||
|
throw new Error(`workflow adapter depth limit exceeded (max ${maxDepth})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = getRegisteredWorkflow(registryResult.value, workflowName);
|
||||||
|
if (entry === null) {
|
||||||
|
throw new Error(`workflow "${workflowName}" not found in registry`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundlePath = join(storageRoot, "bundles", `${entry.hash}.esm.js`);
|
||||||
|
const bundleExportsResult = await extractBundleExports(bundlePath, { storageRoot });
|
||||||
|
if (!bundleExportsResult.ok) {
|
||||||
|
throw new Error(String(bundleExportsResult.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entry, run: bundleExportsResult.value.run };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute the child workflow thread and return a summary + root hash. */
|
||||||
|
async function runChildThread(params: {
|
||||||
|
workflowName: string;
|
||||||
|
storageRoot: string;
|
||||||
|
ctx: ThreadContext;
|
||||||
|
run: WorkflowFn;
|
||||||
|
bundleHash: string;
|
||||||
|
nextDepth: number;
|
||||||
|
}) {
|
||||||
|
const { workflowName, storageRoot, ctx, run, bundleHash, nextDepth } = params;
|
||||||
|
const childThreadId = generateUlid(Date.now());
|
||||||
|
const infoJsonlPath = join(storageRoot, "logs", bundleHash, `${childThreadId}.info.jsonl`);
|
||||||
|
|
||||||
|
const io: ExecuteThreadIo = {
|
||||||
|
threadId: childThreadId,
|
||||||
|
hash: bundleHash,
|
||||||
|
infoJsonlPath,
|
||||||
|
cas: createCasStore(getGlobalCasDir(storageRoot)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = createLogger({ sink: { kind: "file", path: infoJsonlPath } });
|
||||||
|
const parentHeadState = await readParentHeadState(storageRoot, ctx);
|
||||||
|
|
||||||
|
const result = await executeThread(
|
||||||
|
run,
|
||||||
|
workflowName,
|
||||||
|
{ prompt: ctx.start.content, steps: [] },
|
||||||
|
{
|
||||||
|
depth: nextDepth,
|
||||||
|
parentStateHash: parentHeadState,
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
awaitAfterEachYield: async () => {},
|
||||||
|
forkSourceThreadId: ctx.threadId,
|
||||||
|
prefilledDiskSteps: null,
|
||||||
|
forkContinuation: null,
|
||||||
|
replayTimestamps: null,
|
||||||
|
storageRoot,
|
||||||
|
},
|
||||||
|
io,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: `Child workflow "${workflowName}" completed (returnCode=${result.returnCode}).\n\nSummary: ${result.summary}\n\nChild thread root hash: ${result.rootHash}`,
|
||||||
|
rootHash: result.rootHash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link AdapterFn} that runs another registered workflow in a new child thread,
|
||||||
|
* using the parent thread's initial prompt (`ctx.start.content`) as the child prompt.
|
||||||
|
*
|
||||||
|
* The child thread's root hash is returned as `childThread` in the result,
|
||||||
|
* enabling parent→child tracking in the CAS Merkle tree.
|
||||||
|
*/
|
||||||
|
export function workflowAdapter(
|
||||||
|
workflowName: string,
|
||||||
|
options: WorkflowAdapterOptions | null = null,
|
||||||
|
): AdapterFn {
|
||||||
|
return <T>(_prompt: string, schema: z.ZodType<T>) => {
|
||||||
|
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const storageRoot = resolveStorageRoot(options);
|
||||||
|
const { entry, run } = await resolveWorkflowBundle(workflowName, storageRoot, ctx.depth + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { summary, rootHash } = await runChildThread({
|
||||||
|
workflowName,
|
||||||
|
storageRoot,
|
||||||
|
ctx,
|
||||||
|
run,
|
||||||
|
bundleHash: entry.hash,
|
||||||
|
nextDepth: ctx.depth + 1,
|
||||||
|
});
|
||||||
|
const contentHash = await putContentNodeWithRefs(runtime.cas, summary, []);
|
||||||
|
const extracted = await runtime.extract(
|
||||||
|
schema as z.ZodType<Record<string, unknown>>,
|
||||||
|
contentHash,
|
||||||
|
);
|
||||||
|
return { meta: extracted.meta as T, childThread: rootHash };
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
throw new Error(`child workflow "${workflowName}" failed: ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,136 +1,8 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
import { createCasStore } from "@uncaged/workflow-cas";
|
|
||||||
import type { WorkflowConfig } from "@uncaged/workflow-register";
|
|
||||||
import {
|
|
||||||
extractBundleExports,
|
|
||||||
getRegisteredWorkflow,
|
|
||||||
readWorkflowRegistry,
|
|
||||||
} from "@uncaged/workflow-register";
|
|
||||||
import type { AgentContext, AgentFn, AgentFnResult } from "@uncaged/workflow-runtime";
|
|
||||||
import {
|
|
||||||
createLogger,
|
|
||||||
generateUlid,
|
|
||||||
getDefaultWorkflowStorageRoot,
|
|
||||||
getGlobalCasDir,
|
|
||||||
} from "@uncaged/workflow-util";
|
|
||||||
import type { ExecuteThreadIo } from "./engine/index.js";
|
|
||||||
import { executeThread, getBundleDir, readThreadsIndex } from "./engine/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;
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveWorkflowAsAgentStorageRoot(options: WorkflowAsAgentOptions | null): string {
|
|
||||||
if (options !== null && options.storageRoot !== null) {
|
|
||||||
return options.storageRoot;
|
|
||||||
}
|
|
||||||
return getDefaultWorkflowStorageRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readParentHeadState(storageRoot: string, ctx: AgentContext): Promise<string | null> {
|
|
||||||
const bundleDir = getBundleDir(storageRoot, ctx.bundleHash);
|
|
||||||
const index = await readThreadsIndex(bundleDir);
|
|
||||||
const entry = index[ctx.threadId] ?? null;
|
|
||||||
return entry !== null ? entry.head : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an {@link AgentFn} that runs another registered workflow in a new thread,
|
* @deprecated Use `workflowAdapter` from `./workflow-adapter.js` instead.
|
||||||
* using the parent thread's initial prompt (`ctx.start.content`) as the child prompt.
|
* This module is kept for backward compatibility and will be removed in a future release.
|
||||||
*/
|
*/
|
||||||
export function workflowAsAgent(
|
export {
|
||||||
workflowName: string,
|
type WorkflowAdapterOptions as WorkflowAsAgentOptions,
|
||||||
options: WorkflowAsAgentOptions | null = null,
|
workflowAdapter as workflowAsAgent,
|
||||||
): AgentFn {
|
} from "./workflow-adapter.js";
|
||||||
return async (ctx: AgentContext): Promise<AgentFnResult> => {
|
|
||||||
const nextDepth = ctx.depth + 1;
|
|
||||||
|
|
||||||
const storageRoot = resolveWorkflowAsAgentStorageRoot(options);
|
|
||||||
|
|
||||||
const registryResult = await readWorkflowRegistry(storageRoot);
|
|
||||||
if (!registryResult.ok) {
|
|
||||||
return {
|
|
||||||
output: `ERROR: failed to read workflow registry: ${registryResult.error.message}`,
|
|
||||||
childThread: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxDepth = workflowAsAgentMaxDepth(registryResult.value.config);
|
|
||||||
if (nextDepth > maxDepth) {
|
|
||||||
return {
|
|
||||||
output: `ERROR: workflow-as-agent depth limit exceeded (max ${maxDepth})`,
|
|
||||||
childThread: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = getRegisteredWorkflow(registryResult.value, workflowName);
|
|
||||||
if (entry === null) {
|
|
||||||
return {
|
|
||||||
output: `ERROR: workflow "${workflowName}" not found in registry`,
|
|
||||||
childThread: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const bundlePath = join(storageRoot, "bundles", `${entry.hash}.esm.js`);
|
|
||||||
const bundleExportsResult = await extractBundleExports(bundlePath, { storageRoot });
|
|
||||||
if (!bundleExportsResult.ok) {
|
|
||||||
return { output: `ERROR: ${bundleExportsResult.error}`, childThread: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = {
|
|
||||||
prompt: ctx.start.content,
|
|
||||||
steps: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const childThreadId = generateUlid(Date.now());
|
|
||||||
const infoJsonlPath = join(storageRoot, "logs", entry.hash, `${childThreadId}.info.jsonl`);
|
|
||||||
|
|
||||||
const io: ExecuteThreadIo = {
|
|
||||||
threadId: childThreadId,
|
|
||||||
hash: entry.hash,
|
|
||||||
infoJsonlPath,
|
|
||||||
cas: createCasStore(getGlobalCasDir(storageRoot)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const logger = createLogger({ sink: { kind: "file", path: infoJsonlPath } });
|
|
||||||
const signalNever = new AbortController();
|
|
||||||
|
|
||||||
const parentHeadState = await readParentHeadState(storageRoot, ctx);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await executeThread(
|
|
||||||
bundleExportsResult.value.run,
|
|
||||||
workflowName,
|
|
||||||
input,
|
|
||||||
{
|
|
||||||
depth: nextDepth,
|
|
||||||
parentStateHash: parentHeadState,
|
|
||||||
signal: signalNever.signal,
|
|
||||||
awaitAfterEachYield: async () => {},
|
|
||||||
forkSourceThreadId: ctx.threadId,
|
|
||||||
prefilledDiskSteps: null,
|
|
||||||
forkContinuation: null,
|
|
||||||
replayTimestamps: null,
|
|
||||||
storageRoot,
|
|
||||||
},
|
|
||||||
io,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
const summary = `Child workflow "${workflowName}" completed (returnCode=${result.returnCode}).\n\nSummary: ${result.summary}\n\nChild thread root hash: ${result.rootHash}`;
|
|
||||||
return { output: summary, childThread: result.rootHash };
|
|
||||||
} catch (e) {
|
|
||||||
const message = e instanceof Error ? e.message : String(e);
|
|
||||||
return { output: `ERROR: ${message}`, childThread: null };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ export type {
|
|||||||
AdapterBinding,
|
AdapterBinding,
|
||||||
AdapterFn,
|
AdapterFn,
|
||||||
AdvanceOutcome,
|
AdvanceOutcome,
|
||||||
AgentBinding,
|
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentFn,
|
|
||||||
AgentFnResult,
|
|
||||||
CasStore,
|
CasStore,
|
||||||
ExtractFn,
|
ExtractFn,
|
||||||
ExtractResult,
|
ExtractResult,
|
||||||
|
|||||||
@@ -143,18 +143,6 @@ export type ExtractFn = <T extends Record<string, unknown>>(
|
|||||||
contentHash: string,
|
contentHash: string,
|
||||||
) => Promise<ExtractResult<T>>;
|
) => Promise<ExtractResult<T>>;
|
||||||
|
|
||||||
/** @deprecated Use {@link AdapterFn} instead. Will be removed in a future release. */
|
|
||||||
export type AgentFnResult = string | { output: string; childThread: string | null };
|
|
||||||
|
|
||||||
/** @deprecated Use {@link AdapterFn} instead. Will be removed in a future release. */
|
|
||||||
export type AgentFn = (ctx: AgentContext) => Promise<AgentFnResult>;
|
|
||||||
|
|
||||||
/** @deprecated Use {@link AdapterBinding} instead. Will be removed in a future release. */
|
|
||||||
export type AgentBinding = {
|
|
||||||
agent: AgentFn;
|
|
||||||
overrides: Partial<Record<string, AgentFn>> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Adapter (replaces Agent) ────────────────────────────────────────
|
// ── Adapter (replaces Agent) ────────────────────────────────────────
|
||||||
|
|
||||||
export type RoleResult<T> = { meta: T; childThread: string | null };
|
export type RoleResult<T> = { meta: T; childThread: string | null };
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ export { err, ok } from "./result.js";
|
|||||||
export type {
|
export type {
|
||||||
AdapterBinding,
|
AdapterBinding,
|
||||||
AdapterFn,
|
AdapterFn,
|
||||||
AgentBinding,
|
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentFn,
|
|
||||||
AgentFnResult,
|
|
||||||
CasStore,
|
CasStore,
|
||||||
ExtractFn,
|
ExtractFn,
|
||||||
ExtractResult,
|
ExtractResult,
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ export type {
|
|||||||
AdapterBinding,
|
AdapterBinding,
|
||||||
AdapterFn,
|
AdapterFn,
|
||||||
AdvanceOutcome,
|
AdvanceOutcome,
|
||||||
AgentBinding,
|
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentFn,
|
|
||||||
AgentFnResult,
|
|
||||||
CasStore,
|
CasStore,
|
||||||
ExtractFn,
|
ExtractFn,
|
||||||
ExtractResult,
|
ExtractResult,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
||||||
import { createWorkflow } from "@uncaged/workflow-runtime";
|
import { createWorkflow } from "@uncaged/workflow-runtime";
|
||||||
import { optionalEnv, requireEnv } from "@uncaged/workflow-util";
|
import { optionalEnv, requireEnv } from "@uncaged/workflow-util";
|
||||||
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
|
||||||
import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js";
|
import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js";
|
||||||
|
|
||||||
const llmProvider = {
|
const llmProvider = {
|
||||||
@@ -18,7 +17,7 @@ const llmProvider = {
|
|||||||
model: optionalEnv("WORKFLOW_LLM_MODEL", "qwen-plus"),
|
model: optionalEnv("WORKFLOW_LLM_MODEL", "qwen-plus"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const agent = createCursorAgent({
|
const adapter = createCursorAgent({
|
||||||
command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set WORKFLOW_CURSOR_COMMAND (e.g. cursor-agent)"),
|
command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set WORKFLOW_CURSOR_COMMAND (e.g. cursor-agent)"),
|
||||||
model: optionalEnv("WORKFLOW_CURSOR_MODEL"),
|
model: optionalEnv("WORKFLOW_CURSOR_MODEL"),
|
||||||
timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT")
|
timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT")
|
||||||
@@ -28,8 +27,6 @@ const agent = createCursorAgent({
|
|||||||
llmProvider,
|
llmProvider,
|
||||||
});
|
});
|
||||||
|
|
||||||
const adapter = wrapAgentAsAdapter(agent);
|
|
||||||
|
|
||||||
const wf = createWorkflow(developWorkflowDefinition, { adapter, overrides: null });
|
const wf = createWorkflow(developWorkflowDefinition, { adapter, overrides: null });
|
||||||
|
|
||||||
export const descriptor = buildDevelopDescriptor();
|
export const descriptor = buildDevelopDescriptor();
|
||||||
|
|||||||
@@ -2,31 +2,25 @@
|
|||||||
* solve-issue bundle entry — 小橘 🍊
|
* solve-issue bundle entry — 小橘 🍊
|
||||||
*
|
*
|
||||||
* preparer + submitter → hermes agent
|
* preparer + submitter → hermes agent
|
||||||
* developer → workflow-as-agent (delegates to "develop" workflow)
|
* developer → workflow adapter (delegates to "develop" workflow)
|
||||||
*/
|
*/
|
||||||
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
||||||
import { workflowAsAgent } from "@uncaged/workflow-execute";
|
import { workflowAdapter } from "@uncaged/workflow-execute";
|
||||||
import { createWorkflow } from "@uncaged/workflow-runtime";
|
import { createWorkflow } from "@uncaged/workflow-runtime";
|
||||||
import { optionalEnv } from "@uncaged/workflow-util";
|
import { optionalEnv } from "@uncaged/workflow-util";
|
||||||
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
|
||||||
import { buildSolveIssueDescriptor, solveIssueWorkflowDefinition } from "./src/index.js";
|
import { buildSolveIssueDescriptor, solveIssueWorkflowDefinition } from "./src/index.js";
|
||||||
|
|
||||||
const hermesAgent = createHermesAgent({
|
const adapter = createHermesAgent({
|
||||||
model: optionalEnv("WORKFLOW_HERMES_MODEL"),
|
model: optionalEnv("WORKFLOW_HERMES_MODEL"),
|
||||||
timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT")
|
timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT")
|
||||||
? Number(optionalEnv("WORKFLOW_HERMES_TIMEOUT"))
|
? Number(optionalEnv("WORKFLOW_HERMES_TIMEOUT"))
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const developerAgent = workflowAsAgent("develop");
|
|
||||||
|
|
||||||
const adapter = wrapAgentAsAdapter(hermesAgent);
|
|
||||||
const developerAdapter = wrapAgentAsAdapter(developerAgent);
|
|
||||||
|
|
||||||
const wf = createWorkflow(solveIssueWorkflowDefinition, {
|
const wf = createWorkflow(solveIssueWorkflowDefinition, {
|
||||||
adapter,
|
adapter,
|
||||||
overrides: {
|
overrides: {
|
||||||
developer: developerAdapter,
|
developer: workflowAdapter("develop"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { putContentNodeWithRefs } from "@uncaged/workflow-cas";
|
||||||
|
import type {
|
||||||
|
AdapterFn,
|
||||||
|
RoleResult,
|
||||||
|
ThreadContext,
|
||||||
|
WorkflowRuntime,
|
||||||
|
} from "@uncaged/workflow-runtime";
|
||||||
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from a text-producing agent (CLI spawn, LLM call, etc.).
|
||||||
|
* `output` is the raw text; `childThread` links to a spawned sub-workflow.
|
||||||
|
*/
|
||||||
|
export type TextAdapterResult = {
|
||||||
|
output: string;
|
||||||
|
childThread: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that produces raw text output given the thread context and
|
||||||
|
* the system prompt for the current role.
|
||||||
|
*/
|
||||||
|
export type TextProducerFn = (
|
||||||
|
ctx: ThreadContext,
|
||||||
|
prompt: string,
|
||||||
|
) => Promise<string | TextAdapterResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link AdapterFn} from a text-producing function.
|
||||||
|
*
|
||||||
|
* The adapter:
|
||||||
|
* 1. Calls the producer with thread context + system prompt
|
||||||
|
* 2. Stores output in CAS
|
||||||
|
* 3. Runs the extract phase to produce typed meta
|
||||||
|
* 4. Returns `{ meta, childThread }`
|
||||||
|
*/
|
||||||
|
export function createTextAdapter(producer: TextProducerFn): AdapterFn {
|
||||||
|
return <T>(prompt: string, schema: z.ZodType<T>) => {
|
||||||
|
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const result = await producer(ctx, prompt);
|
||||||
|
const output = typeof result === "string" ? result : result.output;
|
||||||
|
const childThread = typeof result === "string" ? null : result.childThread;
|
||||||
|
const contentHash = await putContentNodeWithRefs(runtime.cas, output, []);
|
||||||
|
const extracted = await runtime.extract(
|
||||||
|
schema as z.ZodType<Record<string, unknown>>,
|
||||||
|
contentHash,
|
||||||
|
);
|
||||||
|
return { meta: extracted.meta as T, childThread };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export { buildAgentPrompt, buildThreadInput } from "./build-agent-prompt.js";
|
export { buildAgentPrompt, buildThreadInput } from "./build-agent-prompt.js";
|
||||||
|
export type { TextAdapterResult, TextProducerFn } from "./create-text-adapter.js";
|
||||||
|
export { createTextAdapter } from "./create-text-adapter.js";
|
||||||
export type { SpawnCliConfig, SpawnCliError, SpawnCliResult } from "./spawn-cli.js";
|
export type { SpawnCliConfig, SpawnCliError, SpawnCliResult } from "./spawn-cli.js";
|
||||||
export { spawnCli } from "./spawn-cli.js";
|
export { spawnCli } from "./spawn-cli.js";
|
||||||
export { wrapAgentAsAdapter } from "./wrap-agent-as-adapter.js";
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { putContentNodeWithRefs } from "@uncaged/workflow-cas";
|
|
||||||
import type {
|
|
||||||
AdapterFn,
|
|
||||||
AgentContext,
|
|
||||||
AgentFnResult,
|
|
||||||
RoleResult,
|
|
||||||
ThreadContext,
|
|
||||||
WorkflowRuntime,
|
|
||||||
} from "@uncaged/workflow-runtime";
|
|
||||||
import type * as z from "zod/v4";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a legacy AgentFn into an AdapterFn.
|
|
||||||
* The agent produces a string (or { output, childThread }); the adapter
|
|
||||||
* stores the output in CAS, runs extract, and returns typed meta + childThread.
|
|
||||||
*/
|
|
||||||
export function wrapAgentAsAdapter(
|
|
||||||
agentFn: (ctx: AgentContext) => Promise<AgentFnResult>,
|
|
||||||
): AdapterFn {
|
|
||||||
return <T>(prompt: string, schema: z.ZodType<T>) => {
|
|
||||||
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
|
||||||
const agentCtx: AgentContext = {
|
|
||||||
...ctx,
|
|
||||||
currentRole: { name: "agent", systemPrompt: prompt },
|
|
||||||
};
|
|
||||||
const result = await agentFn(agentCtx);
|
|
||||||
const output = typeof result === "string" ? result : result.output;
|
|
||||||
const childThread = typeof result === "string" ? null : result.childThread;
|
|
||||||
const contentHash = await putContentNodeWithRefs(runtime.cas, output, []);
|
|
||||||
const extracted = await runtime.extract(
|
|
||||||
schema as z.ZodType<Record<string, unknown>>,
|
|
||||||
contentHash,
|
|
||||||
);
|
|
||||||
return { meta: extracted.meta as T, childThread };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user