@aparte/engine 0.16.10 → 0.17.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 CHANGED
@@ -32,10 +32,10 @@ Conversation compaction (the context budget, the selector, the summariser) is
32
32
 
33
33
  ## Owning the history (prefix-cache hosts)
34
34
 
35
- By default the loop keeps the message list and re-sends it every turn, enriched with the
36
- `tool_call` / `tool_result` turns it produced. That is right for a stateless message API, and
37
- wrong for a **prefix cache** — llama.cpp slots, vLLM — where turn N+1 must *extend* turn N
38
- byte for byte or the cache is thrown away.
35
+ By default the loop keeps the message list and re-sends it every turn, enriched with the assistant
36
+ turn that carries the calls and the `tool` turn that answers each one. That is right for a
37
+ stateless message API, and wrong for a **prefix cache** — llama.cpp slots, vLLM — where turn N+1
38
+ must *extend* turn N byte for byte or the cache is thrown away.
39
39
 
40
40
  Such a host owns its own transcript. It already controls the request (`transportCall` receives
41
41
  the built request and may ignore `request.messages`); `onHistoryAppend` is the other half —
@@ -48,11 +48,18 @@ const log = new PromptLog(); // your append-only transcrip
48
48
  await runStreamAgent({
49
49
  // …messageId, emitter, signal, toolLookup
50
50
  baseRequest: { messages: [], modelId: 'my-model' }, // your transport may ignore both
51
- onHistoryAppend: (turn) => log.append(turn), // tool_call · tool_result · phase reply
51
+ onHistoryAppend: (turn) => log.append(turn), // the calls · each result · each reply
52
52
  transportCall: () => myCompletion(log.render()), // your own bytes, extended not rebuilt
53
53
  });
54
54
  ```
55
55
 
56
+ One caveat the example glosses over: the assistant turn is reported the moment its **first** call
57
+ completes, and both its `content` and its `toolCalls` are finalised later in the same turn. A log
58
+ that writes bytes on receipt, like `PromptLog` above, must therefore hold the object and re-read it
59
+ before serialising, or append the assistant turn only once the turn's last `tool` message has
60
+ arrived. A snapshot taken on receipt loses the whole sentence the assistant said before it called
61
+ anything.
62
+
56
63
  Wiring it through core's seam needs no core change — augment the options at injection:
57
64
 
58
65
  ```ts
@@ -107,20 +107,14 @@ export interface StreamImagePart {
107
107
  image: string;
108
108
  mimeType?: string;
109
109
  }
110
- export interface StreamFilePart {
111
- type: 'file';
112
- data: string;
113
- mimeType: string;
114
- name?: string;
115
- }
116
- export type StreamContentPart = StreamTextPart | StreamImagePart | StreamFilePart;
110
+ export type StreamContentPart = StreamTextPart | StreamImagePart;
117
111
  /**
118
112
  * A conversation message (mirrors `AparteChatMessage`).
119
113
  *
120
114
  * `role` used to be a bare `string`, justified by a comment saying the loop needed
121
- * to push the `'tool_call'` / `'tool_result'` envelope roles "without importing
122
- * core's union". But core's union already contains both — so the reason had not
123
- * been true for some time, and the looseness cost something real: `transportCall`
115
+ * to push the envelope roles of a tool turn "without importing core's union". But
116
+ * core's union already contained them — so the reason had not been true for some
117
+ * time, and the looseness cost something real: `transportCall`
124
118
  * is contravariant, so this message type must ALSO be assignable to core's, and a
125
119
  * `string` role is not assignable to a closed union. That was the last link in the
126
120
  * chain that stopped `streamRunner: runStreamAgent` from compiling.
@@ -129,7 +123,7 @@ export type StreamContentPart = StreamTextPart | StreamImagePart | StreamFilePar
129
123
  * runtime; core, which depends on this package, compiles the two against each other.
130
124
  */
131
125
  export interface StreamAgentMessage {
132
- role: 'user' | 'assistant' | 'system' | 'tool_call' | 'tool_result';
126
+ role: 'user' | 'assistant' | 'system' | 'tool';
133
127
  /**
134
128
  * Message content — carried, never inspected by this loop.
135
129
  *
@@ -140,12 +134,15 @@ export interface StreamAgentMessage {
140
134
  * the headline of five docs pages — did not typecheck.
141
135
  */
142
136
  content: string | StreamContentPart[];
143
- /** Present on a `'tool_call'` envelope — the whole turn's calls, grouped. */
137
+ /**
138
+ * Present on an `'assistant'` message — the whole turn's calls, grouped. What it
139
+ * said before them is that message's own `content`, `''` when it said nothing.
140
+ */
144
141
  toolCalls?: StreamToolCall[];
145
- /** Present on a `'tool_result'` message — which call it answers. */
142
+ /** Present on a `'tool'` message — which call it answers. */
146
143
  toolCallId?: string;
147
- /** Assistant text that preceded the tool call(s) this turn. */
148
- precedingText?: string;
144
+ /** Present on a `'tool'` message — the name of the tool that ran. */
145
+ toolName?: string;
149
146
  }
150
147
  /**
151
148
  * The request handed to the transport each turn (mirrors `AparteChatRequest`).
@@ -173,7 +170,12 @@ export interface StreamChatRequest {
173
170
  modelId: string;
174
171
  /** Per-turn hints carried through for the host (`prefixSegments`, and whatever the consumer puts there); the loop reads none. */
175
172
  _meta?: Record<string, unknown>;
