@aparte/engine 0.17.2 → 0.18.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.
|
@@ -211,6 +211,51 @@ export interface StreamToolConfig {
|
|
|
211
211
|
name: string;
|
|
212
212
|
input: Record<string, unknown>;
|
|
213
213
|
}) => boolean | 'ask' | 'deny');
|
|
214
|
+
/**
|
|
215
|
+
* Known to the model, executed elsewhere — a browser tool behind a server loop.
|
|
216
|
+
* The loop never looks up a handler for it: with `deferral` the call waits for its
|
|
217
|
+
* result in `deferral.outcomes`; without it, the call is aborted like a missing
|
|
218
|
+
* handler.
|
|
219
|
+
*/
|
|
220
|
+
external?: boolean;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Why a deferred call waits. `approval`: a person must decide. `external`: the tool
|
|
224
|
+
* runs elsewhere and its result must come back. `queued`: nothing — it came after the
|
|
225
|
+
* pause point in the same reply, and runs when the run resumes.
|
|
226
|
+
*/
|
|
227
|
+
export type StreamDeferredReason = 'approval' | 'external' | 'queued';
|
|
228
|
+
/** A call a suspended run left without an answer. */
|
|
229
|
+
export interface StreamDeferredCall {
|
|
230
|
+
toolCallId: string;
|
|
231
|
+
name: string;
|
|
232
|
+
input: unknown;
|
|
233
|
+
reason: StreamDeferredReason;
|
|
234
|
+
}
|
|
235
|
+
/** The answer to a deferred call, handed to the run that resumes it. */
|
|
236
|
+
export type StreamDeferredOutcome = {
|
|
237
|
+
kind: 'decision';
|
|
238
|
+
approved: boolean;
|
|
239
|
+
reason?: string;
|
|
240
|
+
instruction?: string;
|
|
241
|
+
payload?: unknown;
|
|
242
|
+
} | {
|
|
243
|
+
kind: 'result';
|
|
244
|
+
content: string;
|
|
245
|
+
structuredContent?: unknown;
|
|
246
|
+
} | {
|
|
247
|
+
kind: 'failed';
|
|
248
|
+
error: string;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Defer instead of block — see `StreamRunOptions.deferral`. Data only, so a run it
|
|
252
|
+
* answers can be days old and on another machine.
|
|
253
|
+
*/
|
|
254
|
+
export interface StreamDeferral {
|
|
255
|
+
/** Answers for calls a previous run left pending, by tool call id. */
|
|
256
|
+
outcomes?: Record<string, StreamDeferredOutcome>;
|
|
257
|
+
/** Opaque to the loop, which never reads it: a remote runner carries its continuation ticket here. */
|
|
258
|
+
token?: string;
|
|
214
259
|
}
|
|
215
260
|
/**
|
|
216
261
|
* Resolves a human-in-the-loop approval (mirrors core's
|
|
@@ -256,6 +301,9 @@ export type StreamApprovalResolver = (call: {
|
|
|
256
301
|
* scope:'tool' → updateSegment('aborted')
|
|
257
302
|
* - `run-aborted` → dispatch `aparte-message-aborted` (from the inner-loop abort check or the outer turn-boundary abort check)
|
|
258
303
|
* - `run-done` → updateMessage(status:'completed') always + setUsage if usage
|
|
304
|
+
* - `run-suspended` → (with `deferral`) the run ends with calls left pending; the
|
|
305
|
+
* adapter marks them and completes the message — the TURN goes on
|
|
306
|
+
* through the run that resumes them
|
|
259
307
|
*/
|
|
260
308
|
export type StreamRunEvent = {
|
|
261
309
|
type: 'run-start';
|
|
@@ -310,9 +358,15 @@ export type StreamRunEvent = {
|
|
|
310
358
|
} | {
|
|
311
359
|
type: 'run-done';
|
|
312
360
|
usage?: StreamUsage;
|
|
361
|
+
}
|
|
362
|
+
/** Ends a run in place of `run-done` when calls are left for someone else to answer. */
|
|
363
|
+
| {
|
|
364
|
+
type: 'run-suspended';
|
|
365
|
+
pending: StreamDeferredCall[];
|
|
366
|
+
usage?: StreamUsage;
|
|
313
367
|
};
|
|
314
368
|
/**
|
|
315
|
-
* Synchronous event sink
|
|
369
|
+
* Synchronous event sink. Synchronous by contract: the
|
|
316
370
|
* loop must never yield between emitting an event and its ordered successor, or
|
|
317
371
|
* the adapter's streaming updates would interleave out of order.
|
|
318
372
|
*/
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CAevB;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC,CAAC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG,cAAc,CAAC,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAE5C;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC9D,MAAM,WAAW,eAAe;IAAG,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE;AACpF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,eAAe,CAAC;AAEjE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/C;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAYrB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,iIAAiI;IACjI,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACjF,8EAA8E;IAC9E,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,iEAAiE;AACjE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,WAAW,KAClB,OAAO,CAAC;IACT,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,+GAA+G;IAC/G,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC,CAAC;AAEH,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IAC7H;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEtE,qDAAqD;AACrD,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,oBAAoB,CAAC;CAChC;AAED,wEAAwE;AACxE,MAAM,MAAM,qBAAqB,GAC3B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACjG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACjD,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,CACjC,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAClE,MAAM,EAAE,WAAW,KAClB,OAAO,CAAC;IACT,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAIH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,cAAc,GACpB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACxE;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpF;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC/D,8FAA8F;GAC5F;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,GACzF;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7F;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAA;CAAE;AAC3C,wFAAwF;GACtF;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAEpF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* product's, and a mode nothing in this repository emitted was a contract
|
|
20
20
|
* maintained for nobody.
|
|
21
21
|
*/
|
|
22
|
-
import type { StreamRunEmitter, StreamChatEvent, StreamChatRequest, StreamAgentMessage, StreamToolHandler, StreamToolConfig, StreamApprovalResolver, StreamUsage } from './stream-events.js';
|
|
22
|
+
import type { StreamRunEmitter, StreamChatEvent, StreamChatRequest, StreamAgentMessage, StreamToolCall, StreamToolHandler, StreamToolConfig, StreamApprovalResolver, StreamUsage, StreamDeferral } from './stream-events.js';
|
|
23
23
|
export interface StreamRunOptions {
|
|
24
24
|
/** Id of the assistant message being streamed (opaque; carried in events). */
|
|
25
25
|
messageId: string;
|
|
@@ -87,6 +87,25 @@ export interface StreamRunOptions {
|
|
|
87
87
|
* ids otherwise flow from the stream.
|
|
88
88
|
*/
|
|
89
89
|
idGen?: (prefix: string) => string;
|
|
90
|
+
/**
|
|
91
|
+
* Defer instead of block. With this set, a call the gate sends to a person is not
|
|
92
|
+
* awaited on `approvalResolver`, and an `external` tool is not treated as missing:
|
|
93
|
+
* each takes its answer from `outcomes`, and without one the call is DEFERRED —
|
|
94
|
+
* declared on the turn's envelope with no `tool` answer. The rest of the model's
|
|
95
|
+
* reply is still read to its end (its later calls pending too, nothing run after the
|
|
96
|
+
* pause point), and the run ends on `run-suspended` instead of `run-done`.
|
|
97
|
+
*
|
|
98
|
+
* A run whose `baseRequest.messages` ends on an assistant envelope with unanswered
|
|
99
|
+
* calls RESUMES: those calls go through the same `processCall` as a streamed one,
|
|
100
|
+
* in order, before the first transport call. A call the gate denies is never
|
|
101
|
+
* deferred — it is refused, through `approvalResolver`, as without this option.
|
|
102
|
+
*/
|
|
103
|
+
deferral?: StreamDeferral;
|
|
104
|
+
/**
|
|
105
|
+
* The caller's provider id. Opaque to the loop, like `messageId`: a runner that
|
|
106
|
+
* forwards the run elsewhere (a server) needs it.
|
|
107
|
+
*/
|
|
108
|
+
providerId?: string;
|
|
90
109
|
}
|
|
91
110
|
/**
|
|
92
111
|
* Run the structured-stream agent loop. Resolves the last turn's usage (the
|
|
@@ -104,4 +123,18 @@ export interface StreamRunOptions {
|
|
|
104
123
|
* pushed above an import.
|
|
105
124
|
*/
|
|
106
125
|
export declare function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined>;
|
|
126
|
+
/**
|
|
127
|
+
* Run a tool handler with a per-call timeout, linked to the run's abort signal.
|
|
128
|
+
* Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`
|
|
129
|
+
* if the handler aborts (timeout OR parent abort); any other error is re-thrown so
|
|
130
|
+
* the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared
|
|
131
|
+
* by the generic tool path and the synthetic-toolChoice bypass (mirrors the
|
|
132
|
+
* AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* The calls a history leaves unanswered: those of its LAST assistant envelope that no
|
|
136
|
+
* following `tool` message answers, in declaration order. A resumed run answers them
|
|
137
|
+
* first; a server checks them against the ticket it signed when the run paused.
|
|
138
|
+
*/
|
|
139
|
+
export declare function unansweredTail(messages: readonly StreamAgentMessage[]): StreamToolCall[];
|
|
107
140
|
//# sourceMappingURL=stream-run.d.ts.map
|
|
@@ -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,
|
|
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,EAClB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACX,cAAc,EAGjB,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;IACnC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAuZ7F;AAED;;;;;;;GAOG;AACH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GAAG,cAAc,EAAE,CAWxF"}
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,8 @@ async function runStreamAgent(opts) {
|
|
|
18
18
|
emitter,
|
|
19
19
|
signal,
|
|
20
20
|
maxTurns = DEFAULT_MAX_TURNS,
|
|
21
|
-
toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS
|
|
21
|
+
toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,
|
|
22
|
+
deferral
|
|
22
23
|
} = opts;
|
|
23
24
|
let baseRequest = opts.baseRequest;
|
|
24
25
|
let idSeq = 0;
|
|
@@ -32,6 +33,38 @@ async function runStreamAgent(opts) {
|
|
|
32
33
|
let turns = 0;
|
|
33
34
|
let lastUsage;
|
|
34
35
|
emitter({ type: "run-start" });
|
|
36
|
+
const tail = deferral ? unansweredTail(messages) : [];
|
|
37
|
+
if (tail.length > 0) {
|
|
38
|
+
const stillPending = [];
|
|
39
|
+
const ctx = { turns: 1, maxTurns, toolLookup, toolConfigLookup, approvalResolver, emitter, signal, toolTimeoutMs, declareCall: () => {
|
|
40
|
+
}, append, deferral };
|
|
41
|
+
for (const [i, call] of tail.entries()) {
|
|
42
|
+
if (stillPending.length > 0) {
|
|
43
|
+
stillPending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: queuedReason(call, toolConfigLookup) });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let verdict;
|
|
47
|
+
try {
|
|
48
|
+
verdict = await processCall(call, ctx);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
for (const rest of tail.slice(i + 1)) emitter({ type: "tool-aborted", toolCallId: rest.id });
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (typeof verdict === "object") {
|
|
54
|
+
stillPending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: verdict.defer });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (verdict === "stop-run") {
|
|
58
|
+
for (const rest of tail.slice(i + 1)) emitter({ type: "tool-aborted", toolCallId: rest.id });
|
|
59
|
+
continueLoop = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (stillPending.length > 0 && !signal.aborted) {
|
|
64
|
+
emitter({ type: "run-suspended", pending: stillPending });
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
35
68
|
while (continueLoop) {
|
|
36
69
|
if (signal.aborted) break;
|
|
37
70
|
turns++;
|
|
@@ -104,6 +137,8 @@ async function runStreamAgent(opts) {
|
|
|
104
137
|
idsThisTurn.add(id);
|
|
105
138
|
return id;
|
|
106
139
|
};
|
|
140
|
+
const pending = [];
|
|
141
|
+
const callContext = { turns, maxTurns, toolLookup, toolConfigLookup, approvalResolver, emitter, signal, toolTimeoutMs, declareCall, append, deferral };
|
|
107
142
|
const iterator = response[Symbol.asyncIterator]();
|
|
108
143
|
const bailOnAbort = async () => {
|
|
109
144
|
if (!signal.aborted) return false;
|
|
@@ -144,76 +179,27 @@ async function runStreamAgent(opts) {
|
|
|
144
179
|
sawToolUse = true;
|
|
145
180
|
const callId = uniqueCallId(event.id);
|
|
146
181
|
emitter({ type: "tool-start", toolCallId: callId, name: event.name, input: event.input });
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const handler = toolLookup(event.name);
|
|
155
|
-
if (!handler) {
|
|
156
|
-
emitter({ type: "tool-aborted", toolCallId: callId });
|
|
157
|
-
continueLoop = false;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
let effectiveInput = event.input;
|
|
161
|
-
const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: callId, name: event.name, input: event.input }) : cfg?.needsApproval;
|
|
162
|
-
if (gated) {
|
|
163
|
-
if (gated !== "deny") {
|
|
164
|
-
emitter({ type: "tool-awaiting-approval", toolCallId: callId, name: event.name, input: event.input });
|
|
165
|
-
}
|
|
166
|
-
if (!approvalResolver) {
|
|
167
|
-
emitter({ type: "tool-aborted", toolCallId: callId });
|
|
168
|
-
continueLoop = false;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
let decision;
|
|
172
|
-
if (!signal.aborted) {
|
|
173
|
-
try {
|
|
174
|
-
decision = await approvalResolver(
|
|
175
|
-
{ id: callId, name: event.name, input: event.input },
|
|
176
|
-
signal
|
|
177
|
-
);
|
|
178
|
-
} catch (err) {
|
|
179
|
-
if (!signal.aborted && err?.name !== "AbortError") throw err;
|
|
180
|
-
}
|
|
182
|
+
const call = { id: callId, name: event.name, input: event.input };
|
|
183
|
+
if (pending.length > 0) {
|
|
184
|
+
declareCall(call);
|
|
185
|
+
const reason = queuedReason(call, toolConfigLookup);
|
|
186
|
+
if (reason === "approval") {
|
|
187
|
+
emitter({ type: "tool-awaiting-approval", toolCallId: call.id, name: call.name, input: call.input });
|
|
181
188
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
continueLoop = false;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
if (!decision.approved) {
|
|
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.");
|
|
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 });
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
|
|
195
|
-
effectiveInput = { ...event.input, ...decision.payload };
|
|
196
|
-
}
|
|
197
|
-
emitter({ type: "tool-approved", toolCallId: callId });
|
|
189
|
+
pending.push({ toolCallId: call.id, name: call.name, input: call.input, reason });
|
|
190
|
+
continue;
|
|
198
191
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
emitter({ type: "tool-aborted", toolCallId: callId });
|
|
192
|
+
const verdict = await processCall(call, callContext);
|
|
193
|
+
if (typeof verdict === "object") {
|
|
194
|
+
declareCall(call);
|
|
195
|
+
pending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: verdict.defer });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (verdict === "stop-run") {
|
|
207
199
|
continueLoop = false;
|
|
208
200
|
break;
|
|
209
|
-
} else if (outcome.status === "failed") {
|
|
210
|
-
emitter({ type: "tool-failed", toolCallId: callId, error: errorText(outcome.error) });
|
|
211
|
-
throw outcome.error;
|
|
212
|
-
} else {
|
|
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 });
|
|
216
201
|
}
|
|
202
|
+
if (verdict === "stop-turn") break;
|
|
217
203
|
}
|
|
218
204
|
emitter({ type: "text-flush" });
|
|
219
205
|
syncEnvelopeText();
|
|
@@ -223,12 +209,122 @@ async function runStreamAgent(opts) {
|
|
|
223
209
|
} catch {
|
|
224
210
|
}
|
|
225
211
|
}
|
|
212
|
+
if (pending.length > 0 && !signal.aborted) {
|
|
213
|
+
emitter({ type: "run-suspended", pending, usage: lastUsage });
|
|
214
|
+
return lastUsage;
|
|
215
|
+
}
|
|
226
216
|
if (!sawToolUse) continueLoop = false;
|
|
227
217
|
}
|
|
228
218
|
if (signal.aborted) emitter({ type: "run-aborted" });
|
|
229
219
|
emitter({ type: "run-done", usage: lastUsage });
|
|
230
220
|
return lastUsage;
|
|
231
221
|
}
|
|
222
|
+
function unansweredTail(messages) {
|
|
223
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
224
|
+
const m = messages[i];
|
|
225
|
+
if (m.role === "assistant") {
|
|
226
|
+
if (!m.toolCalls?.length) return [];
|
|
227
|
+
const answered = new Set(messages.slice(i + 1).filter((x) => x.role === "tool").map((x) => x.toolCallId));
|
|
228
|
+
return m.toolCalls.filter((c) => !answered.has(c.id));
|
|
229
|
+
}
|
|
230
|
+
if (m.role !== "tool") return [];
|
|
231
|
+
}
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
function queuedReason(call, lookup) {
|
|
235
|
+
const cfg = lookup?.(call.name);
|
|
236
|
+
if (cfg?.external) return "external";
|
|
237
|
+
const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: call.id, name: call.name, input: call.input }) : cfg?.needsApproval;
|
|
238
|
+
return gated && gated !== "deny" ? "approval" : "queued";
|
|
239
|
+
}
|
|
240
|
+
async function processCall(call, ctx) {
|
|
241
|
+
const { signal } = ctx;
|
|
242
|
+
const input = call.input;
|
|
243
|
+
const cfg = ctx.toolConfigLookup?.(call.name);
|
|
244
|
+
const effectiveMaxTurns = cfg?.maxTurns ?? ctx.maxTurns;
|
|
245
|
+
if (ctx.turns > effectiveMaxTurns) {
|
|
246
|
+
ctx.emitter({ type: "turn-limit-exceeded", scope: "tool", limit: effectiveMaxTurns, toolCallId: call.id });
|
|
247
|
+
return "stop-run";
|
|
248
|
+
}
|
|
249
|
+
const answer = ctx.deferral?.outcomes?.[call.id];
|
|
250
|
+
if (cfg?.external) {
|
|
251
|
+
if (!ctx.deferral) {
|
|
252
|
+
ctx.emitter({ type: "tool-aborted", toolCallId: call.id });
|
|
253
|
+
return "stop-run";
|
|
254
|
+
}
|
|
255
|
+
if (!answer || answer.kind === "decision") return { defer: "external" };
|
|
256
|
+
if (answer.kind === "failed") {
|
|
257
|
+
ctx.emitter({ type: "tool-failed", toolCallId: call.id, error: answer.error });
|
|
258
|
+
throw new Error(answer.error);
|
|
259
|
+
}
|
|
260
|
+
ctx.emitter({ type: "tool-resolved", toolCallId: call.id, result: answer.content, structuredResult: answer.structuredContent });
|
|
261
|
+
ctx.declareCall(call);
|
|
262
|
+
ctx.append({ role: "tool", content: answer.content, toolCallId: call.id, toolName: call.name });
|
|
263
|
+
return "continue";
|
|
264
|
+
}
|
|
265
|
+
const handler = ctx.toolLookup(call.name);
|
|
266
|
+
if (!handler) {
|
|
267
|
+
ctx.emitter({ type: "tool-aborted", toolCallId: call.id });
|
|
268
|
+
return "stop-run";
|
|
269
|
+
}
|
|
270
|
+
let effectiveInput = call.input;
|
|
271
|
+
const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: call.id, name: call.name, input }) : cfg?.needsApproval;
|
|
272
|
+
if (gated) {
|
|
273
|
+
const provided = answer?.kind === "decision" && gated !== "deny" ? answer : void 0;
|
|
274
|
+
if (gated !== "deny" && !provided) {
|
|
275
|
+
ctx.emitter({ type: "tool-awaiting-approval", toolCallId: call.id, name: call.name, input: call.input });
|
|
276
|
+
}
|
|
277
|
+
if (!provided && ctx.deferral && gated !== "deny") return { defer: "approval" };
|
|
278
|
+
if (!provided && !ctx.approvalResolver) {
|
|
279
|
+
ctx.emitter({ type: "tool-aborted", toolCallId: call.id });
|
|
280
|
+
return "stop-run";
|
|
281
|
+
}
|
|
282
|
+
let decision = provided;
|
|
283
|
+
if (!decision && !signal.aborted && ctx.approvalResolver) {
|
|
284
|
+
try {
|
|
285
|
+
decision = await ctx.approvalResolver(
|
|
286
|
+
{ id: call.id, name: call.name, input },
|
|
287
|
+
signal
|
|
288
|
+
);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (!signal.aborted && err?.name !== "AbortError") throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (signal.aborted || !decision) {
|
|
294
|
+
ctx.emitter({ type: "tool-aborted", toolCallId: call.id });
|
|
295
|
+
return "stop-run";
|
|
296
|
+
}
|
|
297
|
+
if (!decision.approved) {
|
|
298
|
+
const rejection = decision.reason?.trim() || (decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.");
|
|
299
|
+
ctx.emitter({ type: "tool-rejected", toolCallId: call.id, reason: rejection });
|
|
300
|
+
ctx.declareCall(call);
|
|
301
|
+
ctx.append({ role: "tool", content: rejection, toolCallId: call.id, toolName: call.name });
|
|
302
|
+
return "stop-turn";
|
|
303
|
+
}
|
|
304
|
+
if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
|
|
305
|
+
effectiveInput = { ...input, ...decision.payload };
|
|
306
|
+
}
|
|
307
|
+
ctx.emitter({ type: "tool-approved", toolCallId: call.id });
|
|
308
|
+
}
|
|
309
|
+
const outcome = await invokeToolHandler(
|
|
310
|
+
handler,
|
|
311
|
+
{ id: call.id, name: call.name, input: effectiveInput },
|
|
312
|
+
signal,
|
|
313
|
+
ctx.toolTimeoutMs
|
|
314
|
+
);
|
|
315
|
+
if (outcome.status === "aborted") {
|
|
316
|
+
ctx.emitter({ type: "tool-aborted", toolCallId: call.id });
|
|
317
|
+
return "stop-run";
|
|
318
|
+
} else if (outcome.status === "failed") {
|
|
319
|
+
ctx.emitter({ type: "tool-failed", toolCallId: call.id, error: errorText(outcome.error) });
|
|
320
|
+
throw outcome.error;
|
|
321
|
+
} else {
|
|
322
|
+
ctx.emitter({ type: "tool-resolved", toolCallId: call.id, result: outcome.content, structuredResult: outcome.structuredContent });
|
|
323
|
+
ctx.declareCall(call);
|
|
324
|
+
ctx.append({ role: "tool", content: outcome.content, toolCallId: call.id, toolName: call.name });
|
|
325
|
+
}
|
|
326
|
+
return "continue";
|
|
327
|
+
}
|
|
232
328
|
async function invokeToolHandler(handler, call, signal, toolTimeoutMs) {
|
|
233
329
|
if (signal.aborted) return { status: "aborted" };
|
|
234
330
|
const controller = new AbortController();
|
|
@@ -267,6 +363,7 @@ function errorText(error) {
|
|
|
267
363
|
return typeof error === "string" ? error : String(error);
|
|
268
364
|
}
|
|
269
365
|
export {
|
|
270
|
-
runStreamAgent
|
|
366
|
+
runStreamAgent,
|
|
367
|
+
unansweredTail
|
|
271
368
|
};
|
|
272
369
|
//# sourceMappingURL=index.js.map
|
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 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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"}
|
|
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 StreamDeferral,\n StreamDeferredCall,\n StreamDeferredReason,\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 * Defer instead of block. With this set, a call the gate sends to a person is not\n * awaited on `approvalResolver`, and an `external` tool is not treated as missing:\n * each takes its answer from `outcomes`, and without one the call is DEFERRED —\n * declared on the turn's envelope with no `tool` answer. The rest of the model's\n * reply is still read to its end (its later calls pending too, nothing run after the\n * pause point), and the run ends on `run-suspended` instead of `run-done`.\n *\n * A run whose `baseRequest.messages` ends on an assistant envelope with unanswered\n * calls RESUMES: those calls go through the same `processCall` as a streamed one,\n * in order, before the first transport call. A call the gate denies is never\n * deferred — it is refused, through `approvalResolver`, as without this option.\n */\n deferral?: StreamDeferral;\n /**\n * The caller's provider id. Opaque to the loop, like `messageId`: a runner that\n * forwards the run elsewhere (a server) needs it.\n */\n providerId?: 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 deferral,\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 // ── Resume: answer what a previous run left pending, before asking the model ──\n // The envelope is already in `messages` (it came with the request), so a resumed\n // call is declared by it: nothing to declare again, and no `tool-start` either —\n // its row exists, from the run that paused.\n const tail = deferral ? unansweredTail(messages) : [];\n if (tail.length > 0) {\n const stillPending: StreamDeferredCall[] = [];\n const ctx: CallContext = { turns: 1, maxTurns, toolLookup, toolConfigLookup, approvalResolver, emitter, signal, toolTimeoutMs, declareCall: () => {}, append, deferral };\n for (const [i, call] of tail.entries()) {\n if (stillPending.length > 0) {\n stillPending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: queuedReason(call, toolConfigLookup) });\n continue;\n }\n let verdict: CallVerdict;\n try {\n verdict = await processCall(call, ctx);\n } catch (error) {\n // A crash ends the run like a `stop-run` below, and the calls after it\n // have rows too: each is told, then the run fails on the crash.\n for (const rest of tail.slice(i + 1)) emitter({ type: 'tool-aborted', toolCallId: rest.id });\n throw error;\n }\n if (typeof verdict === 'object') {\n stillPending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: verdict.defer });\n continue;\n }\n // Unlike a streamed turn, a refusal does not skip the calls after it: each of\n // them is declared by the envelope, and a call left without a `tool` answer\n // makes the provider reject the next request. Each has its own outcome.\n if (verdict === 'stop-run') {\n // Unlike a streamed turn too, the calls after it have rows already, from\n // the run that paused: each is told it will not run, not left waiting.\n for (const rest of tail.slice(i + 1)) emitter({ type: 'tool-aborted', toolCallId: rest.id });\n continueLoop = false;\n break;\n }\n }\n if (stillPending.length > 0 && !signal.aborted) {\n emitter({ type: 'run-suspended', pending: stillPending });\n return undefined;\n }\n }\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 // The calls this turn leaves for someone else to answer (see `deferral`).\n const pending: StreamDeferredCall[] = [];\n const callContext: CallContext = { turns, maxTurns, toolLookup, toolConfigLookup, approvalResolver, emitter, signal, toolTimeoutMs, declareCall, append, deferral };\n\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 call: StreamToolCall = { id: callId, name: event.name, input: event.input };\n if (pending.length > 0) {\n // Past the pause point nothing runs. The call is declared, and says what\n // it waits for, so the client answers every call of the reply in ONE\n // round trip rather than one per call.\n declareCall(call);\n const reason = queuedReason(call, toolConfigLookup);\n if (reason === 'approval') {\n emitter({ type: 'tool-awaiting-approval', toolCallId: call.id, name: call.name, input: call.input });\n }\n pending.push({ toolCallId: call.id, name: call.name, input: call.input, reason });\n continue;\n }\n const verdict = await processCall(call, callContext);\n if (typeof verdict === 'object') {\n declareCall(call);\n pending.push({ toolCallId: call.id, name: call.name, input: call.input, reason: verdict.defer });\n continue;\n }\n if (verdict === 'stop-run') { continueLoop = false; break; }\n // A refusal ends this turn's calls but not the run: see `processCall`.\n if (verdict === 'stop-turn') break;\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 // Calls left pending: the run ends here, without `run-done` — the turn goes on\n // through the run that resumes them. (A Stop takes the abort exit instead.)\n if (pending.length > 0 && !signal.aborted) {\n emitter({ type: 'run-suspended', pending, usage: lastUsage });\n return lastUsage;\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 */\n/**\n * The calls a history leaves unanswered: those of its LAST assistant envelope that no\n * following `tool` message answers, in declaration order. A resumed run answers them\n * first; a server checks them against the ticket it signed when the run paused.\n */\nexport function unansweredTail(messages: readonly StreamAgentMessage[]): StreamToolCall[] {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i]!;\n if (m.role === 'assistant') {\n if (!m.toolCalls?.length) return [];\n const answered = new Set(messages.slice(i + 1).filter((x) => x.role === 'tool').map((x) => x.toolCallId));\n return m.toolCalls.filter((c) => !answered.has(c.id));\n }\n if (m.role !== 'tool') return [];\n }\n return [];\n}\n\n/** Why a call after the pause point waits: what the client must answer, or nothing. */\nfunction queuedReason(call: StreamToolCall, lookup: StreamRunOptions['toolConfigLookup']): StreamDeferredReason {\n const cfg = lookup?.(call.name);\n if (cfg?.external) return 'external';\n const gated = typeof cfg?.needsApproval === 'function'\n ? cfg.needsApproval({ id: call.id, name: call.name, input: call.input as Record<string, unknown> })\n : cfg?.needsApproval;\n return gated && gated !== 'deny' ? 'approval' : 'queued';\n}\n\n/** What the caller does after a call: next event, end this turn's stream, end the run. */\ntype CallVerdict = 'continue' | 'stop-turn' | 'stop-run' | { defer: 'approval' | 'external' };\n\ninterface CallContext {\n turns: number;\n maxTurns: number;\n toolLookup: StreamRunOptions['toolLookup'];\n toolConfigLookup: StreamRunOptions['toolConfigLookup'];\n approvalResolver: StreamRunOptions['approvalResolver'];\n emitter: StreamRunEmitter;\n signal: AbortSignal;\n toolTimeoutMs: number;\n /** Declares the call on the turn's envelope. */\n declareCall: (call: StreamToolCall) => void;\n append: (message: StreamAgentMessage) => void;\n deferral: StreamDeferral | undefined;\n}\n\n/**\n * One call, from its gate to its `tool` message: the ONLY place the loop decides what a\n * call does. The stream reader calls it for each `tool_use` as it arrives; one function,\n * so no second path can drift from it the way core's two loops did before D1.\n */\nasync function processCall(call: StreamToolCall, ctx: CallContext): Promise<CallVerdict> {\n const { signal } = ctx;\n const input = call.input as Record<string, unknown>;\n const cfg = ctx.toolConfigLookup?.(call.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 ?? ctx.maxTurns;\n if (ctx.turns > effectiveMaxTurns) {\n ctx.emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: call.id });\n return 'stop-run';\n }\n\n // The answer a resumed run was handed for this call, if any.\n const answer = ctx.deferral?.outcomes?.[call.id];\n\n // Known to the model, run elsewhere: its result comes back in the answer, and\n // until it does the call waits. Before the handler lookup, because there is no\n // handler here by definition — without `deferral` it is aborted like a missing one.\n if (cfg?.external) {\n if (!ctx.deferral) {\n ctx.emitter({ type: 'tool-aborted', toolCallId: call.id });\n return 'stop-run';\n }\n if (!answer || answer.kind === 'decision') return { defer: 'external' };\n if (answer.kind === 'failed') {\n // Like a crashing handler: the row hears it, then the run ends on it.\n ctx.emitter({ type: 'tool-failed', toolCallId: call.id, error: answer.error });\n throw new Error(answer.error);\n }\n ctx.emitter({ type: 'tool-resolved', toolCallId: call.id, result: answer.content, structuredResult: answer.structuredContent });\n ctx.declareCall(call);\n ctx.append({ role: 'tool', content: answer.content, toolCallId: call.id, toolName: call.name });\n return 'continue';\n }\n\n const handler = ctx.toolLookup(call.name);\n if (!handler) {\n ctx.emitter({ type: 'tool-aborted', toolCallId: call.id });\n return 'stop-run';\n }\n\n let effectiveInput = call.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n const gated = typeof cfg?.needsApproval === 'function'\n ? cfg.needsApproval({ id: call.id, name: call.name, input })\n : cfg?.needsApproval;\n if (gated) {\n // A decision a resumed run was handed answers the gate: nobody is asked again.\n // Never a `'deny'`: that one is decided here, and an answer that came back with\n // the resume — the page's, for a call queued behind a pause — cannot overrule it.\n const provided = answer?.kind === 'decision' && gated !== 'deny' ? answer : undefined;\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' && !provided) {\n ctx.emitter({ type: 'tool-awaiting-approval', toolCallId: call.id, name: call.name, input: call.input });\n }\n // Nobody here can be asked (the run is not in the page): the call waits for a\n // decision a later run will carry. A `'deny'` is never deferred — it is\n // already decided, and refused below as without `deferral`.\n if (!provided && ctx.deferral && gated !== 'deny') return { defer: 'approval' };\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 (!provided && !ctx.approvalResolver) {\n ctx.emitter({ type: 'tool-aborted', toolCallId: call.id });\n return 'stop-run';\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<StreamApprovalResolver>> | undefined = provided;\n if (!decision && !signal.aborted && ctx.approvalResolver) {\n try {\n decision = await ctx.approvalResolver(\n { id: call.id, name: call.name, input },\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 ctx.emitter({ type: 'tool-aborted', toolCallId: call.id });\n return 'stop-run';\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 ctx.emitter({ type: 'tool-rejected', toolCallId: call.id, reason: rejection });\n ctx.declareCall(call);\n ctx.append({ role: 'tool', content: rejection, toolCallId: call.id, toolName: call.name });\n /*\n * `'stop-turn'`, not `'stop-run'`, and the asymmetry is the point.\n * 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 return 'stop-turn';\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...input, ...(decision.payload as Record<string, unknown>) };\n }\n ctx.emitter({ type: 'tool-approved', toolCallId: call.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: call.id, name: call.name, input: effectiveInput },\n signal,\n ctx.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 ctx.emitter({ type: 'tool-aborted', toolCallId: call.id });\n return 'stop-run';\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 ctx.emitter({ type: 'tool-failed', toolCallId: call.id, error: errorText(outcome.error) });\n throw outcome.error;\n } else {\n ctx.emitter({ type: 'tool-resolved', toolCallId: call.id, result: outcome.content, structuredResult: outcome.structuredContent });\n ctx.declareCall(call);\n ctx.append({ role: 'tool', content: outcome.content, toolCallId: call.id, toolName: call.name });\n }\n return 'continue';\n}\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.
|
|
3
|
+
"version": "0.18.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",
|