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