@agent-compose/sdk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +145 -33
  2. package/dist/agent/agent-loop.d.ts +83 -5
  3. package/dist/agent/run-agent.d.ts +34 -9
  4. package/dist/client.d.ts +247 -99
  5. package/dist/index.d.ts +26 -11
  6. package/dist/index.js +1967 -745
  7. package/dist/processors/builtins.d.ts +35 -0
  8. package/dist/processors/index.d.ts +4 -0
  9. package/dist/processors/processor.d.ts +91 -0
  10. package/dist/processors/processor.test.d.ts +1 -0
  11. package/dist/processors/runner.d.ts +19 -0
  12. package/dist/request-context/index.d.ts +2 -0
  13. package/dist/request-context/request-context.d.ts +159 -0
  14. package/dist/request-context/request-context.test.d.ts +1 -0
  15. package/dist/runtimes/claude.d.ts +27 -50
  16. package/dist/runtimes/openai-desktop.js +1918 -741
  17. package/dist/runtimes/vercel.d.ts +34 -0
  18. package/dist/runtimes/vercel.js +474 -0
  19. package/dist/sandbox.d.ts +29 -25
  20. package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
  21. package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
  22. package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
  23. package/dist/step-invocation/index.d.ts +25 -0
  24. package/dist/step-invocation/invoker.d.ts +65 -0
  25. package/dist/step-invocation/protocol.d.ts +44 -0
  26. package/dist/step-invocation/server.d.ts +63 -0
  27. package/dist/step-invocation/types.d.ts +72 -0
  28. package/dist/tools/coding.d.ts +49 -0
  29. package/dist/tools/coding.test.d.ts +1 -0
  30. package/dist/tools/index.d.ts +2 -0
  31. package/dist/types/events.d.ts +36 -0
  32. package/dist/types/execution-context.d.ts +22 -0
  33. package/dist/types/runtime.d.ts +32 -0
  34. package/dist/types/sandbox-environment.d.ts +5 -2
  35. package/dist/types/sandbox.d.ts +14 -12
  36. package/dist/types/workflow-metadata.d.ts +51 -0
  37. package/dist/types/workflow-plan.d.ts +19 -0
  38. package/dist/types/workflow.d.ts +57 -17
  39. package/dist/utils/bundler.d.ts +62 -3
  40. package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
  41. package/dist/workflow-steps/index.d.ts +10 -0
  42. package/dist/workflow-steps/observability.d.ts +58 -0
  43. package/dist/workflow-steps/runner.d.ts +96 -0
  44. package/dist/workflow-steps/step.d.ts +25 -0
  45. package/dist/workflow-steps/types.d.ts +135 -0
  46. package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
  47. package/dist/workflow-steps/workflow.d.ts +50 -0
  48. package/dist/workflows/engine.d.ts +27 -13
  49. package/dist/workflows/invoke-child.d.ts +10 -0
  50. package/package.json +25 -15
  51. package/src/agent/agent-loop.ts +197 -26
  52. package/src/agent/run-agent.ts +40 -15
  53. package/src/client.ts +326 -76
  54. package/src/index.ts +124 -10
  55. package/src/processors/builtins.ts +72 -0
  56. package/src/processors/index.ts +15 -0
  57. package/src/processors/processor.ts +103 -0
  58. package/src/processors/runner.ts +42 -0
  59. package/src/request-context/index.ts +17 -0
  60. package/src/request-context/request-context.ts +302 -0
  61. package/src/runtimes/claude.ts +123 -254
  62. package/src/runtimes/vercel.ts +180 -0
  63. package/src/sandbox.ts +53 -21
  64. package/src/step-invocation/index.ts +33 -0
  65. package/src/step-invocation/invoker.ts +204 -0
  66. package/src/step-invocation/protocol.ts +57 -0
  67. package/src/step-invocation/server.ts +184 -0
  68. package/src/step-invocation/types.ts +70 -0
  69. package/src/tools/coding.ts +126 -0
  70. package/src/tools/index.ts +8 -0
  71. package/src/types/events.ts +40 -0
  72. package/src/types/execution-context.ts +30 -0
  73. package/src/types/runtime.ts +24 -0
  74. package/src/types/sandbox-environment.ts +7 -5
  75. package/src/types/sandbox.ts +16 -12
  76. package/src/types/workflow-metadata.ts +84 -0
  77. package/src/types/workflow-plan.ts +24 -0
  78. package/src/types/workflow.ts +139 -25
  79. package/src/utils/bundler.ts +206 -18
  80. package/src/utils/source-loader.ts +2 -2
  81. package/src/workflow-steps/index.ts +30 -0
  82. package/src/workflow-steps/observability.ts +103 -0
  83. package/src/workflow-steps/runner.ts +244 -0
  84. package/src/workflow-steps/step.ts +38 -0
  85. package/src/workflow-steps/types.ts +134 -0
  86. package/src/workflow-steps/workflow.ts +95 -0
  87. package/src/workflows/engine.ts +69 -40
  88. package/src/workflows/invoke-child.ts +29 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * StepInvocation public types — what callers (the activity) get back, and
