@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/dist/types/protocol.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface AgentMessageToolUse extends AgentMessageBase {
|
|
|
31
31
|
toolName: string;
|
|
32
32
|
toolInput: Record<string, unknown>;
|
|
33
33
|
toolUseId: string;
|
|
34
|
+
/** The spawning subagent call's tool_use id when this call ran INSIDE a
|
|
35
|
+
* subagent (claude stream-json stamps `parent_tool_use_id` on every
|
|
36
|
+
* sidechain event) — lets renderers nest child activity under the
|
|
37
|
+
* Agent/Task call instead of flattening it into the parent transcript.
|
|
38
|
+
* Optional and additive: producers without sidechains omit it. */
|
|
39
|
+
parentToolUseId?: string;
|
|
34
40
|
}
|
|
35
41
|
export interface AgentMessageToolResult extends AgentMessageBase {
|
|
36
42
|
type: "tool_result";
|
|
@@ -54,6 +60,8 @@ export interface AgentMessageToolResult extends AgentMessageBase {
|
|
|
54
60
|
path: string;
|
|
55
61
|
line?: number;
|
|
56
62
|
}[];
|
|
63
|
+
/** Sidechain attribution, mirroring AgentMessageToolUse.parentToolUseId. */
|
|
64
|
+
parentToolUseId?: string;
|
|
57
65
|
}
|
|
58
66
|
export interface AgentMessageDone extends AgentMessageBase {
|
|
59
67
|
type: "done";
|
|
@@ -231,6 +231,14 @@ export interface WorkflowMetadata {
|
|
|
231
231
|
* canonical metadata hash (frozen-metadata rule), so existing workflows are
|
|
232
232
|
* not forced to re-register. */
|
|
233
233
|
environmentBuild?: boolean;
|
|
234
|
+
/** Whether this workflow's runs need the factory drive. ABSENT ⇒
|
|
235
|
+
* `"required"`: on a drive-backed factory the server treats the /factory
|
|
236
|
+
* mount as load-bearing — a mount failure FAILS the run instead of
|
|
237
|
+
* silently proceeding drive-less. Declare `"none"` for a workflow that
|
|
238
|
+
* genuinely never touches /factory: the server skips the mount entirely
|
|
239
|
+
* for its runs (the explicit no-drive mode; there is no silent degrade).
|
|
240
|
+
* Optional + additive (frozen-metadata rule). */
|
|
241
|
+
factoryDrive?: "required" | "none";
|
|
234
242
|
}
|
|
235
243
|
/**
|
|
236
244
|
* Pull the server-readable declarations off a `StepWorkflowDefinition`
|
package/dist/utils/bundler.d.ts
CHANGED
|
@@ -18,6 +18,19 @@
|
|
|
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
|
import type { SnapshotConfig } from "../types/workflow.js";
|
|
23
36
|
import type { IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources, DriveMergePolicy } from "../types/workflow-metadata.js";
|
|
@@ -108,7 +121,50 @@ export interface BundledWorkflow {
|
|
|
108
121
|
/** Set by `defineSandboxEnvironment` — marks an environment build so the
|
|
109
122
|
* server skips the /factory mount for its runs (#13). */
|
|
110
123
|
environmentBuild?: boolean;
|
|
124
|
+
/** Drive requirement declared via `defineWorkflow({ factoryDrive })`.
|
|
125
|
+
* Absent ⇒ `"required"` (a failed /factory mount fails the run);
|
|
126
|
+
* `"none"` = explicit no-drive opt-out. */
|
|
127
|
+
factoryDrive?: "required" | "none";
|
|
111
128
|
}
|
|
129
|
+
/** The one true import specifier for the platform SDK. */
|
|
130
|
+
export declare const SDK_PACKAGE = "@agent-compose/sdk";
|
|
131
|
+
/**
|
|
132
|
+
* Import specifiers that can only have MEANT `@agent-compose/sdk`, rewritten
|
|
133
|
+
* to it at bundle time so a draft written with the wrong spelling builds
|
|
134
|
+
* unedited.
|
|
135
|
+
*
|
|
136
|
+
* Every entry carries an `sdk` segment or suffix, so none of them can be a
|
|
137
|
+
* real third-party package a workflow might legitimately depend on: `a/b`
|
|
138
|
+
* forms are subpaths of packages that don't exist, and the `*-sdk` forms
|
|
139
|
+
* name this platform explicitly. Bare `agentc` / `agent-compose` are
|
|
140
|
+
* deliberately NOT aliased — those are plausible npm package names, and
|
|
141
|
+
* silently redirecting a real dependency is worse than a clear error.
|
|
142
|
+
*
|
|
143
|
+
* Exported for the unit test that pins the table.
|
|
144
|
+
*/
|
|
145
|
+
export declare const SDK_SPECIFIER_ALIASES: readonly string[];
|
|
146
|
+
/** `@agent-compose/sdk` when `specifier` is a known near-miss for it, else
|
|
147
|
+
* null. The single authority: the plugin's regex filter is only a fast
|
|
148
|
+
* pre-filter, and this decides. */
|
|
149
|
+
export declare function resolveSdkAlias(specifier: string): string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The SELF-CORRECTING layer. Turns a Bun bundle failure into a message that
|
|
152
|
+
* names the real fix instead of leaking Bun's internals.
|
|
153
|
+
*
|
|
154
|
+
* An unresolved import is almost always one thing: workflow source naming
|
|
155
|
+
* the platform SDK by a specifier that isn't its package name. Bun answers
|
|
156
|
+
* that with `Could not resolve: "agentc/sdk". Maybe you need to "bun
|
|
157
|
+
* install"?` — advice the author cannot act on (there is no package.json to
|
|
158
|
+
* install into; the drive holds one file). The rewritten message says the
|
|
159
|
+
* package name, so an agent reading its own tool error can fix the import on
|
|
160
|
+
* the next turn.
|
|
161
|
+
*
|
|
162
|
+
* Non-resolve failures (syntax errors, transform failures) keep their
|
|
163
|
+
* verbatim diagnostics — those are already actionable.
|
|
164
|
+
*
|
|
165
|
+
* Exported for the unit test; `bundleWorkflow` is the supported entrypoint.
|
|
166
|
+
*/
|
|
167
|
+
export declare function explainBundleFailure(err: unknown, label: string): string;
|
|
112
168
|
/**
|
|
113
169
|
* Parse the bundled source and assert the default export is a CallExpression
|
|
114
170
|
* to an identifier named `defineWorkflow` (or to a `.step(...).build()` chain
|
|
@@ -72,6 +72,13 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
|
|
|
72
72
|
* server skips mounting the shared factory drive for its runs (#13). See
|
|
73
73
|
* `WorkflowMetadata.environmentBuild`. */
|
|
74
74
|
environmentBuild?: boolean;
|
|
75
|
+
/** Whether this workflow's runs need the factory drive. Omit (⇒
|
|
76
|
+
* `"required"`) for any workflow that reads or writes /factory: a failed
|
|
77
|
+
* mount then FAILS the run instead of silently proceeding drive-less.
|
|
78
|
+
* Declare `"none"` for a drive-agnostic workflow — the server skips the
|
|
79
|
+
* /factory mount entirely for its runs. See
|
|
80
|
+
* `WorkflowMetadata.factoryDrive`. */
|
|
81
|
+
factoryDrive?: "required" | "none";
|
|
75
82
|
}
|
|
76
83
|
export declare function createStepWorkflow<TInput, TOutput>(opts: StepWorkflowDefinition<TInput, TOutput>): WorkflowBuilder<TInput, TInput>;
|
|
77
84
|
/** Type guard — true when `value` is a `Workflow`. */
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { InvokeChild } from "../types/execution-context.js";
|
|
2
|
+
/**
|
|
3
|
+
* Derive the deterministic per-call `Idempotency-Key` for a `ctx.invokeChild`
|
|
4
|
+
* dispatch: `invoke-child:<parentRunId>:step<stepIndex>:<childName>:<ordinal>`.
|
|
5
|
+
*
|
|
6
|
+
* Replay safety: a replayed step re-runs its body from the top, so the k-th
|
|
7
|
+
* `invokeChild(name)` call in a step re-derives the SAME key (the per-scope
|
|
8
|
+
* ordinal counter lives on the active-step state, which resets identically on
|
|
9
|
+
* every (re-)entry — the `nextPauseOrdinalInActiveStep` pattern). The server's
|
|
10
|
+
* idempotency window then returns the original child run instead of
|
|
11
|
+
* double-dispatching. Outside step execution (local dev, tests) there is no
|
|
12
|
+
* stable coordinate to key on — returns null and the dispatch is unkeyed,
|
|
13
|
+
* exactly the old behavior.
|
|
14
|
+
*
|
|
15
|
+
* Key syntax matches the server's `Idempotency-Key` grammar
|
|
16
|
+
* (`[A-Za-z0-9_\-:.]{1,255}`): runId is a UUID, step index a number, and
|
|
17
|
+
* workflow names are kebab-case.
|
|
18
|
+
*/
|
|
19
|
+
export declare function deriveInvokeChildIdempotencyKey(parentRunId: string, childName: string): string | null;
|
|
2
20
|
/**
|
|
3
21
|
* Build the public-API child workflow invoker used by legacy and sandboxed
|
|
4
22
|
* workflow execution. Provider-backed engines may inject a different
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay-safety pin for `ctx.invokeChild` (parity plan P3.3): the
|
|
3
|
+
* Idempotency-Key a child dispatch carries is DETERMINISTIC per
|
|
4
|
+
* (parentRunId, stepIndex, childName, call ordinal) — a step body re-run
|
|
5
|
+
* from the top (pause/resume re-entry, crash-replayed attempt) re-derives
|
|
6
|
+
* byte-identical keys, so the server's idempotency window collapses the
|
|
7
|
+
* replayed dispatch into the original child run instead of double-running.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-compose/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/Chris-Moller/agent-compose.git",
|
|
9
9
|
"directory": "sdk"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
package/src/agent/agent-loop.ts
CHANGED
|
@@ -126,6 +126,11 @@ export type AgentLifecycleEvent =
|
|
|
126
126
|
/** Short runtime self-identifier (`claude`, `openai-desktop`, …).
|
|
127
127
|
* Drives the per-agent runtime icon on the dashboard. */
|
|
128
128
|
runtimeKind?: string;
|
|
129
|
+
/** Authored plan-phase this agent belongs to (e.g. dynamic-task's
|
|
130
|
+
* `phase.name`). Purely observability: the dashboard groups agents
|
|
131
|
+
* under named phases without parsing the label prefix. Optional and
|
|
132
|
+
* additive — absent for agents outside a phased plan. */
|
|
133
|
+
phase?: string;
|
|
129
134
|
}
|
|
130
135
|
| { event: "agent.message"; at: number; agentId: string; label: string; iteration: number; message: AgentMessageSummary }
|
|
131
136
|
| { event: "agent.iteration"; at: number; agentId: string; label: string; iteration: number; status: AgentStatus | null }
|
|
@@ -134,6 +139,9 @@ export type AgentLifecycleEvent =
|
|
|
134
139
|
export interface AgentLoopOpts<TResponse = unknown> {
|
|
135
140
|
agentId?: string;
|
|
136
141
|
label?: string;
|
|
142
|
+
/** Authored plan-phase name carried onto `agent.spawned` (see
|
|
143
|
+
* AgentLifecycleEvent.phase). Optional, observability-only. */
|
|
144
|
+
phase?: string;
|
|
137
145
|
onAgentLifecycleEvent?: (event: AgentLifecycleEvent) => void;
|
|
138
146
|
onIteration?: (iteration: number, status: AgentStatus | null) => void;
|
|
139
147
|
turnsPerIteration?: number;
|
|
@@ -307,6 +315,7 @@ export async function agentLoop<TResponse = unknown>(opts: AgentLoopOpts<TRespon
|
|
|
307
315
|
allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
|
|
308
316
|
...(client.model != null ? { model: client.model } : {}),
|
|
309
317
|
...(client.kind != null ? { runtimeKind: client.kind } : {}),
|
|
318
|
+
...(opts.phase != null ? { phase: opts.phase } : {}),
|
|
310
319
|
});
|
|
311
320
|
}
|
|
312
321
|
|
package/src/agent/run-agent.ts
CHANGED
|
@@ -183,6 +183,10 @@ export interface AgentOpts<T = unknown> {
|
|
|
183
183
|
responseSchema?: z.ZodType<T>;
|
|
184
184
|
/** Label prefix for runtime stderr ("[sbid][agent]" by default). */
|
|
185
185
|
label?: string;
|
|
186
|
+
/** Authored plan-phase this agent belongs to. Rides `agent.spawned` so the
|
|
187
|
+
* dashboard groups agents under named phases. Optional, additive,
|
|
188
|
+
* observability-only — it never affects execution. */
|
|
189
|
+
phase?: string;
|
|
186
190
|
/** Lifecycle event sink from workflow ctx. Emits agent.spawned / iteration / settled. */
|
|
187
191
|
events?: { emit: (event: AgentLifecycleEvent) => void | Promise<void> };
|
|
188
192
|
/** Per-message event callback — wire this to your workflow's event
|
|
@@ -349,6 +353,7 @@ export async function agent<T = unknown>(opts: AgentOpts<T>): Promise<AgentLoopR
|
|
|
349
353
|
runtime: (runtimeOpts: RuntimeOptions) => opts.runtime.create(opts.sandbox, { ...runtimeOpts, agentManual: buildAgentContextDoc(process.env) }),
|
|
350
354
|
agentId,
|
|
351
355
|
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
356
|
+
...(opts.phase !== undefined ? { phase: opts.phase } : {}),
|
|
352
357
|
...(opts.budget?.turnsPerIteration !== undefined ? { turnsPerIteration: opts.budget.turnsPerIteration } : {}),
|
|
353
358
|
...(opts.budget?.maxIterations !== undefined ? { maxIterations: opts.budget.maxIterations } : {}),
|
|
354
359
|
...(opts.tools !== undefined ? { allowedTools: opts.tools } : {}),
|
package/src/client.ts
CHANGED
|
@@ -18,7 +18,8 @@ import type { RunEvent } from "./types/events.js";
|
|
|
18
18
|
import type { ConversationStreamEvent } from "./types/conversation-stream.js";
|
|
19
19
|
import { normalizeConversationStreamEvent } from "./types/conversation-stream.js";
|
|
20
20
|
import type {
|
|
21
|
-
InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, StreamRunLogsOptions,
|
|
21
|
+
InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice, StreamRunLogsOptions,
|
|
22
|
+
InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
|
|
22
23
|
RunStatus, ResumePauseOptions, ResumePauseResponse, AnswerSteerOptions,
|
|
23
24
|
RequestAgentPauseOptions, RequestAgentPauseResponse,
|
|
24
25
|
SendAgentMessageOptions, SendAgentMessageResponse,
|
|
@@ -53,6 +54,7 @@ import type {
|
|
|
53
54
|
SecretOptions, SetSecretResult, SecretListEntry,
|
|
54
55
|
CreateApiKeyInput, ApiKey, ApiKeyCreated, UsageResponse,
|
|
55
56
|
DriveRepoLink, CreateDriveRepoLinkInput,
|
|
57
|
+
DriveMountSession, CreateDriveMountSessionInput,
|
|
56
58
|
} from "./types/api-factory.js";
|
|
57
59
|
import type {
|
|
58
60
|
ComplianceSession, RequestComplianceSessionInput, ListComplianceSessionsOptions,
|
|
@@ -64,7 +66,8 @@ import type {
|
|
|
64
66
|
// here so `client.js` stays the single import surface for the client and its
|
|
65
67
|
// shapes (index.ts re-exports from here).
|
|
66
68
|
export type {
|
|
67
|
-
RunState, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, StreamRunLogsOptions,
|
|
69
|
+
RunState, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice, StreamRunLogsOptions,
|
|
70
|
+
InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
|
|
68
71
|
RunStatus, ResumePauseActor, ResumePauseSuccess, ResumePausePending, ResumePauseResponse,
|
|
69
72
|
ResumePauseOptions, RequestAgentPauseOptions, AnswerSteerOptions, RequestAgentPauseResponse,
|
|
70
73
|
SendAgentMessageOptions, SendAgentMessageResponse,
|
|
@@ -108,6 +111,7 @@ export type {
|
|
|
108
111
|
CreateApiKeyInput, ApiKey, ApiKeyCreated,
|
|
109
112
|
UsageRollupRow, UsageResponse,
|
|
110
113
|
DriveRepoLink, CreateDriveRepoLinkInput,
|
|
114
|
+
DriveMountSession, CreateDriveMountSessionInput,
|
|
111
115
|
} from "./types/api-factory.js";
|
|
112
116
|
export type {
|
|
113
117
|
ComplianceScopeKind, ComplianceStatus, ComplianceSession, ComplianceAccess,
|
|
@@ -296,6 +300,45 @@ export class AgentComposeClient {
|
|
|
296
300
|
...(opts?.size !== undefined ? { size: opts.size } : {}),
|
|
297
301
|
...(parentRunId ? { parentRunId } : {}),
|
|
298
302
|
...(opts?.agentId ? { agentId: opts.agentId } : {}),
|
|
303
|
+
...(opts?.funding !== undefined ? { funding: opts.funding } : {}),
|
|
304
|
+
...(opts?.fundingSecret !== undefined ? { fundingSecret: opts.fundingSecret } : {}),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Invoke an INLINE workflow — the exact payload `bundleWorkflow` produced
|
|
310
|
+
* plus a `name` — WITHOUT registering it. The server validates it through
|
|
311
|
+
* the same core as registration (manifest required + bound to the source
|
|
312
|
+
* bytes) but writes no registry row: the run snapshots the source it
|
|
313
|
+
* executes, and registration stays the door for named/versioned/scheduled
|
|
314
|
+
* workflows. Same parent-child auto-detection and `Idempotency-Key`
|
|
315
|
+
* semantics as `invoke()`. Requires the `invoke` scope. */
|
|
316
|
+
invokeInline(
|
|
317
|
+
workflow: InlineWorkflowPayload,
|
|
318
|
+
input?: Record<string, unknown>,
|
|
319
|
+
opts?: InvokeInlineOptions,
|
|
320
|
+
): Promise<InvokeResult> {
|
|
321
|
+
const parentRunId = opts?.parentRunId === undefined
|
|
322
|
+
? detectAmbientParentRunId()
|
|
323
|
+
: opts.parentRunId;
|
|
324
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
325
|
+
const headers: Record<string, string> = {};
|
|
326
|
+
if (opts?.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
327
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/invoke`, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
...(opts?.idempotencyKey ? { headers } : {}),
|
|
330
|
+
body: {
|
|
331
|
+
workflow,
|
|
332
|
+
input,
|
|
333
|
+
...(opts?.title !== undefined ? { title: opts.title } : {}),
|
|
334
|
+
...(opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {}),
|
|
335
|
+
...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
|
|
336
|
+
...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
|
|
337
|
+
...(opts?.size !== undefined ? { size: opts.size } : {}),
|
|
338
|
+
...(parentRunId ? { parentRunId } : {}),
|
|
339
|
+
...(opts?.agentId ? { agentId: opts.agentId } : {}),
|
|
340
|
+
...(opts?.funding !== undefined ? { funding: opts.funding } : {}),
|
|
341
|
+
...(opts?.fundingSecret !== undefined ? { fundingSecret: opts.fundingSecret } : {}),
|
|
299
342
|
},
|
|
300
343
|
});
|
|
301
344
|
}
|
|
@@ -312,12 +355,33 @@ export class AgentComposeClient {
|
|
|
312
355
|
input?: Record<string, unknown>,
|
|
313
356
|
opts?: InvokeAndWaitOptions,
|
|
314
357
|
): Promise<RunStatus<TOutput>> {
|
|
315
|
-
const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
|
|
316
|
-
const pollMs = opts?.pollIntervalMs ?? 1000;
|
|
317
358
|
// InvokeAndWaitOptions extends InvokeWorkflowOptions, so we can forward
|
|
318
359
|
// `opts` directly — invoke() picks only the fields it sends, so the
|
|
319
360
|
// extra `timeoutMs` / `pollIntervalMs` never leak into the request body.
|
|
320
361
|
const { id: runId } = await this.invoke(name, input, opts);
|
|
362
|
+
return this.waitForRun<TOutput>(runId, opts);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** `invokeInline` + block until the run settles — the "call blocks →
|
|
366
|
+
* result returns on the same turn" contract for inline sub-workflows. */
|
|
367
|
+
async invokeInlineAndWait<TOutput = unknown>(
|
|
368
|
+
workflow: InlineWorkflowPayload,
|
|
369
|
+
input?: Record<string, unknown>,
|
|
370
|
+
opts?: InvokeInlineAndWaitOptions,
|
|
371
|
+
): Promise<RunStatus<TOutput>> {
|
|
372
|
+
const { id: runId } = await this.invokeInline(workflow, input, opts);
|
|
373
|
+
return this.waitForRun<TOutput>(runId, opts);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Poll one run until it settles (success / failed / abandoned) and return
|
|
377
|
+
* its final status. Shared tail of `invokeAndWait` / `invokeInlineAndWait`;
|
|
378
|
+
* also useful to re-attach to a run you dispatched fire-and-forget. */
|
|
379
|
+
async waitForRun<TOutput = unknown>(
|
|
380
|
+
runId: string,
|
|
381
|
+
opts?: { timeoutMs?: number; pollIntervalMs?: number },
|
|
382
|
+
): Promise<RunStatus<TOutput>> {
|
|
383
|
+
const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
|
|
384
|
+
const pollMs = opts?.pollIntervalMs ?? 1000;
|
|
321
385
|
const deadline = Date.now() + timeoutMs;
|
|
322
386
|
while (Date.now() < deadline) {
|
|
323
387
|
const status = await this.getStatus<TOutput>(runId);
|
|
@@ -329,7 +393,7 @@ export class AgentComposeClient {
|
|
|
329
393
|
// Use AgentComposeError (not plain Error) so catch-blocks handling SDK
|
|
330
394
|
// transport failures also handle timeouts uniformly. HTTP 504 is the
|
|
331
395
|
// closest idiomatic status for "upstream didn't answer in time."
|
|
332
|
-
throw new AgentComposeError(504, `
|
|
396
|
+
throw new AgentComposeError(504, `waitForRun: run ${runId} did not settle within ${timeoutMs}ms`);
|
|
333
397
|
}
|
|
334
398
|
|
|
335
399
|
/** List captured snapshots in a factory. */
|
|
@@ -622,6 +686,21 @@ export class AgentComposeClient {
|
|
|
622
686
|
}));
|
|
623
687
|
}
|
|
624
688
|
|
|
689
|
+
/** One run artifact's bytes, resolved server-side through the DRIVE INDEX
|
|
690
|
+
* (never the run's gone sandbox or branch) — a listed artifact with an
|
|
691
|
+
* indexed path is always readable here, including after the run's branch
|
|
692
|
+
* is merged/retired. The path is the listing's `path`, sent as a single
|
|
693
|
+
* query parameter so slashes / spaces / unicode in agent-derived
|
|
694
|
+
* filenames survive verbatim. */
|
|
695
|
+
async getRunArtifactBytes(runId: string, path: string): Promise<Uint8Array> {
|
|
696
|
+
const q = new URLSearchParams({ path });
|
|
697
|
+
const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
|
|
698
|
+
`/api/v1/workflows/${encodeURIComponent(runId)}/artifacts/content?${q}`,
|
|
699
|
+
{ responseType: "arrayBuffer" },
|
|
700
|
+
);
|
|
701
|
+
return new Uint8Array(buf);
|
|
702
|
+
}
|
|
703
|
+
|
|
625
704
|
// ── Factory files ──────────────────────────────────────────────────────────
|
|
626
705
|
// The factory drive: documents surfaced in the dashboard's Files tab.
|
|
627
706
|
// Writes from inside a sandbox automatically carry the run-callback token
|
|
@@ -665,13 +744,17 @@ export class AgentComposeClient {
|
|
|
665
744
|
* U+FFFD and the original bytes are unrecoverable). */
|
|
666
745
|
async getFactoryFileBytes(
|
|
667
746
|
path: string,
|
|
668
|
-
opts?: { factorySlug?: string; revision?: number },
|
|
747
|
+
opts?: { factorySlug?: string; revision?: number; branch?: string },
|
|
669
748
|
): Promise<Uint8Array> {
|
|
670
749
|
const factorySlug = opts?.factorySlug
|
|
671
750
|
?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
|
|
672
751
|
?? DEFAULT_FACTORY;
|
|
673
752
|
const q = new URLSearchParams({ path });
|
|
674
753
|
if (opts?.revision !== undefined) q.set("revision", String(opts.revision));
|
|
754
|
+
// Branch view (read-only): the file AS OF a drive branch — e.g. a cloud
|
|
755
|
+
// session's own `session-<uuid>` branch, where its unmerged work lives.
|
|
756
|
+
// Mutually exclusive with `revision` (the server rejects the combination).
|
|
757
|
+
if (opts?.branch !== undefined) q.set("branch", opts.branch);
|
|
675
758
|
const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
|
|
676
759
|
`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q}`,
|
|
677
760
|
{ responseType: "arrayBuffer" },
|
|
@@ -684,11 +767,57 @@ export class AgentComposeClient {
|
|
|
684
767
|
* text corrupts the bytes irreversibly. */
|
|
685
768
|
async getFactoryFile(
|
|
686
769
|
path: string,
|
|
687
|
-
opts?: { factorySlug?: string; revision?: number },
|
|
770
|
+
opts?: { factorySlug?: string; revision?: number; branch?: string },
|
|
688
771
|
): Promise<string> {
|
|
689
772
|
return new TextDecoder().decode(await this.getFactoryFileBytes(path, opts));
|
|
690
773
|
}
|
|
691
774
|
|
|
775
|
+
// ── User drive mounts ──────────────────────────────────────────────────────
|
|
776
|
+
// `agentc files mount` on a human's own machine: the server forks a user
|
|
777
|
+
// branch, backs it with a mount session (the review surface), and mints a
|
|
778
|
+
// user-principal gateway token. Human-held credentials only (sign-in
|
|
779
|
+
// cookie or a device-flow bridge key) — sandbox session keys are refused.
|
|
780
|
+
|
|
781
|
+
/** Create a local drive mount (or re-mint an existing one's token by
|
|
782
|
+
* passing its `conversationId`). */
|
|
783
|
+
async createDriveMountSession(
|
|
784
|
+
opts?: CreateDriveMountSessionInput & { factorySlug?: string },
|
|
785
|
+
): Promise<DriveMountSession> {
|
|
786
|
+
const factorySlug = opts?.factorySlug
|
|
787
|
+
?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
|
|
788
|
+
?? DEFAULT_FACTORY;
|
|
789
|
+
return this.fetch<DriveMountSession>(
|
|
790
|
+
`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/mount-sessions`,
|
|
791
|
+
{
|
|
792
|
+
method: "POST",
|
|
793
|
+
body: {
|
|
794
|
+
...(opts?.conversationId ? { conversationId: opts.conversationId } : {}),
|
|
795
|
+
...(opts?.host ? { host: opts.host } : {}),
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Release a local mount's gateway branch mount (clean unmount). The
|
|
802
|
+
* branch and its review card survive — this only drops the gateway's
|
|
803
|
+
* in-memory mount so the exclusive branch is not pinned. */
|
|
804
|
+
async releaseDriveMountSession(
|
|
805
|
+
conversationId: string,
|
|
806
|
+
opts?: { factorySlug?: string },
|
|
807
|
+
): Promise<{ released: boolean }> {
|
|
808
|
+
const factorySlug = opts?.factorySlug
|
|
809
|
+
?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
|
|
810
|
+
?? DEFAULT_FACTORY;
|
|
811
|
+
return this.fetch<{ released: boolean }>(
|
|
812
|
+
`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/mount-sessions/${encodeURIComponent(conversationId)}/release`,
|
|
813
|
+
{ method: "POST", body: {} },
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// A mount's change set + merge/discard ride the EXISTING session-branch
|
|
818
|
+
// proposal surface below (`getSessionChanges` / `mergeSessionChanges` /
|
|
819
|
+
// `discardSessionChanges`) — the mount's `conversationId` is a session.
|
|
820
|
+
|
|
692
821
|
// ── Conversations ──────────────────────────────────────────────────────────
|
|
693
822
|
// The chat surfaces (channels / solo chats / sessions) the caller can
|
|
694
823
|
// access. Key callers see what their sender scope allows; the server is the
|
|
@@ -711,9 +840,12 @@ export class AgentComposeClient {
|
|
|
711
840
|
return this.fetch<SessionsPage>(`/api/v1/sessions?${q}`);
|
|
712
841
|
}
|
|
713
842
|
|
|
714
|
-
/** One conversation with its latest page of messages.
|
|
715
|
-
|
|
716
|
-
|
|
843
|
+
/** One conversation with its latest page of messages. `limit` bounds the
|
|
844
|
+
* page (newest N) — metadata-only consumers (e.g. resolving the
|
|
845
|
+
* session's drive branch) pass 1 instead of pulling the full hydrate. */
|
|
846
|
+
getConversation(id: string, opts?: { limit?: number }): Promise<ConversationDetail> {
|
|
847
|
+
const q = opts?.limit !== undefined ? `?limit=${opts.limit}` : "";
|
|
848
|
+
return this.fetch<ConversationDetail>(`/api/v1/conversations/${encodeURIComponent(id)}${q}`);
|
|
717
849
|
}
|
|
718
850
|
|
|
719
851
|
/** Provision a CLOUD-native session (ADR-0037 Phase 3b / ADR-0055 §9): a
|
|
@@ -742,6 +874,9 @@ export class AgentComposeClient {
|
|
|
742
874
|
...(input.sandboxSize ? { sandboxSize: input.sandboxSize } : {}),
|
|
743
875
|
...(input.networkPolicy !== undefined ? { networkPolicy: input.networkPolicy } : {}),
|
|
744
876
|
...(input.connectorProfileId !== undefined ? { connectorProfileId: input.connectorProfileId } : {}),
|
|
877
|
+
...(input.setupCommand !== undefined ? { setupCommand: input.setupCommand } : {}),
|
|
878
|
+
...(input.handoffOrigin !== undefined ? { handoffOrigin: input.handoffOrigin } : {}),
|
|
879
|
+
...(input.seedFiles !== undefined ? { seedFiles: input.seedFiles } : {}),
|
|
745
880
|
},
|
|
746
881
|
});
|
|
747
882
|
}
|
package/src/display.ts
CHANGED
|
@@ -101,8 +101,32 @@ export const TABLE_MAX_COLUMNS = 12;
|
|
|
101
101
|
export const TABLE_MAX_ROWS = 100;
|
|
102
102
|
export const TABLE_CELL_MAX_CHARS = 200;
|
|
103
103
|
|
|
104
|
-
/** Drive-path bound for
|
|
105
|
-
const
|
|
104
|
+
/** Drive-path bound for promoted/marker paths (mirrors the directive cap). */
|
|
105
|
+
export const DRIVE_PATH_MAX_CHARS = 512;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether a string is a PLAUSIBLE factory-drive path — the gate every
|
|
109
|
+
* promoted document/image/preview/diff path and document/image marker
|
|
110
|
+
* passes before it can become a card. A malformed agent command can hand
|
|
111
|
+
* the promoter shell fragments instead of a path (live failure: a partial
|
|
112
|
+
* `agentc display document` call promoted the redirect word `2>&1` — and a
|
|
113
|
+
* bare `/` — into "Showed document" cards whose viewer link was dead), so
|
|
114
|
+
* anything that reads as a shell operator, flag, or empty reference is not
|
|
115
|
+
* a path: reject whitespace-only, over-long, flag-shaped (leading `-`),
|
|
116
|
+
* shell operators / substitution (`|`, `<`, `>`, `;`, backticks, `$(`,
|
|
117
|
+
* which covers `2>&1`), control characters, backslashes, bare/duplicate
|
|
118
|
+
* slashes, and `.`/`..` segments (the server rejects those anyway).
|
|
119
|
+
*/
|
|
120
|
+
export function isPlausibleDrivePath(raw: string): boolean {
|
|
121
|
+
if (raw.trim().length === 0 || raw.length > DRIVE_PATH_MAX_CHARS) return false;
|
|
122
|
+
if (raw.startsWith("-")) return false;
|
|
123
|
+
// eslint-disable-next-line no-control-regex
|
|
124
|
+
if (/[\u0000-\u001f\u007f\\]/.test(raw)) return false;
|
|
125
|
+
if (/[|<>;`]|\$\(/.test(raw)) return false;
|
|
126
|
+
const trimmed = raw.replace(/^\/+|\/+$/g, "");
|
|
127
|
+
if (trimmed.length === 0) return false; // bare "/" (or only slashes)
|
|
128
|
+
return trimmed.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
|
|
129
|
+
}
|
|
106
130
|
|
|
107
131
|
function clampCellString(s: string): string {
|
|
108
132
|
return s.length > TABLE_CELL_MAX_CHARS ? `${s.slice(0, TABLE_CELL_MAX_CHARS - 1)}…` : s;
|
|
@@ -288,7 +312,7 @@ export function parseDisplayMarker(line: string): DisplayMarker | null {
|
|
|
288
312
|
}
|
|
289
313
|
case "document": {
|
|
290
314
|
const path = str("path");
|
|
291
|
-
if (!path) return null;
|
|
315
|
+
if (!path || !isPlausibleDrivePath(path)) return null;
|
|
292
316
|
return {
|
|
293
317
|
kind: "document", path,
|
|
294
318
|
...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
|
|
@@ -301,7 +325,7 @@ export function parseDisplayMarker(line: string): DisplayMarker | null {
|
|
|
301
325
|
}
|
|
302
326
|
case "image": {
|
|
303
327
|
const path = str("path");
|
|
304
|
-
if (!path || path
|
|
328
|
+
if (!path || !isPlausibleDrivePath(path)) return null;
|
|
305
329
|
return {
|
|
306
330
|
kind: "image", path,
|
|
307
331
|
...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
|
|
@@ -446,6 +470,11 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
|
|
|
446
470
|
for (let i = start + 1; i < words.length; i += 1) {
|
|
447
471
|
const w = words[i];
|
|
448
472
|
if (SHELL_STOPPERS.has(w)) break;
|
|
473
|
+
// A merged-stream redirect (`2>&1`) is tolerated by the control-word
|
|
474
|
+
// gate above, but it is shell syntax, never an argument — collecting
|
|
475
|
+
// it turned a truncated `agentc display document 2>&1` into a "Showed
|
|
476
|
+
// document" card whose path was the redirect itself (live failure).
|
|
477
|
+
if (/^\d*>&\d+$/.test(w)) continue;
|
|
449
478
|
if (w.startsWith("--")) {
|
|
450
479
|
if (VALUE_FLAGS.has(w) && i + 1 < words.length && !SHELL_STOPPERS.has(words[i + 1])) {
|
|
451
480
|
flags.set(w, words[i + 1]);
|
|
@@ -458,6 +487,12 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
|
|
|
458
487
|
const factorySlug = flags.get("--factory");
|
|
459
488
|
const withSlug = factorySlug ? { factorySlug } : {};
|
|
460
489
|
const uuid = (v: string | undefined): string | null => (v && UUID_RE.test(v) ? v : null);
|
|
490
|
+
// Path-taking verbs promote only PLAUSIBLE drive paths — a shell
|
|
491
|
+
// fragment or bare `/` from a malformed command is unparseable-argument
|
|
492
|
+
// territory (the pair persists raw, the CLI's own error is the agent's
|
|
493
|
+
// feedback), never a document card.
|
|
494
|
+
const drivePath = (v: string | undefined): string | null =>
|
|
495
|
+
(v && isPlausibleDrivePath(v) ? v : null);
|
|
461
496
|
|
|
462
497
|
if (args[0] === "display") {
|
|
463
498
|
switch (args[1]) {
|
|
@@ -469,19 +504,28 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
|
|
|
469
504
|
const runId = uuid(args[2]);
|
|
470
505
|
return runId ? { verb: "display-changes", runId, ...withSlug } : null;
|
|
471
506
|
}
|
|
472
|
-
case "document":
|
|
473
|
-
|
|
507
|
+
case "document": {
|
|
508
|
+
const path = drivePath(args[2]);
|
|
509
|
+
return path ? { verb: "display-document", path, ...withSlug } : null;
|
|
510
|
+
}
|
|
474
511
|
case "plan":
|
|
475
512
|
return { verb: "display-plan" };
|
|
476
|
-
case "image":
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
513
|
+
case "image": {
|
|
514
|
+
const path = drivePath(args[2]);
|
|
515
|
+
return path ? { verb: "display-image", path, ...withSlug } : null;
|
|
516
|
+
}
|
|
517
|
+
case "preview": {
|
|
518
|
+
const path = drivePath(args[2]);
|
|
519
|
+
return path ? { verb: "display-preview", path } : null;
|
|
520
|
+
}
|
|
480
521
|
case "table": {
|
|
481
522
|
const file = flags.get("--file");
|
|
482
523
|
const hasData = flags.has("--data");
|
|
483
524
|
// Exactly one source; both or neither is an unpromotable combination.
|
|
484
|
-
if (file && !hasData)
|
|
525
|
+
if (file && !hasData) {
|
|
526
|
+
const path = drivePath(file);
|
|
527
|
+
return path ? { verb: "display-table", source: "file", path } : null;
|
|
528
|
+
}
|
|
485
529
|
if (hasData && !file) return { verb: "display-table", source: "inline" };
|
|
486
530
|
return null;
|
|
487
531
|
}
|
|
@@ -494,9 +538,10 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
|
|
|
494
538
|
case "diff": {
|
|
495
539
|
const from = flags.get("--from");
|
|
496
540
|
const to = flags.get("--to");
|
|
497
|
-
|
|
541
|
+
const path = drivePath(args[2]);
|
|
542
|
+
if (!path || !from || !to) return null;
|
|
498
543
|
if (!REVISION_SELECTOR_RE.test(from) || !REVISION_SELECTOR_RE.test(to)) return null;
|
|
499
|
-
return { verb: "display-diff", path
|
|
544
|
+
return { verb: "display-diff", path, from, to };
|
|
500
545
|
}
|
|
501
546
|
case "ask": {
|
|
502
547
|
const prompt = flags.get("--prompt");
|
|
@@ -524,8 +569,9 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
|
|
|
524
569
|
const runId = uuid(args[2]);
|
|
525
570
|
return runId ? { verb: "run-get", runId } : null;
|
|
526
571
|
}
|
|
527
|
-
if (args[0] === "files" && args[1] === "read"
|
|
528
|
-
|
|
572
|
+
if (args[0] === "files" && args[1] === "read") {
|
|
573
|
+
const path = drivePath(args[2]);
|
|
574
|
+
return path ? { verb: "files-read", path, ...withSlug } : null;
|
|
529
575
|
}
|
|
530
576
|
return null;
|
|
531
577
|
}
|