@tangle-network/sandbox 0.30.0 → 0.30.2-develop.20260820100122.dc85046

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.
@@ -1,10 +1,164 @@
1
- import { a as uploadChunked, c as combineAbortSignals, d as applyRequestedModel, f as assertCurrentBackendType, i as sessionHeaders, l as encodePromptForWire, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as normalizeRuntimeBackendConfig, r as appendSessionIdQuery, t as SandboxRuntimeApi } from "./runtime-api-CK-4S7oA.js";
1
+ import { a as uploadChunked, c as combineAbortSignals, d as applyRequestedModel, f as assertCurrentBackendType, h as deriveAgentResultOutcome, i as sessionHeaders, l as encodePromptForWire, m as createAgentRunOutcomeTracker, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as normalizeRuntimeBackendConfig, r as appendSessionIdQuery, t as SandboxRuntimeApi } from "./runtime-api-TVLbV10b.js";
2
2
  import { a as NetworkError, c as QuotaError, d as StateError, f as TimeoutError, l as SandboxError, m as parseErrorResponse, n as CapabilityError, o as NotFoundError, p as ValidationError, t as AuthError, u as ServerError } from "./errors-C6kn-3zt.js";
3
3
  import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, InteractionAcknowledgementSchema, InteractionBindingSchema, InteractionRequestSchema, InteractionResponseCommandSchema, InteractionResponseSchema, agentRunCancellationAcknowledgementMatchesRequest, canonicalCandidateDigest, sha256DigestSchema } from "@tangle-network/agent-interface";
4
4
  import { z } from "zod";