3
+ * the discriminated error union that classifies failure modes.
4
+ */
5
+
6
+ import type { RequestContextWire } from "../request-context/request-context.js";
7
+ import type { StepObservability } from "../workflow-steps/observability.js";
8
+
9
+ /** What the invoker needs to drive one step invocation. The step's
10
+ * human-readable name is *not* on here — `invokeStep` doesn't need it
11
+ * (the result token isolates the result channel from stdout traffic;
12
+ * run id is enough for message attribution). Callers attach step name
13
+ * to thrown errors at their layer (see `StepExecutionError`). */
14
+ export interface StepRequest<TInput = unknown> {
15
+ runId: string;
16
+ stepIndex: number;
17
+ input: TInput;
18
+ requestContext: RequestContextWire;
19
+ }
20
+
21
+ /**
22
+ * Outcome of one step invocation. Successful runs carry the step's output;
23
+ * failed runs carry a kinded error so the caller can distinguish "user
24
+ * code threw" from "runner crashed before emitting" from "wire protocol
25
+ * violation". The dashboard surfaces the kind to operators; the activity
26
+ * uses the kind to pick a useful failRun reason.
27
+ */
28
+ export type StepResult<TOutput = unknown> =
29
+ | { ok: true; output: TOutput; observability?: StepObservability }
30
+ | { ok: false; error: StepInvocationError };
31
+
32
+ /**
33
+ * Discriminated error union.
34
+ *
35
+ * - `protocol` — the runner did not emit a tokenised sentinel line, OR the
36
+ * sentinel JSON was malformed. This is a runner-side bug or a sandbox
37
+ * provider that swallowed stdout. Surface it loudly; do not blame user
38
+ * code.
39
+ *
40
+ * - `user-step` — the step body threw. The runner caught it and emitted
41
+ * `{ ok: false, error: <user message> }`. The message is the user's
42
+ * to read.
43
+ *
44
+ * - `runner-exit` — the runner subprocess exited non-zero before emitting
45
+ * any sentinel. Out-of-memory, SIGKILL from sandbox lifetime cap,
46
+ * bundler crash, etc. Distinct from `protocol` because exit code is
47
+ * meaningful evidence.
48
+ */
49
+ export type StepInvocationError =
50
+ | { kind: "protocol"; message: string }
51
+ | { kind: "user-step"; message: string }
52
+ | { kind: "runner-exit"; message: string; exitCode: number };
53
+
54
+ /**
55
+ * Error thrown by the activity (or any caller) to surface a step failure
56
+ * with its kind preserved. The Temporal activity boundary serialises
57
+ * `message` to `failRun`, so the message is prefixed with `[<kind>]` —
58
+ * dashboard / log consumers can parse the prefix without losing the
59
+ * structured shape carried on the instance itself.
60
+ */
61
+ export class StepExecutionError extends Error {
62
+ readonly kind: StepInvocationError["kind"];
63
+ readonly exitCode: number | undefined;
64
+ constructor(error: StepInvocationError, stepLabel: string) {
65
+ super(`[${error.kind}] ${stepLabel}: ${error.message}`);
66
+ this.name = "StepExecutionError";
67
+ this.kind = error.kind;
68
+ this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
69
+ }
70
+ }
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ import { isAbsolute, join } from "node:path/posix";
3
+ import type { SandboxProvider } from "../types/sandbox.js";
4
+
5
+ function q(value: string): string {
6
+ return JSON.stringify(value);
7
+ }
8
+
9
+ function resolvePath(path: string, cwd?: string): string {
10
+ return cwd && !isAbsolute(path) ? join(cwd, path) : path;
11
+ }
12
+
13
+ async function readFile(sandbox: SandboxProvider, path: string, opts?: { offset?: number; limit?: number; cwd?: string }): Promise<string> {
14
+ const script = `
15
+ const fs = require("fs");
16
+ const path = process.argv[1];
17
+ const offset = process.argv[2] ? Number(process.argv[2]) : 1;
18
+ const limit = process.argv[3] ? Number(process.argv[3]) : 2000;
19
+ const data = fs.readFileSync(path, "utf8");
20
+ const lines = data.split(/\\r?\\n/);
21
+ const start = Math.max(1, offset);
22
+ const end = Math.min(lines.length, start + Math.max(0, limit) - 1);
23
+ for (let i = start; i <= end; i++) console.log(String(i) + ": " + lines[i - 1]);
24
+ `;
25
+ const args = [q(path), opts?.offset !== undefined ? String(opts.offset) : "", opts?.limit !== undefined ? String(opts.limit) : ""]
26
+ .filter(Boolean)
27
+ .map(q)
28
+ .join(" ");
29
+ const { stdout } = await sandbox.commands.run(`node -e ${q(script)} ${args}`, { cwd: opts?.cwd, timeoutMs: 30_000 });
30
+ return stdout;
31
+ }
32
+
33
+ async function readRawFile(sandbox: SandboxProvider, path: string, cwd?: string): Promise<string> {
34
+ const script = `const fs = require("fs"); process.stdout.write(fs.readFileSync(process.argv[1], "utf8"));`;
35
+ const { stdout } = await sandbox.commands.run(`node -e ${q(script)} ${q(path)}`, { cwd, timeoutMs: 30_000 });
36
+ return stdout;
37
+ }
38
+
39
+ export interface CodingTool<TInput extends Record<string, unknown> = Record<string, unknown>> {
40
+ name: string;
41
+ description: string;
42
+ inputSchema: z.ZodType<TInput>;
43
+ execute(input: TInput, ctx: { sandbox: SandboxProvider; cwd?: string; abortSignal?: AbortSignal }): Promise<string>;
44
+ }
45
+
46
+ export const readTool: CodingTool<{
47
+ path: string;
48
+ offset?: number;
49
+ limit?: number;
50
+ }> = {
51
+ name: "Read",
52
+ description: "Read a UTF-8 text file from the sandbox. Returns numbered lines. Use offset/limit for large files.",
53
+ inputSchema: z.object({
54
+ path: z.string().describe("File path to read. Relative paths resolve from the agent working directory."),
55
+ offset: z.number().int().positive().optional().describe("1-based line number to start at."),
56
+ limit: z.number().int().positive().max(5000).optional().describe("Maximum number of lines to return."),
57
+ }),
58
+ execute: ({ path, offset, limit }, ctx) => readFile(ctx.sandbox, path, { offset, limit, cwd: ctx.cwd }),
59
+ };
60
+
61
+ export const writeTool: CodingTool<{
62
+ path: string;
63
+ content: string;
64
+ }> = {
65
+ name: "Write",
66
+ description: "Write UTF-8 content to a file in the sandbox, creating or replacing the file.",
67
+ inputSchema: z.object({
68
+ path: z.string().describe("File path to write. Relative paths resolve from the agent working directory."),
69
+ content: z.string().describe("Complete file contents to write."),
70
+ }),
71
+ execute: async ({ path, content }, ctx) => {
72
+ const target = resolvePath(path, ctx.cwd);
73
+ await ctx.sandbox.files.write(target, content);
74
+ return `Wrote ${content.length} bytes to ${target}`;
75
+ },
76
+ };
77
+
78
+ export const editTool: CodingTool<{
79
+ path: string;
80
+ oldString: string;
81
+ newString: string;
82
+ replaceAll?: boolean;
83
+ }> = {
84
+ name: "Edit",
85
+ description: "Edit a text file by replacing an exact string. Fails if the old string is not found or is ambiguous without replaceAll.",
86
+ inputSchema: z.object({
87
+ path: z.string().describe("File path to edit. Relative paths resolve from the agent working directory."),
88
+ oldString: z.string().min(1).describe("Exact text to replace."),
89
+ newString: z.string().describe("Replacement text."),
90
+ replaceAll: z.boolean().optional().describe("Replace every occurrence instead of requiring exactly one match."),
91
+ }),
92
+ execute: async ({ path, oldString, newString, replaceAll }, ctx) => {
93
+ const target = resolvePath(path, ctx.cwd);
94
+ const before = await readRawFile(ctx.sandbox, target, ctx.cwd);
95
+ const count = before.split(oldString).length - 1;
96
+ if (count === 0) throw new Error(`Edit failed: oldString not found in ${target}`);
97
+ if (!replaceAll && count !== 1) throw new Error(`Edit failed: oldString occurs ${count} times in ${target}; set replaceAll=true or make oldString more specific`);
98
+ const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
99
+ await ctx.sandbox.files.write(target, after);
100
+ return `Edited ${target}: replaced ${replaceAll ? count : 1} occurrence${(replaceAll ? count : 1) === 1 ? "" : "s"}`;
101
+ },
102
+ };
103
+
104
+ export const bashTool: CodingTool<{
105
+ command: string;
106
+ timeoutMs?: number;
107
+ cwd?: string;
108
+ }> = {
109
+ name: "Bash",
110
+ description: "Run a shell command in the sandbox. Use for file discovery/search (e.g. globbing/grep via shell commands), tests, builds, and package commands.",
111
+ inputSchema: z.object({
112
+ command: z.string().min(1).describe("Shell command to execute."),
113
+ timeoutMs: z.number().int().positive().max(3_600_000).optional().describe("Timeout in milliseconds."),
114
+ cwd: z.string().optional().describe("Optional command working directory. Defaults to the agent working directory."),
115
+ }),
116
+ execute: async ({ command, timeoutMs, cwd }, ctx) => {
117
+ const result = await ctx.sandbox.commands.run(command, {
118
+ cwd: cwd ?? ctx.cwd,
119
+ timeoutMs: timeoutMs ?? 120_000,
120
+ });
121
+ if (result.exitCode !== 0) throw new Error(`Command failed with exit code ${result.exitCode}${result.stdout ? `\n${result.stdout}` : ""}`);
122
+ return result.stdout;
123
+ },
124
+ };
125
+
126
+ export const codingTools = [readTool, writeTool, editTool, bashTool] as const;
@@ -0,0 +1,8 @@
1
+ export {
2
+ bashTool,
3
+ codingTools,
4
+ editTool,
5
+ readTool,
6
+ writeTool,
7
+ } from "./coding.js";
8
+ export type { CodingTool } from "./coding.js";
@@ -8,6 +8,46 @@
8
8
  * The `event` field doubles as the SSE `event:` name.
