@agent-compose/sdk 0.7.0 → 0.8.0
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.
- package/README.md +66 -39
- package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +21 -1
- package/dist/agent/agent-loop.d.ts +24 -1
- package/dist/client.d.ts +338 -534
- package/dist/directives.d.ts +112 -0
- package/dist/display.d.ts +242 -0
- package/dist/errors.d.ts +24 -1
- package/dist/index.d.ts +24 -12
- package/dist/index.js +3545 -1667
- package/dist/pause/wrappers.d.ts +31 -9
- package/dist/runtimes/_acp-client.d.ts +46 -1
- package/dist/runtimes/_cli-agent.d.ts +49 -4
- package/dist/runtimes/_jsonl-guard.d.ts +103 -0
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/claude-code.d.ts +59 -0
- package/dist/runtimes/claude-code.test.d.ts +14 -0
- package/dist/runtimes/claude.d.ts +16 -0
- package/dist/runtimes/claude.test.d.ts +8 -0
- package/dist/runtimes/codex.d.ts +9 -3
- package/dist/runtimes/cursor.d.ts +2 -2
- package/dist/runtimes/droid.d.ts +2 -2
- package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
- package/dist/runtimes/openai-desktop.js +2691 -864
- package/dist/runtimes/opencode.d.ts +2 -2
- package/dist/runtimes/vercel.js +12 -1
- package/dist/sandbox/devbox.d.ts +42 -0
- package/dist/sandbox/exec-stream.d.ts +14 -0
- package/dist/sandbox/network-policy.d.ts +100 -0
- package/dist/sandbox/provider-def.d.ts +79 -0
- package/dist/sandbox/providers/desktop.d.ts +10 -0
- package/dist/sandbox/providers/e2b.d.ts +17 -0
- package/dist/sandbox/providers/local.d.ts +11 -0
- package/dist/sandbox/providers/vercel.d.ts +18 -0
- package/dist/sandbox/registry.d.ts +45 -0
- package/dist/sandbox/sizes.d.ts +68 -0
- package/dist/sandbox.d.ts +24 -299
- package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
- package/dist/step-invocation/invoker.d.ts +10 -0
- package/dist/step-invocation/protocol.d.ts +5 -0
- package/dist/types/api-compliance.d.ts +71 -0
- package/dist/types/api-conversations.d.ts +492 -0
- package/dist/types/api-factory.d.ts +309 -0
- package/dist/types/api-projects.d.ts +131 -0
- package/dist/types/api-runs.d.ts +377 -0
- package/dist/types/api-scopes.d.ts +102 -0
- package/dist/types/conversation-stream.d.ts +191 -0
- package/dist/types/execution-context.d.ts +12 -2
- package/dist/types/protocol.d.ts +30 -1
- package/dist/types/sandbox-environment.d.ts +8 -5
- package/dist/types/sandbox.d.ts +74 -4
- package/dist/types/workflow-metadata.d.ts +33 -8
- package/dist/types/workflow-plan.d.ts +10 -0
- package/dist/types/workflow.d.ts +18 -205
- package/dist/utils/bundler.d.ts +12 -1
- package/dist/workflow-steps/index.d.ts +1 -1
- package/dist/workflow-steps/observability.d.ts +8 -1
- package/dist/workflow-steps/runner.d.ts +3 -3
- package/dist/workflow-steps/step.d.ts +15 -1
- package/dist/workflow-steps/types.d.ts +19 -5
- package/dist/workflow-steps/workflow.d.ts +22 -1
- package/dist/workflows/engine.d.ts +3 -2
- package/dist/workflows/invoke-child.d.ts +2 -2
- package/package.json +1 -1
- package/src/agent/agent-context.ts +186 -3
- package/src/agent/agent-loop.ts +31 -2
- package/src/client.ts +909 -621
- package/src/directives.ts +184 -0
- package/src/display.ts +788 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +104 -10
- package/src/pause/wrappers.ts +44 -9
- package/src/runtimes/_acp-client.ts +72 -3
- package/src/runtimes/_cli-agent.ts +159 -36
- package/src/runtimes/_jsonl-guard.ts +219 -0
- package/src/runtimes/claude-code.ts +246 -0
- package/src/runtimes/claude.ts +32 -2
- package/src/runtimes/codex.ts +55 -3
- package/src/runtimes/openai-desktop.ts +59 -14
- package/src/sandbox/devbox.ts +48 -0
- package/src/sandbox/exec-stream.ts +48 -0
- package/src/sandbox/network-policy.ts +181 -0
- package/src/sandbox/provider-def.ts +94 -0
- package/src/sandbox/providers/desktop.ts +57 -0
- package/src/sandbox/providers/e2b.ts +354 -0
- package/src/sandbox/providers/local.ts +106 -0
- package/src/sandbox/providers/vercel.ts +331 -0
- package/src/sandbox/registry.ts +198 -0
- package/src/sandbox/sizes.ts +95 -0
- package/src/sandbox.ts +59 -1275
- package/src/step-invocation/invoker.ts +151 -28
- package/src/step-invocation/protocol.ts +8 -0
- package/src/types/api-compliance.ts +79 -0
- package/src/types/api-conversations.ts +522 -0
- package/src/types/api-factory.ts +336 -0
- package/src/types/api-projects.ts +140 -0
- package/src/types/api-runs.ts +412 -0
- package/src/types/api-scopes.ts +102 -0
- package/src/types/conversation-stream.ts +231 -0
- package/src/types/execution-context.ts +10 -2
- package/src/types/protocol.ts +33 -0
- package/src/types/sandbox-environment.ts +28 -9
- package/src/types/sandbox.ts +73 -4
- package/src/types/workflow-metadata.ts +35 -8
- package/src/types/workflow-plan.ts +11 -0
- package/src/types/workflow.ts +25 -292
- package/src/utils/bundler.ts +32 -5
- package/src/utils/errors.ts +16 -1
- package/src/workflow-steps/index.ts +1 -0
- package/src/workflow-steps/observability.ts +19 -8
- package/src/workflow-steps/runner.ts +4 -4
- package/src/workflow-steps/step.ts +49 -1
- package/src/workflow-steps/types.ts +20 -5
- package/src/workflow-steps/workflow.ts +22 -1
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/invoke-child.ts +2 -2
package/src/types/workflow.ts
CHANGED
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow types —
|
|
2
|
+
* Workflow authoring types — `defineWorkflow` and the shared author-facing
|
|
3
|
+
* type surface.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* and to any helper that takes a SandboxProvider (git utilities, file
|
|
11
|
-
* writers). Constructed once per run; reuse it.
|
|
5
|
+
* Workflows are STEP-FORM only: a builder of discrete, durable steps —
|
|
6
|
+
* `defineWorkflow({ id, input, output }).step(defineStep(...)).build()`.
|
|
7
|
+
* Every step is a replay checkpoint; pause/resume works at step
|
|
8
|
+
* granularity. The legacy run-form (`defineWorkflow({ run(ctx, sandbox)
|
|
9
|
+
* { … } })`) has been removed — passing a `run` key throws at
|
|
10
|
+
* definition time (i.e. at registration, loud and early).
|
|
12
11
|
*
|
|
13
12
|
* LLM agent loops live in `agent(opts)` (sdk/src/agent/run-agent.ts).
|
|
14
13
|
* Invoking other workflows uses `AgentComposeClient.invoke[AndWait](...)`.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
19
|
-
import type { SandboxProvider } from "./sandbox.js";
|
|
20
16
|
import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
|
|
21
|
-
import type { Processor } from "../processors/processor.js";
|
|
22
17
|
import { createStepWorkflow } from "../workflow-steps/workflow.js";
|
|
23
|
-
import type { StepWorkflowDefinition } from "../workflow-steps/workflow.js";
|
|
24
|
-
import type { Step, Workflow } from "../workflow-steps/types.js";
|
|
25
|
-
import { WORKFLOW_BRAND } from "../workflow-steps/types.js";
|
|
26
|
-
import type { BaseExecutionContext, WorkflowRun } from "./execution-context.js";
|
|
27
|
-
import { extractMetadata, type WorkflowMetadata, type ConnectorRequirements, type ConnectorOperationTag, type InvokePolicy } from "./workflow-metadata.js";
|
|
18
|
+
import type { StepWorkflowDefinition, WorkflowBuilder } from "../workflow-steps/workflow.js";
|
|
28
19
|
|
|
29
20
|
export type { WorkflowRun, InvokeChild, BaseExecutionContext } from "./execution-context.js";
|
|
30
21
|
// Re-export so consumers can keep importing `WorkflowMetadata` from
|
|
31
22
|
// `@agent-compose/sdk` — the canonical definition lives in
|
|
32
23
|
// `./workflow-metadata.js` to break a runtime import cycle with
|
|
33
24
|
// `workflow-steps/workflow.ts`.
|
|
34
|
-
export type { WorkflowMetadata } from "./workflow-metadata.js";
|
|
25
|
+
export type { WorkflowMetadata, SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
|
|
35
26
|
|
|
36
27
|
export interface AgentEventSink {
|
|
37
28
|
emit(event: AgentLifecycleEvent): void | Promise<void>;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
|
|
41
|
-
export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources };
|
|
42
|
-
|
|
43
31
|
/** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
|
|
44
32
|
* can type per-invoke budget overrides they pass as workflow input. */
|
|
45
33
|
export interface AgentBudget {
|
|
@@ -47,282 +35,27 @@ export interface AgentBudget {
|
|
|
47
35
|
maxIterations: number;
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
/** Context passed to a workflow function — facts + observability for this run. */
|
|
51
|
-
export interface WorkflowCtx<TInput extends Record<string, unknown> = Record<string, unknown>> extends Omit<BaseExecutionContext, "sandbox"> {
|
|
52
|
-
input?: TInput;
|
|
53
|
-
/** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
|
|
54
|
-
setMetadata: (data: Record<string, unknown>) => Promise<void>;
|
|
55
|
-
/**
|
|
56
|
-
* Durable named step (ADR-0012). Runs `fn` once and memoises its result to
|
|
57
|
-
* the sandbox state-dir; on a pause-resume re-entry the recorded value is
|
|
58
|
-
* returned and `fn` is NOT re-run (a duration-0 "restored" sub-step). Also
|
|
59
|
-
* emits substep_started / substep_completed / substep_failed lifecycle
|
|
60
|
-
* events with duration — use for long phases you want both durable and
|
|
61
|
-
* visible on the run's timeline (setup, external API calls, submit).
|
|
62
|
-
*
|
|
63
|
-
* Names must be unique within a step body (they key the memoise file).
|
|
64
|
-
* A body that pauses is never memoised — the resume re-runs it. For
|
|
65
|
-
* cross-process side effects (DB writes, emails) use `invokeChild`.
|
|
66
|
-
*/
|
|
67
|
-
step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
68
|
-
/** Pass to `agent({ events: ctx.agentEvents })` to stream agent lifecycle events. */
|
|
69
|
-
agentEvents: AgentEventSink;
|
|
70
|
-
/**
|
|
71
|
-
* Workflow-level processors registered via `defineWorkflow({ processors })`.
|
|
72
|
-
* Read-only here. Authors merge with agent-specific lists when calling
|
|
73
|
-
* `agent({ processors: [...ctx.processors, mySpecific] })`.
|
|
74
|
-
*
|
|
75
|
-
* Empty array when the workflow declared no processors.
|
|
76
|
-
*/
|
|
77
|
-
processors: readonly Processor[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** A workflow is `async (ctx, sandbox) => TOutput`. */
|
|
81
|
-
export type WorkflowFn<
|
|
82
|
-
TOutput = unknown,
|
|
83
|
-
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
84
|
-
> = (ctx: WorkflowCtx<TInput>, sandbox: SandboxProvider) => Promise<TOutput>;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Declare a workflow with server-side metadata.
|
|
88
|
-
* Use `export default defineWorkflow({ run, networkPolicy, ... })` to attach
|
|
89
|
-
* a runner network policy so the server brokers credentials for the workflow
|
|
90
|
-
* sandbox.
|
|
91
|
-
*
|
|
92
|
-
* Without defineWorkflow, a plain `export default async (ctx) => {...}` still
|
|
93
|
-
* works — the workflow just runs with secrets passed directly in env.
|
|
94
|
-
*/
|
|
95
|
-
export interface WorkflowDefinition<
|
|
96
|
-
TOutput = unknown,
|
|
97
|
-
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
98
|
-
> {
|
|
99
|
-
/** One-line, human-readable description of what this workflow does.
|
|
100
|
-
* Surfaced on the dashboard template tile + run page header. */
|
|
101
|
-
description?: string;
|
|
102
|
-
/** Zod schema for the workflow's `input`. When declared, the SDK
|
|
103
|
-
* bundler captures it as JSON-Schema-shaped `inputSchema` in
|
|
104
|
-
* template metadata, the dashboard playground renders a typed
|
|
105
|
-
* form, and the engine validates dispatched payloads against it
|
|
106
|
-
* at the step boundary. Omit to leave inputs as `unknown` (the
|
|
107
|
-
* legacy default — playground falls back to a freeform JSON
|
|
108
|
-
* textarea). */
|
|
109
|
-
input?: z.ZodType<TInput>;
|
|
110
|
-
/** Same as `input`, for the workflow's return value. Captured into
|
|
111
|
-
* `outputSchema` metadata and rendered in the IO panel. */
|
|
112
|
-
output?: z.ZodType<TOutput>;
|
|
113
|
-
/**
|
|
114
|
-
* @deprecated Legacy run-form. Prefer step-form — the
|
|
115
|
-
* `.step(defineStep(...))` builder — for per-step durability/replay and
|
|
116
|
-
* working pause. A run-form body compiles to one opaque step
|
|
117
|
-
* (`compileRunForm`), so any failure/resume re-runs the whole body.
|
|
118
|
-
*/
|
|
119
|
-
run: WorkflowFn<TOutput, TInput>;
|
|
120
|
-
/**
|
|
121
|
-
* All snapshot config — boot source plus capture mode.
|
|
122
|
-
*
|
|
123
|
-
* `snapshots.bootFrom`: the runner restores from this exact provider
|
|
124
|
-
* snapshot id at run start. Omit to boot a fresh sandbox.
|
|
125
|
-
*
|
|
126
|
-
* `snapshots.saveLatest`: `true` captures one snapshot after each
|
|
127
|
-
* successful step (latest-only — prior is freed).
|
|
128
|
-
* `{ saveLatest: true, retainSteps: true }` keeps every step's
|
|
129
|
-
* snapshot for fork / replay / time-travel.
|
|
130
|
-
*
|
|
131
|
-
* Snapshots are long-lived (never auto-expire). List + delete via
|
|
132
|
-
* `agentc snapshot list/delete`. Per-invocation
|
|
133
|
-
* `invoke({ snapshots })` overrides this default.
|
|
134
|
-
*/
|
|
135
|
-
snapshots?: SnapshotConfig;
|
|
136
|
-
/**
|
|
137
|
-
* Sandbox resources — machine SKU (`size`, a `SandboxSize` vCPU string:
|
|
138
|
-
* `2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`) and `provider`
|
|
139
|
-
* (`vercel` | `e2b`). `size` maps to provider machine specs at create
|
|
140
|
-
* (Vercel: vCPUs, 2048 MB RAM per vCPU); E2B sizing is template-defined and
|
|
141
|
-
* ignores it. Optional; omit → smallest SKU on the default provider.
|
|
142
|
-
* Per-invocation `invoke({ size })` overrides the size.
|
|
143
|
-
*/
|
|
144
|
-
resources?: SandboxResources;
|
|
145
|
-
/**
|
|
146
|
-
* Outbound network policy for the runner sandbox.
|
|
147
|
-
* Use "*": [] to allow all traffic while still injecting headers for specific domains.
|
|
148
|
-
* Secret values are referenced as $VARIABLE and resolved from GCP at dispatch time.
|
|
149
|
-
* Vercel only — E2B ignores.
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* networkPolicy: {
|
|
153
|
-
* allow: {
|
|
154
|
-
* "*": [],
|
|
155
|
-
* "api.github.com": [{ transform: [{ headers: { "Authorization": "basic:$GITHUB_TOKEN" } }] }],
|
|
156
|
-
* }
|
|
157
|
-
* }
|
|
158
|
-
*/
|
|
159
|
-
networkPolicy?: SandboxNetworkPolicy;
|
|
160
|
-
/**
|
|
161
|
-
* Workflow-level processor chain — runs around every `agent(...)` loop the
|
|
162
|
-
* workflow body launches, ahead of any agent-specific processors. Use for
|
|
163
|
-
* org-wide gating (deny destructive tools, require scopes). Authors merge
|
|
164
|
-
* with agent-specific lists via `[...ctx.processors, ...]`.
|
|
165
|
-
*/
|
|
166
|
-
processors?: readonly Processor[];
|
|
167
|
-
/**
|
|
168
|
-
* Optional placeholder env var values for secrets referenced in the network policy.
|
|
169
|
-
* By default, brokered secrets are removed from the runner env entirely — the real
|
|
170
|
-
* values are only ever present inside the Vercel firewall config, never in the VM.
|
|
171
|
-
* Only set this if a tool or SDK validates the env var format on startup before
|
|
172
|
-
* making any requests (e.g. some CLIs check that ANTHROPIC_API_KEY looks like a
|
|
173
|
-
* real key). The placeholder is a syntactically valid but non-functional stand-in.
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* placeholders: {
|
|
177
|
-
* ANTHROPIC_API_KEY: `sk-ant-api03-${"a".repeat(95)}`,
|
|
178
|
-
* }
|
|
179
|
-
*/
|
|
180
|
-
placeholders?: Record<string, string>;
|
|
181
|
-
/**
|
|
182
|
-
* Connector requirements (ADR-0007). Declaring a provider makes the
|
|
183
|
-
* server resolve an authorized OAuth grant for the calling principal at
|
|
184
|
-
* dispatch time and inject a fresh access token at the network layer —
|
|
185
|
-
* workflow code talks to the provider API with plain fetch/SDKs and
|
|
186
|
-
* never holds the credential.
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* connectors: { github: { scopes: ["repo"] } }
|
|
190
|
-
*/
|
|
191
|
-
connectors?: ConnectorRequirements;
|
|
192
|
-
/**
|
|
193
|
-
* Marks this workflow as a catalogue OPERATION of a connector — e.g.
|
|
194
|
-
* the `create-issue` operation of the `github` connector. Pair with
|
|
195
|
-
* `description` + `input`/`output` schemas so the operation is fully
|
|
196
|
-
* self-describing (MCP-tool-like) to humans and agents browsing the
|
|
197
|
-
* connector catalogue.
|
|
198
|
-
*
|
|
199
|
-
* @example
|
|
200
|
-
* connectorOperation: { provider: "github", operation: "create-issue" }
|
|
201
|
-
*/
|
|
202
|
-
connectorOperation?: ConnectorOperationTag;
|
|
203
|
-
/**
|
|
204
|
-
* Tier-1 invoke ACL (connector credential boundary). When this workflow
|
|
205
|
-
* declares `connectors` (it brokers a credential) AND an `invokePolicy`,
|
|
206
|
-
* the server evaluates the calling principal against the policy BEFORE
|
|
207
|
-
* binding any grant — a caller matching no clause is refused with HTTP
|
|
208
|
-
* 403. Has no effect on workflows that declare no connectors. Omit to
|
|
209
|
-
* leave the workflow invokable by the whole team.
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* connectors: { github: { access: "read" } },
|
|
213
|
-
* invokePolicy: { users: "owner", workflows: ["nightly-orchestrator"] }
|
|
214
|
-
*/
|
|
215
|
-
invokePolicy?: InvokePolicy;
|
|
216
|
-
/**
|
|
217
|
-
* Internal — set by `defineSandboxEnvironment`, not by workflow authors.
|
|
218
|
-
* Marks the workflow as an environment build (base-env / agent-env) so the
|
|
219
|
-
* server skips mounting the shared factory drive for its runs (#13). See
|
|
220
|
-
* `WorkflowMetadata.environmentBuild`.
|
|
221
|
-
*/
|
|
222
|
-
environmentBuild?: boolean;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
38
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* `ctx.agentEvents` proxy directly to the underlying `StepContext` hooks,
|
|
230
|
-
* which the engine flushes to the run timeline exactly as the legacy
|
|
231
|
-
* full-mode runner did.
|
|
39
|
+
* Declare a workflow — the typed step builder. Multiple steps with
|
|
40
|
+
* explicit input/output schemas, durability + replay at every step
|
|
41
|
+
* boundary:
|
|
232
42
|
*
|
|
233
|
-
*
|
|
234
|
-
* `ctx.step("name", fn)` calls inside the body become sub-step events
|
|
235
|
-
* under that one step. Authors keep the same source; the dashboard sees
|
|
236
|
-
* the same timeline shape.
|
|
237
|
-
*/
|
|
238
|
-
// TODO: collapse run-form into pure sugar over step-form. Run-form
|
|
239
|
-
// already compiles to step-form-with-one-step here, so maintaining two
|
|
240
|
-
// authoring surfaces is API duplication that periodically drifts (see
|
|
241
|
-
// the input/output thread-through bug fixed on 2026-05-20: step-form
|
|
242
|
-
// preserved schemas, run-form silently stamped z.unknown()). Cleaner:
|
|
243
|
-
// turn `defineWorkflow({ run })` into a thin wrapper that calls the
|
|
244
|
-
// step builder with a synthesised `{name:"run", input, output, run}`
|
|
245
|
-
// step, then delete this function. Runtime stays identical.
|
|
246
|
-
function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
|
|
247
|
-
def: WorkflowDefinition<TOutput, TInput>,
|
|
248
|
-
metadata: WorkflowMetadata,
|
|
249
|
-
): Workflow<TInput, TOutput> {
|
|
250
|
-
const unknownSchema = z.unknown() as z.ZodType<unknown>;
|
|
251
|
-
const inputSchema: z.ZodType<TInput> = def.input ?? (unknownSchema as z.ZodType<TInput>);
|
|
252
|
-
const outputSchema: z.ZodType<TOutput> = def.output ?? (unknownSchema as z.ZodType<TOutput>);
|
|
253
|
-
const step: Step<TInput, TOutput> = {
|
|
254
|
-
name: "run",
|
|
255
|
-
input: inputSchema,
|
|
256
|
-
output: outputSchema,
|
|
257
|
-
run: async (stepCtx) => {
|
|
258
|
-
// Proxy the legacy WorkflowCtx hooks to the step's collector-backed
|
|
259
|
-
// implementations. The engine harvests the snapshot after execute
|
|
260
|
-
// returns; nothing here is a no-op.
|
|
261
|
-
const workflowCtx: WorkflowCtx<TInput> = {
|
|
262
|
-
input: stepCtx.input,
|
|
263
|
-
run: stepCtx.run,
|
|
264
|
-
requestContext: stepCtx.requestContext,
|
|
265
|
-
invokeChild: stepCtx.invokeChild,
|
|
266
|
-
setMetadata: stepCtx.setMetadata,
|
|
267
|
-
step: stepCtx.step,
|
|
268
|
-
agentEvents: stepCtx.agentEvents,
|
|
269
|
-
pause: stepCtx.pause,
|
|
270
|
-
sleep: stepCtx.sleep,
|
|
271
|
-
waitForEvent: stepCtx.waitForEvent,
|
|
272
|
-
processors: metadata.processors ?? [],
|
|
273
|
-
};
|
|
274
|
-
const sandbox = stepCtx.sandbox;
|
|
275
|
-
if (!sandbox) throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
|
276
|
-
return def.run(workflowCtx, sandbox);
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
const workflow: Workflow<TInput, TOutput> = {
|
|
280
|
-
id: "@run-form",
|
|
281
|
-
input: inputSchema,
|
|
282
|
-
output: outputSchema,
|
|
283
|
-
steps: Object.freeze([step]),
|
|
284
|
-
metadata,
|
|
285
|
-
};
|
|
286
|
-
// Brand BEFORE freezing — defineProperty is the only way to set a
|
|
287
|
-
// non-enumerable key, and frozen objects refuse it.
|
|
288
|
-
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
289
|
-
return Object.freeze(workflow);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Declare a workflow. Two forms; both return a `Workflow` whose
|
|
294
|
-
* `metadata` field carries the server-readable declarations.
|
|
43
|
+
* defineWorkflow({ id, input, output }).step(defineStep(...)).build()
|
|
295
44
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* Step form — the typed builder. Multiple steps with explicit input/
|
|
300
|
-
* output schemas, durability + replay at every step boundary.
|
|
301
|
-
*
|
|
302
|
-
* Both forms produce the same downstream shape: the bundler reads
|
|
303
|
-
* `workflow.metadata.networkPolicy` / `placeholders` / etc.; the server
|
|
304
|
-
* stores the step plan; runner subprocesses execute one step at a time
|
|
305
|
-
* via the StepInvocation seam.
|
|
306
|
-
*/
|
|
307
|
-
/**
|
|
308
|
-
* @deprecated Run-form is legacy. Use the step-form overload —
|
|
309
|
-
* `defineWorkflow({ id, input, output }).step(defineStep(...)).build()` — for
|
|
310
|
-
* durable, replayable steps and working pause. Run-form compiles to a single
|
|
311
|
-
* opaque step (`compileRunForm`); there is no per-step replay.
|
|
45
|
+
* The bundler reads `workflow.metadata.networkPolicy` / `placeholders` /
|
|
46
|
+
* etc. off the built `Workflow`; the server stores the step plan; runner
|
|
47
|
+
* subprocesses execute one step at a time via the StepInvocation seam.
|
|
312
48
|
*/
|
|
313
|
-
export function defineWorkflow<
|
|
314
|
-
TOutput = unknown,
|
|
315
|
-
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
316
|
-
>(
|
|
317
|
-
def: WorkflowDefinition<TOutput, TInput>,
|
|
318
|
-
): Workflow<TInput, TOutput>;
|
|
319
49
|
export function defineWorkflow<TInput, TOutput>(
|
|
320
50
|
def: StepWorkflowDefinition<TInput, TOutput>,
|
|
321
|
-
):
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
51
|
+
): WorkflowBuilder<TInput, TInput> {
|
|
52
|
+
if ("run" in def) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"defineWorkflow: the legacy run-form (`defineWorkflow({ run(ctx, sandbox) { … } })`) has been removed. " +
|
|
55
|
+
"Author workflows in step-form — `defineWorkflow({ id, input, output }).step(defineStep(...)).build()` — " +
|
|
56
|
+
"or run /ac:generate-workflow to scaffold the current shape.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
326
59
|
return createStepWorkflow(def);
|
|
327
60
|
}
|
|
328
61
|
|
package/src/utils/bundler.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { parse as babelParse } from "@babel/parser";
|
|
|
25
25
|
import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
|
|
26
26
|
import { importSourceModule } from "./source-loader.js";
|
|
27
27
|
import type { SnapshotConfig } from "../types/workflow.js";
|
|
28
|
-
import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
|
|
28
|
+
import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources, DriveMergePolicy } from "../types/workflow-metadata.js";
|
|
29
29
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
30
30
|
import { isWorkflow } from "../workflow-steps/workflow.js";
|
|
31
31
|
import type { Workflow } from "../workflow-steps/types.js";
|
|
@@ -101,6 +101,9 @@ export interface BundledWorkflow {
|
|
|
101
101
|
/** Sandbox resources declared via `defineWorkflow({ resources })` — machine
|
|
102
102
|
* SKU (`size`) and `provider` (`vercel` | `e2b`). */
|
|
103
103
|
resources?: SandboxResources;
|
|
104
|
+
/** Run-branch merge policy declared via `defineWorkflow({ mergePolicy })`.
|
|
105
|
+
* Absent ⇒ `"auto"` (today's behaviour). */
|
|
106
|
+
mergePolicy?: DriveMergePolicy;
|
|
104
107
|
workflowPlan: WorkflowPlan;
|
|
105
108
|
/** Compact JSON-Schema-shaped description of the workflow's input
|
|
106
109
|
* type. Extracted from the workflow's declared `input` zod schema
|
|
@@ -173,7 +176,7 @@ async function extractFromBundle<T>(
|
|
|
173
176
|
* the latter would also accept `defineWorkflowAttacker`. The runtime brand
|
|
174
177
|
* check is the second gate, but the syntactic check should be tight.
|
|
175
178
|
*
|
|
176
|
-
* `defineSandboxEnvironment` is sugar over
|
|
179
|
+
* `defineSandboxEnvironment` is sugar over the step builder (see
|
|
177
180
|
* `src/types/sandbox-environment.ts`) and is explicitly registerable via
|
|
178
181
|
* `agentc register setup.ts --build` to capture a snapshot. The AST gate
|
|
179
182
|
* accepts it for the same reason it accepts `defineWorkflow`: the bundled
|
|
@@ -284,6 +287,28 @@ function sha256(text: string): string {
|
|
|
284
287
|
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The registration-time step plan read off an (already evaluated) workflow
|
|
292
|
+
* object. Narrative fields (`summary`, `deliverables`) ride along additively;
|
|
293
|
+
* conditional spreads keep absent fields ABSENT — the plan travels as JSON,
|
|
294
|
+
* so no `undefined` keys. Exported for tests.
|
|
295
|
+
*/
|
|
296
|
+
export function extractWorkflowPlan(workflow: Workflow<unknown, unknown>): WorkflowPlan {
|
|
297
|
+
return workflowPlan(workflow.steps.map((step, index) => ({
|
|
298
|
+
index,
|
|
299
|
+
name: step.name,
|
|
300
|
+
...(step.summary !== undefined ? { summary: step.summary } : {}),
|
|
301
|
+
...(step.deliverables?.length
|
|
302
|
+
? {
|
|
303
|
+
deliverables: step.deliverables.map((d) => ({
|
|
304
|
+
path: d.path,
|
|
305
|
+
...(d.description !== undefined ? { description: d.description } : {}),
|
|
306
|
+
})),
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
309
|
+
})));
|
|
310
|
+
}
|
|
311
|
+
|
|
287
312
|
/** Bundle a workflow from source. */
|
|
288
313
|
export async function bundleWorkflow(
|
|
289
314
|
workflowPath: string,
|
|
@@ -312,7 +337,7 @@ export async function bundleWorkflow(
|
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
const { metadata } = workflow;
|
|
315
|
-
const plan
|
|
340
|
+
const plan = extractWorkflowPlan(workflow);
|
|
316
341
|
const manifest: WorkflowManifest = {
|
|
317
342
|
definitionBrand: true,
|
|
318
343
|
sourceHash: sha256(source),
|
|
@@ -341,6 +366,7 @@ export async function bundleWorkflow(
|
|
|
341
366
|
...(outputSchema !== undefined ? { outputSchema } : {}),
|
|
342
367
|
...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
|
|
343
368
|
...(metadata.resources !== undefined ? { resources: metadata.resources } : {}),
|
|
369
|
+
...(metadata.mergePolicy !== undefined ? { mergePolicy: metadata.mergePolicy } : {}),
|
|
344
370
|
...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
|
|
345
371
|
...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
|
|
346
372
|
...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
|
|
@@ -356,8 +382,9 @@ export async function bundleWorkflow(
|
|
|
356
382
|
*
|
|
357
383
|
* Returns undefined when:
|
|
358
384
|
* - The schema can't be serialised (corrupt / unknown variant)
|
|
359
|
-
* - The schema is effectively `unknown` (the
|
|
360
|
-
*
|
|
385
|
+
* - The schema is effectively `unknown` (e.g. the
|
|
386
|
+
* `defineSandboxEnvironment` sugar's schemas — no meaningful
|
|
387
|
+
* contract to render). */
|
|
361
388
|
function extractIOSchema(
|
|
362
389
|
zodSchema: { _zod?: unknown } | unknown,
|
|
363
390
|
): IOSchema | undefined {
|
package/src/utils/errors.ts
CHANGED
|
@@ -8,7 +8,22 @@
|
|
|
8
8
|
* so the root cause is always visible. Cycle-guarded against self-referential
|
|
9
9
|
* `cause` links. */
|
|
10
10
|
export function formatError(err: unknown): string {
|
|
11
|
-
if (!(err instanceof Error))
|
|
11
|
+
if (!(err instanceof Error)) {
|
|
12
|
+
// Parsed-JSON error payloads (a CLI runtime's `{"error":{"message":…}}`,
|
|
13
|
+
// a provider body) are plain objects, and `String({...})` is the literal
|
|
14
|
+
// "[object Object]" — the exact string that reached session transcripts.
|
|
15
|
+
// Prefer the conventional `.message`; otherwise show the JSON itself.
|
|
16
|
+
if (typeof err === "object" && err !== null) {
|
|
17
|
+
const msg = (err as { message?: unknown }).message;
|
|
18
|
+
if (typeof msg === "string" && msg.length > 0) return msg;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(err) ?? String(err);
|
|
21
|
+
} catch {
|
|
22
|
+
return String(err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return String(err);
|
|
26
|
+
}
|
|
12
27
|
const parts: string[] = [err.message];
|
|
13
28
|
const seen = new Set<unknown>([err]);
|
|
14
29
|
let cause: unknown = (err as { cause?: unknown }).cause;
|
|
@@ -56,7 +56,12 @@ export interface SubStepEvent {
|
|
|
56
56
|
* empty snapshot (and the wire payload omits the field entirely). */
|
|
57
57
|
export interface StepObservability {
|
|
58
58
|
metadata?: Record<string, unknown>;
|
|
59
|
-
events
|
|
59
|
+
/** Residual events that failed live delivery, each carrying its ORIGINAL
|
|
60
|
+
* emit-time `seq`. The server keys its idempotency on that seq — the SAME
|
|
61
|
+
* key the live route used — so a live write whose ack was lost collapses
|
|
62
|
+
* on redelivery instead of duplicating. Array position is NOT the seq:
|
|
63
|
+
* acked events are stripped, so indices shift. */
|
|
64
|
+
events?: Array<AgentLifecycleEvent & { seq: number }>;
|
|
60
65
|
subSteps?: SubStepEvent[];
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -163,11 +168,12 @@ export class StepObservabilityCollector {
|
|
|
163
168
|
|
|
164
169
|
readonly agentEvents: AgentEventSink = {
|
|
165
170
|
emit: (event: AgentLifecycleEvent) => {
|
|
166
|
-
// `seq` is the event's index in `events`, captured BEFORE
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
+
// `seq` is the event's emit-time index in `events`, captured BEFORE
|
|
172
|
+
// push. It is THE idempotency handle on both delivery paths: the
|
|
173
|
+
// live POST carries it per event, and `snapshot()` stamps it onto
|
|
174
|
+
// every residual event so the server's batch backstop builds the
|
|
175
|
+
// byte-identical key. The live route's `acceptedSeqs` response uses
|
|
176
|
+
// this seq; we strip acked events in `snapshot()`.
|
|
171
177
|
const seq = this.events.length;
|
|
172
178
|
this.events.push(event);
|
|
173
179
|
if (this.liveEmitter) {
|
|
@@ -206,8 +212,13 @@ export class StepObservabilityCollector {
|
|
|
206
212
|
|
|
207
213
|
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
208
214
|
const hasSubSteps = this.subSteps.length > 0;
|
|
209
|
-
// Filter out ack'd events — the live route already wrote them
|
|
210
|
-
|
|
215
|
+
// Filter out ack'd events — the live route already wrote them — and
|
|
216
|
+
// stamp each survivor with its ORIGINAL emit-time seq. Filtering shifts
|
|
217
|
+
// array positions, so the seq must travel explicitly for the server's
|
|
218
|
+
// backstop to reproduce the live path's idempotency key.
|
|
219
|
+
const remainingEvents = this.events
|
|
220
|
+
.map((event, seq) => ({ ...event, seq }))
|
|
221
|
+
.filter(({ seq }) => !this.ackedSeqs.has(seq));
|
|
211
222
|
const hasEvents = remainingEvents.length > 0;
|
|
212
223
|
|
|
213
224
|
if (!hasMetadata && !hasEvents && !hasSubSteps) return undefined;
|
|
@@ -26,7 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
import type { Workflow, StepContext, StepRunResult } from "./types.js";
|
|
27
27
|
import type { RequestContext } from "../request-context/request-context.js";
|
|
28
28
|
import type { SandboxProvider } from "../types/sandbox.js";
|
|
29
|
-
import type { WorkflowRun,
|
|
29
|
+
import type { WorkflowRun, InvokeChild } from "../types/execution-context.js";
|
|
30
30
|
import { StepObservabilityCollector, type StepObservability } from "./observability.js";
|
|
31
31
|
import type { LiveAgentEventEmitter } from "./run-callback.js";
|
|
32
32
|
import { scopedMemoize } from "../pause/checkpoint.js";
|
|
@@ -89,7 +89,7 @@ export interface RunWorkflowStepsOpts<TInput, TOutput> {
|
|
|
89
89
|
/** Fire before a step runs (after cache check / before input validation). */
|
|
90
90
|
onStepStarted?(stepIndex: number, stepName: string): void | Promise<void>;
|
|
91
91
|
/** Child workflow invocation implementation. Defaults to a clear unsupported error. */
|
|
92
|
-
invokeChild?:
|
|
92
|
+
invokeChild?: InvokeChild;
|
|
93
93
|
/** Optional live-stream emitter for agent lifecycle events. The runner
|
|
94
94
|
* passes a fetch-based emitter wired to the per-run callback token so
|
|
95
95
|
* the dashboard sees events as the agent loop produces them; tests
|
|
@@ -111,7 +111,7 @@ export interface RunWorkflowSingleStepOpts {
|
|
|
111
111
|
requestContext: RequestContext;
|
|
112
112
|
sandbox?: SandboxProvider;
|
|
113
113
|
abortSignal?: AbortSignal;
|
|
114
|
-
invokeChild?:
|
|
114
|
+
invokeChild?: InvokeChild;
|
|
115
115
|
/** Optional live-stream emitter — see `RunWorkflowStepsOpts.liveAgentEventEmitter`. */
|
|
116
116
|
liveAgentEventEmitter?: LiveAgentEventEmitter;
|
|
117
117
|
}
|
|
@@ -189,7 +189,7 @@ export async function runWorkflowSteps<TInput, TOutput>(
|
|
|
189
189
|
): Promise<RunWorkflowStepsResult<TOutput>> {
|
|
190
190
|
const { workflow, run, requestContext } = opts;
|
|
191
191
|
const abortSignal = opts.abortSignal ?? new AbortController().signal;
|
|
192
|
-
const invokeChild:
|
|
192
|
+
const invokeChild: InvokeChild = opts.invokeChild ?? (() => {
|
|
193
193
|
throw new Error("StepContext.invokeChild is not configured for this workflow engine");
|
|
194
194
|
});
|
|
195
195
|
|
|
@@ -7,32 +7,80 @@
|
|
|
7
7
|
* - `output` Zod schema; validated against `run`'s return value
|
|
8
8
|
* - `run` step body
|
|
9
9
|
*
|
|
10
|
+
* Optional narrative fields (rendered on the dashboard workflow graph):
|
|
11
|
+
* - `summary` one plain sentence of what the step actually does
|
|
12
|
+
* - `deliverables` files the step promises to produce
|
|
13
|
+
*
|
|
10
14
|
* Validation is required (not optional) because the durability story rides
|
|
11
15
|
* on every step boundary being a recordable, replayable JSON value. A step
|
|
12
16
|
* without a schema is invisible to the engine's persistence layer.
|
|
13
17
|
*
|
|
18
|
+
* Narrative caps are enforced HERE, at construction — the bundler evaluates
|
|
19
|
+
* the module at registration, so this is the "length-capped at registration"
|
|
20
|
+
* point and the error names the step in the author's own environment. The
|
|
21
|
+
* server's manifest zod re-checks the same caps (untrusted input).
|
|
22
|
+
*
|
|
14
23
|
* Type inference flows: `defineStep` infers TInput/TOutput from the schemas
|
|
15
24
|
* so `run(ctx)` is fully typed via `ctx.input` at the call site.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
import type { z } from "zod";
|
|
19
|
-
import type { Step, StepContext } from "./types.js";
|
|
28
|
+
import type { Step, StepContext, StepDeliverable } from "./types.js";
|
|
20
29
|
|
|
21
30
|
export interface DefineStepOpts<TInput, TOutput> {
|
|
22
31
|
name: string;
|
|
23
32
|
input: z.ZodType<TInput>;
|
|
24
33
|
output: z.ZodType<TOutput>;
|
|
25
34
|
run(ctx: StepContext<TInput>): TOutput | Promise<TOutput>;
|
|
35
|
+
/** One plain sentence of what the step actually does. 1-200 chars, no
|
|
36
|
+
* control characters (so no newlines). */
|
|
37
|
+
summary?: string;
|
|
38
|
+
/** Files the step promises to produce. At most 8. */
|
|
39
|
+
deliverables?: StepDeliverable[];
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
// C0 controls + DEL — narrative text is one-line prose, never structural.
|
|
43
|
+
// eslint-disable-next-line no-control-regex
|
|
44
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
45
|
+
|
|
28
46
|
export function defineStep<TInput, TOutput>(
|
|
29
47
|
opts: DefineStepOpts<TInput, TOutput>,
|
|
30
48
|
): Step<TInput, TOutput> {
|
|
31
49
|
if (!opts.name) throw new Error("defineStep: 'name' is required");
|
|
50
|
+
if (opts.summary !== undefined) {
|
|
51
|
+
if (opts.summary.length === 0 || opts.summary.length > 200 || CONTROL_CHARS.test(opts.summary)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`defineStep("${opts.name}"): 'summary' must be one plain sentence, 1-200 characters, no control characters`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (opts.deliverables !== undefined) {
|
|
58
|
+
if (opts.deliverables.length > 8) {
|
|
59
|
+
throw new Error(`defineStep("${opts.name}"): at most 8 deliverables`);
|
|
60
|
+
}
|
|
61
|
+
for (const d of opts.deliverables) {
|
|
62
|
+
if (!d.path || d.path.length > 200 || CONTROL_CHARS.test(d.path)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`defineStep("${opts.name}"): each deliverable needs a 'path' of 1-200 characters, no control characters`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (d.description !== undefined && (d.description.length === 0 || d.description.length > 200 || CONTROL_CHARS.test(d.description))) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`defineStep("${opts.name}"): deliverable descriptions must be 1-200 characters, no control characters`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Conditional spreads keep absent fields ABSENT (no `undefined` keys —
|
|
75
|
+
// matters for the canonical-hash discipline used elsewhere).
|
|
32
76
|
return {
|
|
33
77
|
name: opts.name,
|
|
34
78
|
input: opts.input,
|
|
35
79
|
output: opts.output,
|
|
36
80
|
run: opts.run,
|
|
81
|
+
...(opts.summary !== undefined ? { summary: opts.summary } : {}),
|
|
82
|
+
...(opts.deliverables !== undefined
|
|
83
|
+
? { deliverables: Object.freeze(opts.deliverables.map((d) => ({ ...d }))) }
|
|
84
|
+
: {}),
|
|
37
85
|
};
|
|
38
86
|
}
|