Merge pull request 'refactor(workflow-protocol): require AgentFn Opt generic' (#284) from refactor/agent-fn-required-opt into main

This commit is contained in:
2026-05-16 10:27:07 +00:00
10 changed files with 48 additions and 72 deletions
@@ -20,9 +20,6 @@ import { addCliArgs } from "./bundle-fixture.js";
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {}, graph: { edges: [] } };
`;
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
function casStoredForm(raw: string): string {
return serializeMerkleNode(createContentMerkleNode(raw));
}
@@ -52,12 +49,12 @@ describe("cli workflow commands", () => {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}import fs from "node:fs";
`${fixtureDescriptor}import fs from "node:fs";
export const run = async function* (input, options) {
fs.existsSync(".");
const cas = options.cas;
const h = await putContentMerkleNode(cas, input.prompt);
const h = await cas.put(input.prompt);
yield { role: "noop", contentHash: h, meta: { done: true }, refs: [h] };
return { returnCode: 0, summary: "done" };
}
@@ -155,10 +152,9 @@ export const run = async function* (input) { return { returnCode: 0, summary: in
},
graph: { edges: [] },
};
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, input.prompt);
const h = await cas.put( input.prompt);
yield { role: "greeter", contentHash: h, meta: { greeting: "hi" }, refs: [h] };
return { returnCode: 0, summary: "ok" };
};
@@ -197,9 +193,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -228,9 +224,9 @@ export const run = async function* (input, options) {
const dtsPath = join(bundleDir, "types.d.ts");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -261,9 +257,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -284,16 +280,16 @@ export const run = async function* (input, options) {
const bundleDir = join(storageRoot, "src");
await mkdir(bundleDir, { recursive: true });
const bundlePath = join(bundleDir, "demo.esm.js");
const v1 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v1");
const h = await cas.put( "v1");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v1" };
}
`;
const v2 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v2");
const h = await cas.put( "v2");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v2" };
}
@@ -326,16 +322,16 @@ export const run = async function* (input, options) {
const bundleDir = join(storageRoot, "src");
await mkdir(bundleDir, { recursive: true });
const bundlePath = join(bundleDir, "demo.esm.js");
const v1 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v1");
const h = await cas.put( "v1");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v1" };
}
`;
const v2 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v2");
const h = await cas.put( "v2");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v2" };
}
@@ -378,9 +374,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -391,9 +387,9 @@ export const run = async function* (input, options) {
expect(add1.ok).toBe(true);
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "y");
const h = await cas.put( "y");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "y" };
}
@@ -446,9 +442,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -463,9 +459,9 @@ export const run = async function* (input, options) {
const hash1 = add1.value.hash;
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "y");
const h = await cas.put( "y");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "y" };
}
@@ -15,9 +15,7 @@ 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-cas";
export const descriptor = {
const threeRoleBundleSource = `export const descriptor = {
description: "fork-cli",
roles: {
planner: { description: "planner", schema: {} },
@@ -30,16 +28,16 @@ export const run = async function* (input, options) {
const cas = options.cas;
const has = (r) => input.steps.some((s) => s.role === r);
if (!has("planner")) {
const h = await putContentMerkleNode(cas, "p1");
const h = await cas.put( "p1");
yield { role: "planner", contentHash: h, meta: { k: "planner" }, refs: [h] };
}
if (!has("coder")) {
const h = await putContentMerkleNode(cas, "c1");
const h = await cas.put( "c1");
yield { role: "coder", contentHash: h, meta: { k: "coder" }, refs: [h] };
}
if (!has("reviewer")) {
const body = "rev-" + String(input.steps.length);
const h = await putContentMerkleNode(cas, body);
const h = await cas.put( body);
yield { role: "reviewer", contentHash: h, meta: { k: "reviewer" }, refs: [h] };
}
return { returnCode: 0, summary: "done" };
@@ -23,9 +23,6 @@ import { resolveThreadRecord } from "../src/thread-scan.js";
import { addCliArgs } from "./bundle-fixture.js";
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
const threadFixtureDescriptor = `export const descriptor = {
description: "thread-cli",
roles: {
@@ -41,25 +38,23 @@ const threadFixtureDescriptor = `export const descriptor = {
`;
const fastBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const slowPlannerBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
await new Promise((r) => setTimeout(r, 400));
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
@@ -68,37 +63,34 @@ export const run = async function* (input, options) {
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
const abortablePlannerBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
await new Promise((r) => setTimeout(r, 10000));
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const pauseResumeBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (_input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "f");
let h = await cas.put( "f");
yield { role: "first", contentHash: h, meta: {}, refs: [h] };
await new Promise((r) => setTimeout(r, 1500));
h = await putContentMerkleNode(cas, "s");
h = await cas.put( "s");
yield { role: "second", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const delayedFirstYieldBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (_input, options) {
await new Promise((r) => setTimeout(r, 900));
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "only", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "done" };
};
@@ -12,6 +12,7 @@ import type { CursorAgentConfig } from "./types.js";
import { validateCursorAgentConfig } from "./validate-config.js";
export type { CursorAgentConfig } from "./types.js";
export { validateCursorAgentConfig } from "./validate-config.js";
function throwCursorSpawnError(error: SpawnCliError): never {
if (error.kind === "non_zero_exit") {
@@ -14,6 +14,7 @@ const HERMES_DEFAULT_MAX_TURNS = 90;
type HermesAgentOpt = { prompt: string };
export type { HermesAgentConfig } from "./types.js";
export { validateHermesAgentConfig } from "./validate-config.js";
function throwHermesSpawnError(error: SpawnCliError): never {
if (error.kind === "non_zero_exit") {
@@ -3,9 +3,7 @@ import { mkdir, unlink, writeFile } from "node:fs/promises";
import { createServer, type Socket } from "node:net";
import { dirname, join } from "node:path";
import { createCasStore } from "@uncaged/workflow-cas";
import {
importWorkflowBundleModule,
} from "@uncaged/workflow-register";
import { importWorkflowBundleModule } from "@uncaged/workflow-register";
import type { RoleOutput, WorkflowFn } from "@uncaged/workflow-runtime";
import {
createLogger,
+2 -5
View File
@@ -154,12 +154,9 @@ export type AdapterFn = <T>(prompt: string, schema: z.ZodType<T>) => RoleFn<T>;
/**
* Core agent function. Input is always {@link ThreadContext}, output is always string.
* `Opt` captures agent-specific structured options.
* Agents with no extra options use `AgentFn` (Opt defaults to void).
* `Opt` captures agent-specific structured options (required second argument).
*/
export type AgentFn<Opt = void> = Opt extends void
? (ctx: ThreadContext) => Promise<string>
: (ctx: ThreadContext, options: Opt) => Promise<string>;
export type AgentFn<Opt> = (ctx: ThreadContext, options: Opt) => Promise<string>;
export type AdapterBinding = {
adapter: AdapterFn;
+3 -3
View File
@@ -1,6 +1,3 @@
export { buildThreadContext } from "./build-context.js";
export { createWorkflow } from "./create-workflow.js";
export { err, ok } from "./result.js";
export type {
AdapterFn,
AgentContext,
@@ -28,3 +25,6 @@ export type {
WorkflowRuntime,
} from "@uncaged/workflow-protocol";
export { END, START } from "@uncaged/workflow-protocol";
export { buildThreadContext } from "./build-context.js";
export { createWorkflow } from "./create-workflow.js";
export { err, ok } from "./result.js";
@@ -29,10 +29,7 @@ export function createAgentAdapter<Opt>(
return <T>(prompt: string, schema: z.ZodType<T>) => {
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const options = await extract(ctx, prompt, runtime);
const raw = await (agent as (ctx: ThreadContext, optionsParam: Opt) => Promise<string>)(
ctx,
options,
);
const raw = await agent(ctx, options);
const contentHash = await putContentNodeWithRefs(runtime.cas, raw, []);
const extracted = await runtime.extract(
schema as z.ZodType<Record<string, unknown>>,
@@ -42,7 +39,3 @@ export function createAgentAdapter<Opt>(
};
};
}
export function createSimpleAgentAdapter(agent: AgentFn<void>): AdapterFn {
return createAgentAdapter(agent, async () => undefined as unknown as undefined);
}
+1 -1
View File
@@ -1,4 +1,4 @@
export { buildThreadInput } from "./build-agent-prompt.js";
export { buildAgentPrompt, buildThreadInput } from "./build-agent-prompt.js";
export { createAgentAdapter } from "./create-agent-adapter.js";
export type { SpawnCliError } from "./spawn-cli.js";
export { spawnCli } from "./spawn-cli.js";