9
9
  */
10
10
  export type RunEvent =
11
+ | {
12
+ event: "agent.spawned";
13
+ runId: string;
14
+ at: number;
15
+ seq?: number;
16
+ agentId: string;
17
+ label: string;
18
+ }
19
+ | {
20
+ event: "agent.message";
21
+ runId: string;
22
+ at: number;
23
+ seq?: number;
24
+ agentId: string;
25
+ label: string;
26
+ iteration: number;
27
+ message: unknown;
28
+ }
29
+ | {
30
+ event: "agent.iteration";
31
+ runId: string;
32
+ at: number;
33
+ seq?: number;
34
+ agentId: string;
35
+ label: string;
36
+ iteration: number;
37
+ status: unknown;
38
+ }
39
+ | {
40
+ event: "agent.settled";
41
+ runId: string;
42
+ at: number;
43
+ seq?: number;
44
+ agentId: string;
45
+ label: string;
46
+ outcome: "success" | "failed";
47
+ iterations: number;
48
+ durationMs: number;
49
+ failureReason?: string;
50
+ }
11
51
  | {
12
52
  event: "agent_event";
13
53
  runId: string;
@@ -0,0 +1,30 @@
1
+ /** Shared execution context capabilities for workflow functions and steps. */
2
+
3
+ import type { InvokeAndWaitOptions, RunStatus } from "../client.js";
4
+ import type { RequestContext } from "../request-context/request-context.js";
5
+ import type { SandboxProvider } from "./sandbox.js";
6
+
7
+ /** The identity of this workflow run. */
8
+ export interface WorkflowRun {
9
+ id: string;
10
+ }
11
+
12
+ export interface InvokeChild {
13
+ <TOutput = unknown>(
14
+ name: string,
15
+ input?: Record<string, unknown>,
16
+ opts?: Omit<InvokeAndWaitOptions, "parentRunId">,
17
+ ): Promise<RunStatus<TOutput>>;
18
+ }
19
+
20
+ /** Capabilities shared by legacy workflow ctx and step ctx. */
21
+ export interface BaseExecutionContext {
22
+ run: WorkflowRun;
23
+ requestContext: RequestContext;
24
+ /** Present when the engine executes inside a sandbox-backed workspace. */
25
+ sandbox?: SandboxProvider;
26
+ /** Persist key-value metadata on the run record when the engine supports it. */
27
+ setMetadata?: (data: Record<string, unknown>) => Promise<void>;
28
+ /** Invoke another registered workflow and wait for it to settle. */
29
+ invokeChild: InvokeChild;
30
+ }
@@ -4,6 +4,8 @@
4
4
 
5
5
  import type { SandboxProvider } from "./sandbox.js";
6
6
  import type { AgentMessage } from "./protocol.js";
7
+ import type { Processor, ProcessorContext, ToolCall } from "../processors/processor.js";
8
+ import type { RequestContext } from "../request-context/request-context.js";
7
9
 
8
10
  /** Configuration for a single MCP server. */
9
11
  export interface McpServerConfig {
@@ -21,16 +23,38 @@ export interface RuntimeOptions {
21
23
  label?: string;
22
24
  /** Working directory — the claude CLI process starts here so relative paths work correctly. */
23
25
  cwd?: string;
26
+ /** Processor chain registered by workflow/agent code. Runtime adapters that
27
+ * own tool execution should call `processToolCall` before the tool runs. */
28
+ processors?: readonly Processor[];
29
+ /** Per-run typed bag threaded into processor contexts. */
30
+ requestContext?: RequestContext;
31
+ /** Agent id and label for processor context / adapter logs. */
32
+ agentId?: string;
33
+ iteration?: number;
34
+ /** Optional JSON schema for runtimes with native structured-output support. */
35
+ outputFormat?: { type: "json_schema"; schema: Record<string, unknown> };
24
36
  }
25
37
 
38
+ /** Runtime-normalized result of running pre-tool processors. */
39
+ export type ToolCallGateResult =
40
+ | { kind: "allow"; call: ToolCall }
41
+ | { kind: "deny"; reason: string }
42
+ | { kind: "abort"; reason: string };
43
+
26
44
  /**
27
45
  * The execution contract every runtime must satisfy.
28
46
  * Given a prompt, yield a stream of agent events.
29
47
  */
30
48
  export interface ModelExecutionContract {
49
+ /** True when this runtime can run `processToolCall` before tool execution. */
50
+ supportsToolCallProcessor?: boolean;
51
+ /** Runtime-owned pre-tool gate. Adapters call the shared processor chain
52
+ * through this seam; the agent loop stays SDK-agnostic. */
53
+ gateToolCall?(call: ToolCall, ctx: ProcessorContext): Promise<ToolCallGateResult>;
31
54
  sendMessage(opts: {
32
55
  prompt: string;
33
56
  sessionId?: string;
57
+ iteration?: number;
34
58
  signal?: AbortSignal;
35
59
  }): AsyncGenerator<AgentMessage>;
36
60
  }
@@ -37,9 +37,8 @@
37
37
  */
38
38
 
39
39
  import type { SandboxProvider } from "./sandbox.js";
40
- import { makeLocalSandboxProvider } from "../sandbox.js";
41
40
  import { defineWorkflow } from "./workflow.js";
42
- import type { WorkflowFn } from "./workflow.js";
41
+ import type { Workflow } from "../workflow-steps/types.js";
43
42
 
44
43
  export interface SandboxEnvironmentDefinition {
45
44
  name: string;
@@ -51,14 +50,17 @@ export interface SandboxEnvironmentDefinition {
51
50
  saveSnapshot?: boolean;
52
51
  }
53
52
 
53
+ /** Sugar over `defineWorkflow` for setup-only workflows that exist to
54
+ * capture a snapshot. The workflow takes no meaningful input and returns
55
+ * nothing — its value is the side effect on the sandbox VM. */
54
56
  export function defineSandboxEnvironment(
55
57
  env: SandboxEnvironmentDefinition,
56
- ): WorkflowFn<void> {
58
+ ): Workflow<Record<string, unknown>, void> {
57
59
  if (typeof env.setup !== "function") {
58
60
  throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
59
61
  }
60
- return defineWorkflow({
62
+ return defineWorkflow<void, Record<string, unknown>>({
61
63
  saveSnapshot: env.saveSnapshot ?? true,
62
- run: () => env.setup(makeLocalSandboxProvider()),
64
+ run: async (_ctx, sandbox) => env.setup(sandbox),
63
65
  });
64
66
  }
@@ -4,25 +4,29 @@
4
4
  */
5
5
 
6
6
  /** Base compute interface — pure I/O, no filesystem path or git concerns. */
7
+ export interface SandboxCommandRunOptions {
8
+ cwd?: string;
9
+ timeoutMs?: number;
10
+ envs?: Record<string, string>;
11
+ onStdout?: (data: string) => void;
12
+ onStderr?: (data: string) => void;
13
+ background?: boolean;
14
+ }
15
+
16
+ export interface SandboxCommandResult {
17
+ exitCode: number;
18
+ stdout: string;
19
+ }
20
+
7
21
  export interface SandboxProvider {
8
22
  sandboxId: string;
9
23
  /** Working directory for the agent process. Set by onStart after environment setup. */
10
24
  cwd?: string;
11
25
  commands: {
12
- run(
13
- cmd: string,
14
- opts?: {
15
- cwd?: string;
16
- timeoutMs?: number;
17
- envs?: Record<string, string>;
18
- onStdout?: (data: string) => void;
19
- onStderr?: (data: string) => void;
20
- background?: boolean;
21
- },
22
- ): Promise<{ exitCode: number; stdout: string }>;
26
+ run(cmd: string, opts?: SandboxCommandRunOptions): Promise<SandboxCommandResult>;
23
27
  };
24
28
  files: {
25
- write(path: string, content: string): Promise<unknown>;
29
+ write(path: string, content: string): Promise<void>;
26
30
  };
27
31
  kill(): Promise<void>;
28
32
  /** Capture the running sandbox's state as a reusable snapshot. Vercel
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `WorkflowMetadata` — server-readable declarations that ride on every
3
+ * `Workflow` as `workflow.metadata`. Owned here (not in `types/workflow.ts`)
4
+ * because both that file and `workflow-steps/workflow.ts` need the type +
5
+ * helper; co-locating them in `types/workflow.ts` introduced a runtime
6
+ * import cycle.
7
+ *
8
+ * Keep this module free of value imports from `types/workflow.ts` or
9
+ * `workflow-steps/workflow.ts` — it is the cycle-break point.
10
+ */
11
+
12
+ import type { SandboxNetworkPolicy } from "../sandbox.js";
13
+ import type { Processor } from "../processors/processor.js";
14
+
15
+ /**
16
+ * Workflow-level metadata read by the server at registration. Lives on
17
+ * every `Workflow` as `workflow.metadata`, regardless of which form of
18
+ * `defineWorkflow` produced it.
19
+ *
20
+ * The bundler reads these from the default export at registration time
21
+ * and forwards them to the server's POST /api/v1/templates payload.
22
+ */
23
+ /** Server-side knob for whether/how a Workflow Memory agent should run
24
+ * after this workflow completes. `"default"` runs the built-in extractor;
25
+ * `false` disables; an object names a custom memory workflow to dispatch. */
26
+ export type WorkflowMemoryConfig = "default" | false | { workflow: string };
27
+
28
+ export interface WorkflowMetadata {
29
+ networkPolicy?: SandboxNetworkPolicy;
30
+ placeholders?: Record<string, string>;
31
+ snapshot?: string;
32
+ saveSnapshot?: boolean;
33
+ processors?: readonly Processor[];
34
+ memory?: WorkflowMemoryConfig;
35
+ }
36
+
37
+ /**
38
+ * Pull the server-readable declarations off a source object (run-form
39
+ * `WorkflowDefinition` or step-form `StepWorkflowDefinition`) into a
40
+ * single `WorkflowMetadata` bag. Undefined fields are omitted so the
41
+ * canonical metadata hash (server-side) is stable across re-registers
42
+ * that left a field unspecified.
43
+ *
44
+ * Returns a frozen bag — see `freezeMetadataValue` for the depth. The
45
+ * workflow object is meant to be immutable after `defineWorkflow` returns;
46
+ * the bundler reads `metadata` directly and any mutation between
47
+ * construction and bundling would diverge the canonical metadata hash
48
+ * from what the author declared.
49
+ *
50
+ * Accepts any source that overlaps `WorkflowMetadata` in field shape —
51
+ * both definition types are supersets of it.
52
+ */
53
+ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMetadata {
54
+ const out: WorkflowMetadata = {};
55
+ if (source.networkPolicy !== undefined) out.networkPolicy = freezeMetadataValue(source.networkPolicy);
56
+ if (source.placeholders !== undefined) out.placeholders = Object.freeze({ ...source.placeholders });
57
+ if (source.snapshot !== undefined) out.snapshot = source.snapshot;
58
+ if (source.saveSnapshot !== undefined) out.saveSnapshot = source.saveSnapshot;
59
+ if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
60
+ if (source.memory !== undefined) out.memory = typeof source.memory === "object" && source.memory !== null
61
+ ? Object.freeze({ ...source.memory })
62
+ : source.memory;
63
+ return Object.freeze(out);
64
+ }
65
+
66
+ /**
67
+ * Recursively freeze plain-data values (the network-policy shape). The
68
+ * policy is JSON-serializable — strings, arrays, plain objects — so a
69
+ * depth-first freeze is safe. We mutate the original (no clone) because
70
+ * the user already handed it to `defineWorkflow`; if they keep a sibling
71
+ * reference and mutate it later, they're modifying state they explicitly
72
+ * handed off. The freeze makes that intent enforced rather than implicit.
73
+ */
74
+ function freezeMetadataValue<T>(value: T): T {
75
+ if (value === null || typeof value !== "object") return value;
76
+ if (Array.isArray(value)) {
77
+ for (const v of value) freezeMetadataValue(v);
78
+ return Object.freeze(value) as T;
79
+ }
80
+ for (const v of Object.values(value as Record<string, unknown>)) {
81
+ freezeMetadataValue(v);
82
+ }
83
+ return Object.freeze(value) as T;
84
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WorkflowPlan — provider-neutral execution shape extracted at registration.
3
+ *
4
+ * The server must not execute user workflow source to discover shape at
5
+ * dispatch time. The CLI/bundler inspects the bundled module in the user's
6
+ * environment and sends this compact plan as metadata.
7
+ *
8
+ * Every workflow has a step plan. Legacy `defineWorkflow({ run })`
9
+ * workflows are wrapped at the SDK boundary as a single-step compiled
10
+ * workflow (step name = "run"); the bundler sees the same shape regardless.
11
+ */
12
+
13
+ export interface WorkflowStepPlan {
14
+ index: number;
15
+ name: string;
16
+ }
17
+
18
+ export interface WorkflowPlan {
19
+ steps: WorkflowStepPlan[];
20
+ }
21
+
22
+ export function workflowPlan(steps: WorkflowStepPlan[]): WorkflowPlan {
23
+ return { steps };
24
+ }