@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.
- package/README.md +145 -33
- package/dist/agent/agent-loop.d.ts +83 -5
- package/dist/agent/run-agent.d.ts +34 -9
- package/dist/client.d.ts +247 -99
- package/dist/index.d.ts +26 -11
- package/dist/index.js +1967 -745
- package/dist/processors/builtins.d.ts +35 -0
- package/dist/processors/index.d.ts +4 -0
- package/dist/processors/processor.d.ts +91 -0
- package/dist/processors/processor.test.d.ts +1 -0
- package/dist/processors/runner.d.ts +19 -0
- package/dist/request-context/index.d.ts +2 -0
- package/dist/request-context/request-context.d.ts +159 -0
- package/dist/request-context/request-context.test.d.ts +1 -0
- package/dist/runtimes/claude.d.ts +27 -50
- package/dist/runtimes/openai-desktop.js +1918 -741
- package/dist/runtimes/vercel.d.ts +34 -0
- package/dist/runtimes/vercel.js +474 -0
- package/dist/sandbox.d.ts +29 -25
- package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
- package/dist/step-invocation/index.d.ts +25 -0
- package/dist/step-invocation/invoker.d.ts +65 -0
- package/dist/step-invocation/protocol.d.ts +44 -0
- package/dist/step-invocation/server.d.ts +63 -0
- package/dist/step-invocation/types.d.ts +72 -0
- package/dist/tools/coding.d.ts +49 -0
- package/dist/tools/coding.test.d.ts +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/events.d.ts +36 -0
- package/dist/types/execution-context.d.ts +22 -0
- package/dist/types/runtime.d.ts +32 -0
- package/dist/types/sandbox-environment.d.ts +5 -2
- package/dist/types/sandbox.d.ts +14 -12
- package/dist/types/workflow-metadata.d.ts +51 -0
- package/dist/types/workflow-plan.d.ts +19 -0
- package/dist/types/workflow.d.ts +57 -17
- package/dist/utils/bundler.d.ts +62 -3
- package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +10 -0
- package/dist/workflow-steps/observability.d.ts +58 -0
- package/dist/workflow-steps/runner.d.ts +96 -0
- package/dist/workflow-steps/step.d.ts +25 -0
- package/dist/workflow-steps/types.d.ts +135 -0
- package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
- package/dist/workflow-steps/workflow.d.ts +50 -0
- package/dist/workflows/engine.d.ts +27 -13
- package/dist/workflows/invoke-child.d.ts +10 -0
- package/package.json +25 -15
- package/src/agent/agent-loop.ts +197 -26
- package/src/agent/run-agent.ts +40 -15
- package/src/client.ts +326 -76
- package/src/index.ts +124 -10
- package/src/processors/builtins.ts +72 -0
- package/src/processors/index.ts +15 -0
- package/src/processors/processor.ts +103 -0
- package/src/processors/runner.ts +42 -0
- package/src/request-context/index.ts +17 -0
- package/src/request-context/request-context.ts +302 -0
- package/src/runtimes/claude.ts +123 -254
- package/src/runtimes/vercel.ts +180 -0
- package/src/sandbox.ts +53 -21
- package/src/step-invocation/index.ts +33 -0
- package/src/step-invocation/invoker.ts +204 -0
- package/src/step-invocation/protocol.ts +57 -0
- package/src/step-invocation/server.ts +184 -0
- package/src/step-invocation/types.ts +70 -0
- package/src/tools/coding.ts +126 -0
- package/src/tools/index.ts +8 -0
- package/src/types/events.ts +40 -0
- package/src/types/execution-context.ts +30 -0
- package/src/types/runtime.ts +24 -0
- package/src/types/sandbox-environment.ts +7 -5
- package/src/types/sandbox.ts +16 -12
- package/src/types/workflow-metadata.ts +84 -0
- package/src/types/workflow-plan.ts +24 -0
- package/src/types/workflow.ts +139 -25
- package/src/utils/bundler.ts +206 -18
- package/src/utils/source-loader.ts +2 -2
- package/src/workflow-steps/index.ts +30 -0
- package/src/workflow-steps/observability.ts +103 -0
- package/src/workflow-steps/runner.ts +244 -0
- package/src/workflow-steps/step.ts +38 -0
- package/src/workflow-steps/types.ts +134 -0
- package/src/workflow-steps/workflow.ts +95 -0
- package/src/workflows/engine.ts +69 -40
- package/src/workflows/invoke-child.ts +29 -0
package/src/types/workflow.ts
CHANGED
|
@@ -6,23 +6,41 @@
|
|
|
6
6
|
* - `ctx` carries facts + observability about THIS run (id, input,
|
|
7
7
|
* setMetadata, step). Metadata bag.
|
|
8
8
|
* - `sandbox` is a capability handed to you by the engine for doing work
|
|
9
|
-
* (exec commands, write files). Pass it to `
|
|
9
|
+
* (exec commands, write files). Pass it to `agent({ sandbox, ... })`
|
|
10
10
|
* and to any helper that takes a SandboxProvider (git utilities, file
|
|
11
11
|
* writers). Constructed once per run; reuse it.
|
|
12
12
|
*
|
|
13
|
-
* LLM agent loops live in `
|
|
13
|
+
* LLM agent loops live in `agent(opts)` (sdk/src/agent/run-agent.ts).
|
|
14
14
|
* Invoking other workflows uses `AgentComposeClient.invoke[AndWait](...)`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { z } from "zod";
|
|
17
18
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
18
19
|
import type { SandboxProvider } from "./sandbox.js";
|
|
20
|
+
import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
|
|
21
|
+
import type { Processor } from "../processors/processor.js";
|
|
22
|
+
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 } from "./workflow-metadata.js";
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
|
|
29
|
+
export type { WorkflowRun, InvokeChild, BaseExecutionContext } from "./execution-context.js";
|
|
30
|
+
// Re-export so consumers can keep importing `WorkflowMetadata` from
|
|
31
|
+
// `@agent-compose/sdk` — the canonical definition lives in
|
|
32
|
+
// `./workflow-metadata.js` to break a runtime import cycle with
|
|
33
|
+
// `workflow-steps/workflow.ts`.
|
|
34
|
+
export type { WorkflowMetadata } from "./workflow-metadata.js";
|
|
35
|
+
|
|
36
|
+
export interface AgentEventSink {
|
|
37
|
+
emit(event: AgentLifecycleEvent): void | Promise<void>;
|
|
23
38
|
}
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
import type { WorkflowMemoryConfig } from "./workflow-metadata.js";
|
|
41
|
+
export type { WorkflowMemoryConfig };
|
|
42
|
+
|
|
43
|
+
/** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
|
|
26
44
|
* can type per-invoke budget overrides they pass as workflow input. */
|
|
27
45
|
export interface AgentBudget {
|
|
28
46
|
turnsPerIteration: number;
|
|
@@ -30,9 +48,8 @@ export interface AgentBudget {
|
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
/** Context passed to a workflow function — facts + observability for this run. */
|
|
33
|
-
export interface WorkflowCtx {
|
|
34
|
-
|
|
35
|
-
input?: Record<string, unknown>;
|
|
51
|
+
export interface WorkflowCtx<TInput extends Record<string, unknown> = Record<string, unknown>> extends Omit<BaseExecutionContext, "sandbox"> {
|
|
52
|
+
input?: TInput;
|
|
36
53
|
/** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
|
|
37
54
|
setMetadata: (data: Record<string, unknown>) => Promise<void>;
|
|
38
55
|
/**
|
|
@@ -41,10 +58,23 @@ export interface WorkflowCtx {
|
|
|
41
58
|
* visible on the run's timeline (setup, external API calls, submit).
|
|
42
59
|
*/
|
|
43
60
|
step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
61
|
+
/** Pass to `agent({ events: ctx.agentEvents })` to stream agent lifecycle events. */
|
|
62
|
+
agentEvents: AgentEventSink;
|
|
63
|
+
/**
|
|
64
|
+
* Workflow-level processors registered via `defineWorkflow({ processors })`.
|
|
65
|
+
* Read-only here. Authors merge with agent-specific lists when calling
|
|
66
|
+
* `agent({ processors: [...ctx.processors, mySpecific] })`.
|
|
67
|
+
*
|
|
68
|
+
* Empty array when the workflow declared no processors.
|
|
69
|
+
*/
|
|
70
|
+
processors: readonly Processor[];
|
|
44
71
|
}
|
|
45
72
|
|
|
46
|
-
/** A workflow is `async (ctx, sandbox) =>
|
|
47
|
-
export type WorkflowFn<
|
|
73
|
+
/** A workflow is `async (ctx, sandbox) => TOutput`. */
|
|
74
|
+
export type WorkflowFn<
|
|
75
|
+
TOutput = unknown,
|
|
76
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
77
|
+
> = (ctx: WorkflowCtx<TInput>, sandbox: SandboxProvider) => Promise<TOutput>;
|
|
48
78
|
|
|
49
79
|
/**
|
|
50
80
|
* Declare a workflow with server-side metadata.
|
|
@@ -55,8 +85,11 @@ export type WorkflowFn<T = unknown> = (ctx: WorkflowCtx, sandbox: SandboxProvide
|
|
|
55
85
|
* Without defineWorkflow, a plain `export default async (ctx) => {...}` still
|
|
56
86
|
* works — the workflow just runs with secrets passed directly in env.
|
|
57
87
|
*/
|
|
58
|
-
export interface WorkflowDefinition<
|
|
59
|
-
|
|
88
|
+
export interface WorkflowDefinition<
|
|
89
|
+
TOutput = unknown,
|
|
90
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
91
|
+
> {
|
|
92
|
+
run: WorkflowFn<TOutput, TInput>;
|
|
60
93
|
/**
|
|
61
94
|
* Reference to a snapshot the runner should boot from at run start.
|
|
62
95
|
* Accepts a run UUID, a workflow name, or `name@version` — any workflow
|
|
@@ -88,6 +121,13 @@ export interface WorkflowDefinition<T = unknown> {
|
|
|
88
121
|
* }
|
|
89
122
|
*/
|
|
90
123
|
networkPolicy?: SandboxNetworkPolicy;
|
|
124
|
+
/**
|
|
125
|
+
* Workflow-level processor chain — runs around every `agent(...)` loop the
|
|
126
|
+
* workflow body launches, ahead of any agent-specific processors. Use for
|
|
127
|
+
* org-wide gating (deny destructive tools, require scopes). Authors merge
|
|
128
|
+
* with agent-specific lists via `[...ctx.processors, ...]`.
|
|
129
|
+
*/
|
|
130
|
+
processors?: readonly Processor[];
|
|
91
131
|
/**
|
|
92
132
|
* Optional placeholder env var values for secrets referenced in the network policy.
|
|
93
133
|
* By default, brokered secrets are removed from the runner env entirely — the real
|
|
@@ -102,22 +142,95 @@ export interface WorkflowDefinition<T = unknown> {
|
|
|
102
142
|
* }
|
|
103
143
|
*/
|
|
104
144
|
placeholders?: Record<string, string>;
|
|
145
|
+
/** Workflow Memory extraction. Defaults to "default" when omitted.
|
|
146
|
+
* Set false to skip memory extraction for this workflow, or point at a
|
|
147
|
+
* custom memory workflow once custom extractors are supported. */
|
|
148
|
+
memory?: WorkflowMemoryConfig;
|
|
105
149
|
}
|
|
106
150
|
|
|
107
151
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
152
|
+
* Wrap a legacy `(ctx, sandbox) => T` workflow body as a single-step
|
|
153
|
+
* `Workflow` so the engine has one shape to drive. The synthesised step
|
|
154
|
+
* runs the user's `run` body inline; `ctx.setMetadata` / `ctx.step` /
|
|
155
|
+
* `ctx.agentEvents` proxy directly to the underlying `StepContext` hooks,
|
|
156
|
+
* which the engine flushes to the run timeline exactly as the legacy
|
|
157
|
+
* full-mode runner did.
|
|
158
|
+
*
|
|
159
|
+
* Lifecycle granularity is workflow-level (one outer "run" step) — the
|
|
160
|
+
* `ctx.step("name", fn)` calls inside the body become sub-step events
|
|
161
|
+
* under that one step. Authors keep the same source; the dashboard sees
|
|
162
|
+
* the same timeline shape.
|
|
163
|
+
*/
|
|
164
|
+
function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
|
|
165
|
+
def: WorkflowDefinition<TOutput, TInput>,
|
|
166
|
+
metadata: WorkflowMetadata,
|
|
167
|
+
): Workflow<TInput, TOutput> {
|
|
168
|
+
const unknownSchema = z.unknown() as z.ZodType<unknown>;
|
|
169
|
+
const step: Step<TInput, TOutput> = {
|
|
170
|
+
name: "run",
|
|
171
|
+
input: unknownSchema as z.ZodType<TInput>,
|
|
172
|
+
output: unknownSchema as z.ZodType<TOutput>,
|
|
173
|
+
run: async (stepCtx) => {
|
|
174
|
+
// Proxy the legacy WorkflowCtx hooks to the step's collector-backed
|
|
175
|
+
// implementations. The engine harvests the snapshot after execute
|
|
176
|
+
// returns; nothing here is a no-op.
|
|
177
|
+
const workflowCtx: WorkflowCtx<TInput> = {
|
|
178
|
+
input: stepCtx.input,
|
|
179
|
+
run: stepCtx.run,
|
|
180
|
+
requestContext: stepCtx.requestContext,
|
|
181
|
+
invokeChild: stepCtx.invokeChild,
|
|
182
|
+
setMetadata: stepCtx.setMetadata,
|
|
183
|
+
step: stepCtx.step,
|
|
184
|
+
agentEvents: stepCtx.agentEvents,
|
|
185
|
+
processors: metadata.processors ?? [],
|
|
186
|
+
};
|
|
187
|
+
const sandbox = stepCtx.sandbox;
|
|
188
|
+
if (!sandbox) throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
|
189
|
+
return def.run(workflowCtx, sandbox);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const workflow: Workflow<TInput, TOutput> = {
|
|
193
|
+
id: "@run-form",
|
|
194
|
+
input: unknownSchema as z.ZodType<TInput>,
|
|
195
|
+
output: unknownSchema as z.ZodType<TOutput>,
|
|
196
|
+
steps: Object.freeze([step]),
|
|
197
|
+
metadata,
|
|
198
|
+
};
|
|
199
|
+
// Brand BEFORE freezing — defineProperty is the only way to set a
|
|
200
|
+
// non-enumerable key, and frozen objects refuse it.
|
|
201
|
+
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
202
|
+
return Object.freeze(workflow);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Declare a workflow. Two forms; both return a `Workflow` whose
|
|
207
|
+
* `metadata` field carries the server-readable declarations.
|
|
208
|
+
*
|
|
209
|
+
* Run form — wraps the legacy `(ctx, sandbox) => T` body as a single
|
|
210
|
+
* step internally. Lifecycle granularity is workflow-level (one step).
|
|
211
|
+
*
|
|
212
|
+
* Step form — the typed builder. Multiple steps with explicit input/
|
|
213
|
+
* output schemas, durability + replay at every step boundary.
|
|
214
|
+
*
|
|
215
|
+
* Both forms produce the same downstream shape: the bundler reads
|
|
216
|
+
* `workflow.metadata.networkPolicy` / `placeholders` / etc.; the server
|
|
217
|
+
* stores the step plan; runner subprocesses execute one step at a time
|
|
218
|
+
* via the StepInvocation seam.
|
|
111
219
|
*/
|
|
112
|
-
export function defineWorkflow<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
220
|
+
export function defineWorkflow<
|
|
221
|
+
TOutput = unknown,
|
|
222
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
223
|
+
>(
|
|
224
|
+
def: WorkflowDefinition<TOutput, TInput>,
|
|
225
|
+
): Workflow<TInput, TOutput>;
|
|
226
|
+
export function defineWorkflow<TInput, TOutput>(
|
|
227
|
+
def: StepWorkflowDefinition<TInput, TOutput>,
|
|
228
|
+
): import("../workflow-steps/workflow.js").WorkflowBuilder<TInput, TInput>;
|
|
229
|
+
export function defineWorkflow<TOutput = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
|
|
230
|
+
def: WorkflowDefinition<TOutput, TInput> | StepWorkflowDefinition<unknown, unknown>,
|
|
231
|
+
): Workflow<TInput, TOutput> | import("../workflow-steps/workflow.js").WorkflowBuilder<unknown, unknown> {
|
|
232
|
+
if ("run" in def) return compileRunForm(def, extractMetadata(def));
|
|
233
|
+
return createStepWorkflow(def);
|
|
121
234
|
}
|
|
122
235
|
|
|
123
236
|
/** Observability-only lifecycle hooks — passed to the workflow engine, not workflow authors. */
|
|
@@ -125,4 +238,5 @@ export interface WorkflowHooks {
|
|
|
125
238
|
onStepStart?: (step: string) => void;
|
|
126
239
|
onStepComplete?: (step: string, durationMs: number) => void;
|
|
127
240
|
onStepFail?: (step: string, durationMs: number, reason: string) => void;
|
|
241
|
+
onAgentLifecycleEvent?: (event: AgentLifecycleEvent) => void;
|
|
128
242
|
}
|
package/src/utils/bundler.ts
CHANGED
|
@@ -4,17 +4,65 @@
|
|
|
4
4
|
* Shared between the CLI (agentc register) and tests. Uses `Bun.build`
|
|
5
5
|
* to bundle TypeScript sources into a self-contained ESM module.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Two layers of validation run before the bundle leaves this function:
|
|
8
|
+
* 1. AST check (`assertDefaultExportIsDefineWorkflow`) on the bundled
|
|
9
|
+
* output — proves the default export is a `defineWorkflow(...)` call,
|
|
10
|
+
* not a raw `async function`. Catches harnesses that hand-roll their
|
|
11
|
+
* own register flow without going through `defineWorkflow`.
|
|
12
|
+
* 2. Runtime brand check (`isWorkflow`) — confirms the value the module
|
|
13
|
+
* actually exports carries the `WORKFLOW_BRAND` symbol that
|
|
14
|
+
* `defineWorkflow` / `defineWorkflow(...).build()` stamps. Catches
|
|
15
|
+
* indirection (re-exports, aliasing) that the AST walk can't follow.
|
|
16
|
+
*
|
|
17
|
+
* Both checks happen here, in the user's dev environment. The server NEVER
|
|
18
|
+
* parses or imports user source — it only validates the structured manifest
|
|
19
|
+
* this function returns alongside the bundled bytes, and cross-checks the
|
|
20
|
+
* manifest's `sourceHash` against the source it received.
|
|
10
21
|
*/
|
|
11
22
|
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { parse as babelParse } from "@babel/parser";
|
|
25
|
+
import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
|
|
12
26
|
import { importSourceModule } from "./source-loader.js";
|
|
13
|
-
import type {
|
|
27
|
+
import type { WorkflowMemoryConfig } from "../types/workflow.js";
|
|
14
28
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
29
|
+
import { isWorkflow } from "../workflow-steps/workflow.js";
|
|
30
|
+
import type { Workflow } from "../workflow-steps/types.js";
|
|
31
|
+
import { workflowPlan, type WorkflowPlan } from "../types/workflow-plan.js";
|
|
32
|
+
|
|
33
|
+
/** Bumped when the manifest contract changes in a way the server should
|
|
34
|
+
* notice. The server pins its manifest schema to this exact value (no
|
|
35
|
+
* `>=` accepted) so a manifest forged with a future version is refused
|
|
36
|
+
* by zod before any other validation runs. */
|
|
37
|
+
export const BUNDLER_VERSION = 1;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Manifest emitted alongside the bundled source. Fully serialisable JSON;
|
|
41
|
+
* the server validates its shape with zod and never inspects the source
|
|
42
|
+
* bytes themselves. Manifest is bound to source via `sourceHash` so the
|
|
43
|
+
* server can refuse manifests detached from the source they describe.
|
|
44
|
+
*/
|
|
45
|
+
export interface WorkflowManifest {
|
|
46
|
+
/** Always `true`. The bundler stamps this only after both validation
|
|
47
|
+
* gates pass; the server's zod schema pins it as `z.literal(true)`. */
|
|
48
|
+
definitionBrand: true;
|
|
49
|
+
/** sha256 hex digest of `source`. The server cross-checks this against
|
|
50
|
+
* `hashSource(body.source)` to bind the manifest to the bytes it describes. */
|
|
51
|
+
sourceHash: string;
|
|
52
|
+
/** Bundler contract version. See `BUNDLER_VERSION`. */
|
|
53
|
+
bundlerVersion: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class WorkflowSourceValidationError extends Error {
|
|
57
|
+
constructor(message: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "WorkflowSourceValidationError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
15
62
|
|
|
16
63
|
export interface BundledWorkflow {
|
|
17
64
|
source: string;
|
|
65
|
+
manifest: WorkflowManifest;
|
|
18
66
|
networkPolicy?: SandboxNetworkPolicy;
|
|
19
67
|
placeholders?: Record<string, string>;
|
|
20
68
|
/** Run UUID, workflow name, or `name@version` referencing the snapshot
|
|
@@ -23,6 +71,9 @@ export interface BundledWorkflow {
|
|
|
23
71
|
snapshot?: string;
|
|
24
72
|
/** Default capture-on-success flag from the workflow definition. */
|
|
25
73
|
saveSnapshot?: boolean;
|
|
74
|
+
workflowPlan: WorkflowPlan;
|
|
75
|
+
/** Workflow Memory extractor config. */
|
|
76
|
+
memory?: WorkflowMemoryConfig;
|
|
26
77
|
}
|
|
27
78
|
|
|
28
79
|
async function bundle(path: string, label: string): Promise<string> {
|
|
@@ -66,30 +117,167 @@ async function extractFromBundle<T>(
|
|
|
66
117
|
} catch { return undefined; }
|
|
67
118
|
}
|
|
68
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Bun bundles `import { defineWorkflow }` to a local identifier — usually
|
|
122
|
+
* the exact name, or a numeric suffix like `defineWorkflow2` to disambiguate
|
|
123
|
+
* scopes. Match exactly those patterns, NOT `startsWith("defineWorkflow")` —
|
|
124
|
+
* the latter would also accept `defineWorkflowAttacker`. The runtime brand
|
|
125
|
+
* check is the second gate, but the syntactic check should be tight.
|
|
126
|
+
*
|
|
127
|
+
* `defineSandboxEnvironment` is sugar over `defineWorkflow` (see
|
|
128
|
+
* `src/types/sandbox-environment.ts`) and is explicitly registerable via
|
|
129
|
+
* `agentc register setup.ts --build` to capture a snapshot. The AST gate
|
|
130
|
+
* accepts it for the same reason it accepts `defineWorkflow`: the bundled
|
|
131
|
+
* default export is a CallExpression to a known SDK identifier, and the
|
|
132
|
+
* runtime brand check downstream catches anything that lies about being a
|
|
133
|
+
* workflow.
|
|
134
|
+
*/
|
|
135
|
+
const DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
|
|
136
|
+
|
|
137
|
+
function isDefineWorkflowCall(node: CallExpression): boolean {
|
|
138
|
+
if (node.callee.type === "Identifier") {
|
|
139
|
+
return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
|
|
140
|
+
}
|
|
141
|
+
// `defineWorkflow(...).step(...).build()` — walk the call chain back to
|
|
142
|
+
// the leftmost callee; if any step in the chain hands off to a
|
|
143
|
+
// defineWorkflow identifier, accept it.
|
|
144
|
+
if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
|
|
145
|
+
return isDefineWorkflowCall(node.callee.object);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse the bundled source and assert the default export is a CallExpression
|
|
152
|
+
* to an identifier named `defineWorkflow` (or to a `.step(...).build()` chain
|
|
153
|
+
* rooted in one). This catches:
|
|
154
|
+
*
|
|
155
|
+
* - Raw TypeScript shipped to the registry (parse fails on type syntax)
|
|
156
|
+
* - `export default async function run(ctx)` (default is not a call)
|
|
157
|
+
* - `export default { run }` etc. (default is not a call)
|
|
158
|
+
* - `export default someFunction` (default is not a defineWorkflow call)
|
|
159
|
+
*
|
|
160
|
+
* The check is intentionally syntactic — it doesn't follow imports or trace
|
|
161
|
+
* through aliases. The runtime brand check (`isWorkflow`) picks up edge
|
|
162
|
+
* cases the AST can't see, so the two layers compose.
|
|
163
|
+
*
|
|
164
|
+
* Exported so the unit tests can exercise this exact code path; do NOT call
|
|
165
|
+
* from outside the SDK — `bundleWorkflow` is the supported entrypoint.
|
|
166
|
+
*/
|
|
167
|
+
export function assertDefaultExportIsDefineWorkflow(source: string, label: string): void {
|
|
168
|
+
let ast: File;
|
|
169
|
+
try {
|
|
170
|
+
ast = babelParse(source, {
|
|
171
|
+
sourceType: "module",
|
|
172
|
+
// Bundler output is plain JS; no JSX, no TS. Allow modern syntax
|
|
173
|
+
// (Bun's bundler may still emit `??=`/`?.` etc).
|
|
174
|
+
plugins: [],
|
|
175
|
+
errorRecovery: false,
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
throw new WorkflowSourceValidationError(
|
|
180
|
+
`Bundled workflow ${label} failed to parse as JavaScript: ${msg}. ` +
|
|
181
|
+
`If you registered raw .ts source, switch to \`agentc register <file.ts>\` so the source is bundled first.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const defaultExpr = findDefaultExportExpression(ast.program.body);
|
|
186
|
+
if (!defaultExpr) {
|
|
187
|
+
throw new WorkflowSourceValidationError(
|
|
188
|
+
`Bundled workflow ${label} has no default export. ` +
|
|
189
|
+
`Use \`export default defineWorkflow({ run: ... })\`.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (defaultExpr.type !== "CallExpression" || !isDefineWorkflowCall(defaultExpr)) {
|
|
194
|
+
throw new WorkflowSourceValidationError(
|
|
195
|
+
`Bundled workflow ${label} default export must be a \`defineWorkflow({...})\` call ` +
|
|
196
|
+
`(or a \`defineWorkflow(...).step(...).build()\` chain). ` +
|
|
197
|
+
`Got: ${defaultExpr.type}. Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk".`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function findDefaultExportExpression(body: Statement[]): Expression | null {
|
|
203
|
+
const defaultDecl = body.find(
|
|
204
|
+
(node): node is ExportDefaultDeclaration => node.type === "ExportDefaultDeclaration",
|
|
205
|
+
);
|
|
206
|
+
if (defaultDecl) {
|
|
207
|
+
return defaultDecl.declaration as Expression;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Bun bundles `export default defineWorkflow(...)` as:
|
|
211
|
+
// var workflow_default = defineWorkflow(...);
|
|
212
|
+
// export { workflow_default as default };
|
|
213
|
+
const exportNamed = body.find((node) => node.type === "ExportNamedDeclaration" &&
|
|
214
|
+
node.specifiers.some((spec) => spec.type === "ExportSpecifier" &&
|
|
215
|
+
spec.exported.type === "Identifier" && spec.exported.name === "default"));
|
|
216
|
+
if (!exportNamed || exportNamed.type !== "ExportNamedDeclaration") return null;
|
|
217
|
+
const spec = exportNamed.specifiers.find((s) => s.type === "ExportSpecifier" &&
|
|
218
|
+
s.exported.type === "Identifier" && s.exported.name === "default");
|
|
219
|
+
if (!spec || spec.type !== "ExportSpecifier" || spec.local.type !== "Identifier") return null;
|
|
220
|
+
const localName = spec.local.name;
|
|
221
|
+
|
|
222
|
+
for (const stmt of body) {
|
|
223
|
+
if (stmt.type !== "VariableDeclaration") continue;
|
|
224
|
+
for (const decl of stmt.declarations) {
|
|
225
|
+
if (decl.id.type === "Identifier" && decl.id.name === localName && decl.init && decl.init.type !== "TSSatisfiesExpression") {
|
|
226
|
+
return decl.init as Expression;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** SHA-256 hex digest of a string. Used to bind a manifest to its source. */
|
|
234
|
+
function sha256(text: string): string {
|
|
235
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
236
|
+
}
|
|
237
|
+
|
|
69
238
|
/** Bundle a workflow from source. */
|
|
70
239
|
export async function bundleWorkflow(
|
|
71
240
|
workflowPath: string,
|
|
72
241
|
overrides?: { networkPolicy?: SandboxNetworkPolicy; placeholders?: Record<string, string> },
|
|
73
242
|
): Promise<BundledWorkflow> {
|
|
74
|
-
const
|
|
243
|
+
const label = `"${workflowPath}"`;
|
|
244
|
+
const source = await bundle(workflowPath, `workflow ${label}`);
|
|
245
|
+
|
|
246
|
+
// Layer 1: AST gate on the bundled output. Cheapest check; rejects raw TS
|
|
247
|
+
// and non-defineWorkflow defaults before we even try to import the bundle.
|
|
248
|
+
assertDefaultExportIsDefineWorkflow(source, label);
|
|
75
249
|
|
|
76
|
-
|
|
77
|
-
|
|
250
|
+
// Layer 2: actually load the bundle and check the runtime brand on the
|
|
251
|
+
// default export. Catches indirection (re-exports, aliasing) the AST walk
|
|
252
|
+
// can't follow. Reads metadata + steps from the workflow's own fields.
|
|
253
|
+
const workflow = await extractFromBundle<Workflow<unknown, unknown>>(
|
|
78
254
|
source, "workflow",
|
|
79
|
-
(wf) => wf ?
|
|
80
|
-
networkPolicy: (wf as Wf).networkPolicy,
|
|
81
|
-
placeholders: (wf as Wf).placeholders,
|
|
82
|
-
snapshot: (wf as Wf).snapshot,
|
|
83
|
-
saveSnapshot: (wf as Wf).saveSnapshot,
|
|
84
|
-
} : undefined,
|
|
255
|
+
(wf) => (isWorkflow(wf) ? wf : undefined),
|
|
85
256
|
);
|
|
86
|
-
|
|
257
|
+
if (!workflow) {
|
|
258
|
+
throw new WorkflowSourceValidationError(
|
|
259
|
+
`Bundled workflow ${label} default export is not a Workflow. ` +
|
|
260
|
+
`Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` +
|
|
261
|
+
`raw \`export default async function\` is no longer supported.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { metadata } = workflow;
|
|
266
|
+
const plan: WorkflowPlan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
|
|
267
|
+
const manifest: WorkflowManifest = {
|
|
268
|
+
definitionBrand: true,
|
|
269
|
+
sourceHash: sha256(source),
|
|
270
|
+
bundlerVersion: BUNDLER_VERSION,
|
|
271
|
+
};
|
|
87
272
|
|
|
88
273
|
return {
|
|
89
274
|
source,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
275
|
+
manifest,
|
|
276
|
+
workflowPlan: plan,
|
|
277
|
+
networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
|
|
278
|
+
placeholders: overrides?.placeholders ?? metadata.placeholders,
|
|
279
|
+
...(metadata.snapshot !== undefined ? { snapshot: metadata.snapshot } : {}),
|
|
280
|
+
...(metadata.saveSnapshot !== undefined ? { saveSnapshot: metadata.saveSnapshot } : {}),
|
|
281
|
+
...(metadata.memory !== undefined ? { memory: metadata.memory } : {}),
|
|
94
282
|
};
|
|
95
283
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname } from "path";
|
|
2
|
-
import { writeFile,
|
|
2
|
+
import { writeFile, rm, mkdir } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
export const TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
5
5
|
export const LATEST_VERSION = "$LATEST";
|
|
@@ -11,6 +11,6 @@ export async function importSourceModule<T>(
|
|
|
11
11
|
await mkdir(dirname(tmpPath), { recursive: true });
|
|
12
12
|
await writeFile(tmpPath, source, "utf8");
|
|
13
13
|
const mod = await import(tmpPath) as T;
|
|
14
|
-
const cleanup = () =>
|
|
14
|
+
const cleanup = () => rm(tmpPath, { force: true });
|
|
15
15
|
return { mod, cleanup };
|
|
16
16
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { defineStep } from "./step.js";
|
|
2
|
+
export type { DefineStepOpts } from "./step.js";
|
|
3
|
+
|
|
4
|
+
export { createStepWorkflow, isWorkflow } from "./workflow.js";
|
|
5
|
+
export type { StepWorkflowDefinition, WorkflowBuilder } from "./workflow.js";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
runWorkflowSteps,
|
|
9
|
+
runWorkflowSingleStep,
|
|
10
|
+
StepValidationError,
|
|
11
|
+
WorkflowInputValidationError,
|
|
12
|
+
WorkflowOutputValidationError,
|
|
13
|
+
} from "./runner.js";
|
|
14
|
+
export type {
|
|
15
|
+
RunWorkflowStepsOpts,
|
|
16
|
+
RunWorkflowStepsResult,
|
|
17
|
+
RunWorkflowSingleStepOpts,
|
|
18
|
+
RunWorkflowSingleStepResult,
|
|
19
|
+
} from "./runner.js";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
Step,
|
|
23
|
+
StepContext,
|
|
24
|
+
StepRunResult,
|
|
25
|
+
Workflow,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
export { WORKFLOW_BRAND } from "./types.js";
|
|
28
|
+
|
|
29
|
+
export { StepObservabilityCollector } from "./observability.js";
|
|
30
|
+
export type { StepObservability, SubStepEvent } from "./observability.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-step observability collector — buffers metadata writes, agent
|
|
3
|
+
* lifecycle events, and `ctx.step("name", fn)` sub-step records during one
|
|
4
|
+
* step's execution. The runner flushes the snapshot through the
|
|
5
|
+
* tokenised stdout sentinel; the activity persists the snapshot to the
|
|
6
|
+
* run's metadata + lifecycle event tables.
|
|
7
|
+
*
|
|
8
|
+
* STREAMING — known gap. `agentEvents` are currently batched at step
|
|
9
|
+
* end. For long-running steps that emit many agent events, this hides
|
|
10
|
+
* progress from the dashboard until the step completes. The follow-up
|
|
11
|
+
* is to bring back a minimal `/internal/runs/:runId/steps/:stepIndex/events`
|
|
12
|
+
* route gated by a per-step JIT-signed token (mint in the activity,
|
|
13
|
+
* stamp into the runner env, verify on receive) so `agentEvents.emit`
|
|
14
|
+
* POSTs in real time. Until then `metadata` and `subSteps` are
|
|
15
|
+
* effectively boundary events (no streaming need) and batching them
|
|
16
|
+
* matches their natural granularity.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
|
|
20
|
+
import type { AgentEventSink } from "../types/workflow.js";
|
|
21
|
+
|
|
22
|
+
/** One named sub-step (from `ctx.step("name", async () => ...)`).
|
|
23
|
+
* Becomes a `workflow_substep_*` lifecycle event on the run timeline. */
|
|
24
|
+
export interface SubStepEvent {
|
|
25
|
+
name: string;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
status: "completed" | "failed";
|
|
29
|
+
/** Present when status="failed" — the user error's message. */
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Snapshot of everything the step's observability hooks recorded. All
|
|
34
|
+
* fields are optional so a step that uses none of the hooks produces an
|
|
35
|
+
* empty snapshot (and the wire payload omits the field entirely). */
|
|
36
|
+
export interface StepObservability {
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
events?: AgentLifecycleEvent[];
|
|
39
|
+
subSteps?: SubStepEvent[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Append-only collector bound to a single step's `StepContext`. The
|
|
44
|
+
* step's `setMetadata` / `step` / `agentEvents` properties all point at
|
|
45
|
+
* this instance. After the step finishes, the engine calls `snapshot()`
|
|
46
|
+
* to extract the bundle for transport.
|
|
47
|
+
*
|
|
48
|
+
* Metadata writes merge (later keys win) — matches the legacy
|
|
49
|
+
* `mergeRunMetadata` semantics so authors don't see a behaviour change
|
|
50
|
+
* across migrations.
|
|
51
|
+
*/
|
|
52
|
+
export class StepObservabilityCollector {
|
|
53
|
+
private metadata: Record<string, unknown> = {};
|
|
54
|
+
private events: AgentLifecycleEvent[] = [];
|
|
55
|
+
private subSteps: SubStepEvent[] = [];
|
|
56
|
+
|
|
57
|
+
readonly setMetadata = async (data: Record<string, unknown>): Promise<void> => {
|
|
58
|
+
Object.assign(this.metadata, data);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
readonly step = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
const result = await fn();
|
|
65
|
+
this.subSteps.push({
|
|
66
|
+
name,
|
|
67
|
+
startedAt,
|
|
68
|
+
durationMs: Date.now() - startedAt,
|
|
69
|
+
status: "completed",
|
|
70
|
+
});
|
|
71
|
+
return result;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.subSteps.push({
|
|
74
|
+
name,
|
|
75
|
+
startedAt,
|
|
76
|
+
durationMs: Date.now() - startedAt,
|
|
77
|
+
status: "failed",
|
|
78
|
+
error: err instanceof Error ? err.message : String(err),
|
|
79
|
+
});
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
readonly agentEvents: AgentEventSink = {
|
|
85
|
+
emit: (event: AgentLifecycleEvent) => {
|
|
86
|
+
this.events.push(event);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Snapshot the accumulated state. Returns `undefined` when nothing
|
|
91
|
+
* was recorded so the wire payload can drop the field entirely. */
|
|
92
|
+
snapshot(): StepObservability | undefined {
|
|
93
|
+
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
94
|
+
const hasEvents = this.events.length > 0;
|
|
95
|
+
const hasSubSteps = this.subSteps.length > 0;
|
|
96
|
+
if (!hasMetadata && !hasEvents && !hasSubSteps) return undefined;
|
|
97
|
+
const result: StepObservability = {};
|
|
98
|
+
if (hasMetadata) result.metadata = { ...this.metadata };
|
|
99
|
+
if (hasEvents) result.events = [...this.events];
|
|
100
|
+
if (hasSubSteps) result.subSteps = [...this.subSteps];
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
}
|