176
- /** `'none'` makes the loop drop the tool inventory for that turn. */
173
+ /**
174
+ * `'none'` makes the loop drop the tool inventory for that turn. An object forces
175
+ * a tool, and forcing is a turn-1 instruction: the loop sends it on the first
176
+ * transport call of the run and lifts it (to `'auto'`) for every turn after, so
177
+ * the model can answer once the tool has.
178
+ */
177
179
  toolChoice?: 'auto' | 'none' | {
178
180
  name: string;
179
181
  input?: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"stream-events.d.ts","sourceRoot":"","sources":["../../src/agent/stream-events.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stream-events.d.ts","sourceRoot":"","sources":["../../src/agent/stream-events.ts"],"names":[],"mappings":"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"}
@@ -47,10 +47,11 @@ export interface StreamRunOptions {
47
47
  /** Per-tool-call handler timeout in ms. @default 300000 */
48
48
  toolTimeoutMs?: number;
49
49
  /**
50
- * Called for every turn the loop appends to the history — the grouped
51
- * `tool_call` envelope, each `tool_result` (resolved or rejected), and a
52
- * pipeline phase's reply — in order, and always before the transport call that
53
- * would carry it. Never called for the messages you passed in `baseRequest`.
50
+ * Called for every turn the loop appends to the history — the assistant message
51
+ * carrying the turn's calls, each `tool` message answering one (resolved or
52
+ * rejected), and a pipeline phase's reply — in order, and always before the
53
+ * transport call that would carry it. Never called for the messages you passed
54
+ * in `baseRequest`.
54
55
  *
55
56
  * For hosts that own their own transcript. The loop re-sends its `messages`
56
57
  * array every turn, which suits stateless message APIs but not a **prefix
@@ -60,11 +61,23 @@ export interface StreamRunOptions {
60
61
  * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and
61
62
  * ordered, like {@link emitter}.
62
63
  *
63
- * One rule about the envelope: a turn's `tool_call` message is reported ONCE, when
64
+ * One rule about the envelope: a turn's assistant message is reported ONCE, when
64
65
  * its first call completes, and the calls that complete later in the same turn are
65
66
  * already in that same object's `toolCalls` — the array is shared by reference, not
66
67
  * copied. Hold the reference, not a snapshot, and every result you receive after it
67
68
  * is declared by it.
69
+ *
70
+ * Two of its fields are finalised after you are notified, then: `toolCalls` gains
71
+ * the turn's later calls, and `content` is re-read at the turn's end (a provider
72
+ * that emits `[tool, text]` streams the rest of the sentence after the envelope is
73
+ * opened — see `syncEnvelopeText`). A host that must write BYTES at receipt — an
74
+ * append-only log, which cannot hold a reference — should therefore treat the
75
+ * envelope as provisional and write it only once the turn's last `tool` message
76
+ * has arrived, or re-read the object it holds before serialising.
77
+ *
78
+ * `content` is the field that moved here, and the failure is louder than it was:
79
+ * a snapshot no longer loses a trailing clause, it loses the whole sentence the
80
+ * assistant said before it called anything.
68
81
  */
69
82
  onHistoryAppend?: (message: StreamAgentMessage) => void;
70
83
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAElB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;AAO5B,MAAM,WAAW,gBAAgB;IAC7B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,EAAE,iBAAiB,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,aAAa,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,CAAC;IAChG,uFAAuF;IACvF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC5D,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAC;IAClE,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACxD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6a7F"}
1
+ {"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAElB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;AAO5B,MAAM,WAAW,gBAAgB;IAC7B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,EAAE,iBAAiB,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,aAAa,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,CAAC;IAChG,uFAAuF;IACvF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC5D,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAC;IAClE,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACxD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAge7F"}
package/dist/index.js CHANGED
@@ -61,11 +61,13 @@ async function runStreamAgent(opts) {
61
61
  throw outcome.error;
62
62
  }
63
63
  emitter({ type: "tool-resolved", toolCallId: syntheticId, result: outcome.content, structuredResult: outcome.structuredContent });
64
- append({ role: "tool_call", content: "", toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });
65
- append({ role: "tool_result", content: outcome.content, toolCallId: syntheticId });
64
+ append({ role: "assistant", content: "", toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });
65
+ append({ role: "tool", content: outcome.content, toolCallId: syntheticId, toolName: tc.name });
66
66
  baseRequest = { ...baseRequest, toolChoice: "none", tools: void 0 };
67
67
  }
68
68
  const request = { ...baseRequest, messages };
69
+ const forced = baseRequest["toolChoice"];
70
+ if (forced && typeof forced === "object") baseRequest = { ...baseRequest, toolChoice: "auto" };
69
71
  let response;
70
72
  try {
71
73
  response = await transportCall(request);
@@ -89,13 +91,26 @@ async function runStreamAgent(opts) {
89
91
  const declareCall = (call) => {
90
92
  toolCallsThisTurn.push(call);
91
93
  if (envelope) return;
92
- envelope = { role: "tool_call", content: "", toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || void 0 };
94
+ envelope = { role: "assistant", content: precedingText.trim(), toolCalls: toolCallsThisTurn };
93
95
  append(envelope);
94
96
  };
97
+ const syncEnvelopeText = () => {
98
+ if (envelope) envelope.content = precedingText.trim();
99
+ };
100
+ const idsThisTurn = /* @__PURE__ */ new Set();
101
+ const uniqueCallId = (raw) => {
102
+ let id = raw;
103
+ for (let n = 2; idsThisTurn.has(id); n++) id = `${raw}#${n}`;
104
+ idsThisTurn.add(id);
105
+ return id;
106
+ };
95
107
  const iterator = response[Symbol.asyncIterator]();
96
108
  const bailOnAbort = async () => {
97
109
  if (!signal.aborted) return false;
98
- await iterator.return?.(void 0);
110
+ try {
111
+ await iterator.return?.(void 0);
112
+ } catch {
113
+ }
99
114
  continueLoop = false;
100
115
  return true;
101
116
  };
@@ -127,28 +142,29 @@ async function runStreamAgent(opts) {
127
142
  continue;
128
143
  }
129
144
  sawToolUse = true;
130
- emitter({ type: "tool-start", toolCallId: event.id, name: event.name, input: event.input });
145
+ const callId = uniqueCallId(event.id);
146
+ emitter({ type: "tool-start", toolCallId: callId, name: event.name, input: event.input });
131
147
  const cfg = toolConfigLookup?.(event.name);
132
148
  const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;
133
149
  if (turns > effectiveMaxTurns) {
134
- emitter({ type: "turn-limit-exceeded", scope: "tool", limit: effectiveMaxTurns, toolCallId: event.id });
150
+ emitter({ type: "turn-limit-exceeded", scope: "tool", limit: effectiveMaxTurns, toolCallId: callId });
135
151
  continueLoop = false;
136
152
  break;
137
153
  }
138
154
  const handler = toolLookup(event.name);
139
155
  if (!handler) {
140
- emitter({ type: "tool-aborted", toolCallId: event.id });
156
+ emitter({ type: "tool-aborted", toolCallId: callId });
141
157
  continueLoop = false;
142
158
  break;
143
159
  }
144
160
  let effectiveInput = event.input;
145
- const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: event.id, name: event.name, input: event.input }) : cfg?.needsApproval;
161
+ const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: callId, name: event.name, input: event.input }) : cfg?.needsApproval;
146
162
  if (gated) {
147
163
  if (gated !== "deny") {
148
- emitter({ type: "tool-awaiting-approval", toolCallId: event.id, name: event.name, input: event.input });
164
+ emitter({ type: "tool-awaiting-approval", toolCallId: callId, name: event.name, input: event.input });
149
165
  }
150
166
  if (!approvalResolver) {
151
- emitter({ type: "tool-aborted", toolCallId: event.id });
167
+ emitter({ type: "tool-aborted", toolCallId: callId });
152
168
  continueLoop = false;
153
169
  break;
154
170
  }
@@ -156,7 +172,7 @@ async function runStreamAgent(opts) {
156
172
  if (!signal.aborted) {
157
173
  try {
158
174
  decision = await approvalResolver(
159
- { id: event.id, name: event.name, input: event.input },
175
+ { id: callId, name: event.name, input: event.input },
160
176
  signal
161
177
  );
162
178
  } catch (err) {
@@ -164,45 +180,48 @@ async function runStreamAgent(opts) {
164
180
  }
165
181
  }
166
182
  if (signal.aborted || !decision) {
167
- emitter({ type: "tool-aborted", toolCallId: event.id });
183
+ emitter({ type: "tool-aborted", toolCallId: callId });
168
184
  continueLoop = false;
169
185
  break;
170
186
  }
171
187
  if (!decision.approved) {
172
188
  const rejection = decision.reason?.trim() || (decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.");
173
- emitter({ type: "tool-rejected", toolCallId: event.id, reason: rejection });
174
- declareCall({ id: event.id, name: event.name, input: event.input });
175
- append({ role: "tool_result", content: rejection, toolCallId: event.id });
189
+ emitter({ type: "tool-rejected", toolCallId: callId, reason: rejection });
190
+ declareCall({ id: callId, name: event.name, input: event.input });
191
+ append({ role: "tool", content: rejection, toolCallId: callId, toolName: event.name });
176
192
  break;
177
193
  }
178
194
  if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
179
195
  effectiveInput = { ...event.input, ...decision.payload };
180
196
  }
181
- emitter({ type: "tool-approved", toolCallId: event.id });
197
+ emitter({ type: "tool-approved", toolCallId: callId });
182
198
  }
183
199
  const outcome = await invokeToolHandler(
184
200
  handler,
185
- { id: event.id, name: event.name, input: effectiveInput },
201
+ { id: callId, name: event.name, input: effectiveInput },
186
202
  signal,
187
203
  toolTimeoutMs
188
204
  );
189
205
  if (outcome.status === "aborted") {
190
- emitter({ type: "tool-aborted", toolCallId: event.id });
206
+ emitter({ type: "tool-aborted", toolCallId: callId });
191
207
  continueLoop = false;
192
208
  break;
193
209
  } else if (outcome.status === "failed") {
194
- emitter({ type: "tool-failed", toolCallId: event.id, error: errorText(outcome.error) });
210
+ emitter({ type: "tool-failed", toolCallId: callId, error: errorText(outcome.error) });
195
211
  throw outcome.error;
196
212
  } else {
197
- emitter({ type: "tool-resolved", toolCallId: event.id, result: outcome.content, structuredResult: outcome.structuredContent });
198
- declareCall({ id: event.id, name: event.name, input: event.input });
199
- append({ role: "tool_result", content: outcome.content, toolCallId: event.id });
213
+ emitter({ type: "tool-resolved", toolCallId: callId, result: outcome.content, structuredResult: outcome.structuredContent });
214
+ declareCall({ id: callId, name: event.name, input: event.input });
215
+ append({ role: "tool", content: outcome.content, toolCallId: callId, toolName: event.name });
200
216
  }
201
217
  }
202
218
  emitter({ type: "text-flush" });
219
+ syncEnvelopeText();
203
220
  } finally {
204
- await iterator.return?.(void 0).catch(() => {
205
- });
221
+ try {
222
+ await iterator.return?.(void 0);
223
+ } catch {
224
+ }
206
225
  }
