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