@agent-compose/sdk 0.8.0 → 0.8.2
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/dist/agent/agent-context.d.ts +1 -1
- package/dist/agent/agent-loop.d.ts +8 -0
- package/dist/agent/run-agent.d.ts +4 -0
- package/dist/client.d.ts +77 -15
- package/dist/display.d.ts +16 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +522 -123
- package/dist/runtimes/_cli-agent.d.ts +34 -7
- package/dist/runtimes/claude-code.d.ts +10 -8
- package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
- package/dist/runtimes/codex.d.ts +4 -1
- package/dist/runtimes/openai-desktop.js +507 -122
- package/dist/sandbox/sizes.d.ts +120 -30
- package/dist/sandbox.d.ts +1 -1
- package/dist/types/api-conversations.d.ts +198 -0
- package/dist/types/api-factory.d.ts +84 -7
- package/dist/types/api-runs.d.ts +48 -2
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/workflow-metadata.d.ts +14 -5
- package/dist/utils/bundler.d.ts +56 -0
- package/dist/workflow-steps/workflow.d.ts +7 -0
- package/dist/workflows/invoke-child.d.ts +18 -0
- package/dist/workflows/invoke-child.test.d.ts +9 -0
- package/package.json +2 -2
- package/src/agent/agent-context.ts +28 -17
- package/src/agent/agent-loop.ts +9 -0
- package/src/agent/run-agent.ts +5 -0
- package/src/client.ts +201 -30
- package/src/display.ts +61 -15
- package/src/index.ts +22 -9
- package/src/runtimes/_cli-agent.ts +302 -63
- package/src/runtimes/claude-code.ts +25 -15
- package/src/runtimes/codex.ts +19 -5
- package/src/sandbox/providers/e2b.ts +8 -4
- package/src/sandbox/sizes.ts +127 -44
- package/src/sandbox.ts +8 -0
- package/src/types/api-conversations.ts +180 -0
- package/src/types/api-factory.ts +89 -7
- package/src/types/api-runs.ts +50 -2
- package/src/types/protocol.ts +8 -0
- package/src/types/workflow-metadata.ts +15 -5
- package/src/utils/bundler.ts +213 -3
- package/src/workflow-steps/workflow.ts +7 -0
- package/src/workflows/invoke-child.ts +47 -11
package/src/types/api-runs.ts
CHANGED
|
@@ -10,9 +10,19 @@
|
|
|
10
10
|
import type { SandboxNetworkPolicy, SandboxSize } from "../sandbox.js";
|
|
11
11
|
import type { SnapshotConfig } from "./workflow-metadata.js";
|
|
12
12
|
import type { RunContext } from "./api-scopes.js";
|
|
13
|
+
import type { BundledWorkflow } from "../utils/bundler.js";
|
|
13
14
|
|
|
14
15
|
export type RunState = "running" | "success" | "failed" | "abandoned" | "canceled";
|
|
15
16
|
|
|
17
|
+
/** INLINE invoke payload (`POST /factories/:slug/invoke`): everything
|
|
18
|
+
* `bundleWorkflow` produced — source, the REQUIRED manifest binding the
|
|
19
|
+
* bytes, the workflow plan, and the build fields — plus the name the run
|
|
20
|
+
* reports as its workflow. The server runs it through the same
|
|
21
|
+
* validate/build core as registration but writes NO registry row: the run
|
|
22
|
+
* snapshots the validated source (auditable, replayable) and the name
|
|
23
|
+
* stays free for real registrations. */
|
|
24
|
+
export type InlineWorkflowPayload = BundledWorkflow & { name: string };
|
|
25
|
+
|
|
16
26
|
export interface InvokeWorkflowOptions {
|
|
17
27
|
/** Per-invocation snapshot config override. `snapshots.bootFrom`
|
|
18
28
|
* replaces the template's boot source; `snapshots.saveLatest` and
|
|
@@ -41,13 +51,50 @@ export interface InvokeWorkflowOptions {
|
|
|
41
51
|
* with the same key inside the server's dedup window returns the original
|
|
42
52
|
* run instead of starting a new one (matches `resumePause`'s pattern). */
|
|
43
53
|
idempotencyKey?: string;
|
|
44
|
-
|
|
54
|
+
/** Who pays for this run's model calls:
|
|
55
|
+
*
|
|
56
|
+
* - `"default"` (Auto) — the initiating human's connected subscription
|
|
57
|
+
* when one is enabled for workflows, else platform credits;
|
|
58
|
+
* - `"platform"` — always platform credits (metered);
|
|
59
|
+
* - `"subscription"` — REQUIRE the initiating human's plan. If it
|
|
60
|
+
* cannot be honored the invoke FAILS (412) rather than quietly
|
|
61
|
+
* spending credits;
|
|
62
|
+
* - `"byok"` — a factory secret named by `fundingSecret`, injected as
|
|
63
|
+
* the runtime's raw provider key. Never metered.
|
|
64
|
+
*
|
|
65
|
+
* Omitted → the workflow's own default (its `settings.funding`), else
|
|
66
|
+
* Auto. */
|
|
67
|
+
funding?: FundingChoice;
|
|
68
|
+
/** `funding: "byok"` only — the FACTORY SECRET NAME whose value funds the
|
|
69
|
+
* run. Must be a provider key variable (ANTHROPIC_API_KEY,
|
|
70
|
+
* OPENAI_API_KEY, CODEX_API_KEY, OPENROUTER_API_KEY) — that is where the
|
|
71
|
+
* sandbox's runtimes read it. The name only; the value never leaves the
|
|
72
|
+
* server's secret store. */
|
|
73
|
+
fundingSecret?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Inference-funding choice for a run — a lane the caller PINS, or
|
|
77
|
+
* `"default"` (Auto, the automatic ladder). Mirrors the same vocabulary
|
|
78
|
+
* cloud sessions use. */
|
|
79
|
+
export type FundingChoice = "default" | "platform" | "subscription" | "byok";
|
|
45
80
|
|
|
46
81
|
export interface InvokeAndWaitOptions extends InvokeWorkflowOptions {
|
|
47
82
|
timeoutMs?: number;
|
|
48
83
|
pollIntervalMs?: number;
|
|
49
84
|
}
|
|
50
85
|
|
|
86
|
+
/** Options for `invokeInline` — the named-invoke options plus the run-title
|
|
87
|
+
* override (an inline run has no registered template to inherit one from). */
|
|
88
|
+
export interface InvokeInlineOptions extends InvokeWorkflowOptions {
|
|
89
|
+
/** Run title override — defaults to the workflow name. */
|
|
90
|
+
title?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface InvokeInlineAndWaitOptions extends InvokeInlineOptions {
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
pollIntervalMs?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
export interface InvokeResult {
|
|
52
99
|
id: string;
|
|
53
100
|
}
|
|
@@ -256,7 +303,8 @@ export interface ListRunsOptions {
|
|
|
256
303
|
factorySlug?: string;
|
|
257
304
|
/** Substring match on the registered workflow name (`metadata._workflow`). */
|
|
258
305
|
workflow?: string;
|
|
259
|
-
/** Substring match across title / task title / branch
|
|
306
|
+
/** Substring match across title / task title / branch — or an exact run
|
|
307
|
+
* id (full UUID). */
|
|
260
308
|
search?: string;
|
|
261
309
|
outcome?: string;
|
|
262
310
|
sort?: "newest" | "oldest" | "fastest" | "slowest";
|
package/src/types/protocol.ts
CHANGED
|
@@ -37,6 +37,12 @@ export interface AgentMessageToolUse extends AgentMessageBase {
|
|
|
37
37
|
toolName: string;
|
|
38
38
|
toolInput: Record<string, unknown>;
|
|
39
39
|
toolUseId: string;
|
|
40
|
+
/** The spawning subagent call's tool_use id when this call ran INSIDE a
|
|
41
|
+
* subagent (claude stream-json stamps `parent_tool_use_id` on every
|
|
42
|
+
* sidechain event) — lets renderers nest child activity under the
|
|
43
|
+
* Agent/Task call instead of flattening it into the parent transcript.
|
|
44
|
+
* Optional and additive: producers without sidechains omit it. */
|
|
45
|
+
parentToolUseId?: string;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export interface AgentMessageToolResult extends AgentMessageBase {
|
|
@@ -54,6 +60,8 @@ export interface AgentMessageToolResult extends AgentMessageBase {
|
|
|
54
60
|
/** File locations touched by the tool call (ACP `locations` field), enabling
|
|
55
61
|
* "follow-along" UI. Optional and additive (WS-C / ADR-0020 Q3). */
|
|
56
62
|
locations?: { path: string; line?: number }[];
|
|
63
|
+
/** Sidechain attribution, mirroring AgentMessageToolUse.parentToolUseId. */
|
|
64
|
+
parentToolUseId?: string;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
export interface AgentMessageDone extends AgentMessageBase {
|
|
@@ -154,11 +154,12 @@ export interface InvokePolicy {
|
|
|
154
154
|
* today; kept as its own object so finer controls (disk, gpu, …) can be
|
|
155
155
|
* added later without reshaping `WorkflowMetadata`. */
|
|
156
156
|
export interface SandboxResources {
|
|
157
|
-
/** Machine hardware SKU
|
|
158
|
-
*
|
|
159
|
-
* provider specs at create time
|
|
160
|
-
*
|
|
161
|
-
*
|
|
157
|
+
/** Machine hardware SKU. The vocabulary is `SANDBOX_SIZES` in
|
|
158
|
+
* `sandbox/sizes.ts` — the single source; do not restate it here or
|
|
159
|
+
* anywhere else. Maps to provider specs at create time: Vercel takes the
|
|
160
|
+
* vCPU count and allocates RAM at 2048 MB/vCPU; E2B has no create-time
|
|
161
|
+
* cpu/mem knob at all, so the size selects a PRE-BUILT per-size template
|
|
162
|
+
* (`E2B_TEMPLATE_SIZES`). Omit → `DEFAULT_SANDBOX_SIZE`. */
|
|
162
163
|
size?: SandboxSize;
|
|
163
164
|
/** Sandbox provider this workflow's runs execute on — `"vercel"` or
|
|
164
165
|
* `"e2b"`. Optional and additive: omit and the run resolves to the
|
|
@@ -241,6 +242,14 @@ export interface WorkflowMetadata {
|
|
|
241
242
|
* canonical metadata hash (frozen-metadata rule), so existing workflows are
|
|
242
243
|
* not forced to re-register. */
|
|
243
244
|
environmentBuild?: boolean;
|
|
245
|
+
/** Whether this workflow's runs need the factory drive. ABSENT ⇒
|
|
246
|
+
* `"required"`: on a drive-backed factory the server treats the /factory
|
|
247
|
+
* mount as load-bearing — a mount failure FAILS the run instead of
|
|
248
|
+
* silently proceeding drive-less. Declare `"none"` for a workflow that
|
|
249
|
+
* genuinely never touches /factory: the server skips the mount entirely
|
|
250
|
+
* for its runs (the explicit no-drive mode; there is no silent degrade).
|
|
251
|
+
* Optional + additive (frozen-metadata rule). */
|
|
252
|
+
factoryDrive?: "required" | "none";
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
/**
|
|
@@ -274,6 +283,7 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
|
|
|
274
283
|
if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
275
284
|
if (source.invokePolicy !== undefined) out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
276
285
|
if (source.environmentBuild !== undefined) out.environmentBuild = source.environmentBuild;
|
|
286
|
+
if (source.factoryDrive !== undefined) out.factoryDrive = source.factoryDrive;
|
|
277
287
|
return Object.freeze(out);
|
|
278
288
|
}
|
|
279
289
|
|
package/src/utils/bundler.ts
CHANGED
|
@@ -18,9 +18,23 @@
|
|
|
18
18
|
* parses or imports user source — it only validates the structured manifest
|
|
19
19
|
* this function returns alongside the bundled bytes, and cross-checks the
|
|
20
20
|
* manifest's `sourceHash` against the source it received.
|
|
21
|
+
*
|
|
22
|
+
* Module resolution carries two more layers, because most workflow source is
|
|
23
|
+
* now written by an AGENT and the bundler's error is the only feedback it
|
|
24
|
+
* gets (the Workflow Studio's agent authored `import … from "agentc/sdk"`
|
|
25
|
+
* and prod answered with bun's `Maybe you need to "bun install"?` — advice
|
|
26
|
+
* nobody could act on inside a sandbox with no package.json):
|
|
27
|
+
*
|
|
28
|
+
* - TOLERATE (`SDK_SPECIFIER_ALIASES` + `sdkAliasPlugin`) — near-miss
|
|
29
|
+
* spellings of `@agent-compose/sdk` resolve to the real package, so a
|
|
30
|
+
* draft already written with the wrong one builds unedited.
|
|
31
|
+
* - SELF-CORRECT (`explainBundleFailure`) — anything that still fails to
|
|
32
|
+
* resolve produces an error NAMING the real package, which an agent
|
|
33
|
+
* reading its own tool error can fix on the next turn.
|
|
21
34
|
*/
|
|
22
35
|
|
|
23
36
|
import { createHash } from "node:crypto";
|
|
37
|
+
import { dirname } from "node:path";
|
|
24
38
|
import { parse as babelParse } from "@babel/parser";
|
|
25
39
|
import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
|
|
26
40
|
import { importSourceModule } from "./source-loader.js";
|
|
@@ -121,11 +135,191 @@ export interface BundledWorkflow {
|
|
|
121
135
|
/** Set by `defineSandboxEnvironment` — marks an environment build so the
|
|
122
136
|
* server skips the /factory mount for its runs (#13). */
|
|
123
137
|
environmentBuild?: boolean;
|
|
138
|
+
/** Drive requirement declared via `defineWorkflow({ factoryDrive })`.
|
|
139
|
+
* Absent ⇒ `"required"` (a failed /factory mount fails the run);
|
|
140
|
+
* `"none"` = explicit no-drive opt-out. */
|
|
141
|
+
factoryDrive?: "required" | "none";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The one true import specifier for the platform SDK. */
|
|
145
|
+
export const SDK_PACKAGE = "@agent-compose/sdk";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Import specifiers that can only have MEANT `@agent-compose/sdk`, rewritten
|
|
149
|
+
* to it at bundle time so a draft written with the wrong spelling builds
|
|
150
|
+
* unedited.
|
|
151
|
+
*
|
|
152
|
+
* Every entry carries an `sdk` segment or suffix, so none of them can be a
|
|
153
|
+
* real third-party package a workflow might legitimately depend on: `a/b`
|
|
154
|
+
* forms are subpaths of packages that don't exist, and the `*-sdk` forms
|
|
155
|
+
* name this platform explicitly. Bare `agentc` / `agent-compose` are
|
|
156
|
+
* deliberately NOT aliased — those are plausible npm package names, and
|
|
157
|
+
* silently redirecting a real dependency is worse than a clear error.
|
|
158
|
+
*
|
|
159
|
+
* Exported for the unit test that pins the table.
|
|
160
|
+
*/
|
|
161
|
+
export const SDK_SPECIFIER_ALIASES: readonly string[] = [
|
|
162
|
+
"agentc/sdk",
|
|
163
|
+
"@agentc/sdk",
|
|
164
|
+
"agentc-sdk",
|
|
165
|
+
"agent-compose/sdk",
|
|
166
|
+
"agentcompose/sdk",
|
|
167
|
+
"@agentcompose/sdk",
|
|
168
|
+
"agent-compose-sdk",
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const SDK_ALIAS_SET = new Set(SDK_SPECIFIER_ALIASES);
|
|
172
|
+
|
|
173
|
+
/** `@agent-compose/sdk` when `specifier` is a known near-miss for it, else
|
|
174
|
+
* null. The single authority: the plugin's regex filter is only a fast
|
|
175
|
+
* pre-filter, and this decides. */
|
|
176
|
+
export function resolveSdkAlias(specifier: string): string | null {
|
|
177
|
+
return SDK_ALIAS_SET.has(specifier) ? SDK_PACKAGE : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Minimal shape of the Bun surface this module drives. Accessed via
|
|
181
|
+
* globalThis so the SDK keeps no compile-time dependency on @types/bun. */
|
|
182
|
+
interface BunBuildLog { message: string; name?: string; specifier?: string }
|
|
183
|
+
interface BunSurface {
|
|
184
|
+
build(opts: {
|
|
185
|
+
entrypoints: string[]; format: string; target: string;
|
|
186
|
+
plugins?: { name: string; setup(build: BunPluginBuild): void }[];
|
|
187
|
+
}): Promise<{ success: boolean; outputs: { text(): Promise<string> }[]; logs: BunBuildLog[] }>;
|
|
188
|
+
resolveSync(specifier: string, parent: string): string;
|
|
189
|
+
}
|
|
190
|
+
interface BunPluginBuild {
|
|
191
|
+
onResolve(
|
|
192
|
+
constraints: { filter: RegExp; namespace?: string },
|
|
193
|
+
callback: (args: { path: string; importer?: string; resolveDir?: string }) => { path: string } | undefined,
|
|
194
|
+
): void;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Anchored regex over the alias table — the plugin filter. Specifiers are
|
|
198
|
+
* literal package names, but escape anyway so a future entry with a `.`
|
|
199
|
+
* or `+` can't widen the filter. */
|
|
200
|
+
function aliasFilter(): RegExp {
|
|
201
|
+
const alternation = SDK_SPECIFIER_ALIASES
|
|
202
|
+
.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
203
|
+
.join("|");
|
|
204
|
+
return new RegExp(`^(?:${alternation})$`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The TOLERATE layer: a Bun resolve plugin that maps every known near-miss
|
|
209
|
+
* specifier onto the real SDK. Resolution goes through Bun's own resolver
|
|
210
|
+
* from the importing file's directory, so the alias lands on exactly the
|
|
211
|
+
* `@agent-compose/sdk` the build would have used had the source spelled it
|
|
212
|
+
* correctly. When the real SDK can't be resolved either, the plugin declines
|
|
213
|
+
* and Bun's failure flows into `explainBundleFailure` below.
|
|
214
|
+
*/
|
|
215
|
+
function sdkAliasPlugin(bun: BunSurface): { name: string; setup(build: BunPluginBuild): void } {
|
|
216
|
+
return {
|
|
217
|
+
name: "agent-compose-sdk-alias",
|
|
218
|
+
setup(build) {
|
|
219
|
+
build.onResolve({ filter: aliasFilter() }, (args) => {
|
|
220
|
+
const target = resolveSdkAlias(args.path);
|
|
221
|
+
if (!target) return undefined;
|
|
222
|
+
// `importer` is the importing FILE, `resolveDir` already a directory.
|
|
223
|
+
const from = args.importer ? dirname(args.importer) : args.resolveDir;
|
|
224
|
+
try {
|
|
225
|
+
return { path: bun.resolveSync(target, from && from.length > 0 ? from : process.cwd()) };
|
|
226
|
+
} catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The unresolved specifier a Bun diagnostic is about, or null when it isn't
|
|
235
|
+
* a resolve failure. `ResolveMessage` carries it as a field; parsing the
|
|
236
|
+
* message text is the fallback. */
|
|
237
|
+
function readUnresolvedSpecifier(diag: unknown): string | null {
|
|
238
|
+
if (!diag || typeof diag !== "object") return null;
|
|
239
|
+
const d = diag as { specifier?: unknown; message?: unknown };
|
|
240
|
+
if (typeof d.specifier === "string" && d.specifier.length > 0) return d.specifier;
|
|
241
|
+
const message = typeof d.message === "string" ? d.message : "";
|
|
242
|
+
const m = /Could not resolve:?\s*"([^"]+)"/.exec(message);
|
|
243
|
+
return m ? m[1] : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Flatten a bundler failure into the diagnostics it actually carries.
|
|
247
|
+
* Bun reports through two channels and both land here: a THROWN
|
|
248
|
+
* `AggregateError("Bundle failed")` whose real messages hide in `.errors`,
|
|
249
|
+
* and a returned `logs` array on `success: false`. */
|
|
250
|
+
function flattenDiagnostics(failure: unknown): unknown[] {
|
|
251
|
+
const out: unknown[] = [];
|
|
252
|
+
const seen = new Set<unknown>();
|
|
253
|
+
const walk = (node: unknown, depth: number): void => {
|
|
254
|
+
if (!node || depth > 4 || seen.has(node)) return;
|
|
255
|
+
seen.add(node);
|
|
256
|
+
if (Array.isArray(node)) {
|
|
257
|
+
for (const sub of node) walk(sub, depth + 1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
out.push(node);
|
|
261
|
+
if (typeof node === "object") {
|
|
262
|
+
const n = node as { errors?: unknown; cause?: unknown };
|
|
263
|
+
if (Array.isArray(n.errors)) for (const sub of n.errors) walk(sub, depth + 1);
|
|
264
|
+
if (n.cause) walk(n.cause, depth + 1);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
walk(failure, 0);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function diagnosticText(diag: unknown): string {
|
|
272
|
+
if (diag instanceof Error) return diag.message || String(diag);
|
|
273
|
+
if (diag && typeof diag === "object" && typeof (diag as { message?: unknown }).message === "string") {
|
|
274
|
+
return (diag as { message: string }).message;
|
|
275
|
+
}
|
|
276
|
+
return String(diag);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The SELF-CORRECTING layer. Turns a Bun bundle failure into a message that
|
|
281
|
+
* names the real fix instead of leaking Bun's internals.
|
|
282
|
+
*
|
|
283
|
+
* An unresolved import is almost always one thing: workflow source naming
|
|
284
|
+
* the platform SDK by a specifier that isn't its package name. Bun answers
|
|
285
|
+
* that with `Could not resolve: "agentc/sdk". Maybe you need to "bun
|
|
286
|
+
* install"?` — advice the author cannot act on (there is no package.json to
|
|
287
|
+
* install into; the drive holds one file). The rewritten message says the
|
|
288
|
+
* package name, so an agent reading its own tool error can fix the import on
|
|
289
|
+
* the next turn.
|
|
290
|
+
*
|
|
291
|
+
* Non-resolve failures (syntax errors, transform failures) keep their
|
|
292
|
+
* verbatim diagnostics — those are already actionable.
|
|
293
|
+
*
|
|
294
|
+
* Exported for the unit test; `bundleWorkflow` is the supported entrypoint.
|
|
295
|
+
*/
|
|
296
|
+
export function explainBundleFailure(err: unknown, label: string): string {
|
|
297
|
+
const diagnostics = flattenDiagnostics(err);
|
|
298
|
+
const unresolved: string[] = [];
|
|
299
|
+
for (const diag of diagnostics) {
|
|
300
|
+
const specifier = readUnresolvedSpecifier(diag);
|
|
301
|
+
if (specifier && !unresolved.includes(specifier)) unresolved.push(specifier);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (unresolved.length > 0) {
|
|
305
|
+
const quoted = unresolved.map((s) => `"${s}"`).join(", ");
|
|
306
|
+
return (
|
|
307
|
+
`Could not resolve ${quoted} — workflow source imports the platform SDK as "${SDK_PACKAGE}". ` +
|
|
308
|
+
`Fix the import specifier in ${label}; anything that is genuinely a third-party ` +
|
|
309
|
+
`package must be installed where the source is bundled.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const detail = diagnostics
|
|
314
|
+
.map(diagnosticText)
|
|
315
|
+
.filter((t) => t.length > 0 && t !== "Bundle failed")
|
|
316
|
+
.join("\n");
|
|
317
|
+
return `Failed to bundle ${label}:\n${detail || "the bundler reported no diagnostics"}`;
|
|
124
318
|
}
|
|
125
319
|
|
|
126
320
|
async function bundle(path: string, label: string): Promise<string> {
|
|
127
321
|
// Use globalThis to access Bun without a compile-time dependency on @types/bun.
|
|
128
|
-
const bun = globalThis as unknown as { Bun?:
|
|
322
|
+
const bun = globalThis as unknown as { Bun?: BunSurface };
|
|
129
323
|
if (!bun.Bun?.build) throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
130
324
|
// target: "node" — the bundle runs inside the Vercel sandbox under Node 22+.
|
|
131
325
|
//
|
|
@@ -141,9 +335,24 @@ async function bundle(path: string, label: string): Promise<string> {
|
|
|
141
335
|
// Pure-ESM workflows (e.g. only importing `@agent-compose/sdk`) bundled
|
|
142
336
|
// identically under either target — that's why this bug stayed hidden
|
|
143
337
|
// until the first workflow that pulled CJS deps got dispatched.
|
|
144
|
-
|
|
338
|
+
//
|
|
339
|
+
// The alias plugin rewrites known near-miss SDK specifiers before Bun's
|
|
340
|
+
// resolver sees them; whatever still fails to resolve comes back through
|
|
341
|
+
// `explainBundleFailure` naming the real package instead of Bun's
|
|
342
|
+
// "Maybe you need to `bun install`?".
|
|
343
|
+
let result: Awaited<ReturnType<BunSurface["build"]>>;
|
|
344
|
+
try {
|
|
345
|
+
result = await bun.Bun.build({
|
|
346
|
+
entrypoints: [path], format: "esm", target: "node",
|
|
347
|
+
plugins: [sdkAliasPlugin(bun.Bun)],
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// Modern Bun.build THROWS AggregateError("Bundle failed") on a resolve
|
|
351
|
+
// or transform error rather than returning `success: false`.
|
|
352
|
+
throw new WorkflowSourceValidationError(explainBundleFailure(err, label));
|
|
353
|
+
}
|
|
145
354
|
if (!result.success) {
|
|
146
|
-
throw new
|
|
355
|
+
throw new WorkflowSourceValidationError(explainBundleFailure(result.logs, label));
|
|
147
356
|
}
|
|
148
357
|
return result.outputs[0].text();
|
|
149
358
|
}
|
|
@@ -371,6 +580,7 @@ export async function bundleWorkflow(
|
|
|
371
580
|
...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
|
|
372
581
|
...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
|
|
373
582
|
...(metadata.environmentBuild !== undefined ? { environmentBuild: metadata.environmentBuild } : {}),
|
|
583
|
+
...(metadata.factoryDrive !== undefined ? { factoryDrive: metadata.factoryDrive } : {}),
|
|
374
584
|
};
|
|
375
585
|
}
|
|
376
586
|
|
|
@@ -77,6 +77,13 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
|
|
|
77
77
|
* server skips mounting the shared factory drive for its runs (#13). See
|
|
78
78
|
* `WorkflowMetadata.environmentBuild`. */
|
|
79
79
|
environmentBuild?: boolean;
|
|
80
|
+
/** Whether this workflow's runs need the factory drive. Omit (⇒
|
|
81
|
+
* `"required"`) for any workflow that reads or writes /factory: a failed
|
|
82
|
+
* mount then FAILS the run instead of silently proceeding drive-less.
|
|
83
|
+
* Declare `"none"` for a drive-agnostic workflow — the server skips the
|
|
84
|
+
* /factory mount entirely for its runs. See
|
|
85
|
+
* `WorkflowMetadata.factoryDrive`. */
|
|
86
|
+
factoryDrive?: "required" | "none";
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export function createStepWorkflow<TInput, TOutput>(
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { AgentComposeClient } from "../client.js";
|
|
2
|
+
import { getActiveStep, nextPauseOrdinalInActiveStep } from "../active-step.js";
|
|
2
3
|
import type { InvokeChild } from "../types/execution-context.js";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Derive the deterministic per-call `Idempotency-Key` for a `ctx.invokeChild`
|
|
7
|
+
* dispatch: `invoke-child:<parentRunId>:step<stepIndex>:<childName>:<ordinal>`.
|
|
8
|
+
*
|
|
9
|
+
* Replay safety: a replayed step re-runs its body from the top, so the k-th
|
|
10
|
+
* `invokeChild(name)` call in a step re-derives the SAME key (the per-scope
|
|
11
|
+
* ordinal counter lives on the active-step state, which resets identically on
|
|
12
|
+
* every (re-)entry — the `nextPauseOrdinalInActiveStep` pattern). The server's
|
|
13
|
+
* idempotency window then returns the original child run instead of
|
|
14
|
+
* double-dispatching. Outside step execution (local dev, tests) there is no
|
|
15
|
+
* stable coordinate to key on — returns null and the dispatch is unkeyed,
|
|
16
|
+
* exactly the old behavior.
|
|
17
|
+
*
|
|
18
|
+
* Key syntax matches the server's `Idempotency-Key` grammar
|
|
19
|
+
* (`[A-Za-z0-9_\-:.]{1,255}`): runId is a UUID, step index a number, and
|
|
20
|
+
* workflow names are kebab-case.
|
|
21
|
+
*/
|
|
22
|
+
export function deriveInvokeChildIdempotencyKey(parentRunId: string, childName: string): string | null {
|
|
23
|
+
const step = getActiveStep();
|
|
24
|
+
if (!step) return null;
|
|
25
|
+
const ordinal = nextPauseOrdinalInActiveStep(`invoke-child:${childName}`);
|
|
26
|
+
if (ordinal === null) return null;
|
|
27
|
+
return `invoke-child:${parentRunId}:step${step.stepIndex}:${childName}:${ordinal}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
/**
|
|
5
31
|
* Build the public-API child workflow invoker used by legacy and sandboxed
|
|
6
32
|
* workflow execution. Provider-backed engines may inject a different
|
|
@@ -21,15 +47,25 @@ export function buildInvokeChild(
|
|
|
21
47
|
childClient = new AgentComposeClient({ apiKey, baseUrl });
|
|
22
48
|
return childClient;
|
|
23
49
|
};
|
|
24
|
-
return (name, input, childOpts) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
return (name, input, childOpts) => {
|
|
51
|
+
// Replay-safe dispatch: derive a deterministic per-step key so a step
|
|
52
|
+
// re-entered after pause/resume (or a crash-replayed attempt) dedupes to
|
|
53
|
+
// the original child run instead of double-dispatching. An explicit
|
|
54
|
+
// caller-supplied key wins.
|
|
55
|
+
const idempotencyKey = childOpts?.idempotencyKey
|
|
56
|
+
?? deriveInvokeChildIdempotencyKey(runId, name)
|
|
57
|
+
?? undefined;
|
|
58
|
+
return getChildClient().invokeAndWait(name, input, {
|
|
59
|
+
...childOpts,
|
|
60
|
+
...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
|
|
61
|
+
parentRunId: runId,
|
|
62
|
+
// The runner sets `AGENT_COMPOSE_FACTORY` (the run's factory slug — see
|
|
63
|
+
// activities.ts loadRunnerEnvs). Default the child to it so it lands in the
|
|
64
|
+
// SAME factory as the parent — the per-run API key is factory-scoped, so a
|
|
65
|
+
// child dispatched into another factory 403s. (Was reading the never-set
|
|
66
|
+
// `AGENT_COMPOSE_FACTORY_SLUG`, silently falling back to "default".)
|
|
67
|
+
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug
|
|
68
|
+
?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
|
|
69
|
+
});
|
|
70
|
+
};
|
|
35
71
|
}
|