@agent-compose/sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -39
- package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +21 -1
- package/dist/agent/agent-loop.d.ts +24 -1
- package/dist/client.d.ts +338 -534
- package/dist/directives.d.ts +112 -0
- package/dist/display.d.ts +242 -0
- package/dist/errors.d.ts +24 -1
- package/dist/index.d.ts +24 -12
- package/dist/index.js +3545 -1667
- package/dist/pause/wrappers.d.ts +31 -9
- package/dist/runtimes/_acp-client.d.ts +46 -1
- package/dist/runtimes/_cli-agent.d.ts +49 -4
- package/dist/runtimes/_jsonl-guard.d.ts +103 -0
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/claude-code.d.ts +59 -0
- package/dist/runtimes/claude-code.test.d.ts +14 -0
- package/dist/runtimes/claude.d.ts +16 -0
- package/dist/runtimes/claude.test.d.ts +8 -0
- package/dist/runtimes/codex.d.ts +9 -3
- package/dist/runtimes/cursor.d.ts +2 -2
- package/dist/runtimes/droid.d.ts +2 -2
- package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
- package/dist/runtimes/openai-desktop.js +2691 -864
- package/dist/runtimes/opencode.d.ts +2 -2
- package/dist/runtimes/vercel.js +12 -1
- package/dist/sandbox/devbox.d.ts +42 -0
- package/dist/sandbox/exec-stream.d.ts +14 -0
- package/dist/sandbox/network-policy.d.ts +100 -0
- package/dist/sandbox/provider-def.d.ts +79 -0
- package/dist/sandbox/providers/desktop.d.ts +10 -0
- package/dist/sandbox/providers/e2b.d.ts +17 -0
- package/dist/sandbox/providers/local.d.ts +11 -0
- package/dist/sandbox/providers/vercel.d.ts +18 -0
- package/dist/sandbox/registry.d.ts +45 -0
- package/dist/sandbox/sizes.d.ts +68 -0
- package/dist/sandbox.d.ts +24 -299
- package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
- package/dist/step-invocation/invoker.d.ts +10 -0
- package/dist/step-invocation/protocol.d.ts +5 -0
- package/dist/types/api-compliance.d.ts +71 -0
- package/dist/types/api-conversations.d.ts +492 -0
- package/dist/types/api-factory.d.ts +309 -0
- package/dist/types/api-projects.d.ts +131 -0
- package/dist/types/api-runs.d.ts +377 -0
- package/dist/types/api-scopes.d.ts +102 -0
- package/dist/types/conversation-stream.d.ts +191 -0
- package/dist/types/execution-context.d.ts +12 -2
- package/dist/types/protocol.d.ts +30 -1
- package/dist/types/sandbox-environment.d.ts +8 -5
- package/dist/types/sandbox.d.ts +74 -4
- package/dist/types/workflow-metadata.d.ts +33 -8
- package/dist/types/workflow-plan.d.ts +10 -0
- package/dist/types/workflow.d.ts +18 -205
- package/dist/utils/bundler.d.ts +12 -1
- package/dist/workflow-steps/index.d.ts +1 -1
- package/dist/workflow-steps/observability.d.ts +8 -1
- package/dist/workflow-steps/runner.d.ts +3 -3
- package/dist/workflow-steps/step.d.ts +15 -1
- package/dist/workflow-steps/types.d.ts +19 -5
- package/dist/workflow-steps/workflow.d.ts +22 -1
- package/dist/workflows/engine.d.ts +3 -2
- package/dist/workflows/invoke-child.d.ts +2 -2
- package/package.json +1 -1
- package/src/agent/agent-context.ts +186 -3
- package/src/agent/agent-loop.ts +31 -2
- package/src/client.ts +909 -621
- package/src/directives.ts +184 -0
- package/src/display.ts +788 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +104 -10
- package/src/pause/wrappers.ts +44 -9
- package/src/runtimes/_acp-client.ts +72 -3
- package/src/runtimes/_cli-agent.ts +159 -36
- package/src/runtimes/_jsonl-guard.ts +219 -0
- package/src/runtimes/claude-code.ts +246 -0
- package/src/runtimes/claude.ts +32 -2
- package/src/runtimes/codex.ts +55 -3
- package/src/runtimes/openai-desktop.ts +59 -14
- package/src/sandbox/devbox.ts +48 -0
- package/src/sandbox/exec-stream.ts +48 -0
- package/src/sandbox/network-policy.ts +181 -0
- package/src/sandbox/provider-def.ts +94 -0
- package/src/sandbox/providers/desktop.ts +57 -0
- package/src/sandbox/providers/e2b.ts +354 -0
- package/src/sandbox/providers/local.ts +106 -0
- package/src/sandbox/providers/vercel.ts +331 -0
- package/src/sandbox/registry.ts +198 -0
- package/src/sandbox/sizes.ts +95 -0
- package/src/sandbox.ts +59 -1275
- package/src/step-invocation/invoker.ts +151 -28
- package/src/step-invocation/protocol.ts +8 -0
- package/src/types/api-compliance.ts +79 -0
- package/src/types/api-conversations.ts +522 -0
- package/src/types/api-factory.ts +336 -0
- package/src/types/api-projects.ts +140 -0
- package/src/types/api-runs.ts +412 -0
- package/src/types/api-scopes.ts +102 -0
- package/src/types/conversation-stream.ts +231 -0
- package/src/types/execution-context.ts +10 -2
- package/src/types/protocol.ts +33 -0
- package/src/types/sandbox-environment.ts +28 -9
- package/src/types/sandbox.ts +73 -4
- package/src/types/workflow-metadata.ts +35 -8
- package/src/types/workflow-plan.ts +11 -0
- package/src/types/workflow.ts +25 -292
- package/src/utils/bundler.ts +32 -5
- package/src/utils/errors.ts +16 -1
- package/src/workflow-steps/index.ts +1 -0
- package/src/workflow-steps/observability.ts +19 -8
- package/src/workflow-steps/runner.ts +4 -4
- package/src/workflow-steps/step.ts +49 -1
- package/src/workflow-steps/types.ts +20 -5
- package/src/workflow-steps/workflow.ts +22 -1
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/invoke-child.ts +2 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation SSE stream — typed wire events (ADR-0033 phase 1 shapes,
|
|
3
|
+
* extended by ADR-0037 with the live-only `turn_state` / `presence` frames).
|
|
4
|
+
*
|
|
5
|
+
* The server (`server/src/routes/conversations.ts`, `GET
|
|
6
|
+
* /conversations/:id/stream`) emits two classes of event:
|
|
7
|
+
*
|
|
8
|
+
* - **Durable** events (`part`, `message_done`, `error`, `reaction`,
|
|
9
|
+
* `message_deleted`): each is a persisted `conversation_stream_events`
|
|
10
|
+
* row, carries an SSE `id:` field (the conversation-global row id), and
|
|
11
|
+
* replays on reconnect. Clients advance `Last-Event-ID` from these — and
|
|
12
|
+
* ONLY these.
|
|
13
|
+
* - **Live-only** frames (`part_partial`, `turn_state`, `presence`,
|
|
14
|
+
* `session_status`): no row, no SSE `id:` field, never replayed.
|
|
15
|
+
* Advancing `Last-Event-ID` from one would skip durable rows on
|
|
16
|
+
* reconnect and corrupt replay — this is a pinned invariant on every
|
|
17
|
+
* client (dashboard, SDK, TUI).
|
|
18
|
+
*
|
|
19
|
+
* Two sequences ride the wire — do not conflate them:
|
|
20
|
+
* - the SSE `id:` field is the conversation-global stream-event row id
|
|
21
|
+
* (`Last-Event-ID` resumption is keyed on this);
|
|
22
|
+
* - `seq` is the per-message part index (0, 1, …) — part ordering and
|
|
23
|
+
* replay dedupe within one message key on this.
|
|
24
|
+
*
|
|
25
|
+
* `normalizeConversationStreamEvent` folds both transports' framing (SSE
|
|
26
|
+
* `event:`/`id:` fields + JSON data payload) into one union member with a
|
|
27
|
+
* single rule: `id` is present ⇔ the event is durable.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { ConversationMessagePart } from "./api-conversations.js";
|
|
31
|
+
|
|
32
|
+
/** Message author metadata — rides every message-scoped event (live and
|
|
33
|
+
* replayed) so a teammate's user message mirrored onto a shared thread
|
|
34
|
+
* renders as a user message, not as the agent. */
|
|
35
|
+
export interface ConversationStreamAuthor {
|
|
36
|
+
authorKind?: "user" | "agent" | "system";
|
|
37
|
+
authorId?: string | null;
|
|
38
|
+
/** Present only on thread replies — absent = a room message. */
|
|
39
|
+
threadRootId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DurableEventBase extends ConversationStreamAuthor {
|
|
43
|
+
conversationId: string;
|
|
44
|
+
/** Durable stream-event row id (the SSE `id:` field). Present on every
|
|
45
|
+
* durable event; clients advance `Last-Event-ID` from it. */
|
|
46
|
+
id?: number;
|
|
47
|
+
/** Producer timestamp, ms epoch. */
|
|
48
|
+
at: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One persisted message part (final). Replay-safe: a reconnect that
|
|
52
|
+
* re-delivers seq N is a no-op for a buffer that already holds it. */
|
|
53
|
+
export interface ConversationPartEvent extends DurableEventBase {
|
|
54
|
+
event: "part";
|
|
55
|
+
messageId: string;
|
|
56
|
+
seq: number;
|
|
57
|
+
part: ConversationMessagePart;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Live-only progressive re-emit of an in-flight text block: the block's
|
|
61
|
+
* FULL text grown so far, at the same `seq` its final `part` event will
|
|
62
|
+
* use. Never persisted, never carries an SSE id. */
|
|
63
|
+
export interface ConversationPartPartialEvent extends ConversationStreamAuthor {
|
|
64
|
+
event: "part_partial";
|
|
65
|
+
conversationId: string;
|
|
66
|
+
messageId: string;
|
|
67
|
+
seq: number;
|
|
68
|
+
at: number;
|
|
69
|
+
part: ConversationMessagePart;
|
|
70
|
+
partial: true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The message's turn completed — the persisted row is authoritative from
|
|
74
|
+
* here on. */
|
|
75
|
+
export interface ConversationMessageDoneEvent extends DurableEventBase {
|
|
76
|
+
event: "message_done";
|
|
77
|
+
messageId: string;
|
|
78
|
+
seq: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A turn error (durable, message-scoped: carries `part` and/or `error`) —
|
|
82
|
+
* or a stream-level rejection (`messageId` absent, `error` only: the
|
|
83
|
+
* conversation is gone or access was revoked; the server ends the stream). */
|
|
84
|
+
export interface ConversationErrorEvent extends ConversationStreamAuthor {
|
|
85
|
+
event: "error";
|
|
86
|
+
conversationId?: string;
|
|
87
|
+
id?: number;
|
|
88
|
+
messageId?: string | null;
|
|
89
|
+
seq?: number;
|
|
90
|
+
at?: number;
|
|
91
|
+
part?: ConversationMessagePart;
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whole-map replacement of one message's reactions — replays and
|
|
96
|
+
* out-of-order arrivals are harmless (last write wins). */
|
|
97
|
+
export interface ConversationReactionEvent extends DurableEventBase {
|
|
98
|
+
event: "reaction";
|
|
99
|
+
messageId: string;
|
|
100
|
+
seq: number;
|
|
101
|
+
reactions: Record<string, string[]>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A message was deleted. `tombstoned: true` = a thread root that kept its
|
|
105
|
+
* shell; false/absent = hard-deleted. Durable either way (hard deletes
|
|
106
|
+
* persist a null-FK marker row), so it replays on reconnect. */
|
|
107
|
+
export interface ConversationMessageDeletedEvent extends DurableEventBase {
|
|
108
|
+
event: "message_deleted";
|
|
109
|
+
messageId: string;
|
|
110
|
+
seq: number;
|
|
111
|
+
tombstoned?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The bounded replay batch filled before reaching the live head — the
|
|
115
|
+
* server ends the stream after this. Reconnect immediately with
|
|
116
|
+
* `Last-Event-ID: nextAfterSeq` (no backoff). */
|
|
117
|
+
export interface ConversationReplayContinueEvent {
|
|
118
|
+
event: "replay.continue";
|
|
119
|
+
nextAfterSeq: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Live-only turn-machine state (ADR-0037 §6): `running` = a claim is held
|
|
123
|
+
* and a turn is executing; `queued` = a send coalesced behind the in-flight
|
|
124
|
+
* turn (owed work — answered by its release loop); `idle` = nothing owed. */
|
|
125
|
+
export interface ConversationTurnStateEvent {
|
|
126
|
+
event: "turn_state";
|
|
127
|
+
conversationId: string;
|
|
128
|
+
messageId: null;
|
|
129
|
+
state: "running" | "queued" | "idle";
|
|
130
|
+
pendingCount: number;
|
|
131
|
+
at: number;
|
|
132
|
+
partial: true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One attached surface (a dashboard tab, an `agentc session` TUI process)
|
|
136
|
+
* as carried on a `presence` beat. Clients key the roster on `clientId` —
|
|
137
|
+
* the same user attached from two surfaces is two entries — and expire an
|
|
138
|
+
* entry after the server-declared TTL (`ConversationPresenceSnapshot.ttlMs`). */
|
|
139
|
+
export interface ConversationPresenceClient {
|
|
140
|
+
clientId: string;
|
|
141
|
+
userId: string | null;
|
|
142
|
+
/** The attaching user's display name (null for key callers with no
|
|
143
|
+
* user binding). */
|
|
144
|
+
name: string | null;
|
|
145
|
+
surface: "dashboard" | "terminal";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The conversation agent's executor substrate + liveness, derived fresh
|
|
149
|
+
* server-side at emit time: `platform` and `cloud` executors are
|
|
150
|
+
* server-resident (always online); `local` is online iff the bridge
|
|
151
|
+
* daemon heartbeated within the server's TTL. Null = no agent (a dm). */
|
|
152
|
+
export interface ConversationAgentPresence {
|
|
153
|
+
executor: "platform" | "local" | "cloud";
|
|
154
|
+
online: boolean;
|
|
155
|
+
/** The daemon's machine label (local executor only). */
|
|
156
|
+
machineName: string | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Live-only presence beat (ADR-0037 §6, Phase 6): ONE attached client's
|
|
160
|
+
* heartbeat + a fresh agent-liveness snapshot. There is deliberately no
|
|
161
|
+
* server-side roster — every subscriber assembles it from beats (keyed by
|
|
162
|
+
* `clientId`, TTL-expired), the same stateless discipline as the bridge
|
|
163
|
+
* daemon's DB heartbeat. Never persisted, never carries an SSE id, never
|
|
164
|
+
* advances `Last-Event-ID`. */
|
|
165
|
+
export interface ConversationPresenceEvent {
|
|
166
|
+
event: "presence";
|
|
167
|
+
conversationId: string;
|
|
168
|
+
at: number;
|
|
169
|
+
partial: true;
|
|
170
|
+
client: ConversationPresenceClient;
|
|
171
|
+
agent: ConversationAgentPresence | null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Live-only rail-status frame on a CHANNEL's stream (ADR-0057 Seam 5):
|
|
175
|
+
* one ATTACHED session flipped running/idle. `conversationId` is the
|
|
176
|
+
* channel; `sessionConversationId` is the session whose turn machine
|
|
177
|
+
* moved. Same contract as `turn_state`: never persisted, never carries an
|
|
178
|
+
* SSE id, never advances `Last-Event-ID`. Durable rail fields (unread /
|
|
179
|
+
* awaiting / failed) refresh via the channel-sessions list, not this. */
|
|
180
|
+
export interface ConversationSessionStatusEvent {
|
|
181
|
+
event: "session_status";
|
|
182
|
+
conversationId: string;
|
|
183
|
+
sessionConversationId: string;
|
|
184
|
+
state: "running" | "idle";
|
|
185
|
+
at: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ConversationStreamEvent =
|
|
189
|
+
| ConversationPartEvent
|
|
190
|
+
| ConversationPartPartialEvent
|
|
191
|
+
| ConversationMessageDoneEvent
|
|
192
|
+
| ConversationErrorEvent
|
|
193
|
+
| ConversationReactionEvent
|
|
194
|
+
| ConversationMessageDeletedEvent
|
|
195
|
+
| ConversationReplayContinueEvent
|
|
196
|
+
| ConversationTurnStateEvent
|
|
197
|
+
| ConversationPresenceEvent
|
|
198
|
+
| ConversationSessionStatusEvent;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fold one parsed SSE frame into a `ConversationStreamEvent`.
|
|
202
|
+
*
|
|
203
|
+
* Normalizes the two places the wire carries framing metadata:
|
|
204
|
+
* - the event name comes from the SSE `event:` field, falling back to the
|
|
205
|
+
* data payload's own `event` (replayed rows carry both; `replay.continue`
|
|
206
|
+
* carries only the SSE field);
|
|
207
|
+
* - `id` is set from the SSE `id:` field when present and positive —
|
|
208
|
+
* replayed rows carry it ONLY there, live durable notifies carry it in
|
|
209
|
+
* both places (equal), and live-only frames carry a `-1` sentinel inside
|
|
210
|
+
* the data which is stripped here. The result is one invariant: `id`
|
|
211
|
+
* present ⇔ durable ⇔ may advance `Last-Event-ID`.
|
|
212
|
+
*
|
|
213
|
+
* Unknown future event names flow through untyped (cast) — callers using
|
|
214
|
+
* the discriminated union see them via the default branch, mirroring
|
|
215
|
+
* `streamRunLogs`.
|
|
216
|
+
*/
|
|
217
|
+
export function normalizeConversationStreamEvent(
|
|
218
|
+
eventName: string,
|
|
219
|
+
data: Record<string, unknown>,
|
|
220
|
+
sseId: number | undefined,
|
|
221
|
+
): ConversationStreamEvent {
|
|
222
|
+
const name = eventName || (typeof data.event === "string" ? data.event : "message");
|
|
223
|
+
const dataId = typeof data.id === "number" && Number.isFinite(data.id) ? data.id : undefined;
|
|
224
|
+
const durableId = sseId !== undefined && Number.isFinite(sseId) && sseId > 0
|
|
225
|
+
? sseId
|
|
226
|
+
: dataId !== undefined && dataId > 0 ? dataId : undefined;
|
|
227
|
+
const ev: Record<string, unknown> = { ...data, event: name };
|
|
228
|
+
if (durableId !== undefined) ev.id = durableId;
|
|
229
|
+
else delete ev.id;
|
|
230
|
+
return ev as unknown as ConversationStreamEvent;
|
|
231
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Shared execution context capabilities for workflow functions and steps. */
|
|
2
2
|
|
|
3
|
-
import type { InvokeAndWaitOptions, RunStatus } from "
|
|
3
|
+
import type { InvokeAndWaitOptions, RunStatus } from "./api-runs.js";
|
|
4
4
|
import type { RequestContext } from "../request-context/request-context.js";
|
|
5
5
|
import type { PauseRequest } from "../pause/pause-core.js";
|
|
6
|
-
import type { WaitForEventRequest } from "../pause/wrappers.js";
|
|
6
|
+
import type { DecisionRequest, WaitForEventRequest } from "../pause/wrappers.js";
|
|
7
7
|
import type { SandboxProvider } from "./sandbox.js";
|
|
8
8
|
|
|
9
9
|
/** The identity of this workflow run. */
|
|
@@ -42,4 +42,12 @@ export interface BaseExecutionContext {
|
|
|
42
42
|
sleep(durationMs: number): Promise<void>;
|
|
43
43
|
/** Pause until an event resumes by `correlationKey`. Wrapper over `pause`. */
|
|
44
44
|
waitForEvent<T = unknown>(req: WaitForEventRequest<T>): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Pause for a typed human/agent decision (ADR-0042). The prompt, options,
|
|
47
|
+
* draft, and approver refs surface in the dashboard's decision UI (run
|
|
48
|
+
* page AND the spawning conversation's run card); resolves with the frozen
|
|
49
|
+
* `{ decision: string }` payload. With `approvers` named, only those team
|
|
50
|
+
* members can resolve it — server-enforced. Wrapper over `pause`.
|
|
51
|
+
*/
|
|
52
|
+
requestDecision(req: DecisionRequest): Promise<{ decision: string }>;
|
|
45
53
|
}
|
package/src/types/protocol.ts
CHANGED
|
@@ -16,6 +16,17 @@ export interface AgentMessageText extends AgentMessageBase {
|
|
|
16
16
|
text: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** LIVE-ONLY incremental chunk of the in-progress assistant text block
|
|
20
|
+
* (claude stream-json `content_block_delta`/`text_delta` under
|
|
21
|
+
* `--include-partial-messages`). Consumers that stream progressively
|
|
22
|
+
* accumulate deltas; everyone else ignores them — the terminating `text`
|
|
23
|
+
* message always carries the COMPLETE block and is the only durable form.
|
|
24
|
+
* Additive kind: existing producers never emit it. */
|
|
25
|
+
export interface AgentMessageTextDelta extends AgentMessageBase {
|
|
26
|
+
type: "text_delta";
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
export interface AgentMessageThinking extends AgentMessageBase {
|
|
20
31
|
type: "thinking";
|
|
21
32
|
text: string;
|
|
@@ -65,6 +76,26 @@ export interface AgentMessageUsage extends AgentMessageBase {
|
|
|
65
76
|
numTurns: number;
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
/** LIVE-ONLY incremental usage off the harness's raw provider stream — the
|
|
80
|
+
* `text_delta` of token counts. claude-code's `--include-partial-messages`
|
|
81
|
+
* stream events carry per-model-call usage (`message_start` /
|
|
82
|
+
* `message_delta`) that the terminal `usage` report only totals at turn
|
|
83
|
+
* end; this forwards them so a streaming consumer (the cloud executor's
|
|
84
|
+
* live token counter) can tick in real time. Semantics per model call
|
|
85
|
+
* within the turn: `boundary: "call_start"` carries the call's input-side
|
|
86
|
+
* finals (input + cache tokens, known at call start); `"call_delta"`
|
|
87
|
+
* carries the call's CUMULATIVE output tokens so far. Never durable and
|
|
88
|
+
* never a substitute for `usage` — the agent loop drops it exactly like
|
|
89
|
+
* `text_delta`. Additive kind: existing producers never emit it. */
|
|
90
|
+
export interface AgentMessageUsageDelta extends AgentMessageBase {
|
|
91
|
+
type: "usage_delta";
|
|
92
|
+
boundary: "call_start" | "call_delta";
|
|
93
|
+
inputTokens: number;
|
|
94
|
+
outputTokens: number;
|
|
95
|
+
cacheReadTokens: number;
|
|
96
|
+
cacheCreationTokens: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
/** Structured execution plan emitted by an agent (ACP `plan` session update,
|
|
69
100
|
* WS-C / ADR-0020 Q2). Each `plan` notification REPLACES the whole plan — the
|
|
70
101
|
* normaliser emits one `AgentMessagePlan` per notification carrying the entire
|
|
@@ -83,12 +114,14 @@ export interface AgentMessagePlan extends AgentMessageBase {
|
|
|
83
114
|
export type AgentMessage =
|
|
84
115
|
| AgentMessageInit
|
|
85
116
|
| AgentMessageText
|
|
117
|
+
| AgentMessageTextDelta
|
|
86
118
|
| AgentMessageThinking
|
|
87
119
|
| AgentMessageToolUse
|
|
88
120
|
| AgentMessageToolResult
|
|
89
121
|
| AgentMessageDone
|
|
90
122
|
| AgentMessageError
|
|
91
123
|
| AgentMessageUsage
|
|
124
|
+
| AgentMessageUsageDelta
|
|
92
125
|
| AgentMessagePlan;
|
|
93
126
|
|
|
94
127
|
/** Status block the agent emits to signal iteration completion or blockers. */
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* snapshots: { bootFrom: { snapshotId: "snap_..." } }
|
|
12
12
|
*
|
|
13
|
-
* `defineSandboxEnvironment` is sugar over
|
|
14
|
-
* setup
|
|
15
|
-
*
|
|
13
|
+
* `defineSandboxEnvironment` is sugar over the step builder — it wraps the
|
|
14
|
+
* `setup` callback as a single-step workflow, supplying the local sandbox
|
|
15
|
+
* provider (the runner's own VM) so the recipe reads like an imperative
|
|
16
|
+
* script.
|
|
16
17
|
*
|
|
17
18
|
* Typical flow:
|
|
18
19
|
* 1. Author a setup file with `defineSandboxEnvironment`.
|
|
@@ -34,8 +35,9 @@
|
|
|
34
35
|
* ```
|
|
35
36
|
*/
|
|
36
37
|
|
|
38
|
+
import { z } from "zod";
|
|
37
39
|
import type { SandboxProvider } from "./sandbox.js";
|
|
38
|
-
import {
|
|
40
|
+
import { createStepWorkflow } from "../workflow-steps/workflow.js";
|
|
39
41
|
import type { Workflow } from "../workflow-steps/types.js";
|
|
40
42
|
import type { SandboxResources, SnapshotConfig } from "./workflow-metadata.js";
|
|
41
43
|
|
|
@@ -55,24 +57,41 @@ export interface SandboxEnvironmentDefinition {
|
|
|
55
57
|
resources?: SandboxResources;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
/** Sugar over
|
|
60
|
+
/** Sugar over the step builder for setup-only workflows that exist to
|
|
59
61
|
* capture a snapshot. The workflow takes no meaningful input and returns
|
|
60
|
-
* nothing — its value is the side effect on the sandbox VM.
|
|
62
|
+
* nothing — its value is the side effect on the sandbox VM. Both schemas
|
|
63
|
+
* are deliberately `z.unknown()` so no input/output contract is captured
|
|
64
|
+
* into template metadata (there is nothing to render). */
|
|
61
65
|
export function defineSandboxEnvironment(
|
|
62
66
|
env: SandboxEnvironmentDefinition,
|
|
63
67
|
): Workflow<Record<string, unknown>, void> {
|
|
64
68
|
if (typeof env.setup !== "function") {
|
|
65
69
|
throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
const input = z.unknown() as z.ZodType<Record<string, unknown>>;
|
|
72
|
+
const output = z.unknown() as z.ZodType<void>;
|
|
73
|
+
return createStepWorkflow<Record<string, unknown>, void>({
|
|
74
|
+
id: env.name,
|
|
68
75
|
...(env.description !== undefined ? { description: env.description } : {}),
|
|
69
76
|
...(env.resources !== undefined ? { resources: env.resources } : {}),
|
|
77
|
+
input,
|
|
78
|
+
output,
|
|
70
79
|
snapshots: env.snapshots ?? { saveLatest: true },
|
|
71
80
|
// Mark this as an environment build so the server skips the /factory mount
|
|
72
81
|
// for its runs — an env build builds a platform image and never touches the
|
|
73
82
|
// shared drive; baking a live Archil mount into its snapshot breaks the
|
|
74
83
|
// re-mount of every workflow that later boots from it (#13).
|
|
75
84
|
environmentBuild: true,
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
})
|
|
86
|
+
.step({
|
|
87
|
+
name: "setup",
|
|
88
|
+
input,
|
|
89
|
+
output,
|
|
90
|
+
run: async (ctx) => {
|
|
91
|
+
const sandbox = ctx.sandbox;
|
|
92
|
+
if (!sandbox) throw new Error(`defineSandboxEnvironment(${env.name}): setup requires a sandbox in StepContext`);
|
|
93
|
+
await env.setup(sandbox);
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
.build();
|
|
78
97
|
}
|
package/src/types/sandbox.ts
CHANGED
|
@@ -78,6 +78,37 @@ export interface SandboxSpawnDuplexOptions {
|
|
|
78
78
|
envs?: Record<string, string>;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Options for opening a brokered interactive PTY in the sandbox
|
|
82
|
+
* (ADR-0055 — the session Terminal surface). */
|
|
83
|
+
export interface SandboxPtyOpts {
|
|
84
|
+
cols: number;
|
|
85
|
+
rows: number;
|
|
86
|
+
/** Working directory the shell opens in. Omitted → the guest user's home. */
|
|
87
|
+
cwd?: string;
|
|
88
|
+
/** Extra env for the shell. */
|
|
89
|
+
envs?: Record<string, string>;
|
|
90
|
+
/** Raw PTY output. */
|
|
91
|
+
onData: (data: Uint8Array) => void;
|
|
92
|
+
/** Fired once when the PTY process ends — shell exit, kill, or sandbox
|
|
93
|
+
* fault. The server's broker closes the WebSocket off it. */
|
|
94
|
+
onExit?: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A live PTY inside the guest, brokered over the server's attach WebSocket. */
|
|
98
|
+
export interface SandboxPtyHandle {
|
|
99
|
+
/** OS pid inside the VM. */
|
|
100
|
+
pid: number;
|
|
101
|
+
sendInput(data: Uint8Array): Promise<void>;
|
|
102
|
+
resize(size: { cols: number; rows: number }): Promise<void>;
|
|
103
|
+
kill(): Promise<void>;
|
|
104
|
+
/** Detach this handle's event stream WITHOUT killing the guest process —
|
|
105
|
+
* the PTY keeps running (and survives a VM pause) and is re-attachable
|
|
106
|
+
* later via `connectPty(pid)`. The detach half of the ADR-0055 §7
|
|
107
|
+
* persistent-terminal rework: the server's broker disconnects on socket
|
|
108
|
+
* close; killing is a separate, explicit route decision. */
|
|
109
|
+
disconnect(): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
|
|
81
112
|
/**
|
|
82
113
|
* A sandbox provider implements the RAW provider operations only. It does NOT
|
|
83
114
|
* implement transient-failure retry/backoff: reconnecting and snapshotting both
|
|
@@ -126,10 +157,12 @@ export interface SandboxProvider {
|
|
|
126
157
|
* is the envd HTTP API (`Sandbox.files.read`), a DIFFERENT transport from
|
|
127
158
|
* `commands` — so a large readback is immune to the connect-web gRPC
|
|
128
159
|
* message-compression that can abort `commands.run` output on a big frame
|
|
129
|
-
* ("received unsupported compressed output").
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
160
|
+
* ("received unsupported compressed output"). On Vercel it is the HTTP file
|
|
161
|
+
* API (`readFileToBuffer`), independent of a faulted runCommand log stream.
|
|
162
|
+
* This is what lets the step recovery paths (`launchStep`'s wait,
|
|
163
|
+
* `invokeStep`'s foreground stream-fault recovery) backfill the full logs +
|
|
164
|
+
* result after a live-stream fault. OPTIONAL — implemented where durable
|
|
165
|
+
* file-readback is needed (E2B, Vercel, local). */
|
|
133
166
|
read?(path: string): Promise<string>;
|
|
134
167
|
};
|
|
135
168
|
kill(): Promise<void>;
|
|
@@ -142,6 +175,15 @@ export interface SandboxProvider {
|
|
|
142
175
|
* be omitted when the provider doesn't expose it; the server stores
|
|
143
176
|
* `null` for missing values rather than estimating. */
|
|
144
177
|
snapshot?(): Promise<{ snapshotId: string; sizeBytes?: number }>;
|
|
178
|
+
/** Resolve a public forwarding host for a port bound inside the sandbox, or
|
|
179
|
+
* null when the provider cannot expose ports. E2B returns its native
|
|
180
|
+
* `*.e2b.app` host (`sb.getHost(port)`); Vercel/local leave it undefined.
|
|
181
|
+
* Pure string op — no retry wrapper (unlike reconnect/snapshot). The
|
|
182
|
+
* returned host is the RAW capability and MUST NOT reach a browser
|
|
183
|
+
* (ADR-0052 §2.1); the server's member-gated preview proxy is the only
|
|
184
|
+
* caller. The presence of this method IS the port-exposure capability
|
|
185
|
+
* flag; providers without it leave it undefined (the no-shim rule). */
|
|
186
|
+
getHost?(port: number): string | null;
|
|
145
187
|
/** Suspend the live VM in place and return a handle to resume it (ADR-0027).
|
|
146
188
|
* Present ONLY on process-resume-capable providers (E2B via `sandbox.pause()`,
|
|
147
189
|
* returning the sandbox id; resume is `Sandbox.connect(handle)`, which
|
|
@@ -152,6 +194,33 @@ export interface SandboxProvider {
|
|
|
152
194
|
* flag: providers without native VM-suspend leave it undefined and fall back
|
|
153
195
|
* to `snapshot()` + re-run. */
|
|
154
196
|
pauseProcess?(): Promise<{ resumeHandle: string }>;
|
|
197
|
+
/** Reset the PROVIDER-side kill-clock: the sandbox lives at least `ms`
|
|
198
|
+
* more from now (shorter values SHORTEN the remaining lifetime — E2B's
|
|
199
|
+
* `setTimeout` replaces the deadline rather than extending it). The
|
|
200
|
+
* server's session lifecycle calls this on every attach-heartbeat tick
|
|
201
|
+
* and when it arms the deferred hot-window suspend, so its own suspend
|
|
202
|
+
* timer always beats the provider deadline — without it a long-attached
|
|
203
|
+
* terminal outlives the create-time timeout and E2B kills the VM before
|
|
204
|
+
* the pause can run (persistence silently lost; user-hit 2026-07-25).
|
|
205
|
+
* Presence-is-capability: only providers with a live timeout primitive
|
|
206
|
+
* (E2B `sandbox.setTimeout`) implement it; others leave it undefined and
|
|
207
|
+
* ride their create-time lifetime. */
|
|
208
|
+
extendLifetime?(ms: number): Promise<void>;
|
|
209
|
+
/** Open an interactive PTY in the guest (ADR-0055 — the brokered session
|
|
210
|
+
* terminal). Presence-is-capability, like `runBackground`: only E2B
|
|
211
|
+
* implements it (native `sb.pty`); Vercel/local leave it undefined and
|
|
212
|
+
* the server's terminal route answers 400 instead of shimming. */
|
|
213
|
+
createPty?(opts: SandboxPtyOpts): Promise<SandboxPtyHandle>;
|
|
214
|
+
/** Re-attach to a PTY that an earlier handle `disconnect()`ed from, by
|
|
215
|
+
* pid (ADR-0055 §7 rework — the silent-reattach half of `disconnect`).
|
|
216
|
+
* The process kept running (it survives socket close AND a VM
|
|
217
|
+
* pause/resume); connecting resumes its output stream on `opts.onData`.
|
|
218
|
+
* `opts.cols`/`rows`/`cwd`/`envs` describe the CALLER's viewport intent
|
|
219
|
+
* only — the guest process already exists, so providers apply what their
|
|
220
|
+
* transport accepts (E2B: onData only; the server jiggles a resize after
|
|
221
|
+
* connect to repaint). Presence-is-capability, E2B-only; throws when no
|
|
222
|
+
* PTY with that pid is running. */
|
|
223
|
+
connectPty?(pid: number, opts: SandboxPtyOpts): Promise<SandboxPtyHandle>;
|
|
155
224
|
/** Replace the live sandbox's egress policy in place — so the server can
|
|
156
225
|
* push a freshly resolved policy (with re-minted connector access tokens)
|
|
157
226
|
* before each step instead of relying on the policy baked at create.
|
|
@@ -169,6 +169,24 @@ export interface SandboxResources {
|
|
|
169
169
|
provider?: WorkflowSandboxProvider;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* What happens to a drive branch at its producer's TERMINUS — the one
|
|
174
|
+
* vocabulary shared by both branch-producing surfaces (workflow runs and
|
|
175
|
+
* agent sessions).
|
|
176
|
+
*
|
|
177
|
+
* - `"auto"` — the branch folds into `main` at the terminus.
|
|
178
|
+
* - `"manual"` — at the SAME terminus a merge APPROVAL is proposed instead.
|
|
179
|
+
* The branch is durable, so `manual` never means "silently strand the
|
|
180
|
+
* work": it means one click instead of zero.
|
|
181
|
+
*
|
|
182
|
+
* **Default for a workflow is `"auto"`** — absent (`mergePolicy` unset) is
|
|
183
|
+
* exactly what every already-registered workflow does today, so no existing
|
|
184
|
+
* template needs re-registering. (Sessions default the other way, `manual`,
|
|
185
|
+
* for the same reason: it is what they do today. The default belongs to the
|
|
186
|
+
* SURFACE, not to this type.)
|
|
187
|
+
*/
|
|
188
|
+
export type DriveMergePolicy = "auto" | "manual";
|
|
189
|
+
|
|
172
190
|
export interface WorkflowMetadata {
|
|
173
191
|
/** One-line, human-readable description of what the workflow does.
|
|
174
192
|
* Surfaced on the dashboard template tile + run page header. Authors
|
|
@@ -178,20 +196,28 @@ export interface WorkflowMetadata {
|
|
|
178
196
|
networkPolicy?: SandboxNetworkPolicy;
|
|
179
197
|
placeholders?: Record<string, string>;
|
|
180
198
|
/** Input schema captured at bundle time from the workflow's
|
|
181
|
-
* declared `input` zod schema.
|
|
182
|
-
*
|
|
183
|
-
* defaults to `z.unknown()`) leave it undefined. */
|
|
199
|
+
* declared `input` zod schema. Undefined when the schema is
|
|
200
|
+
* effectively `z.unknown()` — no contract to render. */
|
|
184
201
|
inputSchema?: IOSchema;
|
|
185
202
|
/** Output schema captured at bundle time from the workflow's
|
|
186
|
-
* declared `output` zod schema.
|
|
187
|
-
*
|
|
188
|
-
* arbitrarily-typed values leave it undefined. */
|
|
203
|
+
* declared `output` zod schema. Undefined when the schema is
|
|
204
|
+
* effectively `z.unknown()`. */
|
|
189
205
|
outputSchema?: IOSchema;
|
|
190
206
|
/** All snapshot config — boot source + capture mode. */
|
|
191
207
|
snapshots?: SnapshotConfig;
|
|
192
208
|
/** Sandbox machine resources — size + provider. Optional; omit → smallest
|
|
193
209
|
* SKU on the platform-default provider (`vercel`). */
|
|
194
210
|
resources?: SandboxResources;
|
|
211
|
+
/** What happens to this workflow's run branch at the run's terminus:
|
|
212
|
+
* `"auto"` folds it into `main` (success AND failure — a failed run still
|
|
213
|
+
* produced real outputs); `"manual"` proposes a merge approval at the same
|
|
214
|
+
* terminus instead. A CANCELLED run does neither under either policy.
|
|
215
|
+
*
|
|
216
|
+
* Optional + additive: an ABSENT policy means `"auto"` — today's hard-coded
|
|
217
|
+
* behaviour — and contributes nothing to the canonical metadata hash
|
|
218
|
+
* (frozen-metadata rule), so existing workflows are not forced to
|
|
219
|
+
* re-register. */
|
|
220
|
+
mergePolicy?: DriveMergePolicy;
|
|
195
221
|
processors?: readonly Processor[];
|
|
196
222
|
/** Connector requirements — providers whose APIs this workflow calls.
|
|
197
223
|
* Dispatch resolves an authorized grant per provider and injects a
|
|
@@ -218,8 +244,8 @@ export interface WorkflowMetadata {
|
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
/**
|
|
221
|
-
* Pull the server-readable declarations off a
|
|
222
|
-
* `
|
|
247
|
+
* Pull the server-readable declarations off a `StepWorkflowDefinition`
|
|
248
|
+
* (or any source overlapping `WorkflowMetadata` in field shape) into a
|
|
223
249
|
* single `WorkflowMetadata` bag. Undefined fields are omitted so the
|
|
224
250
|
* canonical metadata hash (server-side) is stable across re-registers
|
|
225
251
|
* that left a field unspecified.
|
|
@@ -242,6 +268,7 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
|
|
|
242
268
|
if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
|
|
243
269
|
if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
|
|
244
270
|
if (source.resources !== undefined) out.resources = Object.freeze({ ...source.resources });
|
|
271
|
+
if (source.mergePolicy !== undefined) out.mergePolicy = source.mergePolicy;
|
|
245
272
|
if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
|
|
246
273
|
if (source.connectors !== undefined) out.connectors = freezeMetadataValue(source.connectors);
|
|
247
274
|
if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
@@ -10,9 +10,20 @@
|
|
|
10
10
|
* workflow (step name = "run"); the bundler sees the same shape regardless.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/** One artifact a step promises to produce — mirrors `StepDeliverable`,
|
|
14
|
+
* restated here so the plan stays a self-contained wire shape. */
|
|
15
|
+
export interface WorkflowStepDeliverable {
|
|
16
|
+
path: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface WorkflowStepPlan {
|
|
14
21
|
index: number;
|
|
15
22
|
name: string;
|
|
23
|
+
/** Author-declared narrative (defineStep `summary`) — optional, additive. */
|
|
24
|
+
summary?: string;
|
|
25
|
+
/** Author-declared artifacts (defineStep `deliverables`) — optional, additive. */
|
|
26
|
+
deliverables?: WorkflowStepDeliverable[];
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export interface WorkflowPlan {
|