@gaunt-sloth/core 2.0.0-beta.1 → 2.0.0-beta.3
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/LICENSE +6 -6
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +55 -11
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.js +46 -24
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +144 -3
- package/dist/config/shell-policy.js +117 -10
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/tool-descriptions.d.ts +5 -5
- package/dist/config/tool-descriptions.js +3 -3
- package/dist/config/types.d.ts +19 -0
- package/dist/config/types.js.map +1 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +32 -1
- package/dist/core/GthAbstractAgent.js +222 -7
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +323 -27
- package/dist/core/GthAgentRunner.js +1092 -112
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.js +1 -1
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/approvalRequest.d.ts +247 -0
- package/dist/core/approvals/approvalRequest.js +315 -0
- package/dist/core/approvals/approvalRequest.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +165 -9
- package/dist/core/approvals/grants.js +702 -57
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/plainToolIndication.d.ts +11 -1
- package/dist/core/plainToolIndication.js +25 -10
- package/dist/core/plainToolIndication.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/shell/ShellCommandFailedError.d.ts +4 -4
- package/dist/core/shell/ShellCommandFailedError.js +4 -4
- package/dist/core/shell/abstention.d.ts +1 -1
- package/dist/core/shell/abstention.js +65 -11
- package/dist/core/shell/abstention.js.map +1 -1
- package/dist/core/shell/alignment.d.ts +491 -0
- package/dist/core/shell/alignment.js +687 -0
- package/dist/core/shell/alignment.js.map +1 -0
- package/dist/core/shell/approvalCapture.d.ts +53 -41
- package/dist/core/shell/approvalCapture.js +0 -42
- package/dist/core/shell/approvalCapture.js.map +1 -1
- package/dist/core/shell/approvalStop.d.ts +121 -3
- package/dist/core/shell/approvalStop.js +153 -90
- package/dist/core/shell/approvalStop.js.map +1 -1
- package/dist/core/shell/hardline.d.ts +26 -4
- package/dist/core/shell/hardline.js +321 -16
- package/dist/core/shell/hardline.js.map +1 -1
- package/dist/core/shell/negotiation.d.ts +352 -63
- package/dist/core/shell/negotiation.js +295 -123
- package/dist/core/shell/negotiation.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +136 -0
- package/dist/core/shell/openWorld.js +573 -35
- package/dist/core/shell/openWorld.js.map +1 -1
- package/dist/core/shell/provenance.d.ts +91 -0
- package/dist/core/shell/provenance.js +136 -0
- package/dist/core/shell/provenance.js.map +1 -0
- package/dist/core/shell/rater.d.ts +394 -163
- package/dist/core/shell/rater.js +536 -238
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/raterHealth.d.ts +101 -0
- package/dist/core/shell/raterHealth.js +121 -0
- package/dist/core/shell/raterHealth.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +17 -8
- package/dist/core/shell/raterModel.js +11 -8
- package/dist/core/shell/raterModel.js.map +1 -1
- package/dist/core/shell/rejection.d.ts +20 -5
- package/dist/core/shell/rejection.js +18 -4
- package/dist/core/shell/rejection.js.map +1 -1
- package/dist/core/toolDisplay.d.ts +91 -2
- package/dist/core/toolDisplay.js +168 -19
- package/dist/core/toolDisplay.js.map +1 -1
- package/dist/core/types.d.ts +140 -22
- package/dist/core/types.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +36 -6
- package/dist/providers/configurationPassthrough.js +40 -28
- package/dist/providers/configurationPassthrough.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/providers/openrouter.js +36 -6
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +21 -0
- package/dist/providers/vertexai.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/utils/aiignoreUtils.d.ts +6 -0
- package/dist/utils/aiignoreUtils.js +69 -1
- package/dist/utils/aiignoreUtils.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js +103 -21
- package/dist/utils/binaryOutputUtils.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/fileUtils.d.ts +7 -1
- package/dist/utils/fileUtils.js +17 -3
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/globalConfigUtils.d.ts +5 -2
- package/dist/utils/globalConfigUtils.js +14 -3
- package/dist/utils/globalConfigUtils.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 +160 -3
- package/dist/utils/vertexaiUtils.js.map +1 -1
- package/package.json +6 -5
- package/schema/gsloth-config.schema.json +34 -0
|
@@ -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
|
*
|
package/dist/config/types.d.ts
CHANGED
|
@@ -662,6 +662,25 @@ export interface RatingConfig {
|
|
|
662
662
|
* @default true
|
|
663
663
|
*/
|
|
664
664
|
errorOnReviewFail?: boolean;
|
|
665
|
+
/**
|
|
666
|
+
* Wall-clock budget (ms) for the single rating model call.
|
|
667
|
+
*
|
|
668
|
+
* GS2-105 — rating is a SECOND model call fired from `afterAgent` once the review prose is
|
|
669
|
+
* already written, and it renders nothing while it runs. Without a bound, a model that fails to
|
|
670
|
+
* emit the rating tool call generates prose until its context window stops it: measured at 15,385
|
|
671
|
+
* tokens and 485 seconds on `gemma4:12b`, against a healthy rating call of ~50 seconds. The budget
|
|
672
|
+
* turns that silent hang into a bounded, legible failure, after which the existing
|
|
673
|
+
* missing-artifact path reports it and exits non-zero.
|
|
674
|
+
*
|
|
675
|
+
* It is a WALL-CLOCK budget for one call, not a per-token cap, because the per-token knob is not
|
|
676
|
+
* portable: `numPredict` is a constructor field on `ChatOllama` rather than a call option, and
|
|
677
|
+
* that provider declares `tool_choice?: never`, so neither of the obvious source-level remedies is
|
|
678
|
+
* available where this actually bites. `timeout` is a standard `RunnableConfig` field, so it works
|
|
679
|
+
* on every provider and genuinely aborts the request instead of merely abandoning it.
|
|
680
|
+
*
|
|
681
|
+
* @default 120000
|
|
682
|
+
*/
|
|
683
|
+
timeoutMs?: number;
|
|
665
684
|
}
|
|
666
685
|
/**
|
|
667
686
|
* Validation checks that can be skipped for custom command parameters.
|
package/dist/config/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAsvBA,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,UAAU;IACV,WAAW;IACX,MAAM;IACN,UAAU;IACV,QAAQ;IACR,cAAc;IACd,KAAK;IACL,YAAY;IACZ,aAAa;IACb,QAAQ;CACA,CAAC"}
|
package/dist/constants.d.ts
CHANGED
|
@@ -42,6 +42,16 @@ export declare const CONFIG_SCHEMA_POINTER = "https://gauntsloth.app/schema/v2/g
|
|
|
42
42
|
* Lives under `.gsloth/.gsloth-settings/` like other project settings.
|
|
43
43
|
*/
|
|
44
44
|
export declare const SHELL_ALLOWLIST_FILE = "shell-allowlist.json";
|
|
45
|
+
/**
|
|
46
|
+
* [[EXT-107]]: project-scoped persisted shell deny-list — the escalation menu's *always reject*
|
|
47
|
+
* choice. Its mirror on the refusal side, in the same directory and the same EXT-71 entry grammar,
|
|
48
|
+
* so the two files read identically to anyone who opens them.
|
|
49
|
+
*
|
|
50
|
+
* A separate file rather than a second array in {@link SHELL_ALLOWLIST_FILE}: the two lists have
|
|
51
|
+
* opposite failure modes, and a parse error that took the refusals down with the approvals would
|
|
52
|
+
* fail in the one direction this design never lets anything fail in.
|
|
53
|
+
*/
|
|
54
|
+
export declare const SHELL_DENYLIST_FILE = "shell-denylist.json";
|
|
45
55
|
/**
|
|
46
56
|
* The `additionalToolNamePrefix` handed to `MultiServerMCPClient` so every MCP tool name is
|
|
47
57
|
* namespaced. Combined with `prefixToolNameWithServerName`, the adapter emits tool names shaped
|
package/dist/constants.js
CHANGED
|
@@ -42,6 +42,16 @@ export const CONFIG_SCHEMA_POINTER = 'https://gauntsloth.app/schema/v2/gsloth-co
|
|
|
42
42
|
* Lives under `.gsloth/.gsloth-settings/` like other project settings.
|
|
43
43
|
*/
|
|
44
44
|
export const SHELL_ALLOWLIST_FILE = 'shell-allowlist.json';
|
|
45
|
+
/**
|
|
46
|
+
* [[EXT-107]]: project-scoped persisted shell deny-list — the escalation menu's *always reject*
|
|
47
|
+
* choice. Its mirror on the refusal side, in the same directory and the same EXT-71 entry grammar,
|
|
48
|
+
* so the two files read identically to anyone who opens them.
|
|
49
|
+
*
|
|
50
|
+
* A separate file rather than a second array in {@link SHELL_ALLOWLIST_FILE}: the two lists have
|
|
51
|
+
* opposite failure modes, and a parse error that took the refusals down with the approvals would
|
|
52
|
+
* fail in the one direction this design never lets anything fail in.
|
|
53
|
+
*/
|
|
54
|
+
export const SHELL_DENYLIST_FILE = 'shell-denylist.json';
|
|
45
55
|
/**
|
|
46
56
|
* The `additionalToolNamePrefix` handed to `MultiServerMCPClient` so every MCP tool name is
|
|
47
57
|
* namespaced. Combined with `prefixToolNameWithServerName`, the adapter emits tool names shaped
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AACtD,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,MAAM,CAAC,MAAM,wBAAwB,GAAG,qBAAqB,CAAC;AAC9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,sBAAsB,CAAC;AAChE,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAC5D,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAC1D,MAAM,CAAC,MAAM,2BAA2B,GAAG,mBAAmB,CAAC;AAC/D,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAC;AACzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4DAA4D,CAAC;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,aAAa,CAAC;AAC3D,MAAM,CAAC,MAAM,8BAA8B,GAAG,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AACtD,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,MAAM,CAAC,MAAM,wBAAwB,GAAG,qBAAqB,CAAC;AAC9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,sBAAsB,CAAC;AAChE,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAC5D,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAC1D,MAAM,CAAC,MAAM,2BAA2B,GAAG,mBAAmB,CAAC;AAC/D,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAC;AACzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4DAA4D,CAAC;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,aAAa,CAAC;AAC3D,MAAM,CAAC,MAAM,8BAA8B,GAAG,qBAAqB,CAAC"}
|
|
@@ -132,7 +132,7 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
132
132
|
*/
|
|
133
133
|
protected compactHeaderStatus(): void;
|
|
134
134
|
/**
|
|
135
|
-
* EXT-58 (spec §4.5) — the ONE tool-registration hook
|
|
135
|
+
* EXT-58 (spec §4.5) — the ONE tool-registration hook every backend calls with its final tool
|
|
136
136
|
* array, just before handing it to the graph builder. It does two things:
|
|
137
137
|
*
|
|
138
138
|
* 1. Appends the rung's approval sentence to every tool that is **not** auto-approved at that
|
|
@@ -263,6 +263,37 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
|
|
|
263
263
|
* setup degrades to "no approval needed" instead of breaking the run.
|
|
264
264
|
*/
|
|
265
265
|
getPendingToolInterrupts(runConfig: RunnableConfig): Promise<PendingToolInterrupt[]>;
|
|
266
|
+
/**
|
|
267
|
+
* [[TUI-C69]] §5.4 — remember that this call was bounced back to the agent as a negotiation
|
|
268
|
+
* round, so both display paths can tone its result row as a clarification request.
|
|
269
|
+
*
|
|
270
|
+
* A plain set rather than a queue: the ids are LangChain tool-call ids, unique per call, and the
|
|
271
|
+
* two display paths read the same one without either consuming it — a session drives one of them,
|
|
272
|
+
* never both. {@link clearRaterClarifications} empties it with the rest of the turn's state, so a
|
|
273
|
+
* long run cannot accumulate.
|
|
274
|
+
*/
|
|
275
|
+
noteRaterClarification(toolCallId: string): void;
|
|
276
|
+
/**
|
|
277
|
+
* [[TUI-C69]] §5.4 — **forget the noted ids**, called at the top of every turn and on `/clear`.
|
|
278
|
+
*
|
|
279
|
+
* The set is keyed on tool-call ids, and an id is only meaningful while the result carrying it is
|
|
280
|
+
* being rendered. Holding them past that costs a string per rater rejection for the life of the
|
|
281
|
+
* process — small — and risks the thing that is not small: a LATER call that reuses a noted id
|
|
282
|
+
* renders as a clarification request whatever it actually was, so a successful command inherits
|
|
283
|
+
* the tone of an argument it had no part in.
|
|
284
|
+
*
|
|
285
|
+
* That reuse is not reachable with the providers installed today — `@langchain/ollama` mints
|
|
286
|
+
* `v4()` uuids, `@langchain/google` `lc-tool-call-${v4()}`, and OpenAI and Anthropic supply their
|
|
287
|
+
* own unique ids — but **the protection was the providers', not this code's**, which is a
|
|
288
|
+
* property no test here can hold and no bump has to preserve.
|
|
289
|
+
*
|
|
290
|
+
* On `/clear` it is the same argument `GthAgentRunner.resetThread` makes about the negotiation:
|
|
291
|
+
* state from before the user asked for the conversation to be forgotten must not decide how the
|
|
292
|
+
* conversation after it is drawn.
|
|
293
|
+
*/
|
|
294
|
+
clearRaterClarifications(): void;
|
|
295
|
+
/** [[TUI-C69]] — the ids {@link noteRaterClarification} has been told about this turn. */
|
|
296
|
+
protected raterClarifications: Set<string>;
|
|
266
297
|
protected processEventStream(stream: IterableReadableStream<[BaseMessage, Record<string, unknown>]>): AsyncGenerator<AgentStreamEvent>;
|
|
267
298
|
cleanup(): Promise<void>;
|
|
268
299
|
getEffectiveConfig(config: GthConfig, command: GthCommand | undefined): GthConfig;
|
|
@@ -118,6 +118,84 @@ function pickReasoningDelta(kwargs) {
|
|
|
118
118
|
return direct;
|
|
119
119
|
return '';
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* [[TUI-C69]] §5.4 — **the tool-call ids a suspended graph's pending requests were built from.**
|
|
123
|
+
*
|
|
124
|
+
* LangChain's HITL middleware assembles its `actionRequests` from the last AI message's
|
|
125
|
+
* `tool_calls`, keeping `{ name, args, description }` and dropping the id — so a decision about a
|
|
126
|
+
* pending call has nothing to attribute it to on screen. The ids are still right there: the same
|
|
127
|
+
* `getState` snapshot that carries `tasks[].interrupts` carries `values.messages`, whose last
|
|
128
|
+
* `AIMessage` holds the very calls the middleware filtered. Measured against a real suspended
|
|
129
|
+
* graph rather than reasoned from the library's source, because a middleware hook that ran inside
|
|
130
|
+
* the model node instead of after it would leave this empty.
|
|
131
|
+
*
|
|
132
|
+
* Returns them in message order, for {@link claimToolCallId} to consume. Defensive throughout: an
|
|
133
|
+
* unexpected shape yields an empty list, and every id is then simply absent.
|
|
134
|
+
*/
|
|
135
|
+
function pendingToolCallIds(state) {
|
|
136
|
+
const messages = state?.values?.messages;
|
|
137
|
+
if (!Array.isArray(messages))
|
|
138
|
+
return [];
|
|
139
|
+
// The LAST message carrying tool calls is the one the interrupt suspended on. Earlier AI
|
|
140
|
+
// messages in the thread carry calls that already ran, and their ids must never be claimed.
|
|
141
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
142
|
+
const toolCalls = messages[index]?.tool_calls;
|
|
143
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
const claimable = [];
|
|
146
|
+
for (const call of toolCalls) {
|
|
147
|
+
const name = call?.name;
|
|
148
|
+
const id = call?.id;
|
|
149
|
+
if (typeof name !== 'string' || typeof id !== 'string' || id.length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
claimable.push({
|
|
152
|
+
name,
|
|
153
|
+
args: stableArgs(call?.args),
|
|
154
|
+
id,
|
|
155
|
+
claimed: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return claimable;
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Match one pending action request back to its tool call and take that call's id.
|
|
164
|
+
*
|
|
165
|
+
* Name AND arguments, then FIRST unclaimed — which is exact rather than a correlation, because the
|
|
166
|
+
* request was built from that very object. The claim flag is what keeps two identical calls in one
|
|
167
|
+
* message (a model proposing the same command twice) from both taking the first id.
|
|
168
|
+
*
|
|
169
|
+
* **The arguments are half the match, and only one arrangement can show it.** Two calls of one name
|
|
170
|
+
* with the SAME arguments come back correctly under a name-only match too, because the claim flag
|
|
171
|
+
* alone puts them in order — so the case that discriminates is two calls of one name with DIFFERENT
|
|
172
|
+
* arguments whose requests arrive in the opposite order to the `tool_calls` they were built from.
|
|
173
|
+
* There, name-only gives every request an id and attaches each to the wrong command.
|
|
174
|
+
*/
|
|
175
|
+
function claimToolCallId(candidates, name, args) {
|
|
176
|
+
const wanted = stableArgs(args);
|
|
177
|
+
const match = candidates.find((c) => !c.claimed && c.name === name && c.args === wanted);
|
|
178
|
+
if (!match)
|
|
179
|
+
return undefined;
|
|
180
|
+
match.claimed = true;
|
|
181
|
+
return match.id;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A tool call's arguments as one comparable string, with the keys sorted so two objects that
|
|
185
|
+
* differ only in insertion order still match. Fail-soft: an unserialisable argument (a BigInt, a
|
|
186
|
+
* cycle) yields a token that matches nothing, so the id is simply not attributed.
|
|
187
|
+
*/
|
|
188
|
+
function stableArgs(args) {
|
|
189
|
+
if (!args || typeof args !== 'object')
|
|
190
|
+
return '{}';
|
|
191
|
+
try {
|
|
192
|
+
const entries = Object.entries(args).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
193
|
+
return JSON.stringify(entries);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return '<unserialisable>';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
121
199
|
/**
|
|
122
200
|
* Shared, graph-agnostic agent plumbing.
|
|
123
201
|
*
|
|
@@ -262,7 +340,7 @@ export class GthAbstractAgent {
|
|
|
262
340
|
this.statusUpdate(StatusLevel.INFO, runHeaderLine(command, label));
|
|
263
341
|
}
|
|
264
342
|
/**
|
|
265
|
-
* EXT-58 (spec §4.5) — the ONE tool-registration hook
|
|
343
|
+
* EXT-58 (spec §4.5) — the ONE tool-registration hook every backend calls with its final tool
|
|
266
344
|
* array, just before handing it to the graph builder. It does two things:
|
|
267
345
|
*
|
|
268
346
|
* 1. Appends the rung's approval sentence to every tool that is **not** auto-approved at that
|
|
@@ -405,7 +483,7 @@ export class GthAbstractAgent {
|
|
|
405
483
|
// tool call gets its `✓ 📁 name(args…)` block here too. Only the plain surface reaches
|
|
406
484
|
// invoke (the TUI uses processMessagesWithEvents); observe() is fail-soft internally.
|
|
407
485
|
const allMessages = Array.isArray(response.messages) ? response.messages : [];
|
|
408
|
-
const toolIndication = createPlainToolIndication();
|
|
486
|
+
const toolIndication = createPlainToolIndication(undefined, (id) => this.raterClarifications.has(id));
|
|
409
487
|
for (const m of allMessages.slice(priorMessageCount)) {
|
|
410
488
|
this.recordRunStats(m);
|
|
411
489
|
toolIndication.observe(m);
|
|
@@ -516,7 +594,13 @@ export class GthAbstractAgent {
|
|
|
516
594
|
// canonical 10-line greyed preview when each ToolMessage lands). Per-stream state; emits at
|
|
517
595
|
// INFO level so the existing consoleLevel gate governs it like the historical tool notices.
|
|
518
596
|
// The TUI never runs this string path (it renders the typed event stream itself).
|
|
519
|
-
|
|
597
|
+
//
|
|
598
|
+
// [[TUI-C69]] §5.4 — the plain surface's twin of the typed event's `raterClarification`, read
|
|
599
|
+
// through a closure rather than handed a snapshot: the set is filled WHILE this stream is
|
|
600
|
+
// drained, because the runner notes the id at the moment it refuses the call, which is after
|
|
601
|
+
// this observer was built and before the refusal's own result arrives.
|
|
602
|
+
const raterClarifications = this.raterClarifications;
|
|
603
|
+
const toolIndication = createPlainToolIndication(undefined, (id) => raterClarifications.has(id));
|
|
520
604
|
const interruptState = { escape: false, messageShown: false };
|
|
521
605
|
const abortController = new AbortController();
|
|
522
606
|
const showInterruptMessage = () => {
|
|
@@ -774,6 +858,8 @@ export class GthAbstractAgent {
|
|
|
774
858
|
if (!Array.isArray(tasks)) {
|
|
775
859
|
return [];
|
|
776
860
|
}
|
|
861
|
+
// [[TUI-C69]] — the ids the action requests were built from, ready to be claimed in order.
|
|
862
|
+
const unclaimedIds = pendingToolCallIds(state);
|
|
777
863
|
const pending = [];
|
|
778
864
|
for (const task of tasks) {
|
|
779
865
|
const interrupts = task?.interrupts;
|
|
@@ -789,15 +875,54 @@ export class GthAbstractAgent {
|
|
|
789
875
|
if (typeof name !== 'string')
|
|
790
876
|
continue;
|
|
791
877
|
const args = action?.args;
|
|
878
|
+
const resolvedArgs = args && typeof args === 'object' ? args : {};
|
|
879
|
+
const id = claimToolCallId(unclaimedIds, name, resolvedArgs);
|
|
792
880
|
pending.push({
|
|
793
881
|
name,
|
|
794
|
-
args:
|
|
882
|
+
args: resolvedArgs,
|
|
883
|
+
...(id === undefined ? {} : { id }),
|
|
795
884
|
});
|
|
796
885
|
}
|
|
797
886
|
}
|
|
798
887
|
}
|
|
799
888
|
return pending;
|
|
800
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* [[TUI-C69]] §5.4 — remember that this call was bounced back to the agent as a negotiation
|
|
892
|
+
* round, so both display paths can tone its result row as a clarification request.
|
|
893
|
+
*
|
|
894
|
+
* A plain set rather than a queue: the ids are LangChain tool-call ids, unique per call, and the
|
|
895
|
+
* two display paths read the same one without either consuming it — a session drives one of them,
|
|
896
|
+
* never both. {@link clearRaterClarifications} empties it with the rest of the turn's state, so a
|
|
897
|
+
* long run cannot accumulate.
|
|
898
|
+
*/
|
|
899
|
+
noteRaterClarification(toolCallId) {
|
|
900
|
+
if (toolCallId)
|
|
901
|
+
this.raterClarifications.add(toolCallId);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* [[TUI-C69]] §5.4 — **forget the noted ids**, called at the top of every turn and on `/clear`.
|
|
905
|
+
*
|
|
906
|
+
* The set is keyed on tool-call ids, and an id is only meaningful while the result carrying it is
|
|
907
|
+
* being rendered. Holding them past that costs a string per rater rejection for the life of the
|
|
908
|
+
* process — small — and risks the thing that is not small: a LATER call that reuses a noted id
|
|
909
|
+
* renders as a clarification request whatever it actually was, so a successful command inherits
|
|
910
|
+
* the tone of an argument it had no part in.
|
|
911
|
+
*
|
|
912
|
+
* That reuse is not reachable with the providers installed today — `@langchain/ollama` mints
|
|
913
|
+
* `v4()` uuids, `@langchain/google` `lc-tool-call-${v4()}`, and OpenAI and Anthropic supply their
|
|
914
|
+
* own unique ids — but **the protection was the providers', not this code's**, which is a
|
|
915
|
+
* property no test here can hold and no bump has to preserve.
|
|
916
|
+
*
|
|
917
|
+
* On `/clear` it is the same argument `GthAgentRunner.resetThread` makes about the negotiation:
|
|
918
|
+
* state from before the user asked for the conversation to be forgotten must not decide how the
|
|
919
|
+
* conversation after it is drawn.
|
|
920
|
+
*/
|
|
921
|
+
clearRaterClarifications() {
|
|
922
|
+
this.raterClarifications.clear();
|
|
923
|
+
}
|
|
924
|
+
/** [[TUI-C69]] — the ids {@link noteRaterClarification} has been told about this turn. */
|
|
925
|
+
raterClarifications = new Set();
|
|
801
926
|
async *processEventStream(stream) {
|
|
802
927
|
// Aggregate AIMessageChunks via concat so tool_call_chunks collapse into
|
|
803
928
|
// tool_calls with complete args (per-chunk tool_calls only ever sees that
|
|
@@ -805,11 +930,30 @@ export class GthAbstractAgent {
|
|
|
805
930
|
let aggregatedAIChunk = null;
|
|
806
931
|
let reasoningOpen = false;
|
|
807
932
|
const flushed = new Set();
|
|
933
|
+
/**
|
|
934
|
+
* [[TUI-C100]] — the calls THIS stream has announced (`tool_start` + `tool_args`) and whose
|
|
935
|
+
* own result it has not yet seen.
|
|
936
|
+
*
|
|
937
|
+
* Kept here rather than derived from {@link aggregatedAIChunk} because the aggregate is nulled
|
|
938
|
+
* after every round (see the reset below) while the calls it announced can still be waiting —
|
|
939
|
+
* at the approval gate, or simply behind a sibling in the same message. A set that died with
|
|
940
|
+
* the aggregate would lose exactly the calls this exists to track.
|
|
941
|
+
*
|
|
942
|
+
* **Scoped to one invocation, deliberately.** A call held at the approval gate is announced in
|
|
943
|
+
* the stream that suspends and resolved in the stream that resumes, so its result lands where
|
|
944
|
+
* this set is empty — and it therefore never receives a `tool_end` at all. That is the correct
|
|
945
|
+
* outcome, not a leak: a stream may only speak for what it announced. What closes a call that
|
|
946
|
+
* produces no result anywhere is the turn-level drain in `GthAgentRunner`'s
|
|
947
|
+
* `processMessagesWithEvents`, which spans both streams.
|
|
948
|
+
*/
|
|
949
|
+
const pendingEnds = new Set();
|
|
808
950
|
// EXT-41: a content-policy refusal on this typed-event path was de-scoped by EXT-37 (there is
|
|
809
951
|
// no empty-response retry here, so no wrong-retry bug), but it still rendered as a SILENT empty
|
|
810
952
|
// turn. Capture it (first per-chunk signal wins; aggregate fallback at stream end) and surface
|
|
811
953
|
// it as a `text` event so every consumer (Ink TUI viewModel, AG-UI SSE) shows a clear notice.
|
|
812
954
|
let refusalInfo = null;
|
|
955
|
+
const seenBinaryKeys = new Set();
|
|
956
|
+
const binaryBlocks = [];
|
|
813
957
|
// TUI-C22 — one splitter for the whole stream so a <think> opened in one chunk and closed
|
|
814
958
|
// several chunks later is tracked across the boundary. Reset at message boundaries via flush().
|
|
815
959
|
const thinkSplitter = createThinkTagSplitter();
|
|
@@ -851,6 +995,18 @@ export class GthAbstractAgent {
|
|
|
851
995
|
}
|
|
852
996
|
}
|
|
853
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* Announce every call the assistant message carries, once its arguments are complete.
|
|
1000
|
+
*
|
|
1001
|
+
* [[TUI-C100]] — **announcing a call is not ending it.** This runs when the round's FIRST
|
|
1002
|
+
* `ToolMessage` lands, because that is what makes the streamed aggregate complete, so it speaks
|
|
1003
|
+
* for every call in the message at once: the one that just returned, the ones still queued, and
|
|
1004
|
+
* the one suspended at the approval gate with a human being asked about it. `tool_start` and
|
|
1005
|
+
* `tool_args` are true for all of them — the model has finished emitting the arguments — and
|
|
1006
|
+
* that is what puts a call on screen, with the arguments the human is about to rule on.
|
|
1007
|
+
* `tool_end` is not: it waits for the call's own result, and is emitted beside it when that
|
|
1008
|
+
* result reaches THIS stream — which for a gated call it never does. See `pendingEnds` above.
|
|
1009
|
+
*/
|
|
854
1010
|
function* flushAggregated() {
|
|
855
1011
|
if (!aggregatedAIChunk)
|
|
856
1012
|
return;
|
|
@@ -863,10 +1019,15 @@ export class GthAbstractAgent {
|
|
|
863
1019
|
flushed.add(id);
|
|
864
1020
|
yield { type: 'tool_start', id, name: tc.name };
|
|
865
1021
|
yield { type: 'tool_args', id, delta: JSON.stringify(tc.args ?? {}) };
|
|
866
|
-
|
|
1022
|
+
pendingEnds.add(id);
|
|
867
1023
|
}
|
|
868
1024
|
// Surface invalid tool calls too so the client at least sees the raw args
|
|
869
1025
|
// string the model produced, instead of silently dropping them.
|
|
1026
|
+
//
|
|
1027
|
+
// These END INLINE, unlike the calls above, and the asymmetry is the point: an invalid call
|
|
1028
|
+
// is never dispatched, so there is no result to wait for and nothing that could later change
|
|
1029
|
+
// what it says. Deferring it would leave a row that cannot progress sitting at `running`
|
|
1030
|
+
// until the turn ended, in exchange for nothing.
|
|
870
1031
|
for (const tc of invalidToolCalls) {
|
|
871
1032
|
const id = tc.id;
|
|
872
1033
|
if (!id || flushed.has(id))
|
|
@@ -887,6 +1048,16 @@ export class GthAbstractAgent {
|
|
|
887
1048
|
if (!refusalInfo) {
|
|
888
1049
|
refusalInfo = detectRefusal(chunk);
|
|
889
1050
|
}
|
|
1051
|
+
if (this.config?.writeBinaryOutputsToFile &&
|
|
1052
|
+
(AIMessageChunk.isInstance(chunk) || AIMessage.isInstance(chunk))) {
|
|
1053
|
+
for (const block of extractInlineBinaryBlocks(chunk.content)) {
|
|
1054
|
+
const binaryKey = `${block.mimeType}:${block.data.length}:${block.data}`;
|
|
1055
|
+
if (!seenBinaryKeys.has(binaryKey)) {
|
|
1056
|
+
seenBinaryKeys.add(binaryKey);
|
|
1057
|
+
binaryBlocks.push({ mimeType: block.mimeType, data: block.data });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
890
1061
|
if (AIMessageChunk.isInstance(chunk)) {
|
|
891
1062
|
aggregatedAIChunk = aggregatedAIChunk ? aggregatedAIChunk.concat(chunk) : chunk;
|
|
892
1063
|
// Reasoning deltas — Ollama (Qwen3, deepseek-r1), Anthropic, and OpenRouter surface
|
|
@@ -948,11 +1119,31 @@ export class GthAbstractAgent {
|
|
|
948
1119
|
// Surface the real tool-result error signal (LangChain `ToolMessage.status`) so
|
|
949
1120
|
// consumers render the ✗/error affordance from fact, not from sniffing the result
|
|
950
1121
|
// text. Only attach the flag on error to keep the success event shape unchanged.
|
|
1122
|
+
// [[TUI-C69]] §5.4 — the gate's own account of WHY this result is an error, when it was
|
|
1123
|
+
// the auto-rater asking for a clarification rather than a tool that failed. Additive:
|
|
1124
|
+
// `isError` still says the call did not run, which is what the model, `gth eval`'s
|
|
1125
|
+
// tool-result assertions and the ACP bridge all read.
|
|
1126
|
+
const toolCallId = chunk.tool_call_id;
|
|
1127
|
+
// [[TUI-C100]] — the call's own result is here, so NOW it has ended.
|
|
1128
|
+
//
|
|
1129
|
+
// **The guard is the stream's own honesty, not a favour to a downstream reader.** This
|
|
1130
|
+
// generator may only close a call it opened, and the id it is holding a result for is
|
|
1131
|
+
// routinely one it never announced: a gated call is announced in the stream that suspends
|
|
1132
|
+
// and returns in the stream that resumes, where this set is empty. So the common gated flow
|
|
1133
|
+
// is `tool_start`, `tool_args`, then a `tool_result` in a later stream with **no `tool_end`
|
|
1134
|
+
// between them, and none at all** — do not read the line below as a promise that an end
|
|
1135
|
+
// always precedes a result. Every consumer treats `tool_result` as terminal on its own, and
|
|
1136
|
+
// the AG-UI bridge additionally closes its own framing from what it actually emitted rather
|
|
1137
|
+
// than trusting this event to arrive.
|
|
1138
|
+
if (pendingEnds.delete(toolCallId)) {
|
|
1139
|
+
yield { type: 'tool_end', id: toolCallId };
|
|
1140
|
+
}
|
|
951
1141
|
yield {
|
|
952
1142
|
type: 'tool_result',
|
|
953
|
-
id:
|
|
1143
|
+
id: toolCallId,
|
|
954
1144
|
content,
|
|
955
1145
|
...(chunk.status === 'error' ? { isError: true } : {}),
|
|
1146
|
+
...(this.raterClarifications.has(toolCallId) ? { raterClarification: true } : {}),
|
|
956
1147
|
};
|
|
957
1148
|
}
|
|
958
1149
|
}
|
|
@@ -963,8 +1154,32 @@ export class GthAbstractAgent {
|
|
|
963
1154
|
if (reasoningOpen) {
|
|
964
1155
|
yield { type: 'reasoning_end' };
|
|
965
1156
|
}
|
|
966
|
-
//
|
|
1157
|
+
// Announce any tool calls not followed by a ToolMessage (e.g. terminal tool calls), so a call
|
|
1158
|
+
// the model asked for is never silently absent from the stream.
|
|
1159
|
+
//
|
|
1160
|
+
// [[TUI-C100]] — and deliberately NO `tool_end` sweep of {@link pendingEnds} here, which is
|
|
1161
|
+
// the tempting shape and the wrong one. **A stream that ends with calls outstanding is what
|
|
1162
|
+
// suspending at the approval gate LOOKS like**: the graph interrupts, this generator returns,
|
|
1163
|
+
// and `GthAgentRunner.processMessagesWithEvents` only then opens the prompt
|
|
1164
|
+
// (`resolveToolInterruptsWithEvents`). Ending them here would put the tick and the word `done`
|
|
1165
|
+
// back on the row a moment before the human is asked about it — the defect, moved rather than
|
|
1166
|
+
// fixed. A call held at the gate and a genuinely terminal one are indistinguishable from
|
|
1167
|
+
// inside the stream, and the turn's end is the first point at which the human has been asked
|
|
1168
|
+
// and the answer acted on — so a call still without a result THERE is one that will never have
|
|
1169
|
+
// one, and the runner closes it as exactly that.
|
|
967
1170
|
yield* flushAggregated();
|
|
1171
|
+
if (this.config?.writeBinaryOutputsToFile && binaryBlocks.length > 0) {
|
|
1172
|
+
const processedContent = materializeBinaryOutputs(binaryBlocks.map((block) => ({
|
|
1173
|
+
type: 'inlineData',
|
|
1174
|
+
inlineData: block,
|
|
1175
|
+
})), this.command);
|
|
1176
|
+
for (const successMessage of processedContent.successMessages) {
|
|
1177
|
+
this.statusUpdate(StatusLevel.SUCCESS, successMessage);
|
|
1178
|
+
}
|
|
1179
|
+
if (processedContent.renderedContent.trim().length > 0) {
|
|
1180
|
+
yield { type: 'text', delta: '\n' + processedContent.renderedContent + '\n' };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
968
1183
|
// EXT-41: aggregate-level fallback (I-1's robustness on this path too) — if no per-chunk
|
|
969
1184
|
// metadata flagged a refusal, inspect the final aggregated message's stop/finish reason. Then
|
|
970
1185
|
// surface any refusal as a `text` event so the user sees a clear notice instead of a silent
|