@gaunt-sloth/core 2.0.0-beta.2 → 2.0.0-beta.4
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/dist/config/shell-policy.d.ts +2 -2
- package/dist/config/shell-policy.js +3 -3
- package/dist/config/tool-descriptions.d.ts +5 -5
- package/dist/config/tool-descriptions.js +3 -3
- package/dist/core/GthAbstractAgent.d.ts +67 -1
- package/dist/core/GthAbstractAgent.js +248 -4
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +91 -10
- package/dist/core/GthAgentRunner.js +327 -21
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +3 -2
- package/dist/core/GthLangChainAgent.js +25 -5
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/reasoningBlocks.d.ts +4 -6
- package/dist/core/reasoningBlocks.js +4 -6
- package/dist/core/reasoningBlocks.js.map +1 -1
- package/dist/core/refusal.d.ts +54 -1
- package/dist/core/refusal.js +109 -1
- package/dist/core/refusal.js.map +1 -1
- package/dist/core/shell/ShellCommandFailedError.d.ts +4 -4
- package/dist/core/shell/ShellCommandFailedError.js +4 -4
- package/dist/core/shell/openWorld.d.ts +4 -3
- package/dist/core/shell/openWorld.js +4 -3
- package/dist/core/shell/openWorld.js.map +1 -1
- package/dist/core/terminationNotice.d.ts +111 -0
- package/dist/core/terminationNotice.js +209 -0
- package/dist/core/terminationNotice.js.map +1 -0
- package/dist/core/terminationReason.d.ts +271 -0
- package/dist/core/terminationReason.js +405 -0
- package/dist/core/terminationReason.js.map +1 -0
- package/dist/core/toolDisplay.d.ts +49 -2
- package/dist/core/toolDisplay.js +137 -19
- package/dist/core/toolDisplay.js.map +1 -1
- package/dist/core/types.d.ts +33 -6
- package/dist/core/types.js.map +1 -1
- package/dist/providers/geminiSchemaSanitizer.d.ts +2 -2
- package/dist/providers/geminiSchemaSanitizer.js +2 -2
- package/dist/providers/geminiThinking.d.ts +10 -5
- package/dist/providers/geminiThinking.js +10 -5
- package/dist/providers/geminiThinking.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +22 -0
- package/dist/runtime/askStructured.js +53 -0
- package/dist/runtime/askStructured.js.map +1 -1
- package/dist/runtime/conversation.d.ts +11 -0
- package/dist/runtime/conversation.js +13 -1
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.d.ts +11 -0
- package/dist/runtime/singleShot.js +21 -1
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/aiignoreUtils.d.ts +6 -0
- package/dist/utils/aiignoreUtils.js +69 -1
- package/dist/utils/aiignoreUtils.js.map +1 -1
- package/dist/utils/debugDump.d.ts +54 -0
- package/dist/utils/debugDump.js +32 -0
- package/dist/utils/debugDump.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +10 -5
- package/dist/utils/displayWidth.js +148 -54
- package/dist/utils/displayWidth.js.map +1 -1
- package/dist/utils/llmUtils.d.ts +1 -1
- package/dist/utils/llmUtils.js +1 -1
- package/dist/utils/systemPromptNotes.d.ts +3 -3
- package/dist/utils/systemPromptNotes.js +3 -3
- package/dist/utils/vertexaiUtils.js +100 -1
- package/dist/utils/vertexaiUtils.js.map +1 -1
- package/package.json +4 -3
|
@@ -591,7 +591,7 @@ export declare function isToolGatedAtRung(options: {
|
|
|
591
591
|
* summary (§4.4) are both built from this, so neither can tell the model a tool is free while the
|
|
592
592
|
* gate escalates it.
|
|
593
593
|
*
|
|
594
|
-
* **It is NOT what the
|
|
594
|
+
* **It is NOT what the agent wires into the interrupt.** That is
|
|
595
595
|
* {@link resolveInterruptToolNames}, and the two are different sets on purpose: the interrupt is
|
|
596
596
|
* installed once, at agent init, while `/approvals <rung>` moves the rung underneath it for the rest
|
|
597
597
|
* of the session. A set that carried the rung would be frozen at the rung the session started on —
|
|
@@ -626,7 +626,7 @@ export declare function resolveGatedToolNames(options: {
|
|
|
626
626
|
*/
|
|
627
627
|
export declare function commandAnswersApprovals(command: GthCommand | undefined): boolean;
|
|
628
628
|
/**
|
|
629
|
-
* **The interrupt set: which tool names the
|
|
629
|
+
* **The interrupt set: which tool names the agent wires into the approval interrupt.** Every bound
|
|
630
630
|
* tool that ANY rung could gate — the union of {@link resolveGatedToolNames} over
|
|
631
631
|
* {@link APPROVAL_RUNGS}, which in practice is the shell plus every bound tool that is not a
|
|
632
632
|
* built-in READ tool.
|
|
@@ -660,7 +660,7 @@ export function isToolGatedAtRung(options) {
|
|
|
660
660
|
* summary (§4.4) are both built from this, so neither can tell the model a tool is free while the
|
|
661
661
|
* gate escalates it.
|
|
662
662
|
*
|
|
663
|
-
* **It is NOT what the
|
|
663
|
+
* **It is NOT what the agent wires into the interrupt.** That is
|
|
664
664
|
* {@link resolveInterruptToolNames}, and the two are different sets on purpose: the interrupt is
|
|
665
665
|
* installed once, at agent init, while `/approvals <rung>` moves the rung underneath it for the rest
|
|
666
666
|
* of the session. A set that carried the rung would be frozen at the rung the session started on —
|
|
@@ -695,7 +695,7 @@ export function resolveGatedToolNames(options) {
|
|
|
695
695
|
* unanswered. Measured, not inferred.
|
|
696
696
|
*
|
|
697
697
|
* So a command that answers nothing must be handed **no approval interrupt beyond what the shell
|
|
698
|
-
* gate itself requires** — see the `interruptTools` wiring in
|
|
698
|
+
* gate itself requires** — see the `interruptTools` wiring in the agent. In particular it must
|
|
699
699
|
* NOT be handed the LIVE set for its configured rung: that set is non-empty at `manual` and
|
|
700
700
|
* `write`, so it carries exactly the same trap, and `commands.api.approvals` (plus a root-level
|
|
701
701
|
* `approvals`, which applies to every command) puts an ordinary config on those rungs.
|
|
@@ -735,7 +735,7 @@ export function commandAnswersApprovals(command) {
|
|
|
735
735
|
return command === undefined ? true : (COMMAND_ANSWERS_APPROVALS[command] ?? true);
|
|
736
736
|
}
|
|
737
737
|
/**
|
|
738
|
-
* **The interrupt set: which tool names the
|
|
738
|
+
* **The interrupt set: which tool names the agent wires into the approval interrupt.** Every bound
|
|
739
739
|
* tool that ANY rung could gate — the union of {@link resolveGatedToolNames} over
|
|
740
740
|
* {@link APPROVAL_RUNGS}, which in practice is the shell plus every bound tool that is not a
|
|
741
741
|
* built-in READ tool.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 2. **The suffix is derived from the rung in force AND from the set of tools that rung actually
|
|
18
18
|
* gates.** "A description that disagrees with what the gate will actually do is worse than no
|
|
19
19
|
* description at all" (§4.5), so {@link isGrantedAtRung} takes the LIVE gated set as a parameter
|
|
20
|
-
* and
|
|
20
|
+
* and the agent passes the SAME `resolveGatedToolNames` result. The two can therefore not
|
|
21
21
|
* drift: widening what a rung gates ([[EXT-30]]) widens the descriptions in the same breath.
|
|
22
22
|
* That set is narrower than the rung-independent one wired into the interrupt
|
|
23
23
|
* (`resolveInterruptToolNames`), because a call the live rung does not gate is approved on
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* **Scope, per §4.3.** The *rater* covers the shell only: every other gated tool goes to the human
|
|
27
27
|
* without a rating call until [[EXT-30]] widens the rater. The *gate* is wider than the rater. At
|
|
28
|
-
* the two deterministic rungs (`manual`, `write`)
|
|
28
|
+
* the two deterministic rungs (`manual`, `write`) the agent gates every bound tool the rung's
|
|
29
29
|
* access class does not auto-grant — the write built-ins, the shell, MCP tools (whatever their
|
|
30
30
|
* `readOnlyHint` says) and custom tools — so at those rungs a non-granted call always reaches the
|
|
31
31
|
* human. At `assisted`, `auto` and `bypass` the gated set is the shell alone.
|
|
@@ -132,7 +132,7 @@ export declare function getRungToolDescriptionSuffix(rung: ApprovalRung): string
|
|
|
132
132
|
*
|
|
133
133
|
* **This is the one implementation of that rule.** {@link isGrantedAtRung} decides a grant with it,
|
|
134
134
|
* and `config/shell-policy.ts`'s `isToolGatedAtRung` decides gated-set membership with it — which
|
|
135
|
-
* is in turn what the
|
|
135
|
+
* is in turn what the agent's interrupt set and `GthAgentRunner`'s own live-rung check are built
|
|
136
136
|
* from. So the gate cannot escalate a call the descriptions and the rater's granted list call free
|
|
137
137
|
* — the drift §4.5 names as worse than having no description at all. Two derivations of one rule is
|
|
138
138
|
* exactly what this function exists to prevent; do not inline it back into either caller.
|
|
@@ -150,7 +150,7 @@ export declare function isAccessClassGrantedAtRung(toolName: string, rung: Appro
|
|
|
150
150
|
* `resolveGatedToolNames` for the rung in force. **This is the parameter that keeps the
|
|
151
151
|
* descriptions honest**: a tool the rung does not gate cannot require approval, whatever a rung's
|
|
152
152
|
* table row says about tool classes, so it is granted. It is deliberately NOT the wider,
|
|
153
|
-
* rung-independent set the
|
|
153
|
+
* rung-independent set the agent wires into `interruptOn` (`resolveInterruptToolNames`): a call
|
|
154
154
|
* the live rung does not gate is auto-approved the moment it arrives at the runner, so describing
|
|
155
155
|
* it as needing approval would be a promise nothing keeps.
|
|
156
156
|
*
|
|
@@ -182,7 +182,7 @@ export interface DescribableTool {
|
|
|
182
182
|
* that is NOT auto-approved at that rung, and leave every granted tool's description untouched.
|
|
183
183
|
*
|
|
184
184
|
* Mutates in place (and returns the same array): the tools are about to be handed to
|
|
185
|
-
* `createAgent
|
|
185
|
+
* `createAgent`, and the array is freshly resolved per init. Idempotent — any
|
|
186
186
|
* previously-appended suffix is stripped first, so re-registration at a different rung replaces
|
|
187
187
|
* the sentence rather than stacking one.
|
|
188
188
|
*
|
|
@@ -142,7 +142,7 @@ export function getRungToolDescriptionSuffix(rung) {
|
|
|
142
142
|
*
|
|
143
143
|
* **This is the one implementation of that rule.** {@link isGrantedAtRung} decides a grant with it,
|
|
144
144
|
* and `config/shell-policy.ts`'s `isToolGatedAtRung` decides gated-set membership with it — which
|
|
145
|
-
* is in turn what the
|
|
145
|
+
* is in turn what the agent's interrupt set and `GthAgentRunner`'s own live-rung check are built
|
|
146
146
|
* from. So the gate cannot escalate a call the descriptions and the rater's granted list call free
|
|
147
147
|
* — the drift §4.5 names as worse than having no description at all. Two derivations of one rule is
|
|
148
148
|
* exactly what this function exists to prevent; do not inline it back into either caller.
|
|
@@ -167,7 +167,7 @@ export function isAccessClassGrantedAtRung(toolName, rung) {
|
|
|
167
167
|
* `resolveGatedToolNames` for the rung in force. **This is the parameter that keeps the
|
|
168
168
|
* descriptions honest**: a tool the rung does not gate cannot require approval, whatever a rung's
|
|
169
169
|
* table row says about tool classes, so it is granted. It is deliberately NOT the wider,
|
|
170
|
-
* rung-independent set the
|
|
170
|
+
* rung-independent set the agent wires into `interruptOn` (`resolveInterruptToolNames`): a call
|
|
171
171
|
* the live rung does not gate is auto-approved the moment it arrives at the runner, so describing
|
|
172
172
|
* it as needing approval would be a promise nothing keeps.
|
|
173
173
|
*
|
|
@@ -214,7 +214,7 @@ export function stripRungToolDescriptionSuffix(description) {
|
|
|
214
214
|
* that is NOT auto-approved at that rung, and leave every granted tool's description untouched.
|
|
215
215
|
*
|
|
216
216
|
* Mutates in place (and returns the same array): the tools are about to be handed to
|
|
217
|
-
* `createAgent
|
|
217
|
+
* `createAgent`, and the array is freshly resolved per init. Idempotent — any
|
|
218
218
|
* previously-appended suffix is stripped first, so re-registration at a different rung replaces
|
|
219
219
|
* the sentence rather than stacking one.
|
|
220
220
|
*
|
|
@@ -9,6 +9,7 @@ import { BaseToolkit, StructuredToolInterface } from '@langchain/core/tools';
|
|
|
9
9
|
import { IterableReadableStream } from '@langchain/core/utils/stream';
|
|
10
10
|
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
11
11
|
import { type RefusalInfo } from '#src/core/refusal.js';
|
|
12
|
+
import { type GthFinishReasonObservation, type GthTerminationReason, type GthTerminationSite } from '#src/core/terminationReason.js';
|
|
12
13
|
/**
|
|
13
14
|
* Shared, graph-agnostic agent plumbing.
|
|
14
15
|
*
|
|
@@ -70,6 +71,19 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
70
71
|
* fully fail-soft (accumulation is guarded and never throws into a run).
|
|
71
72
|
*/
|
|
72
73
|
private runStatsAcc;
|
|
74
|
+
/**
|
|
75
|
+
* [[EXT-159]] — why the current turn ended, set by whichever site inside this agent ended it.
|
|
76
|
+
* Reset at each turn boundary by {@link resetTerminationReason}, read by `GthAgentRunner`.
|
|
77
|
+
*/
|
|
78
|
+
private terminationReason;
|
|
79
|
+
/**
|
|
80
|
+
* [[EXT-159]] — what the provider said about why each finished model message stopped, this turn.
|
|
81
|
+
*
|
|
82
|
+
* Reset with {@link terminationReason} at each turn boundary and read by `GthAgentRunner` for the
|
|
83
|
+
* debug dump. Bounded, because a long agentic turn produces one entry per model round and this
|
|
84
|
+
* has to be safe to keep for the whole session.
|
|
85
|
+
*/
|
|
86
|
+
private finishReasonObservations;
|
|
73
87
|
/**
|
|
74
88
|
* EXT-58 — the names of the tools registered with the graph at the last {@link init}, recorded by
|
|
75
89
|
* {@link registerApprovalsAwareTools}. Read by `GthAgentRunner` to build the rater's
|
|
@@ -132,7 +146,7 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
132
146
|
*/
|
|
133
147
|
protected compactHeaderStatus(): void;
|
|
134
148
|
/**
|
|
135
|
-
* EXT-58 (spec §4.5) — the ONE tool-registration hook
|
|
149
|
+
* EXT-58 (spec §4.5) — the ONE tool-registration hook every backend calls with its final tool
|
|
136
150
|
* array, just before handing it to the graph builder. It does two things:
|
|
137
151
|
*
|
|
138
152
|
* 1. Appends the rung's approval sentence to every tool that is **not** auto-approved at that
|
|
@@ -189,6 +203,51 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
189
203
|
getRunStats(): GthRunStats;
|
|
190
204
|
/** GS2-16 — fold one message (or chunk) into the run tally. Fully guarded (fail-soft). */
|
|
191
205
|
protected recordRunStats(message: unknown): void;
|
|
206
|
+
/**
|
|
207
|
+
* [[EXT-159]] — forget the previous turn's termination reason. Called by `GthAgentRunner` at each
|
|
208
|
+
* turn boundary, for the same reason {@link resetRunStats} is.
|
|
209
|
+
*/
|
|
210
|
+
resetTerminationReason(): void;
|
|
211
|
+
/** [[EXT-159]] — why this turn ended, or `null` when no site inside this agent classified it. */
|
|
212
|
+
getTerminationReason(): GthTerminationReason | null;
|
|
213
|
+
/**
|
|
214
|
+
* [[EXT-159]] — what the provider said about why each model message stopped, this turn.
|
|
215
|
+
*
|
|
216
|
+
* A copy, so a reader (the debug dump) cannot mutate the live record. An EMPTY list is itself
|
|
217
|
+
* meaningful and is not the same as a list of absences: it says no finished model message was
|
|
218
|
+
* observed at all, where an entry with a `null` token says one was observed and the provider said
|
|
219
|
+
* nothing.
|
|
220
|
+
*/
|
|
221
|
+
getFinishReasonObservations(): readonly GthFinishReasonObservation[];
|
|
222
|
+
/**
|
|
223
|
+
* [[EXT-159]] — record the provider's stop/finish reason for one finished model message, and
|
|
224
|
+
* write it to the debug log.
|
|
225
|
+
*
|
|
226
|
+
* Called on EVERY finished message rather than only on the interesting ones, because the fact
|
|
227
|
+
* this exists to capture is as much "the provider said nothing" as "the provider said `length`".
|
|
228
|
+
* The debug log is where a maintainer looks first and it carried none of this; the ring buffer
|
|
229
|
+
* behind `debugLog` is populated whether or not on-disk debug logging was ever switched on, so
|
|
230
|
+
* the observation reaches `/debug-dump` from a default install too.
|
|
231
|
+
*
|
|
232
|
+
* Fail-soft, and bounded: a long agentic turn logs one entry per model round.
|
|
233
|
+
*/
|
|
234
|
+
protected noteFinishReason(path: GthFinishReasonObservation['path'], message: unknown): void;
|
|
235
|
+
/**
|
|
236
|
+
* [[EXT-159]] — record why the turn ended, **first-write-wins**.
|
|
237
|
+
*
|
|
238
|
+
* The sites that classify are nested: the metadata reader and the cancellation paths sit inside
|
|
239
|
+
* the stream, which sits inside the runner's catches. Last-write-wins would let each outer layer
|
|
240
|
+
* overwrite the truer inner classification with its own coarser one — which is exactly the funnel
|
|
241
|
+
* this taxonomy replaces, rebuilt one level up. Fail-soft: classification never throws into a run.
|
|
242
|
+
*/
|
|
243
|
+
protected noteTermination(reason: GthTerminationReason): void;
|
|
244
|
+
/**
|
|
245
|
+
* [[EXT-159]] — run the metadata feeder over a finished message and record what it found.
|
|
246
|
+
*
|
|
247
|
+
* The metadata reader is the half of the taxonomy that only works here, at the layer where a
|
|
248
|
+
* message's `response_metadata` is visible. Returns whether anything was classified.
|
|
249
|
+
*/
|
|
250
|
+
protected noteStopMetadata(site: GthTerminationSite, message: unknown): boolean;
|
|
192
251
|
/**
|
|
193
252
|
* GS2-16 — best-effort count of messages already in the checkpointed thread state, used by
|
|
194
253
|
* {@link invoke} as the baseline so it harvests only THIS turn's new messages rather than the
|
|
@@ -253,6 +312,13 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
253
312
|
* the client sends in `forwardedProps.command.resume`.
|
|
254
313
|
*/
|
|
255
314
|
streamWithEventsResume(resumeValue: unknown, runConfig: RunnableConfig, queuedMessages?: BaseMessage[], signal?: AbortSignal): AsyncGenerator<AgentStreamEvent>;
|
|
315
|
+
/**
|
|
316
|
+
* [[EXT-159]] — which of the two clean endings a typed-event stream just took.
|
|
317
|
+
*
|
|
318
|
+
* Shared by {@link streamWithEvents} and {@link streamWithEventsResume} so the pair cannot drift:
|
|
319
|
+
* they catch the same union and owe the same distinction.
|
|
320
|
+
*/
|
|
321
|
+
private classifyEventStreamEnd;
|
|
256
322
|
/**
|
|
257
323
|
* Inspect the checkpointed state for the thread and return the tool calls currently pending
|
|
258
324
|
* human approval (empty array when the run finished normally). A LangGraph
|
|
@@ -12,10 +12,22 @@ import { AIMessage, AIMessageChunk, ToolMessage } from '@langchain/core/messages
|
|
|
12
12
|
import { IterableReadableStream } from '@langchain/core/utils/stream';
|
|
13
13
|
import { interrupt, Command, GraphInterrupt } from '@langchain/langgraph';
|
|
14
14
|
import { extractInlineBinaryBlocks, materializeBinaryOutputs, renderAssistantContent, } from '#src/utils/binaryOutputUtils.js';
|
|
15
|
-
import { detectRefusal, buildRefusalMessage } from '#src/core/refusal.js';
|
|
15
|
+
import { classifyRefusal, detectRefusal, detectStopMetadata, buildRefusalMessage, readStopReasonToken, } from '#src/core/refusal.js';
|
|
16
|
+
import { terminationReason, } from '#src/core/terminationReason.js';
|
|
17
|
+
import { terminationLogLine } from '#src/core/terminationNotice.js';
|
|
16
18
|
import { answerTextOf, segmentAssistantContent, stripReasoningBlocks, } from '#src/core/reasoningBlocks.js';
|
|
17
19
|
const THINK_OPEN = '<think>';
|
|
18
20
|
const THINK_CLOSE = '</think>';
|
|
21
|
+
/**
|
|
22
|
+
* [[EXT-159]] — how many per-message `finish_reason` observations one turn keeps.
|
|
23
|
+
*
|
|
24
|
+
* An agentic turn produces one per model round and the list is held for the life of the turn, so it
|
|
25
|
+
* is bounded rather than trusted to stay small. The cap drops the TAIL rather than the head: the
|
|
26
|
+
* observations that explain how a turn ENDED are the last ones, but a turn that has already made a
|
|
27
|
+
* thousand model rounds has a different problem, and keeping the head keeps the record of how it
|
|
28
|
+
* got there. Every observation reaches the debug log either way, so nothing is lost outright.
|
|
29
|
+
*/
|
|
30
|
+
const FINISH_REASON_OBSERVATION_MAX = 1000;
|
|
19
31
|
/**
|
|
20
32
|
* TUI-C22 — length of the longest suffix of `s` that is a *proper* (shorter-than-full) prefix of
|
|
21
33
|
* `tag`. Used by {@link createThinkTagSplitter} to hold back a trailing partial that might complete
|
|
@@ -259,6 +271,19 @@ export class GthAbstractAgent {
|
|
|
259
271
|
* fully fail-soft (accumulation is guarded and never throws into a run).
|
|
260
272
|
*/
|
|
261
273
|
runStatsAcc = createRunStatsAccumulator();
|
|
274
|
+
/**
|
|
275
|
+
* [[EXT-159]] — why the current turn ended, set by whichever site inside this agent ended it.
|
|
276
|
+
* Reset at each turn boundary by {@link resetTerminationReason}, read by `GthAgentRunner`.
|
|
277
|
+
*/
|
|
278
|
+
terminationReason = null;
|
|
279
|
+
/**
|
|
280
|
+
* [[EXT-159]] — what the provider said about why each finished model message stopped, this turn.
|
|
281
|
+
*
|
|
282
|
+
* Reset with {@link terminationReason} at each turn boundary and read by `GthAgentRunner` for the
|
|
283
|
+
* debug dump. Bounded, because a long agentic turn produces one entry per model round and this
|
|
284
|
+
* has to be safe to keep for the whole session.
|
|
285
|
+
*/
|
|
286
|
+
finishReasonObservations = [];
|
|
262
287
|
/**
|
|
263
288
|
* EXT-58 — the names of the tools registered with the graph at the last {@link init}, recorded by
|
|
264
289
|
* {@link registerApprovalsAwareTools}. Read by `GthAgentRunner` to build the rater's
|
|
@@ -340,7 +365,7 @@ export class GthAbstractAgent {
|
|
|
340
365
|
this.statusUpdate(StatusLevel.INFO, runHeaderLine(command, label));
|
|
341
366
|
}
|
|
342
367
|
/**
|
|
343
|
-
* EXT-58 (spec §4.5) — the ONE tool-registration hook
|
|
368
|
+
* EXT-58 (spec §4.5) — the ONE tool-registration hook every backend calls with its final tool
|
|
344
369
|
* array, just before handing it to the graph builder. It does two things:
|
|
345
370
|
*
|
|
346
371
|
* 1. Appends the rung's approval sentence to every tool that is **not** auto-approved at that
|
|
@@ -416,6 +441,95 @@ export class GthAbstractAgent {
|
|
|
416
441
|
recordRunStats(message) {
|
|
417
442
|
accumulateMessage(this.runStatsAcc, message);
|
|
418
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* [[EXT-159]] — forget the previous turn's termination reason. Called by `GthAgentRunner` at each
|
|
446
|
+
* turn boundary, for the same reason {@link resetRunStats} is.
|
|
447
|
+
*/
|
|
448
|
+
resetTerminationReason() {
|
|
449
|
+
this.terminationReason = null;
|
|
450
|
+
this.finishReasonObservations = [];
|
|
451
|
+
}
|
|
452
|
+
/** [[EXT-159]] — why this turn ended, or `null` when no site inside this agent classified it. */
|
|
453
|
+
getTerminationReason() {
|
|
454
|
+
return this.terminationReason;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* [[EXT-159]] — what the provider said about why each model message stopped, this turn.
|
|
458
|
+
*
|
|
459
|
+
* A copy, so a reader (the debug dump) cannot mutate the live record. An EMPTY list is itself
|
|
460
|
+
* meaningful and is not the same as a list of absences: it says no finished model message was
|
|
461
|
+
* observed at all, where an entry with a `null` token says one was observed and the provider said
|
|
462
|
+
* nothing.
|
|
463
|
+
*/
|
|
464
|
+
getFinishReasonObservations() {
|
|
465
|
+
return [...this.finishReasonObservations];
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* [[EXT-159]] — record the provider's stop/finish reason for one finished model message, and
|
|
469
|
+
* write it to the debug log.
|
|
470
|
+
*
|
|
471
|
+
* Called on EVERY finished message rather than only on the interesting ones, because the fact
|
|
472
|
+
* this exists to capture is as much "the provider said nothing" as "the provider said `length`".
|
|
473
|
+
* The debug log is where a maintainer looks first and it carried none of this; the ring buffer
|
|
474
|
+
* behind `debugLog` is populated whether or not on-disk debug logging was ever switched on, so
|
|
475
|
+
* the observation reaches `/debug-dump` from a default install too.
|
|
476
|
+
*
|
|
477
|
+
* Fail-soft, and bounded: a long agentic turn logs one entry per model round.
|
|
478
|
+
*/
|
|
479
|
+
noteFinishReason(path, message) {
|
|
480
|
+
try {
|
|
481
|
+
// No message means no observation — NOT an observation of absence. A turn whose stream
|
|
482
|
+
// produced nothing to aggregate has no provider statement to be missing, and recording a
|
|
483
|
+
// `null` token for it would make "the provider said nothing about this message" and "there
|
|
484
|
+
// was no message" read the same, which is the conflation this whole node is about.
|
|
485
|
+
if (!message || typeof message !== 'object')
|
|
486
|
+
return;
|
|
487
|
+
const token = readStopReasonToken(message);
|
|
488
|
+
if (this.finishReasonObservations.length < FINISH_REASON_OBSERVATION_MAX) {
|
|
489
|
+
this.finishReasonObservations.push({ at: new Date().toISOString(), path, token });
|
|
490
|
+
}
|
|
491
|
+
debugLog(`EXT-159 finish_reason path=${path} token=${token ?? '<absent>'}`);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
/* fail-soft: observing why a message stopped must never be what stops a run */
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* [[EXT-159]] — record why the turn ended, **first-write-wins**.
|
|
499
|
+
*
|
|
500
|
+
* The sites that classify are nested: the metadata reader and the cancellation paths sit inside
|
|
501
|
+
* the stream, which sits inside the runner's catches. Last-write-wins would let each outer layer
|
|
502
|
+
* overwrite the truer inner classification with its own coarser one — which is exactly the funnel
|
|
503
|
+
* this taxonomy replaces, rebuilt one level up. Fail-soft: classification never throws into a run.
|
|
504
|
+
*/
|
|
505
|
+
noteTermination(reason) {
|
|
506
|
+
try {
|
|
507
|
+
if (this.terminationReason)
|
|
508
|
+
return;
|
|
509
|
+
this.terminationReason = reason;
|
|
510
|
+
// [[EXT-159]] — written where the reason is DECIDED, not where a surface later reads it. The
|
|
511
|
+
// terminating error reached only `transcript.json` and never the debug log, so the artifact a
|
|
512
|
+
// maintainer opens first could not say why a run ended; logging at the decision means the
|
|
513
|
+
// line exists even for a run whose surface never got to ask.
|
|
514
|
+
debugLog(terminationLogLine(reason));
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
/* fail-soft: recording why a run ended must never be what ends it */
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* [[EXT-159]] — run the metadata feeder over a finished message and record what it found.
|
|
522
|
+
*
|
|
523
|
+
* The metadata reader is the half of the taxonomy that only works here, at the layer where a
|
|
524
|
+
* message's `response_metadata` is visible. Returns whether anything was classified.
|
|
525
|
+
*/
|
|
526
|
+
noteStopMetadata(site, message) {
|
|
527
|
+
const stop = detectStopMetadata(message);
|
|
528
|
+
if (!stop)
|
|
529
|
+
return false;
|
|
530
|
+
this.noteTermination(terminationReason(site, 'metadata', stop));
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
419
533
|
/**
|
|
420
534
|
* GS2-16 — best-effort count of messages already in the checkpointed thread state, used by
|
|
421
535
|
* {@link invoke} as the baseline so it harvests only THIS turn's new messages rather than the
|
|
@@ -498,6 +612,15 @@ export class GthAbstractAgent {
|
|
|
498
612
|
// (if declined) response as "Failed to get answer". A fallback-model attempt would hang
|
|
499
613
|
// here (see the extension point in GthAgentRunner.processMessages), but no runtime
|
|
500
614
|
// fallback-model config exists today, so we surface terminally.
|
|
615
|
+
// [[EXT-159]] — the metadata feeder, at the one layer that can see `response_metadata`.
|
|
616
|
+
// Records why the turn ended (a refusal, or an answer cut off against the output cap)
|
|
617
|
+
// before the refusal branch below decides what to SAY about it; a truncation says nothing
|
|
618
|
+
// and is classification only.
|
|
619
|
+
// [[EXT-159]] — and record what the provider said about why the message stopped, or that it
|
|
620
|
+
// said nothing. Separate from the classification above: the metadata feeder speaks only for
|
|
621
|
+
// a refusal or a truncation, while this is the raw fact, kept for every ending.
|
|
622
|
+
this.noteFinishReason('invoke', finalMessage);
|
|
623
|
+
this.noteStopMetadata('agent.invoke-stop-metadata', finalMessage);
|
|
501
624
|
const refusal = detectRefusal(finalMessage);
|
|
502
625
|
if (refusal) {
|
|
503
626
|
return this.surfaceRefusal(refusal);
|
|
@@ -541,6 +664,13 @@ export class GthAbstractAgent {
|
|
|
541
664
|
debugLogError('invoke outer', error);
|
|
542
665
|
if (error instanceof Error) {
|
|
543
666
|
if (error?.name === 'ToolException') {
|
|
667
|
+
// [[EXT-159]] — a tool failure that becomes the turn's ANSWER ends the turn as much as a
|
|
668
|
+
// throw does, and it is the one ending whose diagnostic string is returned rather than
|
|
669
|
+
// raised, so nothing further up ever sees an error to classify.
|
|
670
|
+
this.noteTermination(terminationReason('agent.invoke-tool-exception', 'exception', {
|
|
671
|
+
category: 'tool_error',
|
|
672
|
+
detail: error.name,
|
|
673
|
+
}));
|
|
544
674
|
this.statusUpdate(StatusLevel.ERROR, `Tool execution failed: ${error?.message}`);
|
|
545
675
|
return `Tool execution failed: ${error?.message}`;
|
|
546
676
|
}
|
|
@@ -590,6 +720,11 @@ export class GthAbstractAgent {
|
|
|
590
720
|
// EXT-37: bound so the stream `start()` closure can surface a detected refusal (WARNING +
|
|
591
721
|
// returns the message to enqueue) without a `this` reference.
|
|
592
722
|
const surfaceRefusal = (info) => this.surfaceRefusal(info);
|
|
723
|
+
// [[EXT-159]] — bound for the same reason as the two above: the stream `start()` closure's
|
|
724
|
+
// `this` is the stream source, not the agent, and the sites that end this turn live in it.
|
|
725
|
+
const noteTermination = (reason) => this.noteTermination(reason);
|
|
726
|
+
const noteStopMetadata = (site, message) => this.noteStopMetadata(site, message);
|
|
727
|
+
const noteFinishReason = (path, message) => this.noteFinishReason(path, message);
|
|
593
728
|
// TUI-C30 — compact per-tool-call indication for the plain surface (`name(args…)` + the
|
|
594
729
|
// canonical 10-line greyed preview when each ToolMessage lands). Per-stream state; emits at
|
|
595
730
|
// INFO level so the existing consoleLevel gate governs it like the historical tool notices.
|
|
@@ -725,14 +860,33 @@ export class GthAbstractAgent {
|
|
|
725
860
|
// (so the drained result is non-empty and bypasses the empty-response retry) and print it
|
|
726
861
|
// once at WARNING level (surfaceRefusal). Any partial content already streamed is kept;
|
|
727
862
|
// the refusal notice follows it, and its explanation carries any model-provided text.
|
|
863
|
+
// [[EXT-159]] — the provider's own word on why this message stopped, recorded before
|
|
864
|
+
// either branch decides what to make of it, so the raw fact is kept on the refusal path
|
|
865
|
+
// as well as the ordinary one.
|
|
866
|
+
noteFinishReason('stream', aggregatedChunk);
|
|
728
867
|
if (refusalInfo) {
|
|
868
|
+
// [[EXT-159]] — classified through `classifyRefusal` rather than mapped here, so this
|
|
869
|
+
// site and the metadata feeder cannot come to disagree about what a refusal is.
|
|
870
|
+
noteTermination(terminationReason('agent.stream-stop-metadata', 'metadata', classifyRefusal(refusalInfo)));
|
|
729
871
|
controller.enqueue(surfaceRefusal(refusalInfo));
|
|
730
872
|
}
|
|
873
|
+
else if (aggregatedChunk) {
|
|
874
|
+
// No refusal: the same reader still has something to say about an answer that was cut
|
|
875
|
+
// off against the output cap. Classification only — nothing is surfaced.
|
|
876
|
+
noteStopMetadata('agent.stream-stop-metadata', aggregatedChunk);
|
|
877
|
+
}
|
|
731
878
|
debugLog(`Stream completed. Total chunks: ${totalChunks}`);
|
|
732
879
|
controller.close();
|
|
733
880
|
}
|
|
734
881
|
catch (error) {
|
|
735
882
|
if (interruptState.escape || (error instanceof Error && error.name === 'AbortError')) {
|
|
883
|
+
// [[EXT-159]] — Esc, or a caller's abort. This turn ends with no error reaching the
|
|
884
|
+
// runner at all (the stream is CLOSED, not errored), so if this site does not classify
|
|
885
|
+
// it nothing downstream can: it is indistinguishable from a turn that simply finished.
|
|
886
|
+
noteTermination(terminationReason('agent.stream-cancelled', 'control', {
|
|
887
|
+
category: 'cancelled',
|
|
888
|
+
detail: interruptState.escape ? 'escape' : 'AbortError',
|
|
889
|
+
}));
|
|
736
890
|
showInterruptMessage();
|
|
737
891
|
controller.close();
|
|
738
892
|
}
|
|
@@ -787,6 +941,11 @@ export class GthAbstractAgent {
|
|
|
787
941
|
if (e instanceof GraphInterrupt ||
|
|
788
942
|
e.name === 'GraphInterrupt' ||
|
|
789
943
|
e.name === 'AbortError') {
|
|
944
|
+
// [[EXT-159]] — two different endings share this branch and must not share a reason. A
|
|
945
|
+
// suspend is not a failure at all (the run is parked on an `interrupt()` and continues on
|
|
946
|
+
// a resume); an abort is the user stopping it. Both return CLEANLY, so this is the last
|
|
947
|
+
// point at which either is knowable.
|
|
948
|
+
this.noteTermination(this.classifyEventStreamEnd('agent.events-ended', e));
|
|
790
949
|
debugLog('Graph suspended (GraphInterrupt) or aborted by caller');
|
|
791
950
|
return;
|
|
792
951
|
}
|
|
@@ -827,12 +986,27 @@ export class GthAbstractAgent {
|
|
|
827
986
|
if (e instanceof GraphInterrupt ||
|
|
828
987
|
e.name === 'GraphInterrupt' ||
|
|
829
988
|
e.name === 'AbortError') {
|
|
989
|
+
// [[EXT-159]] — the resume path's twin of the same two endings; see {@link streamWithEvents}.
|
|
990
|
+
this.noteTermination(this.classifyEventStreamEnd('agent.events-resume-ended', e));
|
|
830
991
|
debugLog('Graph suspended (GraphInterrupt) or aborted by caller');
|
|
831
992
|
return;
|
|
832
993
|
}
|
|
833
994
|
throw e;
|
|
834
995
|
}
|
|
835
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* [[EXT-159]] — which of the two clean endings a typed-event stream just took.
|
|
999
|
+
*
|
|
1000
|
+
* Shared by {@link streamWithEvents} and {@link streamWithEventsResume} so the pair cannot drift:
|
|
1001
|
+
* they catch the same union and owe the same distinction.
|
|
1002
|
+
*/
|
|
1003
|
+
classifyEventStreamEnd(site, error) {
|
|
1004
|
+
const suspended = error instanceof GraphInterrupt || error?.name === 'GraphInterrupt';
|
|
1005
|
+
return terminationReason(site, suspended ? 'control' : 'exception', {
|
|
1006
|
+
category: suspended ? 'suspended' : 'cancelled',
|
|
1007
|
+
detail: suspended ? 'GraphInterrupt' : 'AbortError',
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
836
1010
|
/**
|
|
837
1011
|
* Inspect the checkpointed state for the thread and return the tool calls currently pending
|
|
838
1012
|
* human approval (empty array when the run finished normally). A LangGraph
|
|
@@ -930,6 +1104,23 @@ export class GthAbstractAgent {
|
|
|
930
1104
|
let aggregatedAIChunk = null;
|
|
931
1105
|
let reasoningOpen = false;
|
|
932
1106
|
const flushed = new Set();
|
|
1107
|
+
/**
|
|
1108
|
+
* [[TUI-C100]] — the calls THIS stream has announced (`tool_start` + `tool_args`) and whose
|
|
1109
|
+
* own result it has not yet seen.
|
|
1110
|
+
*
|
|
1111
|
+
* Kept here rather than derived from {@link aggregatedAIChunk} because the aggregate is nulled
|
|
1112
|
+
* after every round (see the reset below) while the calls it announced can still be waiting —
|
|
1113
|
+
* at the approval gate, or simply behind a sibling in the same message. A set that died with
|
|
1114
|
+
* the aggregate would lose exactly the calls this exists to track.
|
|
1115
|
+
*
|
|
1116
|
+
* **Scoped to one invocation, deliberately.** A call held at the approval gate is announced in
|
|
1117
|
+
* the stream that suspends and resolved in the stream that resumes, so its result lands where
|
|
1118
|
+
* this set is empty — and it therefore never receives a `tool_end` at all. That is the correct
|
|
1119
|
+
* outcome, not a leak: a stream may only speak for what it announced. What closes a call that
|
|
1120
|
+
* produces no result anywhere is the turn-level drain in `GthAgentRunner`'s
|
|
1121
|
+
* `processMessagesWithEvents`, which spans both streams.
|
|
1122
|
+
*/
|
|
1123
|
+
const pendingEnds = new Set();
|
|
933
1124
|
// EXT-41: a content-policy refusal on this typed-event path was de-scoped by EXT-37 (there is
|
|
934
1125
|
// no empty-response retry here, so no wrong-retry bug), but it still rendered as a SILENT empty
|
|
935
1126
|
// turn. Capture it (first per-chunk signal wins; aggregate fallback at stream end) and surface
|
|
@@ -978,6 +1169,18 @@ export class GthAbstractAgent {
|
|
|
978
1169
|
}
|
|
979
1170
|
}
|
|
980
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Announce every call the assistant message carries, once its arguments are complete.
|
|
1174
|
+
*
|
|
1175
|
+
* [[TUI-C100]] — **announcing a call is not ending it.** This runs when the round's FIRST
|
|
1176
|
+
* `ToolMessage` lands, because that is what makes the streamed aggregate complete, so it speaks
|
|
1177
|
+
* for every call in the message at once: the one that just returned, the ones still queued, and
|
|
1178
|
+
* the one suspended at the approval gate with a human being asked about it. `tool_start` and
|
|
1179
|
+
* `tool_args` are true for all of them — the model has finished emitting the arguments — and
|
|
1180
|
+
* that is what puts a call on screen, with the arguments the human is about to rule on.
|
|
1181
|
+
* `tool_end` is not: it waits for the call's own result, and is emitted beside it when that
|
|
1182
|
+
* result reaches THIS stream — which for a gated call it never does. See `pendingEnds` above.
|
|
1183
|
+
*/
|
|
981
1184
|
function* flushAggregated() {
|
|
982
1185
|
if (!aggregatedAIChunk)
|
|
983
1186
|
return;
|
|
@@ -990,10 +1193,15 @@ export class GthAbstractAgent {
|
|
|
990
1193
|
flushed.add(id);
|
|
991
1194
|
yield { type: 'tool_start', id, name: tc.name };
|
|
992
1195
|
yield { type: 'tool_args', id, delta: JSON.stringify(tc.args ?? {}) };
|
|
993
|
-
|
|
1196
|
+
pendingEnds.add(id);
|
|
994
1197
|
}
|
|
995
1198
|
// Surface invalid tool calls too so the client at least sees the raw args
|
|
996
1199
|
// string the model produced, instead of silently dropping them.
|
|
1200
|
+
//
|
|
1201
|
+
// These END INLINE, unlike the calls above, and the asymmetry is the point: an invalid call
|
|
1202
|
+
// is never dispatched, so there is no result to wait for and nothing that could later change
|
|
1203
|
+
// what it says. Deferring it would leave a row that cannot progress sitting at `running`
|
|
1204
|
+
// until the turn ended, in exchange for nothing.
|
|
997
1205
|
for (const tc of invalidToolCalls) {
|
|
998
1206
|
const id = tc.id;
|
|
999
1207
|
if (!id || flushed.has(id))
|
|
@@ -1090,6 +1298,20 @@ export class GthAbstractAgent {
|
|
|
1090
1298
|
// `isError` still says the call did not run, which is what the model, `gth eval`'s
|
|
1091
1299
|
// tool-result assertions and the ACP bridge all read.
|
|
1092
1300
|
const toolCallId = chunk.tool_call_id;
|
|
1301
|
+
// [[TUI-C100]] — the call's own result is here, so NOW it has ended.
|
|
1302
|
+
//
|
|
1303
|
+
// **The guard is the stream's own honesty, not a favour to a downstream reader.** This
|
|
1304
|
+
// generator may only close a call it opened, and the id it is holding a result for is
|
|
1305
|
+
// routinely one it never announced: a gated call is announced in the stream that suspends
|
|
1306
|
+
// and returns in the stream that resumes, where this set is empty. So the common gated flow
|
|
1307
|
+
// is `tool_start`, `tool_args`, then a `tool_result` in a later stream with **no `tool_end`
|
|
1308
|
+
// between them, and none at all** — do not read the line below as a promise that an end
|
|
1309
|
+
// always precedes a result. Every consumer treats `tool_result` as terminal on its own, and
|
|
1310
|
+
// the AG-UI bridge additionally closes its own framing from what it actually emitted rather
|
|
1311
|
+
// than trusting this event to arrive.
|
|
1312
|
+
if (pendingEnds.delete(toolCallId)) {
|
|
1313
|
+
yield { type: 'tool_end', id: toolCallId };
|
|
1314
|
+
}
|
|
1093
1315
|
yield {
|
|
1094
1316
|
type: 'tool_result',
|
|
1095
1317
|
id: toolCallId,
|
|
@@ -1106,7 +1328,19 @@ export class GthAbstractAgent {
|
|
|
1106
1328
|
if (reasoningOpen) {
|
|
1107
1329
|
yield { type: 'reasoning_end' };
|
|
1108
1330
|
}
|
|
1109
|
-
//
|
|
1331
|
+
// Announce any tool calls not followed by a ToolMessage (e.g. terminal tool calls), so a call
|
|
1332
|
+
// the model asked for is never silently absent from the stream.
|
|
1333
|
+
//
|
|
1334
|
+
// [[TUI-C100]] — and deliberately NO `tool_end` sweep of {@link pendingEnds} here, which is
|
|
1335
|
+
// the tempting shape and the wrong one. **A stream that ends with calls outstanding is what
|
|
1336
|
+
// suspending at the approval gate LOOKS like**: the graph interrupts, this generator returns,
|
|
1337
|
+
// and `GthAgentRunner.processMessagesWithEvents` only then opens the prompt
|
|
1338
|
+
// (`resolveToolInterruptsWithEvents`). Ending them here would put the tick and the word `done`
|
|
1339
|
+
// back on the row a moment before the human is asked about it — the defect, moved rather than
|
|
1340
|
+
// fixed. A call held at the gate and a genuinely terminal one are indistinguishable from
|
|
1341
|
+
// inside the stream, and the turn's end is the first point at which the human has been asked
|
|
1342
|
+
// and the answer acted on — so a call still without a result THERE is one that will never have
|
|
1343
|
+
// one, and the runner closes it as exactly that.
|
|
1110
1344
|
yield* flushAggregated();
|
|
1111
1345
|
if (this.config?.writeBinaryOutputsToFile && binaryBlocks.length > 0) {
|
|
1112
1346
|
const processedContent = materializeBinaryOutputs(binaryBlocks.map((block) => ({
|
|
@@ -1128,10 +1362,20 @@ export class GthAbstractAgent {
|
|
|
1128
1362
|
if (!refusalInfo && aggregatedAIChunk) {
|
|
1129
1363
|
refusalInfo = detectRefusal(aggregatedAIChunk);
|
|
1130
1364
|
}
|
|
1365
|
+
// [[EXT-159]] — the typed-event path's raw provider statement, on both branches. This is the
|
|
1366
|
+
// surface most users watch, and it recorded no `finish_reason` anywhere at all.
|
|
1367
|
+
this.noteFinishReason('events', aggregatedAIChunk);
|
|
1131
1368
|
if (refusalInfo) {
|
|
1369
|
+
// [[EXT-159]] — the typed-event path's metadata site; classified through the same
|
|
1370
|
+
// `classifyRefusal` the other two use.
|
|
1371
|
+
this.noteTermination(terminationReason('agent.events-stop-metadata', 'metadata', classifyRefusal(refusalInfo)));
|
|
1132
1372
|
debugLog(`Content-policy refusal detected on typed-event path (provider=${refusalInfo.provider} reason=${refusalInfo.reason})`);
|
|
1133
1373
|
yield { type: 'text', delta: buildRefusalMessage(refusalInfo) };
|
|
1134
1374
|
}
|
|
1375
|
+
else if (aggregatedAIChunk) {
|
|
1376
|
+
// No refusal, but the same reader still sees an answer cut off against the output cap.
|
|
1377
|
+
this.noteStopMetadata('agent.events-stop-metadata', aggregatedAIChunk);
|
|
1378
|
+
}
|
|
1135
1379
|
}
|
|
1136
1380
|
async cleanup() {
|
|
1137
1381
|
debugLog('Cleaning up agent...');
|