@agent-compose/sdk 0.8.0 → 0.8.1
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-loop.d.ts +8 -0
- package/dist/agent/run-agent.d.ts +4 -0
- package/dist/client.d.ts +50 -6
- package/dist/display.d.ts +16 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +256 -39
- package/dist/runtimes/_cli-agent.d.ts +9 -7
- package/dist/runtimes/claude-code.d.ts +10 -8
- package/dist/runtimes/codex.d.ts +4 -1
- package/dist/runtimes/openai-desktop.js +249 -38
- package/dist/types/api-conversations.d.ts +31 -0
- package/dist/types/api-factory.d.ts +25 -0
- package/dist/types/api-runs.d.ts +46 -1
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/workflow-metadata.d.ts +8 -0
- 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-loop.ts +9 -0
- package/src/agent/run-agent.ts +5 -0
- package/src/client.ts +145 -10
- package/src/display.ts +61 -15
- package/src/index.ts +11 -3
- package/src/runtimes/_cli-agent.ts +9 -7
- package/src/runtimes/claude-code.ts +25 -15
- package/src/runtimes/codex.ts +11 -3
- package/src/types/api-conversations.ts +25 -0
- package/src/types/api-factory.ts +32 -0
- package/src/types/api-runs.ts +48 -1
- package/src/types/protocol.ts +8 -0
- package/src/types/workflow-metadata.ts +9 -0
- 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/index.ts
CHANGED
|
@@ -113,7 +113,8 @@ export type {
|
|
|
113
113
|
export { AgentComposeClient } from "./client.js";
|
|
114
114
|
export type {
|
|
115
115
|
RegisterResult, RegisterWorkflowInput, RuntimeSourceInput, TemplateSourceRef,
|
|
116
|
-
InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult,
|
|
116
|
+
InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice,
|
|
117
|
+
InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
|
|
117
118
|
ListSnapshotsOptions, TemplateRow, ListTemplatesOptions,
|
|
118
119
|
CreateFactoryInput, UpdateFactoryInput,
|
|
119
120
|
SecretOptions, SetSecretResult, SecretListEntry,
|
|
@@ -141,6 +142,8 @@ export type {
|
|
|
141
142
|
// Session branch proposals (ADR-0053)
|
|
142
143
|
SessionFileChange, SessionChangeSet,
|
|
143
144
|
SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
|
|
145
|
+
// User drive mounts (`agentc files mount` on a human's own machine)
|
|
146
|
+
DriveMountSession, CreateDriveMountSessionInput,
|
|
144
147
|
// Channel-attached sessions (ADR-0057)
|
|
145
148
|
ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
|
|
146
149
|
FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, PublicFileLinkState,
|
|
@@ -206,6 +209,10 @@ export {
|
|
|
206
209
|
BUNDLER_VERSION,
|
|
207
210
|
WorkflowSourceValidationError,
|
|
208
211
|
assertDefaultExportIsDefineWorkflow,
|
|
212
|
+
SDK_PACKAGE,
|
|
213
|
+
SDK_SPECIFIER_ALIASES,
|
|
214
|
+
resolveSdkAlias,
|
|
215
|
+
explainBundleFailure,
|
|
209
216
|
} from "./utils/bundler.js";
|
|
210
217
|
export type { BundledWorkflow, WorkflowManifest } from "./utils/bundler.js";
|
|
211
218
|
|
|
@@ -232,7 +239,7 @@ export type { GatewayModelId } from "ai";
|
|
|
232
239
|
// sandbox and stream-parse its JSONL. No heavy npm deps (the CLI lives in the
|
|
233
240
|
// sandbox image), so these are root-exported like claudeRuntime.
|
|
234
241
|
export { createCodexRuntime, codexSpec } from "./runtimes/codex.js";
|
|
235
|
-
export type { CodexRuntimeConfig } from "./runtimes/codex.js";
|
|
242
|
+
export type { CodexRuntimeConfig, CodexReasoningEffort } from "./runtimes/codex.js";
|
|
236
243
|
export { default as codexRuntime } from "./runtimes/codex.js";
|
|
237
244
|
// Reasoning-effort level a CLI runtime turn may carry (claude-code / codex).
|
|
238
245
|
export type { CliReasoningEffort } from "./runtimes/_cli-agent.js";
|
|
@@ -255,7 +262,7 @@ export { default as droidRuntime } from "./runtimes/droid.js";
|
|
|
255
262
|
// adapter) — the cloud-hostable counterpart of `claudeRuntime` (claude.ts,
|
|
256
263
|
// which drives the Agent SDK in the calling process and so can never run a
|
|
257
264
|
// server-driven cloud-session turn).
|
|
258
|
-
export { createClaudeCodeRuntime, claudeCodeSpec, CLAUDE_CODE_ACP_ADAPTER,
|
|
265
|
+
export { createClaudeCodeRuntime, claudeCodeSpec, CLAUDE_CODE_ACP_ADAPTER, CLAUDE_CODE_EFFORT_LEVELS } from "./runtimes/claude-code.js";
|
|
259
266
|
export type { ClaudeCodeRuntimeConfig } from "./runtimes/claude-code.js";
|
|
260
267
|
export { default as claudeCodeRuntime } from "./runtimes/claude-code.js";
|
|
261
268
|
|
|
@@ -386,6 +393,7 @@ export {
|
|
|
386
393
|
TABLE_MAX_COLUMNS, TABLE_MAX_ROWS, TABLE_CELL_MAX_CHARS,
|
|
387
394
|
CHART_MAX_SERIES, CHART_MAX_POINTS_PER_SERIES, CHART_LABEL_MAX_CHARS,
|
|
388
395
|
ASK_PROMPT_MAX_CHARS, ASK_MAX_OPTIONS, ASK_OPTION_ID_MAX_CHARS, ASK_OPTION_LABEL_MAX_CHARS,
|
|
396
|
+
DRIVE_PATH_MAX_CHARS, isPlausibleDrivePath,
|
|
389
397
|
serializeDisplayMarker, parseDisplayMarker, findDisplayMarker, clampPlanEntries,
|
|
390
398
|
clampTableData, clampChartSeries, clampChartAxisLabel, clampAskOptions,
|
|
391
399
|
detectAgentcInvocation, shellWords, createDisplayPromoter,
|
|
@@ -119,13 +119,15 @@ async function withHandshakeTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/** Reasoning-effort level a CLI turn may carry (T2 session effort)
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `-c model_reasoning_effort=<level
|
|
126
|
-
* (
|
|
127
|
-
*
|
|
128
|
-
|
|
122
|
+
/** Reasoning-effort level a CLI turn may carry (T2 session effort) — the
|
|
123
|
+
* UNION of what the effort-capable CLIs accept. The per-CLI mapping lives in
|
|
124
|
+
* each spec's `buildCommand` — Claude Code takes its own `--effort` flag
|
|
125
|
+
* (all five levels), codex takes `-c model_reasoning_effort=<level>`
|
|
126
|
+
* (low|medium|high|xhigh — no "max"; the spec clamps it). Specs without a
|
|
127
|
+
* real knob (opencode/droid/cursor) never receive one: the server hides +
|
|
128
|
+
* rejects effort for those runtimes, and rejects levels a runtime lacks
|
|
129
|
+
* (sessionEffortLockError). */
|
|
130
|
+
export type CliReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
129
131
|
|
|
130
132
|
/** Per-CLI behaviour. The base owns the lifecycle (init/done/error) and the
|
|
131
133
|
* transport (spawn + JSONL parse); a spec owns the CLI-specific bits. */
|
|
@@ -72,13 +72,15 @@ function toolResultText(content: unknown): string {
|
|
|
72
72
|
return content == null ? "" : JSON.stringify(content) ?? "";
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
/** Claude Code's real reasoning knob is its own `--effort <level>` flag
|
|
76
|
+
* (low|medium|high|xhigh|max — verified against `claude -p --help`). The
|
|
77
|
+
* CliReasoningEffort union IS the CLI's vocabulary, so the level rides the
|
|
78
|
+
* flag verbatim; the CLI itself downgrades a level the selected model lacks
|
|
79
|
+
* (its documented behaviour, e.g. xhigh → high off Opus). The old
|
|
80
|
+
* `MAX_THINKING_TOKENS` env mapping is gone: the CLI deprecated it (treated
|
|
81
|
+
* as on/off on current models) and it could never express xhigh/max. */
|
|
82
|
+
export const CLAUDE_CODE_EFFORT_LEVELS: readonly CliReasoningEffort[] =
|
|
83
|
+
["low", "medium", "high", "xhigh", "max"];
|
|
82
84
|
|
|
83
85
|
export const claudeCodeSpec: CliAgentSpec = {
|
|
84
86
|
kind: "claude-code",
|
|
@@ -115,17 +117,25 @@ export const claudeCodeSpec: CliAgentSpec = {
|
|
|
115
117
|
// root-user refusal (the E2B agent-env user is root).
|
|
116
118
|
"--dangerously-skip-permissions",
|
|
117
119
|
...(model ? [`--model ${shellQuote(model)}`] : []),
|
|
120
|
+
// Reasoning effort is the CLI's own flag; the value comes from the
|
|
121
|
+
// closed CliReasoningEffort set, so it is shell-safe unquoted.
|
|
122
|
+
...(effort ? [`--effort ${effort}`] : []),
|
|
118
123
|
...(sessionId ? [`--resume ${shellQuote(sessionId)}`] : []),
|
|
119
124
|
].join(" ");
|
|
120
|
-
|
|
121
|
-
// per-invocation idiom as IS_SANDBOX (never persisted into settings).
|
|
122
|
-
const thinking = effort ? `MAX_THINKING_TOKENS=${CLAUDE_CODE_THINKING_TOKENS[effort]} ` : "";
|
|
123
|
-
return `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}${thinking}IS_SANDBOX=1 claude ${flags} < ${shellQuote(promptPath)}`;
|
|
125
|
+
return `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}IS_SANDBOX=1 claude ${flags} < ${shellQuote(promptPath)}`;
|
|
124
126
|
},
|
|
125
127
|
// Every stream-json event carries the session id; the init event is first.
|
|
126
128
|
extractSessionId: (p) => (typeof p.session_id === "string" ? p.session_id : undefined),
|
|
127
129
|
mapEvent: (p): AgentMessage[] => {
|
|
128
130
|
const ts = now();
|
|
131
|
+
// Sidechain attribution: stream-json stamps `parent_tool_use_id` on
|
|
132
|
+
// every event emitted INSIDE a subagent (the spawning Agent/Task call's
|
|
133
|
+
// tool_use id; null at top level). Forwarded on tool_use/tool_result so
|
|
134
|
+
// renderers can nest child activity under the spawning call instead of
|
|
135
|
+
// flattening it into the parent transcript unattributed.
|
|
136
|
+
const parent = typeof p.parent_tool_use_id === "string" && p.parent_tool_use_id.length > 0
|
|
137
|
+
? { parentToolUseId: p.parent_tool_use_id }
|
|
138
|
+
: {};
|
|
129
139
|
switch (p.type) {
|
|
130
140
|
// Assistant API message: content blocks → text / thinking / tool_use.
|
|
131
141
|
case "assistant": {
|
|
@@ -141,7 +151,7 @@ export const claudeCodeSpec: CliAgentSpec = {
|
|
|
141
151
|
if (b.type === "tool_use") {
|
|
142
152
|
return [{
|
|
143
153
|
type: "tool_use", toolName: String(b.name ?? "tool"),
|
|
144
|
-
toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts,
|
|
154
|
+
toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), ...parent, timestamp: ts,
|
|
145
155
|
}];
|
|
146
156
|
}
|
|
147
157
|
return [];
|
|
@@ -155,7 +165,7 @@ export const claudeCodeSpec: CliAgentSpec = {
|
|
|
155
165
|
b.type === "tool_result"
|
|
156
166
|
? [{
|
|
157
167
|
type: "tool_result", toolUseId: String(b.tool_use_id ?? ""),
|
|
158
|
-
output: toolResultText(b.content), isError: b.is_error === true, timestamp: ts,
|
|
168
|
+
output: toolResultText(b.content), isError: b.is_error === true, ...parent, timestamp: ts,
|
|
159
169
|
}]
|
|
160
170
|
: []);
|
|
161
171
|
}
|
|
@@ -234,8 +244,8 @@ export const claudeCodeSpec: CliAgentSpec = {
|
|
|
234
244
|
export interface ClaudeCodeRuntimeConfig {
|
|
235
245
|
/** Claude model id (`--model`). Omit to use the CLI's configured default. */
|
|
236
246
|
model?: string;
|
|
237
|
-
/**
|
|
238
|
-
*
|
|
247
|
+
/** Reasoning effort (`--effort <level>`; the CLI accepts all five levels).
|
|
248
|
+
* Omit for the CLI's default behaviour. */
|
|
239
249
|
effort?: CliReasoningEffort;
|
|
240
250
|
}
|
|
241
251
|
|
package/src/runtimes/codex.ts
CHANGED
|
@@ -88,9 +88,13 @@ export const codexSpec: CliAgentSpec = {
|
|
|
88
88
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
89
89
|
...(model ? ["-m", shellQuote(model)] : []),
|
|
90
90
|
// codex's own reasoning knob — a config override, valid values
|
|
91
|
-
// low|medium|high (
|
|
91
|
+
// none|minimal|low|medium|high|xhigh (codex docs; xhigh is the
|
|
92
|
+
// codex-max-tier deep-reasoning level). There is NO "max" in codex's
|
|
93
|
+
// vocabulary: the server rejects it for codex sessions
|
|
94
|
+
// (sessionEffortLockError), and this clamp to xhigh is the type-level
|
|
95
|
+
// backstop for a caller that bypasses that gate. The value comes from
|
|
92
96
|
// the closed CliReasoningEffort set, so it is shell-safe unquoted.
|
|
93
|
-
...(effort ? ["-c", `model_reasoning_effort=${effort}`] : []),
|
|
97
|
+
...(effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : []),
|
|
94
98
|
...(cwd ? ["-C", shellQuote(cwd)] : []),
|
|
95
99
|
].join(" ");
|
|
96
100
|
// Fresh turn: `codex exec <flags> - < prompt`. Continue a thread:
|
|
@@ -171,12 +175,16 @@ export const codexSpec: CliAgentSpec = {
|
|
|
171
175
|
},
|
|
172
176
|
};
|
|
173
177
|
|
|
178
|
+
/** The effort levels codex actually has (`model_reasoning_effort`):
|
|
179
|
+
* low|medium|high|xhigh — no "max" (that level is Claude Code's alone). */
|
|
180
|
+
export type CodexReasoningEffort = Exclude<CliReasoningEffort, "max">;
|
|
181
|
+
|
|
174
182
|
export interface CodexRuntimeConfig {
|
|
175
183
|
/** Codex model id (`-m`). Omit to use the codex CLI's configured default. */
|
|
176
184
|
model?: string;
|
|
177
185
|
/** Reasoning effort (`-c model_reasoning_effort=<level>`). Omit to use the
|
|
178
186
|
* codex CLI's configured default. */
|
|
179
|
-
effort?:
|
|
187
|
+
effort?: CodexReasoningEffort;
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
export function createCodexRuntime(config: CodexRuntimeConfig = {}) {
|
|
@@ -155,6 +155,13 @@ export interface ConversationDetail {
|
|
|
155
155
|
* in drivers. Null/absent when never reported (platform agents, older
|
|
156
156
|
* daemons/servers). */
|
|
157
157
|
sessionCommands?: Array<{ name: string; description: string }> | null;
|
|
158
|
+
/** Where this conversation's UNMERGED work lives: a cloud session writes
|
|
159
|
+
* its own private drive branch, and every drive read a client makes for
|
|
160
|
+
* THAT factory must carry `?branch=` or it resolves against `main`,
|
|
161
|
+
* where session-born files do not exist. Non-null only for a session
|
|
162
|
+
* with a drive branch and a resolvable factory; null/absent = main
|
|
163
|
+
* only (read loosely — older servers omit it). */
|
|
164
|
+
sessionDrive?: { factorySlug: string; branch: string } | null;
|
|
158
165
|
viewerLastReadAt: string | null;
|
|
159
166
|
/** The caller's role in this conversation (ADR-0045) — drives client
|
|
160
167
|
* affordances only; the server remains the authority on every action.
|
|
@@ -226,6 +233,24 @@ export interface CreateCloudSessionInput {
|
|
|
226
233
|
* profile ids, or "none" for a bare session. Omitted = the owner's
|
|
227
234
|
* default profile. */
|
|
228
235
|
connectorProfileId?: string;
|
|
236
|
+
// ── Session import (session-import spec) ────────────────────────────────
|
|
237
|
+
/** Idempotent dependency-setup command run by every fresh sandbox acquire
|
|
238
|
+
* after the drive mounts (`bun install`, `npm ci`, …). Strict charset —
|
|
239
|
+
* no shell metacharacters (400). Mutually exclusive with `templateId`
|
|
240
|
+
* (a snapshot carries its own installed state). */
|
|
241
|
+
setupCommand?: string;
|
|
242
|
+
/** Provenance of a session born by `agentc session import`: where the
|
|
243
|
+
* imported local session lived and which harness thread seeded it. */
|
|
244
|
+
handoffOrigin?: {
|
|
245
|
+
cwd: string;
|
|
246
|
+
sourceAcpSessionId: string | null;
|
|
247
|
+
agentKind: string;
|
|
248
|
+
mirrorConversationId: string | null;
|
|
249
|
+
};
|
|
250
|
+
/** Imported claude memories (session-import spec §memories): guest-home
|
|
251
|
+
* seeds re-applied on every fresh acquire. Paths under .claude/ only;
|
|
252
|
+
* strict base64; 256KB decoded total (server-capped). */
|
|
253
|
+
seedFiles?: Array<{ path: string; contentB64: string; mode: "write" | "append" }>;
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
export interface CloudSessionCreated {
|
package/src/types/api-factory.ts
CHANGED
|
@@ -95,6 +95,11 @@ export interface RegisterWorkflowInput {
|
|
|
95
95
|
* server skips the /factory mount for its runs (#13). See
|
|
96
96
|
* `WorkflowMetadata.environmentBuild`. */
|
|
97
97
|
environmentBuild?: boolean;
|
|
98
|
+
/** Drive requirement declared via `defineWorkflow({ factoryDrive })`.
|
|
99
|
+
* Omitted ⇒ `"required"` on the server (a failed /factory mount fails
|
|
100
|
+
* the run); `"none"` = explicit no-drive opt-out. See
|
|
101
|
+
* `WorkflowMetadata.factoryDrive`. */
|
|
102
|
+
factoryDrive?: "required" | "none";
|
|
98
103
|
/** Factory slug. Defaults to `"default"`. */
|
|
99
104
|
factorySlug?: string;
|
|
100
105
|
}
|
|
@@ -194,6 +199,33 @@ export interface FactoryFileWriteResult {
|
|
|
194
199
|
created: boolean;
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
// ── User drive mounts (`agentc files mount` on a human's own machine) ────────
|
|
203
|
+
|
|
204
|
+
/** A minted local-mount grant: the user branch, the signed gateway token, and
|
|
205
|
+
* where to dial. The backing `conversationId` is the mount's review surface
|
|
206
|
+
* (its branch changes list + merge ride the session-changes routes). */
|
|
207
|
+
export interface DriveMountSession {
|
|
208
|
+
conversationId: string;
|
|
209
|
+
branch: string;
|
|
210
|
+
/** Signed gateway mount token (user principal, exclusive mode). Treat as a
|
|
211
|
+
* secret: write it to a 0600 token file, never argv/env of children. */
|
|
212
|
+
token: string;
|
|
213
|
+
gatewayWsUrl: string;
|
|
214
|
+
/** Token expiry, unix seconds — re-mint (same `conversationId`) before it. */
|
|
215
|
+
expiresAtS: number;
|
|
216
|
+
diskId: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface CreateDriveMountSessionInput {
|
|
220
|
+
/** Re-mint for an existing mount session (remount / token refresh). */
|
|
221
|
+
conversationId?: string;
|
|
222
|
+
/** The mounting machine's hostname — carried in the mount's title. */
|
|
223
|
+
host?: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// A mount's change set + merge ride the existing session-branch proposal
|
|
227
|
+
// types in `api-conversations.ts` (`SessionChangeSet`, `SessionMergeReport`).
|
|
228
|
+
|
|
197
229
|
/** A factory: a project-level grouping of workflows inside a team. */
|
|
198
230
|
export interface FactoryRow {
|
|
199
231
|
id: string;
|
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
|
}
|
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 {
|
|
@@ -241,6 +241,14 @@ export interface WorkflowMetadata {
|
|
|
241
241
|
* canonical metadata hash (frozen-metadata rule), so existing workflows are
|
|
242
242
|
* not forced to re-register. */
|
|
243
243
|
environmentBuild?: boolean;
|
|
244
|
+
/** Whether this workflow's runs need the factory drive. ABSENT ⇒
|
|
245
|
+
* `"required"`: on a drive-backed factory the server treats the /factory
|
|
246
|
+
* mount as load-bearing — a mount failure FAILS the run instead of
|
|
247
|
+
* silently proceeding drive-less. Declare `"none"` for a workflow that
|
|
248
|
+
* genuinely never touches /factory: the server skips the mount entirely
|
|
249
|
+
* for its runs (the explicit no-drive mode; there is no silent degrade).
|
|
250
|
+
* Optional + additive (frozen-metadata rule). */
|
|
251
|
+
factoryDrive?: "required" | "none";
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
/**
|
|
@@ -274,6 +282,7 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
|
|
|
274
282
|
if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
275
283
|
if (source.invokePolicy !== undefined) out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
276
284
|
if (source.environmentBuild !== undefined) out.environmentBuild = source.environmentBuild;
|
|
285
|
+
if (source.factoryDrive !== undefined) out.factoryDrive = source.factoryDrive;
|
|
277
286
|
return Object.freeze(out);
|
|
278
287
|
}
|
|
279
288
|
|
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
|
|