207
226
  if (!sawToolUse) continueLoop = false;
208
227
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/agent/stream-run.ts"],"sourcesContent":["/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The agent loop — extracted from core's inline `_streamLoop`, which it has since\n * replaced (audit 2026-08-28, D1): a `while(tool_use)` loop\n * that consumes a structured `AsyncIterable<StreamChatEvent>` from a transport,\n * runs approved tools, feeds their results back into the history, and re-calls\n * the transport until the model stops asking for tools. It performs **no DOM\n * work** — it emits {@link StreamRunEvent}s (in the inline loop's exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Formerly the parity target of core's inline loop; now the one loop, its call\n * sequences pinned by core's `stream-parity` snapshots.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · synthetic toolChoice\n * bypass. Tagged blocks in the text (`<artifact>`, `<cite>`…) are adapter-side: core's\n * parser reads the grammars registered on the config. The built-in `create_artifact`\n * left with the artifact (D7) — it is a registered tool now. The raw / XML artifact\n * modes and the multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the\n * product's, and a mode nothing in this repository emitted was a contract\n * maintained for nobody.\n */\n\nimport type {\n StreamRunEmitter,\n StreamChatEvent,\n StreamChatRequest,\n StreamAgentMessage,\n StreamToolCall,\n StreamToolHandler,\n StreamToolConfig,\n StreamApprovalResolver,\n StreamUsage,\n} from './stream-events.js';\n\n/** Default per-tool-call handler timeout — mirrors `TOOL_HANDLER_TIMEOUT_MS`. */\nconst DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000;\n/** Default global turn cap — mirrors `AparteClientOptions.maxTurns ?? 10`. */\nconst DEFAULT_MAX_TURNS = 10;\n\nexport interface StreamRunOptions {\n /** Id of the assistant message being streamed (opaque; carried in events). */\n messageId: string;\n /** Turn-1 request; the loop clones its `messages` and enriches them per turn. */\n baseRequest: StreamChatRequest;\n /**\n * Calls the transport with the (possibly enriched) request. Returns the\n * structured stream, or a plain string for a non-streaming provider. Mirrors\n * `getTransport().chat(provider, request, auth, ctx)` with provider/auth/ctx\n * closed over by the adapter.\n */\n transportCall: (request: StreamChatRequest) => Promise<AsyncIterable<StreamChatEvent> | string>;\n /** Resolves a tool's handler by name (mirrors `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the grouped\n * `tool_call` envelope, each `tool_result` (resolved or rejected), and a\n * pipeline phase's reply — in order, and always before the transport call that\n * would carry it. Never called for the messages you passed in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n *\n * One rule about the envelope: a turn's `tool_call` message is reported ONCE, when\n * its first call completes, and the calls that complete later in the same turn are\n * already in that same object's `toolCalls` — the array is shared by reference, not\n * copied. Hold the reference, not a snapshot, and every result you receive after it\n * is declared by it.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates the ids the loop mints itself — today only the synthetic call of a\n * forced `toolChoice` (`prefix` is `'synthetic-tool'`). The default is a\n * deterministic per-run counter; core's client injects a crypto-based one. Tool\n * ids otherwise flow from the stream.\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with tool_call/tool_result turns. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n let continueLoop = true;\n let turns = 0;\n let lastUsage: StreamUsage | undefined;\n\n // Leading write before turn 1: mark the message streaming.\n emitter({ type: 'run-start' });\n\n // ── outer (turn) loop — one iteration = one transport call ────────────────\n while (continueLoop) {\n // Abort at the top of a turn: no stream open yet, so no cancel needed. The\n // `run-aborted` itself is emitted once, at the loop's exit (see below).\n if (signal.aborted) break;\n\n turns++;\n if (turns > maxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'global', limit: maxTurns });\n break;\n }\n\n // ── Synthetic toolChoice bypass (mirrors _streamLoop) ──────\n // toolChoice = { name, input } (orchestrator-forced): skip the LLM for\n // turn 1, run the handler directly, inject its result as a tool_result,\n // then strip toolChoice/tools and fall through to the transport call in\n // the SAME turn so the model answers with the tool result already in\n // history. (The adapter's tool-start handler injects renderer CSS here\n // too — a benign, idempotent gain over _streamLoop's synthetic path,\n // which skipped it; reconciled in the parity test.)\n const toolChoice = baseRequest['toolChoice'];\n if (\n turns === 1 &&\n toolChoice &&\n typeof toolChoice === 'object' &&\n !Array.isArray(toolChoice) &&\n (toolChoice as { input?: unknown }).input !== undefined\n ) {\n // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n if (outcome.status === 'failed') {\n emitter({ type: 'tool-failed', toolCallId: syntheticId, error: errorText(outcome.error) });\n throw outcome.error;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content, structuredResult: outcome.structuredContent });\n append({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n const request: StreamChatRequest = { ...baseRequest, messages };\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n // (`run-aborted` is emitted once, at the loop's exit.)\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n // \"Did this turn ask for tools\" — a different question from \"which calls are\n // declared\" (`toolCallsThisTurn`, which now holds only committed ones).\n let sawToolUse = false;\n /**\n * The turn's ONE `tool_call` envelope, held by reference. Created — and\n * appended, so the host is notified once — the first time a call needs it;\n * every later call of the turn is already in the SAME `toolCalls` array, so a\n * `tool_result` appended afterwards is always declared by it.\n *\n * This replaces an id scan over `messages` that guessed whether the envelope\n * was already there. The `create_artifact` fast path pushed a fresh envelope\n * of its own; the scan then found the artifact's id in it, concluded \"already\n * pushed\", and the next tool's result went out with no call declaring it —\n * the P0 of the 2026-08-28 audit, a history an Anthropic-shaped API rejects\n * outright. A reference cannot be guessed wrong.\n */\n let envelope: StreamAgentMessage | null = null;\n /*\n * A call is DECLARED in the envelope only once it is committed to producing\n * a `tool_result` — at the three sites that append one right after. It used\n * to be pushed the moment the `tool_use` event was read, into the same array\n * the envelope holds by reference, so a call halted before its result (no\n * handler, turn limit, missing resolver, an abort during the wait) still\n * appeared in a history the host was told to hold by reference: a\n * `tool_call` declaring a call that never gets a result.\n */\n const declareCall = (call: StreamToolCall): void => {\n toolCallsThisTurn.push(call);\n if (envelope) return;\n envelope = { role: 'tool_call', content: '', toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || undefined };\n append(envelope);\n };\n\n // ── inner (SSE) loop — manual iteration so we can abort around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n await iterator.return?.(undefined);\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n emitter({ type: 'text-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'done') {\n if (event.usage) lastUsage = event.usage;\n continue;\n }\n\n if (event.type === 'error') {\n // Mirror `_streamLoop`: throw; the caller's catch handles it.\n throw new Error(event.message);\n }\n\n // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n sawToolUse = true;\n\n // No built-in tool of any name: `create_artifact` used to be dispatched\n // here, before the tool path, with no handler, no approval and its own\n // result. A tool is a tool — the app registers it (`@aparte/plugin-artifacts`\n // does), and it goes through tool-start, the gate, the handler and the\n // envelope like every other.\n emitter({ type: 'tool-start', toolCallId: event.id, name: event.name, input: event.input });\n\n const cfg = toolConfigLookup?.(event.name);\n\n // Per-tool maxTurns — the same arithmetic as the global cap (`>`), so one\n // number means one thing on both knobs. It was `>=`: `maxTurns: 1` on a\n // tool made it un-callable on the very first turn, and `maxTurns: N`\n // allowed N-1 calls against the global option's N.\n const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;\n if (turns > effectiveMaxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n const handler = toolLookup(event.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n let effectiveInput = event.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n const gated = typeof cfg?.needsApproval === 'function'\n ? cfg.needsApproval({ id: event.id, name: event.name, input: event.input as Record<string, unknown> })\n : cfg?.needsApproval;\n if (gated) {\n // Announced only when someone is actually being asked: a `'deny'` is a\n // decision already made, and announcing a pause for it painted\n // `awaiting-approval` on the row and raised `aparte-tool-approval-request`\n // for a call nobody was ever going to be asked about.\n if (gated !== 'deny') {\n emitter({ type: 'tool-awaiting-approval', toolCallId: event.id, name: event.name, input: event.input });\n }\n /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool_result`: an aborted call has nothing true to tell the model.\n *\n * Checked on three sides of the wait, because the stop can land on\n * any of them: while the gate was being shown (a host that reacts to\n * `awaiting-approval` synchronously — the panel's own Stop button),\n * inside the resolver (a channel that rejects with an AbortError\n * rather than resolving), or after it resolved. The first and second\n * used to fall through to the run-level abort, which never marked the\n * call: the segment stayed `awaiting-approval` for good.\n */\n let decision: Awaited<ReturnType<typeof approvalResolver>> | undefined;\n if (!signal.aborted) {\n try {\n decision = await approvalResolver(\n { id: event.id, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n } catch (err) {\n if (!signal.aborted && (err as { name?: string } | undefined)?.name !== 'AbortError') throw err;\n }\n }\n if (signal.aborted || !decision) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n // Truthiness, like `instruction` beside it: an empty `reason` is no\n // reason, not an empty tool_result.\n const rejection = decision.reason?.trim()\n || (decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.');\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n declareCall({ id: event.id, name: event.name, input: event.input });\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: event.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: event.id, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n } else if (outcome.status === 'failed') {\n // A crash is the handler's, and the run ends on it as before —\n // but the row hears about it first, instead of spinning for good.\n emitter({ type: 'tool-failed', toolCallId: event.id, error: errorText(outcome.error) });\n throw outcome.error;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content, structuredResult: outcome.structuredContent });\n declareCall({ id: event.id, name: event.name, input: event.input });\n append({ role: 'tool_result', content: outcome.content, toolCallId: event.id });\n }\n }\n\n // Turn boundary: finalize the parser (flush residual text). Mirrors\n // `_streamLoop`'s `textParser.finalize()` call — runs on normal\n // end AND abort-break, but NOT after a thrown `error` (which escapes\n // this try before reaching here, exactly like `_streamLoop`).\n emitter({ type: 'text-flush' });\n\n } finally {\n // Mirror `reader.releaseLock()` in the finally: settle the iterator.\n await iterator.return?.(undefined).catch(() => { /* best effort */ });\n }\n\n // No tool calls this turn → the final answer.\n if (!sawToolUse) continueLoop = false;\n }\n\n // Post-loop finalization runs on every exit path (normal / abort / maxTurns).\n //\n // `run-aborted` is decided HERE, once, from the signal — not at each site that\n // notices the abort. It used to be emitted at three of the six abort exits\n // (top of turn, transport rejection, mid-stream bail), and the three tool-level\n // ones — a Stop during a handler, during an approval wait, or with no resolver —\n // set `continueLoop = false` and fell through to `run-done` alone: no terminal\n // lifecycle event reached the host, so the typing indicator and the streaming\n // id were never cleared and the turn was never persisted. A tool TIMEOUT aborts\n // only the child controller, so `signal.aborted` stays false and that turn still\n // ends as done, unchanged.\n if (signal.aborted) emitter({ type: 'run-aborted' });\n emitter({ type: 'run-done', usage: lastUsage });\n return lastUsage;\n}\n\n/**\n * Run a tool handler with a per-call timeout, linked to the run's abort signal.\n * Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`\n * if the handler aborts (timeout OR parent abort); any other error is re-thrown so\n * the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared\n * by the generic tool path and the synthetic-toolChoice bypass (mirrors the\n * AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).\n */\nasync function invokeToolHandler(\n handler: StreamToolHandler,\n call: StreamToolCall,\n signal: AbortSignal,\n toolTimeoutMs: number,\n): Promise<\n | { status: 'resolved'; content: string; structuredContent?: unknown }\n | { status: 'aborted' }\n /** The handler threw something that is not an abort: a crash, reported by the caller then re-thrown. */\n | { status: 'failed'; error: unknown }\n> {\n // If the run was already aborted before we got here, don't invoke the\n // handler at all: a past 'abort' event will never re-fire on the listener\n // below, so the handler would otherwise run to completion despite cancel.\n if (signal.aborted) return { status: 'aborted' };\n\n const controller = new AbortController();\n const onParentAbort = () => controller.abort();\n signal.addEventListener('abort', onParentAbort, { once: true });\n\n /*\n * The parent abort is RACED for the same reason the timeout is.\n *\n * `onParentAbort` above only forwards the Stop to the child controller, and a\n * deaf handler ignores that exactly as it ignores the timeout's. So a Stop\n * pressed during a tool call unwound nothing: the loop stayed parked on the\n * handler until `toolTimeoutMs` expired — five minutes by default — with the\n * typing indicator still up and the stop button already pressed. The signal is\n * still fired first, so a handler that honours it gets to reject on its own\n * terms; this racer is what makes the Stop true when it does not.\n */\n let onParentAbortRace: (() => void) | undefined;\n const parentAborted = new Promise<{ status: 'aborted' }>((resolve) => {\n onParentAbortRace = () => resolve({ status: 'aborted' });\n signal.addEventListener('abort', onParentAbortRace, { once: true });\n });\n\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content, structuredContent: r.structuredContent })),\n timedOut,\n parentAborted,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n // Not thrown from here: the call was announced with `tool-start`, and the\n // loop's invariant is that an announced call gets exactly one terminal\n // event. Throwing from inside skipped it — the row spun \"Running\" for good\n // while the message showed an error card. The caller reports, then throws.\n return { status: 'failed', error: err };\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n if (onParentAbortRace) signal.removeEventListener('abort', onParentAbortRace);\n }\n}\n\n/** The one-line text of a handler's crash, for the `tool-failed` event and the row. */\nfunction errorText(error: unknown): string {\n if (error instanceof Error) return error.message || error.name;\n return typeof error === 'string' ? error : String(error);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/agent/stream-run.ts"],"sourcesContent":["/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The agent loop — extracted from core's inline `_streamLoop`, which it has since\n * replaced (audit 2026-08-28, D1): a `while(tool_use)` loop\n * that consumes a structured `AsyncIterable<StreamChatEvent>` from a transport,\n * runs approved tools, feeds their results back into the history, and re-calls\n * the transport until the model stops asking for tools. It performs **no DOM\n * work** — it emits {@link StreamRunEvent}s (in the inline loop's exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Formerly the parity target of core's inline loop; now the one loop, its call\n * sequences pinned by core's `stream-parity` snapshots.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · synthetic toolChoice\n * bypass. Tagged blocks in the text (`<artifact>`, `<cite>`…) are adapter-side: core's\n * parser reads the grammars registered on the config. The built-in `create_artifact`\n * left with the artifact (D7) — it is a registered tool now. The raw / XML artifact\n * modes and the multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the\n * product's, and a mode nothing in this repository emitted was a contract\n * maintained for nobody.\n */\n\nimport type {\n StreamRunEmitter,\n StreamChatEvent,\n StreamChatRequest,\n StreamAgentMessage,\n StreamToolCall,\n StreamToolHandler,\n StreamToolConfig,\n StreamApprovalResolver,\n StreamUsage,\n} from './stream-events.js';\n\n/** Default per-tool-call handler timeout — mirrors `TOOL_HANDLER_TIMEOUT_MS`. */\nconst DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000;\n/** Default global turn cap — mirrors `AparteClientOptions.maxTurns ?? 10`. */\nconst DEFAULT_MAX_TURNS = 10;\n\nexport interface StreamRunOptions {\n /** Id of the assistant message being streamed (opaque; carried in events). */\n messageId: string;\n /** Turn-1 request; the loop clones its `messages` and enriches them per turn. */\n baseRequest: StreamChatRequest;\n /**\n * Calls the transport with the (possibly enriched) request. Returns the\n * structured stream, or a plain string for a non-streaming provider. Mirrors\n * `getTransport().chat(provider, request, auth, ctx)` with provider/auth/ctx\n * closed over by the adapter.\n */\n transportCall: (request: StreamChatRequest) => Promise<AsyncIterable<StreamChatEvent> | string>;\n /** Resolves a tool's handler by name (mirrors `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the assistant message\n * carrying the turn's calls, each `tool` message answering one (resolved or\n * rejected), and a pipeline phase's reply — in order, and always before the\n * transport call that would carry it. Never called for the messages you passed\n * in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n *\n * One rule about the envelope: a turn's assistant message is reported ONCE, when\n * its first call completes, and the calls that complete later in the same turn are\n * already in that same object's `toolCalls` — the array is shared by reference, not\n * copied. Hold the reference, not a snapshot, and every result you receive after it\n * is declared by it.\n *\n * Two of its fields are finalised after you are notified, then: `toolCalls` gains\n * the turn's later calls, and `content` is re-read at the turn's end (a provider\n * that emits `[tool, text]` streams the rest of the sentence after the envelope is\n * opened — see `syncEnvelopeText`). A host that must write BYTES at receipt — an\n * append-only log, which cannot hold a reference — should therefore treat the\n * envelope as provisional and write it only once the turn's last `tool` message\n * has arrived, or re-read the object it holds before serialising.\n *\n * `content` is the field that moved here, and the failure is louder than it was:\n * a snapshot no longer loses a trailing clause, it loses the whole sentence the\n * assistant said before it called anything.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates the ids the loop mints itself — today only the synthetic call of a\n * forced `toolChoice` (`prefix` is `'synthetic-tool'`). The default is a\n * deterministic per-run counter; core's client injects a crypto-based one. Tool\n * ids otherwise flow from the stream.\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with the assistant and `tool` turns of a tool round-trip. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n let continueLoop = true;\n let turns = 0;\n let lastUsage: StreamUsage | undefined;\n\n // Leading write before turn 1: mark the message streaming.\n emitter({ type: 'run-start' });\n\n // ── outer (turn) loop — one iteration = one transport call ────────────────\n while (continueLoop) {\n // Abort at the top of a turn: no stream open yet, so no cancel needed. The\n // `run-aborted` itself is emitted once, at the loop's exit (see below).\n if (signal.aborted) break;\n\n turns++;\n if (turns > maxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'global', limit: maxTurns });\n break;\n }\n\n // ── Synthetic toolChoice bypass (mirrors _streamLoop) ──────\n // toolChoice = { name, input } (orchestrator-forced): skip the LLM for\n // turn 1, run the handler directly, inject its result as a `tool` message,\n // then strip toolChoice/tools and fall through to the transport call in\n // the SAME turn so the model answers with the tool result already in\n // history. (The adapter's tool-start handler injects renderer CSS here\n // too — a benign, idempotent gain over _streamLoop's synthetic path,\n // which skipped it; reconciled in the parity test.)\n const toolChoice = baseRequest['toolChoice'];\n if (\n turns === 1 &&\n toolChoice &&\n typeof toolChoice === 'object' &&\n !Array.isArray(toolChoice) &&\n (toolChoice as { input?: unknown }).input !== undefined\n ) {\n // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n if (outcome.status === 'failed') {\n emitter({ type: 'tool-failed', toolCallId: syntheticId, error: errorText(outcome.error) });\n throw outcome.error;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content, structuredResult: outcome.structuredContent });\n append({ role: 'assistant', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool', content: outcome.content, toolCallId: syntheticId, toolName: tc.name });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n const request: StreamChatRequest = { ...baseRequest, messages };\n /*\n * Forcing a tool is a TURN-1 instruction, and only the loop can lift it: the\n * request is rebuilt from `baseRequest` every turn, so a `toolChoice` object\n * left there was re-sent after the tool had already answered — the model was\n * made to call it again, every turn, until `maxTurns`, and the run ended on\n * `turn-limit-exceeded` instead of a reply. Every object shape, not only the\n * `{ name, input }` the synthetic bypass above strips for itself: `{ name }`\n * alone is what a consumer sets, and it never enters that branch.\n */\n const forced = baseRequest['toolChoice'];\n if (forced && typeof forced === 'object') baseRequest = { ...baseRequest, toolChoice: 'auto' };\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n // (`run-aborted` is emitted once, at the loop's exit.)\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n // \"Did this turn ask for tools\" — a different question from \"which calls are\n // declared\" (`toolCallsThisTurn`, which now holds only committed ones).\n let sawToolUse = false;\n /**\n * The turn's ONE assistant envelope, held by reference. Created — and\n * appended, so the host is notified once — the first time a call needs it;\n * every later call of the turn is already in the SAME `toolCalls` array, so a\n * `tool` message appended afterwards is always declared by it.\n *\n * This replaces an id scan over `messages` that guessed whether the envelope\n * was already there. The `create_artifact` fast path pushed a fresh envelope\n * of its own; the scan then found the artifact's id in it, concluded \"already\n * pushed\", and the next tool's result went out with no call declaring it —\n * the P0 of the 2026-08-28 audit, a history an Anthropic-shaped API rejects\n * outright. A reference cannot be guessed wrong.\n */\n let envelope: StreamAgentMessage | null = null;\n /*\n * A call is DECLARED in the envelope only once it is committed to producing\n * a `tool` message — at the three sites that append one right after. It used\n * to be pushed the moment the `tool_use` event was read, into the same array\n * the envelope holds by reference, so a call halted before its result (no\n * handler, turn limit, missing resolver, an abort during the wait) still\n * appeared in a history the host was told to hold by reference: an\n * assistant message declaring a call that never gets a result.\n */\n const declareCall = (call: StreamToolCall): void => {\n toolCallsThisTurn.push(call);\n if (envelope) return;\n envelope = { role: 'assistant', content: precedingText.trim(), toolCalls: toolCallsThisTurn };\n append(envelope);\n };\n /*\n * The envelope's text is a SNAPSHOT taken when the first call is declared, and\n * a turn can go on streaming afterwards — a provider that emits `[tool, text]`\n * in that order (`@aparte/provider-scenario` does; openai-compat flushes its\n * calls at the end of the stream and so cannot) had the sentence shown to the\n * user and dropped from the history, where nothing else carries it. Re-read at\n * the turn's end, through the closure: the envelope is held by reference for\n * exactly this reason, the way `toolCalls` is.\n */\n const syncEnvelopeText = (): void => {\n if (envelope) envelope.content = precedingText.trim();\n };\n /*\n * A call id must be unique WITHIN the turn: the transcript keys a row on it\n * (`tool-${id}`, and `updateSegment` takes the first match) and the history\n * files a `tool` message under it. A vendor that omits `id` used to give every\n * call of a turn the same one, so the second call's result was written onto the\n * first call's row and the pair could not be matched at all on the next turn.\n * A repeat is renamed rather than refused — the model asked for both.\n */\n const idsThisTurn = new Set<string>();\n const uniqueCallId = (raw: string): string => {\n let id = raw;\n for (let n = 2; idsThisTurn.has(id); n++) id = `${raw}#${n}`;\n idsThisTurn.add(id);\n return id;\n };\n\n // ── inner (SSE) loop — manual iteration so we can abort around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n // Guarded like the `finally` below: a transport whose iterator throws on\n // the way out turned a deliberate Stop into a thrown run error, and core\n // paints an error card over the reply the user had just stopped. `try`,\n // not `.catch()`: a hand-written iterator throws BEFORE there is a promise\n // to reject, and one that returns a plain object has no `.catch` at all.\n try { await iterator.return?.(undefined); } catch { /* best effort */ }\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n emitter({ type: 'text-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'done') {\n if (event.usage) lastUsage = event.usage;\n continue;\n }\n\n if (event.type === 'error') {\n // Mirror `_streamLoop`: throw; the caller's catch handles it.\n throw new Error(event.message);\n }\n\n // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n sawToolUse = true;\n // From here the CALL's id, not the event's: see `uniqueCallId`. The\n // row, the handler, the approval and the history slot must all name\n // the same call, so the rename happens once, before the first emit.\n const callId = uniqueCallId(event.id);\n\n // No built-in tool of any name: `create_artifact` used to be dispatched\n // here, before the tool path, with no handler, no approval and its own\n // result. A tool is a tool — the app registers it (`@aparte/plugin-artifacts`\n // does), and it goes through tool-start, the gate, the handler and the\n // envelope like every other.\n emitter({ type: 'tool-start', toolCallId: callId, name: event.name, input: event.input });\n\n const cfg = toolConfigLookup?.(event.name);\n\n // Per-tool maxTurns — the same arithmetic as the global cap (`>`), so one\n // number means one thing on both knobs. It was `>=`: `maxTurns: 1` on a\n // tool made it un-callable on the very first turn, and `maxTurns: N`\n // allowed N-1 calls against the global option's N.\n const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;\n if (turns > effectiveMaxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: callId });\n continueLoop = false;\n break;\n }\n\n const handler = toolLookup(event.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: callId });\n continueLoop = false;\n break;\n }\n\n let effectiveInput = event.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n const gated = typeof cfg?.needsApproval === 'function'\n ? cfg.needsApproval({ id: callId, name: event.name, input: event.input as Record<string, unknown> })\n : cfg?.needsApproval;\n if (gated) {\n // Announced only when someone is actually being asked: a `'deny'` is a\n // decision already made, and announcing a pause for it painted\n // `awaiting-approval` on the row and raised `aparte-tool-approval-request`\n // for a call nobody was ever going to be asked about.\n if (gated !== 'deny') {\n emitter({ type: 'tool-awaiting-approval', toolCallId: callId, name: event.name, input: event.input });\n }\n /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: callId });\n continueLoop = false;\n break;\n }\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool` message: an aborted call has nothing true to tell the model.\n *\n * Checked on three sides of the wait, because the stop can land on\n * any of them: while the gate was being shown (a host that reacts to\n * `awaiting-approval` synchronously — the panel's own Stop button),\n * inside the resolver (a channel that rejects with an AbortError\n * rather than resolving), or after it resolved. The first and second\n * used to fall through to the run-level abort, which never marked the\n * call: the segment stayed `awaiting-approval` for good.\n */\n let decision: Awaited<ReturnType<typeof approvalResolver>> | undefined;\n if (!signal.aborted) {\n try {\n decision = await approvalResolver(\n { id: callId, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n } catch (err) {\n if (!signal.aborted && (err as { name?: string } | undefined)?.name !== 'AbortError') throw err;\n }\n }\n if (signal.aborted || !decision) {\n emitter({ type: 'tool-aborted', toolCallId: callId });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n // Truthiness, like `instruction` beside it: an empty `reason` is no\n // reason, not an empty `tool` message.\n const rejection = decision.reason?.trim()\n || (decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.');\n emitter({ type: 'tool-rejected', toolCallId: callId, reason: rejection });\n declareCall({ id: callId, name: event.name, input: event.input });\n append({ role: 'tool', content: rejection, toolCallId: callId, toolName: event.name });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: callId });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: callId, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: callId });\n continueLoop = false;\n break;\n } else if (outcome.status === 'failed') {\n // A crash is the handler's, and the run ends on it as before —\n // but the row hears about it first, instead of spinning for good.\n emitter({ type: 'tool-failed', toolCallId: callId, error: errorText(outcome.error) });\n throw outcome.error;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: callId, result: outcome.content, structuredResult: outcome.structuredContent });\n declareCall({ id: callId, name: event.name, input: event.input });\n append({ role: 'tool', content: outcome.content, toolCallId: callId, toolName: event.name });\n }\n }\n\n // Turn boundary: finalize the parser (flush residual text). Mirrors\n // `_streamLoop`'s `textParser.finalize()` call — runs on normal\n // end AND abort-break, but NOT after a thrown `error` (which escapes\n // this try before reaching here, exactly like `_streamLoop`).\n emitter({ type: 'text-flush' });\n // Everything this turn streamed belongs to the turn, including what came\n // after the call that opened the envelope.\n syncEnvelopeText();\n\n } finally {\n // Mirror `reader.releaseLock()` in the finally: settle the iterator,\n // however it fails on the way out (see `bailOnAbort`).\n try { await iterator.return?.(undefined); } catch { /* best effort */ }\n }\n\n // No tool calls this turn → the final answer.\n if (!sawToolUse) continueLoop = false;\n }\n\n // Post-loop finalization runs on every exit path (normal / abort / maxTurns).\n //\n // `run-aborted` is decided HERE, once, from the signal — not at each site that\n // notices the abort. It used to be emitted at three of the six abort exits\n // (top of turn, transport rejection, mid-stream bail), and the three tool-level\n // ones — a Stop during a handler, during an approval wait, or with no resolver —\n // set `continueLoop = false` and fell through to `run-done` alone: no terminal\n // lifecycle event reached the host, so the typing indicator and the streaming\n // id were never cleared and the turn was never persisted. A tool TIMEOUT aborts\n // only the child controller, so `signal.aborted` stays false and that turn still\n // ends as done, unchanged.\n if (signal.aborted) emitter({ type: 'run-aborted' });\n emitter({ type: 'run-done', usage: lastUsage });\n return lastUsage;\n}\n\n/**\n * Run a tool handler with a per-call timeout, linked to the run's abort signal.\n * Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`\n * if the handler aborts (timeout OR parent abort); any other error is re-thrown so\n * the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared\n * by the generic tool path and the synthetic-toolChoice bypass (mirrors the\n * AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).\n */\nasync function invokeToolHandler(\n handler: StreamToolHandler,\n call: StreamToolCall,\n signal: AbortSignal,\n toolTimeoutMs: number,\n): Promise<\n | { status: 'resolved'; content: string; structuredContent?: unknown }\n | { status: 'aborted' }\n /** The handler threw something that is not an abort: a crash, reported by the caller then re-thrown. */\n | { status: 'failed'; error: unknown }\n> {\n // If the run was already aborted before we got here, don't invoke the\n // handler at all: a past 'abort' event will never re-fire on the listener\n // below, so the handler would otherwise run to completion despite cancel.\n if (signal.aborted) return { status: 'aborted' };\n\n const controller = new AbortController();\n const onParentAbort = () => controller.abort();\n signal.addEventListener('abort', onParentAbort, { once: true });\n\n /*\n * The parent abort is RACED for the same reason the timeout is.\n *\n * `onParentAbort` above only forwards the Stop to the child controller, and a\n * deaf handler ignores that exactly as it ignores the timeout's. So a Stop\n * pressed during a tool call unwound nothing: the loop stayed parked on the\n * handler until `toolTimeoutMs` expired — five minutes by default — with the\n * typing indicator still up and the stop button already pressed. The signal is\n * still fired first, so a handler that honours it gets to reject on its own\n * terms; this racer is what makes the Stop true when it does not.\n */\n let onParentAbortRace: (() => void) | undefined;\n const parentAborted = new Promise<{ status: 'aborted' }>((resolve) => {\n onParentAbortRace = () => resolve({ status: 'aborted' });\n signal.addEventListener('abort', onParentAbortRace, { once: true });\n });\n\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content, structuredContent: r.structuredContent })),\n timedOut,\n parentAborted,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n // Not thrown from here: the call was announced with `tool-start`, and the\n // loop's invariant is that an announced call gets exactly one terminal\n // event. Throwing from inside skipped it — the row spun \"Running\" for good\n // while the message showed an error card. The caller reports, then throws.\n return { status: 'failed', error: err };\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n if (onParentAbortRace) signal.removeEventListener('abort', onParentAbortRace);\n }\n}\n\n/** The one-line text of a handler's crash, for the `tool-failed` event and the row. */\nfunction errorText(error: unknown): string {\n if (error instanceof Error) return error.message || error.name;\n return typeof error === 'string' ? error : String(error);\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/engine",
3
- "version": "0.16.10",
3
+ "version": "0.17.0",
4
4
  "homepage": "https://apartejs.dev/reference/engine/",
5
5
  "description": "Framework-agnostic AI agent loop (runStreamAgent) + parsers/orchestrator — zero runtime deps, headless (browser or Node).",
6
6
  "type": "module",