5
5
  import { readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
6
+ //#region ../../../packages/sse-core/dist/index.js
7
+ /**
8
+ * Incremental SSE frame parser. Feed decoded text via `push()`; call
9
+ * `flush()` when the stream ends.
10
+ */
11
+ var SSEFrameParser = class {
12
+ buffer = "";
13
+ frameEvent = "";
14
+ frameId;
15
+ frameDataLines = null;
16
+ bomChecked = false;
17
+ /**
18
+ * Append a decoded text chunk and return every frame the chunk
19
+ * completes. Frame state survives across calls: one frame can
20
+ * straddle any number of chunks.
21
+ */
22
+ push(chunk) {
23
+ let text = chunk;
24
+ if (!this.bomChecked && text.length > 0) {
25
+ this.bomChecked = true;
26
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
27
+ }
28
+ this.buffer += text;
29
+ const lines = this.buffer.split("\n");
30
+ this.buffer = lines.pop() ?? "";
31
+ return this.processLines(lines);
32
+ }
33
+ /**
34
+ * End-of-stream: process the unterminated trailing line, then
35
+ * dispatch a dangling frame that never got its blank line.
36
+ * Well-behaved servers end frames with "\n\n", but some close early.
37
+ */
38
+ flush() {
39
+ const rest = this.buffer;
40
+ this.buffer = "";
41
+ const frames = rest.length > 0 ? this.processLines([rest]) : [];
42
+ const dangling = this.dispatch();
43
+ if (dangling) frames.push(dangling);
44
+ return frames;
45
+ }
46
+ processLines(lines) {
47
+ const frames = [];
48
+ for (const rawLine of lines) {
49
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
50
+ if (line === "") {
51
+ const frame = this.dispatch();
52
+ if (frame) frames.push(frame);
53
+ continue;
54
+ }
55
+ if (line.startsWith(":")) continue;
56
+ if (line.startsWith("data:")) {
57
+ let value = line.slice(5);
58
+ if (value.startsWith(" ")) value = value.slice(1);
59
+ if (this.frameDataLines === null) this.frameDataLines = [];
60
+ this.frameDataLines.push(value);
61
+ continue;
62
+ }
63
+ if (line.startsWith("event:")) {
64
+ this.frameEvent = line.slice(6).trim();
65
+ continue;
66
+ }
67
+ if (line.startsWith("id:")) this.frameId = line.slice(3).trim();
68
+ }
69
+ return frames;
70
+ }
71
+ /**
72
+ * Dispatch the accumulated frame. A frame with no `data:` field (an
73
+ * `event:`/`id:`-only frame, or a bare comment) produces nothing, and
74
+ * its field state is consumed and reset either way.
75
+ */
76
+ dispatch() {
77
+ const dataLines = this.frameDataLines;
78
+ const frame = dataLines ? {
79
+ event: this.frameEvent,
80
+ data: dataLines.join("\n"),
81
+ id: this.frameId || void 0
82
+ } : null;
83
+ this.frameEvent = "";
84
+ this.frameId = void 0;
85
+ this.frameDataLines = null;
86
+ return frame;
87
+ }
88
+ };
89
+ /**
90
+ * Read a streaming SSE response body into parsed frames.
91
+ *
92
+ * @throws `TypeError` when the body is missing.
93
+ * @throws `DOMException` with `name === "AbortError"` on cancellation —
94
+ * including the WHATWG-fetch variant where an aborted body resolves
95
+ * `done: true` instead of rejecting the read.
96
+ */
97
+ async function* streamSSEFrames(body, options) {
98
+ if (!body) throw new TypeError("SSE stream has no response body");
99
+ const reader = body.getReader();
100
+ const decoder = new TextDecoder();
101
+ const parser = new SSEFrameParser();
102
+ const { signal } = options ?? {};
103
+ let completed = false;
104
+ try {
105
+ for (;;) {
106
+ if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
107
+ const { done, value } = await reader.read();
108
+ if (done) {
109
+ if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
110
+ break;
111
+ }
112
+ for (const frame of parser.push(decoder.decode(value, { stream: true }))) yield frame;
113
+ }
114
+ for (const frame of parser.push(decoder.decode())) yield frame;
115
+ for (const frame of parser.flush()) yield frame;
116
+ completed = true;
117
+ } finally {
118
+ if (!completed) await reader.cancel().catch(() => void 0);
119
+ reader.releaseLock();
120
+ }
121
+ }
122
+ //#endregion
6
123
  //#region src/lib/sse-parser.ts
7
124
  /**
125
+ * SSE Stream Parser
126
+ *
127
+ * EventSource stream parsing for SDK consumers. Framing — buffering
128
+ * chunk-straddling frames, LF/CRLF terminators, multi-line `data:` joins,
129
+ * comment heartbeats, BOM — lives in `@tangle-network/sse-core`, the one
130
+ * wire-format implementation shared across the repo (bundled into this
131
+ * package's dist, so the SDK stays dependency-free at runtime). This
132
+ * module owns the SDK's payload contract on top of it: JSON parsing, the
133
+ * `"message"` default event type, and the malformed-frame policy. Used by
134
+ * `Sandbox.streamBatch` and by the web dashboard's batch page; exported
135
+ * so downstream surfaces don't maintain their own copies.
136
+ *
137
+ * Guarantees:
138
+ * - Correctly buffers frames that straddle chunk boundaries.
139
+ * - Accepts LF- and CRLF-terminated streams interchangeably.
140
+ * - Joins multi-line `data:` per the spec (dispatch joins with
141
+ * "\n" before JSON parse).
142
+ * - Defaults event type to `"message"` when a frame carries
143
+ * `data:` but no `event:` field (SSE spec §9.2.6 step 3).
144
+ * - Surfaces optional `id:` so callers can implement reconnection.
145
+ * - Flushes a dangling event when the stream closes without a
146
+ * terminating blank line (well-behaved servers emit `\n\n`, but
147
+ * some close early).
148
+ * - Throws `AbortError` when the caller-supplied signal fires, so
149
+ * consumers can distinguish "ran to completion" from "cancelled"
150
+ * without peeking at signal state.
151
+ * - Rejects malformed JSON by default. Callers handling recoverable output
152
+ * streams can explicitly warn and skip one invalid frame.
153
+ *
154
+ * What it does NOT do:
155
+ * - CR-only (legacy Mac) line terminators. No known emitter uses
156
+ * them; supporting them would risk splitting payloads that
157
+ * legitimately contain CR.
158
+ * - Retry/reconnect (caller's responsibility).
159
+ * - Event-type filtering (caller's responsibility).
160
+ */
161
+ /**
8
162
  * Parse a streaming SSE response body into an async iterable of
9
163
  * structured events.
10
164
  *
@@ -12,81 +166,31 @@ import { readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
12
166
  * @throws `DOMException` with `name === "AbortError"` on cancellation.
13
167
  */
14
168
  async function* parseSSEStream(body, options) {
15
- if (!body) throw new TypeError("SSE stream has no response body");
16
- const reader = body.getReader();
17
- const decoder = new TextDecoder();
18
169
  const { signal, invalidEvent = "throw" } = options ?? {};
19
- let buffer = "";
20
- let currentEvent = "";
21
- let dataLines = [];
22
- let currentId = "";
23
- let completed = false;
24
- const flush = function* () {
25
- if (dataLines.length === 0) {
26
- currentEvent = "";
27
- currentId = "";
28
- return;
29
- }
30
- const effectiveEvent = currentEvent || "message";
31
- const joined = dataLines.join("\n");
170
+ for await (const frame of streamSSEFrames(body, { signal })) {
171
+ const effectiveEvent = frame.event || "message";
32
172
  let data;
33
173
  try {
34
- data = JSON.parse(joined);
174
+ data = JSON.parse(frame.data);
35
175
  } catch (error) {
36
176
  if (invalidEvent === "warn-and-skip") {
37
- console.warn(`[sandbox-sdk] Malformed SSE JSON for event "${effectiveEvent}": ${joined.slice(0, 200)}`);
38
- currentEvent = "";
39
- dataLines = [];
40
- currentId = "";
41
- return;
177
+ console.warn(`[sandbox-sdk] Malformed SSE JSON for event "${effectiveEvent}": ${frame.data.slice(0, 200)}`);
178
+ continue;
42
179
  }
43
180
  throw new SyntaxError(`Malformed SSE JSON for event "${effectiveEvent}"`, { cause: error });
44
181
  }
45
182
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
46
183
  if (invalidEvent === "warn-and-skip") {
47
184
  console.warn(`[sandbox-sdk] SSE event "${effectiveEvent}" data must be a JSON object`);
48
- currentEvent = "";
49
- dataLines = [];
50
- currentId = "";
51
- return;
185
+ continue;
52
186
  }
53
187
  throw new TypeError(`SSE event "${effectiveEvent}" data must be a JSON object`);
54
188
  }
55
189
  yield {
56
190
  type: effectiveEvent,
57
191
  data,
58
- id: currentId || void 0
192
+ id: frame.id
59
193
  };
60
- currentEvent = "";
61
- dataLines = [];
62
- currentId = "";
63
- };
64
- const processLine = (line) => {
65
- if (line.startsWith("event:")) currentEvent = line.slice(6).trim();
66
- else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
67
- else if (line.startsWith("id:")) currentId = line.slice(3).trim();
68
- };
69
- try {
70
- while (true) {
71
- if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
72
- const { done, value } = await reader.read();
73
- if (done) {
74
- if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
75
- break;
76
- }
77
- buffer += decoder.decode(value, { stream: true });
78
- const lines = buffer.split(/\r?\n/);
79
- buffer = lines.pop() ?? "";
80
- for (const line of lines) if (line === "") yield* flush();
81
- else processLine(line);
82
- }
83
- buffer += decoder.decode();
84
- if (buffer.length > 0) processLine(buffer);
85
- yield* flush();
86
- completed = true;
87
- } finally {
88
- if (!completed) await reader.cancel().catch(() => void 0);
89
- reader.releaseLock();
90
194
  }
91
195
  }
92
196
  //#endregion
@@ -736,7 +840,7 @@ async function submitSandboxInteractionCommand(transport, sessionId, command, op
736
840
  /**
737
841
  * The text carried by a single event, or `undefined` when the event carries no
738
842
  * text payload. Mirrors the event shapes the sidecar/OpenCode runtime emits:
739
- * - `result` — the terminal event; final text under `finalText`/`response`/
843
+ * - `result` — the final response summary; text under `finalText`/`response`/
740
844
  * `output`/`content`/`text`.
741
845
  * - `token` — an incremental token; text under `value`/`delta`/`text`/
742
846
  * `content`.
@@ -1907,153 +2011,6 @@ function runtimeFilesystemPath(endpoint, path) {
1907
2011
  const encodedPath = segments.map(encodeURIComponent).join("/");
1908
2012
  return `${endpoint}/${isAbsolute ? "/" : ""}${encodedPath}`;
1909
2013
  }
1910
- /**
1911
- * Single source of truth for a run's outcome, shared by
1912
- * `prompt`/`task`/`_sessionResult`/turn settling. A run succeeds only when it
1913
- * reached a terminal event, raised no run-level error, hit no approval gate,
1914
- * left no unanswered question, and recorded no failed (`isError`) tool. A
1915
- * tool-only turn with no text succeeds unless the server enforces visible-output
1916
- * requirements; a turn whose tool failed (even if narrated around) fails.
1917
- */
1918
- function deriveOutcome(inputs) {
1919
- if (inputs.plan) return {
1920
- status: "awaiting_plan_decision",
1921
- success: false,
1922
- error: "Agent is awaiting a durable plan decision"
1923
- };
1924
- if (inputs.question) return {
1925
- status: "awaiting_question",
1926
- success: false,
1927
- error: inputs.runError ?? "Agent is awaiting an answer to a question"
1928
- };
1929
- if (inputs.approval) return {
1930
- status: "blocked_on_approval",
1931
- success: false,
1932
- error: inputs.runError ?? inputs.approval.message ?? "Agent is blocked on a hub approval"
1933
- };
1934
- if (inputs.runError) return {
1935
- status: "failed",
1936
- success: false,
1937
- error: inputs.runError,
1938
- errorCode: inputs.runErrorCode
1939
- };
1940
- const failedTool = inputs.toolInvocations?.find((t) => t.isError === true);
1941
- if (failedTool) return {
1942
- status: "failed",
1943
- success: false,
1944
- error: `Tool "${failedTool.toolName}" failed`
1945
- };
1946
- if (!inputs.terminalReached) return {
1947
- status: "failed",
1948
- success: false,
1949
- error: "Agent stream ended without a terminal event"
1950
- };
1951
- return {
1952
- status: "success",
1953
- success: true
1954
- };
1955
- }
1956
- function readAwaitingPlan(data) {
1957
- const direct = data.plan;
1958
- const outcome = data.outcome;
1959
- const candidate = direct && typeof direct === "object" ? direct : outcome && typeof outcome === "object" && outcome.type === "awaiting_plan_decision" ? outcome.plan : void 0;
1960
- if (!candidate || typeof candidate !== "object") return void 0;
1961
- const plan = candidate;
1962
- if (typeof plan.id !== "string" || typeof plan.revision !== "number" || typeof plan.body !== "string" || typeof plan.submittedAt !== "string") return;
1963
- return plan;
1964
- }
1965
- function readPlanFromEvent(event) {
1966
- return event.type === "result" || event.type === "done" || event.type === "plan.submitted" ? readAwaitingPlan(event.data) : void 0;
1967
- }
1968
- /** Read tool invocations off a `result` event payload. */
1969
- function readToolInvocations(data) {
1970
- const raw = data.toolInvocations;
1971
- return Array.isArray(raw) ? raw : void 0;
1972
- }
1973
- /** Hub error code surfaced when a write tool hits an approval gate. */
1974
- const HUB_APPROVAL_CODE = "HUB_APPROVAL_REQUIRED";
1975
- /** Best-effort string view of an arbitrary tool result for signal matching. */
1976
- function stringifyToolResult(result) {
1977
- if (typeof result === "string") return result;
1978
- try {
1979
- return JSON.stringify(result) ?? "";
1980
- } catch {
1981
- return String(result);
1982
- }
1983
- }
1984
- /**
1985
- * Walk a tool result for an `approval` object and return its `id`/`connectionId`.
1986
- * The in-sandbox hub bridge carries these in `structuredContent.details.approval`,
1987
- * but the exact nesting depends on how the backend serialized the MCP result, so
1988
- * search defensively. Returns `undefined` when not present — the id is then
1989
- * surfaced honestly as `undefined`, never invented.
1990
- */
1991
- function findApprovalDetails(value, depth = 0) {
1992
- if (depth > 6 || value === null || typeof value !== "object") return void 0;
1993
- const obj = value;
1994
- const approval = obj.approval;
1995
- if (approval && typeof approval === "object") {
1996
- const a = approval;
1997
- return {
1998
- ...typeof a.id === "string" ? { id: a.id } : {},
1999
- ...typeof a.connectionId === "string" ? { connectionId: a.connectionId } : {}
2000
- };
2001
- }
2002
- for (const v of Object.values(obj)) {
2003
- const found = findApprovalDetails(v, depth + 1);
2004
- if (found) return found;
2005
- }
2006
- }
2007
- /** Find a human-readable `message` string anywhere in a tool result. */
2008
- function findResultMessage(value, depth = 0) {
2009
- if (depth > 6 || value === null || typeof value !== "object") return void 0;
2010
- const obj = value;
2011
- if (typeof obj.message === "string" && obj.message.length > 0) return obj.message;
2012
- for (const v of Object.values(obj)) {
2013
- const found = findResultMessage(v, depth + 1);
2014
- if (found) return found;
2015
- }
2016
- }
2017
- /**
2018
- * Detect a hub approval gate among a run's tool invocations: a failed tool whose
2019
- * result carries `HUB_APPROVAL_REQUIRED`. Returns the approval id/connectionId
2020
- * when the structured signal survived serialization, else just the message — so
2021
- * the run can settle as `blocked_on_approval` even when the id is unrecoverable.
2022
- */
2023
- function detectHubApproval(toolInvocations) {
2024
- if (!toolInvocations) return void 0;
2025
- for (const tool of toolInvocations) {
2026
- if (tool.isError !== true) continue;
2027
- if (!stringifyToolResult(tool.result).includes(HUB_APPROVAL_CODE)) continue;
2028
- const approval = findApprovalDetails(tool.result);
2029
- const message = findResultMessage(tool.result) ?? "Hub action requires approval";
2030
- return {
2031
- ...approval?.id ? { approvalId: approval.id } : {},
2032
- ...approval?.connectionId ? { connectionId: approval.connectionId } : {},
2033
- message
2034
- };
2035
- }
2036
- }
2037
- /** Read an `interaction` event (kind `question`) into an awaiting-question requirement. */
2038
- function readQuestionEvent(data) {
2039
- const request = data.request;
2040
- if (!request || request.kind !== "question" || typeof request.id !== "string") return;
2041
- return {
2042
- questionId: request.id,
2043
- questions: questionsFromAnswerSpec(request)
2044
- };
2045
- }
2046
- /** Reconstruct the prompt/options shape from a question interaction's answerSpec. */
2047
- function questionsFromAnswerSpec(request) {
2048
- return request.answerSpec.fields.map((field) => field.type === "select" ? {
2049
- question: field.label,
2050
- options: field.options.map((o) => ({
2051
- label: o.label,
2052
- description: o.description
2053
- })),
2054
- multiSelect: field.multi === true
2055
- } : { question: field.label });
2056
- }
2057
2014
  function isProfileNotCachedError(err) {
2058
2015
  return err instanceof SandboxError && err.code === "PROFILE_NOT_CACHED";
2059
2016
  }
@@ -2841,16 +2798,11 @@ var SandboxInstance = class SandboxInstance {
2841
2798
  await this.ensureRunning();
2842
2799
  const startTime = Date.now();
2843
2800
  let responseText;
2844
- let runError;
2845
- let runErrorCode;
2801
+ const outcomeTracker = createAgentRunOutcomeTracker();
2846
2802
  let executionId = options?.lastEventId ? options.executionId : void 0;
2847
2803
  let traceId;
2848
2804
  let usage;
2849
2805
  let costUsd;
2850
- let toolInvocations;
2851
- let question;
2852
- let plan;
2853
- let terminalReached = false;
2854
2806
  const controller = new AbortController();
2855
2807
  try {
2856
2808
  const signal = options?.signal ? combineAbortSignals([options.signal, controller.signal]) : controller.signal;
@@ -2858,6 +2810,7 @@ var SandboxInstance = class SandboxInstance {
2858
2810
  ...options,
2859
2811
  signal
2860
2812
  })) {
2813
+ outcomeTracker.observe(event);
2861
2814
  responseText = applySandboxEventText(responseText, event);
2862
2815
  const eventExecutionId = event.data.executionId;
2863
2816
  if (typeof eventExecutionId === "string" && eventExecutionId.length > 0) executionId = eventExecutionId;
@@ -2865,26 +2818,9 @@ var SandboxInstance = class SandboxInstance {
2865
2818
  usage = readTokenUsage(event.data) ?? usage;
2866
2819
  costUsd = readTokenCostUsd(event.data) ?? costUsd;
2867
2820
  }
2868
- plan = readPlanFromEvent(event) ?? plan;
2869
- if (event.type === "result") toolInvocations = readToolInvocations(event.data) ?? toolInvocations;
2870
- if (event.type === "result" || event.type === "done") terminalReached = true;
2871
- if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
2872
2821
  if (event.type === "trace.id") traceId = event.data.traceId;
2873
- if (event.type === "error") {
2874
- runError = event.data.message;
2875
- runErrorCode = event.data.code;
2876
- }
2877
2822
  }
2878
- const approval = detectHubApproval(toolInvocations);
2879
- const outcome = deriveOutcome({
2880
- terminalReached,
2881
- runError,
2882
- runErrorCode,
2883
- toolInvocations,
2884
- approval,
2885
- question,
2886
- plan
2887
- });
2823
+ const outcome = outcomeTracker.finish();
2888
2824
  return {
2889
2825
  success: outcome.success,
2890
2826
  status: outcome.status,
@@ -2892,10 +2828,11 @@ var SandboxInstance = class SandboxInstance {
2892
2828
  response: responseText,
2893
2829
  error: outcome.error,
2894
2830
  errorCode: outcome.errorCode,
2895
- toolInvocations,
2896
- approval,
2897
- question,
2898
- plan,
2831
+ toolInvocations: outcome.toolInvocations,
2832
+ approval: outcome.approval,
2833
+ interaction: outcome.interaction,
2834
+ question: outcome.question,
2835
+ plan: outcome.plan,
2899
2836
  traceId,
2900
2837
  durationMs: Date.now() - startTime,
2901
2838
  usage,
@@ -2924,15 +2861,13 @@ var SandboxInstance = class SandboxInstance {
2924
2861
  * Stream events from an agent prompt.
2925
2862
  * Use this for real-time updates during agent execution.
2926
2863
  *
2927
- * Guarantees a terminal event on every non-cancelled path: a clean run
2928
- * ends with the runtime's `result`/`done`; a failure (pre-stream HTTP
2929
- * error, mid-stream network drop, timeout, or reconnect exhaustion) is
2930
- * surfaced as an in-band `error` event followed by a synthetic `done`,
2931
- * never a thrown generator. Caller-initiated cancellation (aborting
2932
- * `options.signal`) ends the stream silently with no synthetic terminal.
2864
+ * Guarantees terminal completion on every non-cancelled path. A clean run
2865
+ * emits a `result` summary and ends with `done`. A failure is an in-band
2866
+ * `error` followed by `done`, never a thrown generator. Caller cancellation
2867
+ * ends the stream silently with no synthetic terminal.
2933
2868
  *
2934
2869
  * Automatically reconnects via the runtime event replay endpoint if the
2935
- * SSE stream drops before a terminal event (`result` or `done`) is
2870
+ * SSE stream drops before a terminal event (`done` or `error`) is
2936
2871
  * received. Reconnection is transparent — replayed events that were
2937
2872
  * already yielded (based on event ID tracking) are deduplicated.
2938
2873
  */
@@ -3015,7 +2950,7 @@ var SandboxInstance = class SandboxInstance {
3015
2950
  try {
3016
2951
  for await (const event of this.streamPromptInner(message, options, { forceFullProfile: profileResendAttempted })) {
3017
2952
  yieldedThisAttempt = true;
3018
- if (event.type === "result" || event.type === "done") receivedTerminal = true;
2953
+ if (event.type === "done") receivedTerminal = true;
3019
2954
  if (event.type === "execution.started" && event.data.executionId) executionId = event.data.executionId;
3020
2955
  const sid = event.data.sessionId;
3021
2956
  if (typeof sid === "string" && sid.length > 0) sessionId = sid;
@@ -3174,7 +3109,7 @@ var SandboxInstance = class SandboxInstance {
3174
3109
  if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
3175
3110
  const eventSessionId = event.data?.sessionId;
3176
3111
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
3177
- if (event.type === "result" || event.type === "done") {
3112
+ if (event.type === "done" || event.type === "error") {
3178
3113
  receivedTerminal = true;
3179
3114
  logStreamCheckpoint("terminal_event", {
3180
3115
  eventType: event.type,
@@ -3244,6 +3179,7 @@ var SandboxInstance = class SandboxInstance {
3244
3179
  sessionId,
3245
3180
  executionId,
3246
3181
  status: info.status,
3182
+ ...info.status === "completed" ? { outcome: { type: "completed" } } : {},
3247
3183
  replayUnavailable: true
3248
3184
  }
3249
3185
  };
@@ -3268,7 +3204,7 @@ var SandboxInstance = class SandboxInstance {
3268
3204
  if (event.id) lastEventId = event.id;
3269
3205
  const eventSessionId = event.data?.sessionId;
3270
3206
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
3271
- if (event.type === "result" || event.type === "done") {
3207
+ if (event.type === "done" || event.type === "error") {
3272
3208
  receivedTerminal = true;
3273
3209
  logStreamCheckpoint("terminal_event", {
3274
3210
  eventType: event.type,
@@ -4578,37 +4514,16 @@ var SandboxInstance = class SandboxInstance {
4578
4514
  };
4579
4515
  }
4580
4516
  async *parseProcessLogStream(response) {
4581
- const reader = response.body?.getReader();
4582
- if (!reader) throw new NetworkError("No response body");
4583
- const decoder = new TextDecoder();
4584
- let buffer = "";
4585
- let currentEvent = "";
4586
- let currentData = "";
4587
- try {
4588
- while (true) {
4589
- const { done, value } = await reader.read();
4590
- if (done) break;
4591
- buffer += decoder.decode(value, { stream: true });
4592
- const lines = buffer.split("\n");
4593
- buffer = lines.pop() ?? "";
4594
- for (const line of lines) if (line.startsWith("event:")) currentEvent = line.slice(6).trim();
4595
- else if (line.startsWith("data:")) currentData = line.slice(5).trim();
4596
- else if (line === "" && currentEvent && currentData) {
4597
- if (currentEvent === "stdout" || currentEvent === "stderr") try {
4598
- const parsed = JSON.parse(currentData);
4599
- yield {
4600
- type: currentEvent,
4601
- data: parsed.data,
4602
- timestamp: parsed.timestamp
4603
- };
4604
- } catch {}
4605
- else if (currentEvent === "exit") return;
4606
- currentEvent = "";
4607
- currentData = "";
4608
- }
4609
- }
4610
- } finally {
4611
- reader.releaseLock();
4517
+ if (!response.body) throw new NetworkError("No response body");
4518
+ for await (const event of parseSSEStream(response.body, { invalidEvent: "warn-and-skip" })) {
4519
+ if (event.type === "exit") return;
4520
+ if (event.type !== "stdout" && event.type !== "stderr") continue;
4521
+ if (typeof event.data.data !== "string" || typeof event.data.timestamp !== "number") continue;
4522
+ yield {
4523
+ type: event.type,
4524
+ data: event.data.data,
4525
+ timestamp: event.data.timestamp
4526
+ };
4612
4527
  }
4613
4528
  }
4614
4529
  /**
@@ -6367,7 +6282,7 @@ var SandboxInstance = class SandboxInstance {
6367
6282
  }
6368
6283
  for await (const event of this.parseSSEStream(response, opts?.signal)) {
6369
6284
  yield event;
6370
- const turnTerminal = event.type === "result" || event.type === "done" || event.type === "error";
6285
+ const turnTerminal = event.type === "done" || event.type === "error";
6371
6286
  if ((stopOnTurnTerminal || opts?.executionId !== void 0) && turnTerminal) return;
6372
6287
  }
6373
6288
  }
@@ -6386,7 +6301,7 @@ var SandboxInstance = class SandboxInstance {
6386
6301
  }
6387
6302
  for await (const event of this.parseSSEStream(response)) {
6388
6303
  yield event;
6389
- if (event.type === "result" || event.type === "done" || event.type === "error") return;
6304
+ if (event.type === "done" || event.type === "error") return;
6390
6305
  }
6391
6306
  }
6392
6307
  /** @internal — invoked by SandboxSession.result(). */
@@ -6408,20 +6323,13 @@ var SandboxInstance = class SandboxInstance {
6408
6323
  }
6409
6324
  let resultExecutionId = executionId;
6410
6325
  let response;
6411
- let runError;
6412
- let runErrorCode;
6326
+ const outcomeTracker = createAgentRunOutcomeTracker();
6413
6327
  let traceId;
6414
6328
  let usage;
6415
6329
  let costUsd;
6416
- let toolInvocations;
6417
- let question;
6418
- let plan;
6419
- let terminalReached = false;
6420
- const events = options?.executionId ? this._executionEvents(id, executionId) : this._sessionEvents(id, {
6421
- since: "0",
6422
- executionId
6423
- }, true);
6330
+ const events = this._executionEvents(id, executionId);
6424
6331
  for await (const event of events) {
6332
+ outcomeTracker.observe(event);
6425
6333
  response = applySandboxEventText(response, event);
6426
6334
  if (event.type === "result" || event.type === "done") {
6427
6335
  const data = event.data;
@@ -6429,33 +6337,10 @@ var SandboxInstance = class SandboxInstance {
6429
6337
  usage = readTokenUsage(data) ?? usage;
6430
6338
  costUsd = readTokenCostUsd(data) ?? costUsd;
6431
6339
  }
6432
- plan = readPlanFromEvent(event) ?? plan;
6433
- if (event.type === "result") {
6434
- const data = event.data;
6435
- toolInvocations = readToolInvocations(data) ?? toolInvocations;
6436
- }
6437
- if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
6438
6340
  if (event.type === "trace.id") traceId = event.data.traceId;
6439
- if (event.type === "error") {
6440
- const data = event.data;
6441
- runError = data.message;
6442
- runErrorCode = data.code;
6443
- }
6444
- if (event.type === "done" || event.type === "result") {
6445
- terminalReached = true;
6446
- break;
6447
- }
6341
+ if (event.type === "done" || event.type === "error") break;
6448
6342
  }
6449
- const approval = detectHubApproval(toolInvocations);
6450
- const outcome = deriveOutcome({
6451
- terminalReached,
6452
- runError,
6453
- runErrorCode,
6454
- toolInvocations,
6455
- approval,
6456
- question,
6457
- plan
6458
- });
6343
+ const outcome = outcomeTracker.finish();
6459
6344
  return {
6460
6345
  success: outcome.success,
6461
6346
  status: outcome.status,
@@ -6463,10 +6348,11 @@ var SandboxInstance = class SandboxInstance {
6463
6348
  response,
6464
6349
  error: outcome.error,
6465
6350
  errorCode: outcome.errorCode,
6466
- toolInvocations,
6467
- approval,
6468
- question,
6469
- plan,
6351
+ toolInvocations: outcome.toolInvocations,
6352
+ approval: outcome.approval,
6353
+ interaction: outcome.interaction,
6354
+ question: outcome.question,
6355
+ plan: outcome.plan,
6470
6356
  traceId,
6471
6357
  durationMs: Date.now() - startTime,
6472
6358
  usage,
@@ -6760,21 +6646,16 @@ function isSessionInFlight(info) {
6760
6646
  return info.status === "queued" && !isSessionAwaitingFirstTurn(info);
6761
6647
  }
6762
6648
  function settleTurnDrive(result) {
6763
- const plan = readAwaitingPlan(result);
6764
- if (plan) return {
6649
+ const outcome = deriveAgentResultOutcome(result);
6650
+ if (outcome.status === "awaiting_plan_decision" && outcome.plan) return {
6765
6651
  state: "awaiting_plan_decision",
6766
- plan,
6652
+ plan: outcome.plan,
6767
6653
  result
6768
6654
  };
6769
6655
  const text = typeof result.text === "string" ? result.text : typeof result.response === "string" ? result.response : void 0;
6770
- const status = typeof result.status === "string" ? result.status : deriveOutcome({
6771
- terminalReached: true,
6772
- runError: typeof result.error === "string" ? result.error : void 0,
6773
- toolInvocations: Array.isArray(result.toolInvocations) ? result.toolInvocations : void 0
6774
- }).status;
6775
- if (status !== "success") return {
6656
+ if (outcome.status !== "success") return {
6776
6657
  state: "failed",
6777
- error: typeof result.error === "string" && result.error.length > 0 ? result.error : `turn ${status}`
6658
+ error: outcome.error ?? `turn ${outcome.status}`
6778
6659
  };
6779
6660
  const usage = readTokenUsage(result);
6780
6661
  return {