@gaunt-sloth/core 2.0.0-alpha.3 → 2.0.0-alpha.31
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 +52 -20
- package/dist/config/colour.d.ts +38 -0
- package/dist/config/colour.js +36 -0
- package/dist/config/colour.js.map +1 -0
- package/dist/config/defaults.d.ts +84 -0
- package/dist/config/defaults.js +99 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/filesystem-tools.d.ts +41 -0
- package/dist/config/filesystem-tools.js +56 -0
- package/dist/config/filesystem-tools.js.map +1 -0
- package/dist/config/jsonc.d.ts +12 -0
- package/dist/config/jsonc.js +41 -0
- package/dist/config/jsonc.js.map +1 -0
- package/dist/config/loader.d.ts +246 -0
- package/dist/config/loader.js +1321 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/mouse.d.ts +50 -0
- package/dist/config/mouse.js +44 -0
- package/dist/config/mouse.js.map +1 -0
- package/dist/config/profiles.d.ts +68 -0
- package/dist/config/profiles.js +93 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providerKeys.d.ts +69 -0
- package/dist/config/providerKeys.js +69 -0
- package/dist/config/providerKeys.js.map +1 -0
- package/dist/config/schema.d.ts +2999 -0
- package/dist/config/schema.js +1519 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/shell-policy.d.ts +896 -0
- package/dist/config/shell-policy.js +750 -0
- package/dist/config/shell-policy.js.map +1 -0
- package/dist/config/tool-descriptions.d.ts +211 -0
- package/dist/config/tool-descriptions.js +272 -0
- package/dist/config/tool-descriptions.js.map +1 -0
- package/dist/config/types.d.ts +760 -0
- package/dist/config/types.js +13 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config.d.ts +34 -827
- package/dist/config.js +25 -657
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +46 -0
- package/dist/constants.js +46 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +135 -2
- package/dist/core/GthAbstractAgent.js +496 -30
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +568 -54
- package/dist/core/GthAgentRunner.js +1537 -136
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +115 -0
- package/dist/core/GthLangChainAgent.js +644 -17
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/annotations.d.ts +122 -0
- package/dist/core/approvals/annotations.js +137 -0
- package/dist/core/approvals/annotations.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +216 -0
- package/dist/core/approvals/grants.js +469 -0
- package/dist/core/approvals/grants.js.map +1 -0
- package/dist/core/approvals/matcher.d.ts +202 -0
- package/dist/core/approvals/matcher.js +267 -0
- package/dist/core/approvals/matcher.js.map +1 -0
- package/dist/core/approvals/mcpSubjects.d.ts +40 -0
- package/dist/core/approvals/mcpSubjects.js +99 -0
- package/dist/core/approvals/mcpSubjects.js.map +1 -0
- package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
- package/dist/core/approvals/toolAnnotationSources.js +277 -0
- package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
- package/dist/core/approvals/toolHost.d.ts +46 -0
- package/dist/core/approvals/toolHost.js +108 -0
- package/dist/core/approvals/toolHost.js.map +1 -0
- package/dist/core/debugCapture.d.ts +75 -0
- package/dist/core/debugCapture.js +101 -0
- package/dist/core/debugCapture.js.map +1 -0
- package/dist/core/gthLeanAgentFactory.d.ts +9 -0
- package/dist/core/gthLeanAgentFactory.js +10 -0
- package/dist/core/gthLeanAgentFactory.js.map +1 -0
- package/dist/core/launchBanner.d.ts +120 -0
- package/dist/core/launchBanner.js +418 -0
- package/dist/core/launchBanner.js.map +1 -0
- package/dist/core/plainToolIndication.d.ts +15 -0
- package/dist/core/plainToolIndication.js +174 -0
- package/dist/core/plainToolIndication.js.map +1 -0
- package/dist/core/reasoningBlocks.d.ts +60 -0
- package/dist/core/reasoningBlocks.js +98 -0
- package/dist/core/reasoningBlocks.js.map +1 -0
- package/dist/core/refusal.d.ts +53 -0
- package/dist/core/refusal.js +133 -0
- package/dist/core/refusal.js.map +1 -0
- package/dist/core/runStats.d.ts +52 -0
- package/dist/core/runStats.js +118 -0
- package/dist/core/runStats.js.map +1 -0
- package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
- package/dist/core/shell/ShellCommandFailedError.js +68 -0
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
- package/dist/core/shell/abstention.d.ts +88 -0
- package/dist/core/shell/abstention.js +184 -0
- package/dist/core/shell/abstention.js.map +1 -0
- package/dist/core/shell/approvalCapture.d.ts +271 -0
- package/dist/core/shell/approvalCapture.js +108 -0
- package/dist/core/shell/approvalCapture.js.map +1 -0
- package/dist/core/shell/approvalStop.d.ts +84 -0
- package/dist/core/shell/approvalStop.js +119 -0
- package/dist/core/shell/approvalStop.js.map +1 -0
- package/dist/core/shell/arity.d.ts +6 -0
- package/dist/core/shell/arity.js +20 -6
- package/dist/core/shell/arity.js.map +1 -1
- package/dist/core/shell/denylist.d.ts +11 -0
- package/dist/core/shell/denylist.js +37 -0
- package/dist/core/shell/denylist.js.map +1 -0
- package/dist/core/shell/escalationSeverity.d.ts +141 -0
- package/dist/core/shell/escalationSeverity.js +89 -0
- package/dist/core/shell/escalationSeverity.js.map +1 -0
- package/dist/core/shell/framing.d.ts +190 -0
- package/dist/core/shell/framing.js +633 -0
- package/dist/core/shell/framing.js.map +1 -0
- package/dist/core/shell/hardline.d.ts +81 -0
- package/dist/core/shell/hardline.js +607 -0
- package/dist/core/shell/hardline.js.map +1 -0
- package/dist/core/shell/negotiation.d.ts +249 -0
- package/dist/core/shell/negotiation.js +355 -0
- package/dist/core/shell/negotiation.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +44 -4
- package/dist/core/shell/normalize.js +61 -7
- package/dist/core/shell/normalize.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +263 -0
- package/dist/core/shell/openWorld.js +1188 -0
- package/dist/core/shell/openWorld.js.map +1 -0
- package/dist/core/shell/rater.d.ts +935 -0
- package/dist/core/shell/rater.js +1473 -0
- package/dist/core/shell/rater.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +41 -0
- package/dist/core/shell/raterModel.js +51 -0
- package/dist/core/shell/raterModel.js.map +1 -0
- package/dist/core/shell/rejection.d.ts +69 -0
- package/dist/core/shell/rejection.js +38 -0
- package/dist/core/shell/rejection.js.map +1 -0
- package/dist/core/toolCallRepair/grammar.d.ts +41 -0
- package/dist/core/toolCallRepair/grammar.js +116 -0
- package/dist/core/toolCallRepair/grammar.js.map +1 -0
- package/dist/core/toolCallRepair/index.d.ts +2 -0
- package/dist/core/toolCallRepair/index.js +7 -0
- package/dist/core/toolCallRepair/index.js.map +1 -0
- package/dist/core/toolCallRepair/payload.d.ts +36 -0
- package/dist/core/toolCallRepair/payload.js +341 -0
- package/dist/core/toolCallRepair/payload.js.map +1 -0
- package/dist/core/toolCallRepair/promote.d.ts +45 -0
- package/dist/core/toolCallRepair/promote.js +90 -0
- package/dist/core/toolCallRepair/promote.js.map +1 -0
- package/dist/core/toolDisplay.d.ts +123 -0
- package/dist/core/toolDisplay.js +451 -0
- package/dist/core/toolDisplay.js.map +1 -0
- package/dist/core/toolOutputChannel.d.ts +95 -0
- package/dist/core/toolOutputChannel.js +165 -0
- package/dist/core/toolOutputChannel.js.map +1 -0
- package/dist/core/types.d.ts +294 -12
- package/dist/core/types.js.map +1 -1
- package/dist/history/historyFormat.d.ts +28 -0
- package/dist/history/historyFormat.js +127 -0
- package/dist/history/historyFormat.js.map +1 -0
- package/dist/history/historyStore.d.ts +198 -0
- package/dist/history/historyStore.js +482 -0
- package/dist/history/historyStore.js.map +1 -0
- package/dist/history/recordSession.d.ts +37 -0
- package/dist/history/recordSession.js +56 -0
- package/dist/history/recordSession.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +5 -10
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/deepseek.d.ts +1 -1
- package/dist/providers/deepseek.js +5 -10
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
- package/dist/providers/geminiSchemaSanitizer.js +347 -0
- package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
- package/dist/providers/geminiThinking.d.ts +52 -0
- package/dist/providers/geminiThinking.js +72 -0
- package/dist/providers/geminiThinking.js.map +1 -0
- package/dist/providers/google-genai.d.ts +1 -1
- package/dist/providers/google-genai.js +12 -11
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +5 -10
- package/dist/providers/groq.js.map +1 -1
- package/dist/providers/huggingface.d.ts +25 -0
- package/dist/providers/huggingface.js +69 -0
- package/dist/providers/huggingface.js.map +1 -0
- package/dist/providers/modelCatalog.d.ts +109 -0
- package/dist/providers/modelCatalog.js +245 -0
- package/dist/providers/modelCatalog.js.map +1 -0
- package/dist/providers/modelDiscovery.d.ts +153 -6
- package/dist/providers/modelDiscovery.js +295 -41
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.d.ts +19 -5
- package/dist/providers/ollama.js +60 -52
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +39 -10
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +4 -5
- package/dist/providers/openrouter.js +20 -35
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.d.ts +1 -1
- package/dist/providers/vertexai.js +12 -11
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/xai.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +105 -0
- package/dist/runtime/askStructured.js +120 -0
- package/dist/runtime/askStructured.js.map +1 -0
- package/dist/runtime/conversation.d.ts +60 -0
- package/dist/runtime/conversation.js +151 -0
- package/dist/runtime/conversation.js.map +1 -0
- package/dist/runtime/singleShot.d.ts +25 -5
- package/dist/runtime/singleShot.js +95 -36
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/runtime/structuredOutput.d.ts +104 -0
- package/dist/runtime/structuredOutput.js +393 -0
- package/dist/runtime/structuredOutput.js.map +1 -0
- package/dist/utils/ProgressIndicator.d.ts +21 -0
- package/dist/utils/ProgressIndicator.js +30 -3
- package/dist/utils/ProgressIndicator.js.map +1 -1
- package/dist/utils/aiignoreUtils.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +52 -0
- package/dist/utils/consoleUtils.js +72 -2
- package/dist/utils/consoleUtils.js.map +1 -1
- package/dist/utils/crashHandler.d.ts +87 -0
- package/dist/utils/crashHandler.js +128 -0
- package/dist/utils/crashHandler.js.map +1 -0
- package/dist/utils/debugDump.d.ts +134 -0
- package/dist/utils/debugDump.js +381 -0
- package/dist/utils/debugDump.js.map +1 -0
- package/dist/utils/debugUtils.d.ts +13 -4
- package/dist/utils/debugUtils.js +36 -13
- package/dist/utils/debugUtils.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +53 -0
- package/dist/utils/displayWidth.js +195 -0
- package/dist/utils/displayWidth.js.map +1 -0
- package/dist/utils/fileUtils.d.ts +24 -2
- package/dist/utils/fileUtils.js +54 -12
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/llmUtils.d.ts +39 -8
- package/dist/utils/llmUtils.js +76 -8
- package/dist/utils/llmUtils.js.map +1 -1
- package/dist/utils/redactSecrets.d.ts +63 -0
- package/dist/utils/redactSecrets.js +286 -0
- package/dist/utils/redactSecrets.js.map +1 -0
- package/dist/utils/systemPromptNotes.d.ts +225 -0
- package/dist/utils/systemPromptNotes.js +340 -0
- package/dist/utils/systemPromptNotes.js.map +1 -0
- package/dist/utils/systemUtils.d.ts +49 -1
- package/dist/utils/systemUtils.js +67 -3
- package/dist/utils/systemUtils.js.map +1 -1
- package/dist/utils/toolMatching.d.ts +30 -0
- package/dist/utils/toolMatching.js +44 -0
- package/dist/utils/toolMatching.js.map +1 -0
- package/dist/utils/untrustedText.d.ts +66 -0
- package/dist/utils/untrustedText.js +80 -0
- package/dist/utils/untrustedText.js.map +1 -0
- package/package.json +22 -6
- package/schema/gsloth-config.schema.json +3134 -0
- package/dist/core/shell/allowlist.d.ts +0 -75
- package/dist/core/shell/allowlist.js +0 -187
- package/dist/core/shell/allowlist.js.map +0 -1
- package/dist/core/shell/judge.d.ts +0 -161
- package/dist/core/shell/judge.js +0 -261
- package/dist/core/shell/judge.js.map +0 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { applyRungAwareToolDescriptions, } from '#src/config.js';
|
|
1
2
|
import { StatusLevel, } from '#src/core/types.js';
|
|
3
|
+
import { accumulateMessage, createRunStatsAccumulator, finalizeRunStats, } from '#src/core/runStats.js';
|
|
4
|
+
import { collectDeclaredMcpToolAnnotations } from '#src/core/approvals/toolAnnotationSources.js';
|
|
5
|
+
import { createPlainToolIndication } from '#src/core/plainToolIndication.js';
|
|
2
6
|
import { debugLog, debugLogError, debugLogObject } from '#src/utils/debugUtils.js';
|
|
3
7
|
import { ProgressIndicator } from '#src/utils/ProgressIndicator.js';
|
|
4
8
|
import { stopWaitingForEscape, waitForEscape } from '#src/utils/systemUtils.js';
|
|
@@ -6,6 +10,112 @@ import { AIMessage, AIMessageChunk, ToolMessage } from '@langchain/core/messages
|
|
|
6
10
|
import { IterableReadableStream } from '@langchain/core/utils/stream';
|
|
7
11
|
import { interrupt, Command, GraphInterrupt } from '@langchain/langgraph';
|
|
8
12
|
import { extractInlineBinaryBlocks, materializeBinaryOutputs, renderAssistantContent, } from '#src/utils/binaryOutputUtils.js';
|
|
13
|
+
import { detectRefusal, buildRefusalMessage } from '#src/core/refusal.js';
|
|
14
|
+
import { answerTextOf, segmentAssistantContent, stripReasoningBlocks, } from '#src/core/reasoningBlocks.js';
|
|
15
|
+
const THINK_OPEN = '<think>';
|
|
16
|
+
const THINK_CLOSE = '</think>';
|
|
17
|
+
/**
|
|
18
|
+
* TUI-C22 — length of the longest suffix of `s` that is a *proper* (shorter-than-full) prefix of
|
|
19
|
+
* `tag`. Used by {@link createThinkTagSplitter} to hold back a trailing partial that might complete
|
|
20
|
+
* into `tag` on the next chunk (e.g. a chunk ending in `<thi` when the tag is `<think>`).
|
|
21
|
+
*/
|
|
22
|
+
function trailingPartialLen(s, tag) {
|
|
23
|
+
const max = Math.min(s.length, tag.length - 1);
|
|
24
|
+
for (let k = max; k > 0; k--) {
|
|
25
|
+
if (s.slice(s.length - k) === tag.slice(0, k))
|
|
26
|
+
return k;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* TUI-C22 — stateful separator of inline `<think>...</think>` thinking from answer text, robust to
|
|
32
|
+
* tags split across streamed chunks. Many thinking models served over an OpenAI-compatible `/v1`
|
|
33
|
+
* shim (qwen3 / deepseek-r1 over Ollama) inline their reasoning as `<think>…</think>` in the
|
|
34
|
+
* message `content` rather than in `additional_kwargs.reasoning_content`; without this it would
|
|
35
|
+
* render as answer text and the `/reasoning` panel would stay empty.
|
|
36
|
+
*
|
|
37
|
+
* `push(text)` returns the segments it can classify unambiguously *now*, buffering any trailing
|
|
38
|
+
* partial tag (so a `<think>` arriving as `<thi` + `nk>` across two chunks is still detected) and
|
|
39
|
+
* the run of thinking between an open and a not-yet-seen close tag. `flush()` drains the buffer at
|
|
40
|
+
* a message/stream boundary: an unterminated `<think>` at EOF yields its remainder as reasoning; a
|
|
41
|
+
* dangling non-tag partial (e.g. a lone `<` or `<thi` that never completed) yields as answer, so no
|
|
42
|
+
* text is ever dropped. Purely additive — text with no `<think>` passes straight through as answer.
|
|
43
|
+
*/
|
|
44
|
+
function createThinkTagSplitter() {
|
|
45
|
+
let buffer = '';
|
|
46
|
+
let inThink = false;
|
|
47
|
+
function push(text) {
|
|
48
|
+
const segments = [];
|
|
49
|
+
if (text.length === 0 && buffer.length === 0)
|
|
50
|
+
return segments;
|
|
51
|
+
buffer += text;
|
|
52
|
+
for (;;) {
|
|
53
|
+
if (inThink) {
|
|
54
|
+
const idx = buffer.indexOf(THINK_CLOSE);
|
|
55
|
+
if (idx >= 0) {
|
|
56
|
+
if (idx > 0)
|
|
57
|
+
segments.push({ kind: 'reasoning', text: buffer.slice(0, idx) });
|
|
58
|
+
buffer = buffer.slice(idx + THINK_CLOSE.length);
|
|
59
|
+
inThink = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// No full close tag yet — emit reasoning except a trailing partial of `</think>`.
|
|
63
|
+
const hold = trailingPartialLen(buffer, THINK_CLOSE);
|
|
64
|
+
const emit = buffer.slice(0, buffer.length - hold);
|
|
65
|
+
if (emit.length > 0)
|
|
66
|
+
segments.push({ kind: 'reasoning', text: emit });
|
|
67
|
+
buffer = hold > 0 ? buffer.slice(buffer.length - hold) : '';
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const idx = buffer.indexOf(THINK_OPEN);
|
|
72
|
+
if (idx >= 0) {
|
|
73
|
+
if (idx > 0)
|
|
74
|
+
segments.push({ kind: 'answer', text: buffer.slice(0, idx) });
|
|
75
|
+
buffer = buffer.slice(idx + THINK_OPEN.length);
|
|
76
|
+
inThink = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// No full open tag yet — emit answer except a trailing partial of `<think>`.
|
|
80
|
+
const hold = trailingPartialLen(buffer, THINK_OPEN);
|
|
81
|
+
const emit = buffer.slice(0, buffer.length - hold);
|
|
82
|
+
if (emit.length > 0)
|
|
83
|
+
segments.push({ kind: 'answer', text: emit });
|
|
84
|
+
buffer = hold > 0 ? buffer.slice(buffer.length - hold) : '';
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
|
90
|
+
function flush() {
|
|
91
|
+
const segments = [];
|
|
92
|
+
if (buffer.length > 0) {
|
|
93
|
+
segments.push({ kind: inThink ? 'reasoning' : 'answer', text: buffer });
|
|
94
|
+
}
|
|
95
|
+
buffer = '';
|
|
96
|
+
inThink = false;
|
|
97
|
+
return segments;
|
|
98
|
+
}
|
|
99
|
+
return { push, flush };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Pick this chunk's or message's reasoning delta/content.
|
|
103
|
+
* Precedence:
|
|
104
|
+
* 1. `additional_kwargs.reasoning_content` — standard DeepSeek/Anthropic/OpenRouter convention.
|
|
105
|
+
* 2. `additional_kwargs.reasoning` — direct reasoning fallback if present.
|
|
106
|
+
*/
|
|
107
|
+
function pickReasoningDelta(kwargs) {
|
|
108
|
+
if (!kwargs)
|
|
109
|
+
return '';
|
|
110
|
+
const reasoningContent = kwargs.reasoning_content;
|
|
111
|
+
if (typeof reasoningContent === 'string' && reasoningContent.length > 0) {
|
|
112
|
+
return reasoningContent;
|
|
113
|
+
}
|
|
114
|
+
const direct = kwargs.reasoning;
|
|
115
|
+
if (typeof direct === 'string' && direct.length > 0)
|
|
116
|
+
return direct;
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
9
119
|
/**
|
|
10
120
|
* Shared, graph-agnostic agent plumbing.
|
|
11
121
|
*
|
|
@@ -25,12 +135,183 @@ export class GthAbstractAgent {
|
|
|
25
135
|
agent = null;
|
|
26
136
|
config = null;
|
|
27
137
|
command = undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Opt-in debug sink for the TUI `/debug` panel. Set AFTER {@link init} via
|
|
140
|
+
* `runner.getAgent()`; read lazily inside each backend's `wrapModelCall` capture middleware
|
|
141
|
+
* so that when it is `undefined` (the normal path) the middleware is a transparent
|
|
142
|
+
* pass-through. Lives on the base so BOTH the lean and deep backends support it; the AG-UI
|
|
143
|
+
* server / non-TUI callers simply never set it, so those contracts are unchanged.
|
|
144
|
+
*/
|
|
145
|
+
debugCapture;
|
|
146
|
+
/**
|
|
147
|
+
* GS2-56 — the ALWAYS-ON snapshot of the most recent model request (extras + the as-sent,
|
|
148
|
+
* post-summarization messages), populated UNCONDITIONALLY at each backend's `wrapModelCall` feed
|
|
149
|
+
* site — NOT gated on {@link debugCapture} being attached. This is what lets `/debug-dump` render
|
|
150
|
+
* the full model input even when the TUI `/debug` panel was never opened and on non-TUI surfaces
|
|
151
|
+
* (the sink only ever fed the live `/debug` panel). O(1): a single overwritten reference retaining
|
|
152
|
+
* only the LAST call — no accumulation, so the "pay nothing until you need it" intent is kept.
|
|
153
|
+
*/
|
|
154
|
+
lastModelRequest;
|
|
155
|
+
/**
|
|
156
|
+
* GS2-56 — stash the last model request (the as-sent messages + {@link DebugRequestExtras}).
|
|
157
|
+
* Called unconditionally from each backend's capture middleware, independent of the debug sink,
|
|
158
|
+
* so the snapshot is available to `/debug-dump` on every surface. Overwrites (retains only the
|
|
159
|
+
* most recent call). Callers already guard the invocation; kept trivial so it can never throw.
|
|
160
|
+
*/
|
|
161
|
+
setLastModelRequest(messages, extras) {
|
|
162
|
+
this.lastModelRequest = { messages, extras };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* GS2-16 — per-run analytics tally (token usage + invoked tool names) folded from the messages
|
|
166
|
+
* flowing through {@link invoke} / the streaming paths. Reset at each turn boundary via
|
|
167
|
+
* {@link resetRunStats} (the runner is reused across turns), read via {@link getRunStats}, and
|
|
168
|
+
* fully fail-soft (accumulation is guarded and never throws into a run).
|
|
169
|
+
*/
|
|
170
|
+
runStatsAcc = createRunStatsAccumulator();
|
|
171
|
+
/**
|
|
172
|
+
* EXT-58 — the names of the tools registered with the graph at the last {@link init}, recorded by
|
|
173
|
+
* {@link registerApprovalsAwareTools}. Read by `GthAgentRunner` to build the rater's
|
|
174
|
+
* granted-built-in list (§4.4), so a suggestion can only ever name a tool the model actually has.
|
|
175
|
+
*/
|
|
176
|
+
registeredToolNames = [];
|
|
177
|
+
/**
|
|
178
|
+
* EXT-70 §4.7.1 — what the connected MCP servers DECLARED about their own tools, captured from
|
|
179
|
+
* the same registration hook as {@link registeredToolNames} and keyed by the registered tool
|
|
180
|
+
* name. Read by `GthAgentRunner` as the `mcp` half of a `DeclaredToolAnnotationLookup`.
|
|
181
|
+
*
|
|
182
|
+
* It is a record of claims, never of decisions: no trust is applied here (that is
|
|
183
|
+
* `createEffectiveToolAnnotationSource`'s only job), and an absent tool yields the fail-closed
|
|
184
|
+
* defaults rather than "declared nothing".
|
|
185
|
+
*/
|
|
186
|
+
declaredMcpToolAnnotations = new Map();
|
|
28
187
|
constructor(statusUpdate, resolvers) {
|
|
29
188
|
this.statusUpdate = (level, message) => {
|
|
30
189
|
statusUpdate(level, message);
|
|
31
190
|
};
|
|
32
191
|
this.resolvers = resolvers;
|
|
33
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* GS2-63 — emit one line of the technical run-header preamble (the Workdir/Model/Tools/Middleware
|
|
195
|
+
* block) UNLESS it is opted out via `output.header: false`. The opt-out only ever reaches here in
|
|
196
|
+
* non-TUI text modes: the interactive TUI forces `output.header` on before init (see
|
|
197
|
+
* `createTuiSession`), and the TUI event path never goes through the interrupt-hint site, so the
|
|
198
|
+
* whole preamble stays visible there. Only INFO header lines route through this — real model/tool
|
|
199
|
+
* output, warnings and errors keep using {@link statusUpdate} directly.
|
|
200
|
+
*/
|
|
201
|
+
headerStatus(message) {
|
|
202
|
+
if (this.config?.output?.header === false)
|
|
203
|
+
return;
|
|
204
|
+
this.statusUpdate(StatusLevel.INFO, message);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* EXT-58 (spec §4.5) — the ONE tool-registration hook both backends call with their final tool
|
|
208
|
+
* array, just before handing it to the graph builder. It does two things:
|
|
209
|
+
*
|
|
210
|
+
* 1. Appends the rung's approval sentence to every tool that is **not** auto-approved at that
|
|
211
|
+
* rung, and leaves every granted tool's description untouched (the absence of the sentence is
|
|
212
|
+
* what marks a tool free). See {@link applyRungAwareToolDescriptions}.
|
|
213
|
+
* 2. Records the registered tool names for {@link getRegisteredToolNames}, which feeds the
|
|
214
|
+
* rater's granted-alternative list (§4.4).
|
|
215
|
+
* 3. EXT-70 §4.7.1 — records what the MCP servers declared about their own tools, for
|
|
216
|
+
* {@link getDeclaredMcpToolAnnotations}. This is the ONE place a `tools/list` annotation
|
|
217
|
+
* enters the approvals stack, and it enters as a claim: nothing here decides whether it is
|
|
218
|
+
* believed.
|
|
219
|
+
*
|
|
220
|
+
* `gatedTools` MUST be the **LIVE gated set for the rung in force** — `resolveGatedToolNames` for
|
|
221
|
+
* that rung — and NOT the set the caller wires into the approval interrupt. The two are different
|
|
222
|
+
* on purpose: the interrupt is installed once, at agent init, and is deliberately
|
|
223
|
+
* rung-independent (`resolveInterruptToolNames`, the union over every rung) so that
|
|
224
|
+
* `/approvals <mode>` can move the mode underneath it for the rest of the session. Passing that
|
|
225
|
+
* wider set here would describe tools as needing approval that the live mode does not gate — and
|
|
226
|
+
* a call the live mode does not gate is auto-approved the moment it reaches the runner, so the
|
|
227
|
+
* sentence would be a promise nothing keeps.
|
|
228
|
+
*
|
|
229
|
+
* What keeps a description from promising an approval the gate will not ask for is therefore that
|
|
230
|
+
* both this and `GthAgentRunner`'s own check are projections of the SAME rule,
|
|
231
|
+
* `isToolGatedAtRung`, evaluated against the SAME live mode — §4.5's "a description that disagrees
|
|
232
|
+
* with what the gate will actually do is worse than no description at all".
|
|
233
|
+
*
|
|
234
|
+
* `additionalToolNames` covers tools the graph builder registers itself and that therefore never
|
|
235
|
+
* appear in `tools` — deepagents' own filesystem tools on the deep backend. Their descriptions
|
|
236
|
+
* are deepagents', not ours, so they cannot be suffixed here; they are recorded only so the
|
|
237
|
+
* rater's suggestion list reflects what the deep model actually has.
|
|
238
|
+
*/
|
|
239
|
+
registerApprovalsAwareTools(tools, options) {
|
|
240
|
+
applyRungAwareToolDescriptions(tools, {
|
|
241
|
+
rung: options.rung,
|
|
242
|
+
gatedTools: options.gatedTools,
|
|
243
|
+
});
|
|
244
|
+
const names = tools
|
|
245
|
+
.map((tool) => tool?.name)
|
|
246
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
247
|
+
this.registeredToolNames = [...names, ...(options.additionalToolNames ?? [])];
|
|
248
|
+
// `additionalToolNames` are deliberately NOT consulted: they are names the graph builder
|
|
249
|
+
// registers itself, with no tool object and therefore no declaration to read.
|
|
250
|
+
this.declaredMcpToolAnnotations = collectDeclaredMcpToolAnnotations(tools);
|
|
251
|
+
return tools;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* EXT-58 — the tool names registered with the graph at the last {@link init} (empty before it).
|
|
255
|
+
* The runner intersects these with the built-in summaries table to build the rater's
|
|
256
|
+
* granted-alternative list, so the rater can never name a tool this session does not have.
|
|
257
|
+
*/
|
|
258
|
+
getRegisteredToolNames() {
|
|
259
|
+
return [...this.registeredToolNames];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* EXT-70 §4.7.1 — what the MCP servers declared for their tools at the last {@link init}, keyed
|
|
263
|
+
* by registered tool name (empty before it, and on a session with no MCP servers).
|
|
264
|
+
*/
|
|
265
|
+
getDeclaredMcpToolAnnotations() {
|
|
266
|
+
return this.declaredMcpToolAnnotations;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* GS2-16 — clear the per-run analytics tally so the next turn starts from zero. The runner
|
|
270
|
+
* calls this at each turn boundary because it (and this agent) are reused across turns in an
|
|
271
|
+
* interactive session.
|
|
272
|
+
*/
|
|
273
|
+
resetRunStats() {
|
|
274
|
+
this.runStatsAcc = createRunStatsAccumulator();
|
|
275
|
+
}
|
|
276
|
+
/** GS2-16 — the analytics harvested since the last {@link resetRunStats}. Never throws. */
|
|
277
|
+
getRunStats() {
|
|
278
|
+
return finalizeRunStats(this.runStatsAcc);
|
|
279
|
+
}
|
|
280
|
+
/** GS2-16 — fold one message (or chunk) into the run tally. Fully guarded (fail-soft). */
|
|
281
|
+
recordRunStats(message) {
|
|
282
|
+
accumulateMessage(this.runStatsAcc, message);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* GS2-16 — best-effort count of messages already in the checkpointed thread state, used by
|
|
286
|
+
* {@link invoke} as the baseline so it harvests only THIS turn's new messages rather than the
|
|
287
|
+
* whole accumulated conversation a checkpointer returns. Fail-soft: a missing `getState`, an odd
|
|
288
|
+
* state shape, or any error yields 0 (worst case a one-turn over-count, never a throw).
|
|
289
|
+
*/
|
|
290
|
+
async getStateMessageCount(runConfig) {
|
|
291
|
+
try {
|
|
292
|
+
if (!this.agent || typeof this.agent.getState !== 'function')
|
|
293
|
+
return 0;
|
|
294
|
+
const state = await this.agent.getState(runConfig);
|
|
295
|
+
const messages = state?.values?.messages;
|
|
296
|
+
return Array.isArray(messages) ? messages.length : 0;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* EXT-37 — surface a detected content-policy refusal: emit the clear, user-facing explanation at
|
|
304
|
+
* WARNING level (an empty-content refusal streams nothing, so without this the console shows
|
|
305
|
+
* nothing) and return the same message so it becomes the turn's terminal answer. Shared by the
|
|
306
|
+
* non-streaming {@link invoke} and streaming {@link streamFromInput} paths so both render a
|
|
307
|
+
* refusal identically. A refusal is a *successful* (if declined) response — never a retry.
|
|
308
|
+
*/
|
|
309
|
+
surfaceRefusal(info) {
|
|
310
|
+
const message = buildRefusalMessage(info);
|
|
311
|
+
debugLog(`Content-policy refusal detected (provider=${info.provider} reason=${info.reason})`);
|
|
312
|
+
this.statusUpdate(StatusLevel.WARNING, message);
|
|
313
|
+
return message;
|
|
314
|
+
}
|
|
34
315
|
/**
|
|
35
316
|
* Invoke LLM with a message and runnable config.
|
|
36
317
|
* For streaming use {@link #stream} method, streaming is preferred if model API supports it.
|
|
@@ -48,9 +329,50 @@ export class GthAbstractAgent {
|
|
|
48
329
|
const progress = new ProgressIndicator('Thinking.');
|
|
49
330
|
try {
|
|
50
331
|
debugLog('Calling agent.invoke...');
|
|
332
|
+
// GS2-16: capture the prior conversation length BEFORE invoking so we harvest ONLY this
|
|
333
|
+
// turn's NEW messages. With a checkpointer + persistent thread (a multi-turn `--no-tui`
|
|
334
|
+
// interactive session with `streamOutput: false`), `response.messages` is the FULL
|
|
335
|
+
// accumulated conversation, not just this turn — folding all of it would re-sum prior
|
|
336
|
+
// turns' usage_metadata and re-collect prior tools (per-turn over-count). This baseline
|
|
337
|
+
// slice also prevents a double-harvest by the empty-stream fallback invoke in
|
|
338
|
+
// GthAgentRunner: by then the streamed turn is checkpointed, so it is BEFORE the baseline.
|
|
339
|
+
// Fail-soft: an unreadable baseline yields 0 (a one-turn over-count at worst, never a throw).
|
|
340
|
+
const priorMessageCount = await this.getStateMessageCount(runConfig);
|
|
51
341
|
const response = await this.agent.invoke({ messages }, runConfig);
|
|
342
|
+
// Harvest token usage + invoked tool names from THIS turn's new messages only (fail-soft)
|
|
343
|
+
// so the opt-in history recorder can populate `gth insights`.
|
|
344
|
+
// TUI-C32 residual f — the streaming path renders the compact per-tool indication via
|
|
345
|
+
// streamFromInput's observer; the non-streaming invoke path (`streamOutput: false`) had
|
|
346
|
+
// none, so a plain-surface tool call surfaced nothing after the legacy fs notices were
|
|
347
|
+
// dropped (residual b). Feed THIS turn's new messages through the SAME observer so each
|
|
348
|
+
// tool call gets its `✓ 📁 name(args…)` block here too. Only the plain surface reaches
|
|
349
|
+
// invoke (the TUI uses processMessagesWithEvents); observe() is fail-soft internally.
|
|
350
|
+
const allMessages = Array.isArray(response.messages) ? response.messages : [];
|
|
351
|
+
const toolIndication = createPlainToolIndication();
|
|
352
|
+
for (const m of allMessages.slice(priorMessageCount)) {
|
|
353
|
+
this.recordRunStats(m);
|
|
354
|
+
toolIndication.observe(m);
|
|
355
|
+
}
|
|
52
356
|
const finalMessage = response.messages[response.messages.length - 1];
|
|
53
|
-
|
|
357
|
+
// EXT-37: content-policy refusal. A successful response whose stop/finish reason is a
|
|
358
|
+
// refusal (OpenAI content_filter / Anthropic stop_reason=refusal / Bedrock
|
|
359
|
+
// guardrail_intervened) is terminal-but-clear: surface the model's explanation and RETURN
|
|
360
|
+
// it as the answer. It must NOT flow into the empty-response retry (a refusal is
|
|
361
|
+
// deterministic — retrying just burns a paid call). Returning a non-empty message means the
|
|
362
|
+
// caller writes it to the output file and exits ok, rather than re-wrapping a *successful*
|
|
363
|
+
// (if declined) response as "Failed to get answer". A fallback-model attempt would hang
|
|
364
|
+
// here (see the extension point in GthAgentRunner.processMessages), but no runtime
|
|
365
|
+
// fallback-model config exists today, so we surface terminally.
|
|
366
|
+
const refusal = detectRefusal(finalMessage);
|
|
367
|
+
if (refusal) {
|
|
368
|
+
return this.surfaceRefusal(refusal);
|
|
369
|
+
}
|
|
370
|
+
// CFG-33: Gemini's thought summaries ride inside `content` as `thought: true` text blocks,
|
|
371
|
+
// which renderAssistantContent would print as part of the answer (and write to the output
|
|
372
|
+
// file). The plain surface has never shown reasoning — every other provider's arrives
|
|
373
|
+
// out-of-band in additional_kwargs — so drop them for rendering only; graph state keeps the
|
|
374
|
+
// message whole so the thought parts still replay as history.
|
|
375
|
+
const finalContent = stripReasoningBlocks(finalMessage?.content);
|
|
54
376
|
const processedContent = !this.config.writeBinaryOutputsToFile
|
|
55
377
|
? {
|
|
56
378
|
renderedContent: renderAssistantContent(finalContent),
|
|
@@ -127,6 +449,17 @@ export class GthAbstractAgent {
|
|
|
127
449
|
const statusUpdate = this.statusUpdate;
|
|
128
450
|
const config = this.config;
|
|
129
451
|
const command = this.command;
|
|
452
|
+
// GS2-16: bound so the stream `start()` closure (whose `this` is the stream source, not the
|
|
453
|
+
// agent) can fold each chunk into the run tally. Fail-soft inside recordRunStats.
|
|
454
|
+
const recordRunStats = (m) => this.recordRunStats(m);
|
|
455
|
+
// EXT-37: bound so the stream `start()` closure can surface a detected refusal (WARNING +
|
|
456
|
+
// returns the message to enqueue) without a `this` reference.
|
|
457
|
+
const surfaceRefusal = (info) => this.surfaceRefusal(info);
|
|
458
|
+
// TUI-C30 — compact per-tool-call indication for the plain surface (`name(args…)` + the
|
|
459
|
+
// canonical 10-line greyed preview when each ToolMessage lands). Per-stream state; emits at
|
|
460
|
+
// INFO level so the existing consoleLevel gate governs it like the historical tool notices.
|
|
461
|
+
// The TUI never runs this string path (it renders the typed event stream itself).
|
|
462
|
+
const toolIndication = createPlainToolIndication();
|
|
130
463
|
const interruptState = { escape: false, messageShown: false };
|
|
131
464
|
const abortController = new AbortController();
|
|
132
465
|
const showInterruptMessage = () => {
|
|
@@ -141,7 +474,11 @@ export class GthAbstractAgent {
|
|
|
141
474
|
if (!abortController.signal.aborted) {
|
|
142
475
|
abortController.abort();
|
|
143
476
|
}
|
|
144
|
-
}, this.config.canInterruptInferenceWithEsc
|
|
477
|
+
}, this.config.canInterruptInferenceWithEsc,
|
|
478
|
+
// GS2-63: the interrupt hint is part of the run-header preamble. Suppress the hint box (while
|
|
479
|
+
// still arming the Esc/Q handler) when the header is opted out. This site only runs in the
|
|
480
|
+
// non-TUI text path (`streamFromInput`); the TUI event path never reaches it.
|
|
481
|
+
this.config.output?.header !== false);
|
|
145
482
|
let stream;
|
|
146
483
|
try {
|
|
147
484
|
stream = await this.agent.stream(input, {
|
|
@@ -166,10 +503,44 @@ export class GthAbstractAgent {
|
|
|
166
503
|
let totalChunks = 0;
|
|
167
504
|
const seenBinaryBlocks = new Set();
|
|
168
505
|
const binaryBlocks = [];
|
|
506
|
+
// EXT-37: a content-policy refusal's stop/finish reason rides on a chunk's
|
|
507
|
+
// response_metadata (usually with empty content). Capture it here and surface it AFTER
|
|
508
|
+
// the stream drains, so the returned text is non-empty and the run loop treats it as
|
|
509
|
+
// terminal-but-clear instead of routing an empty streamed turn into the retry.
|
|
510
|
+
let refusalInfo = null;
|
|
511
|
+
// EXT-41: belt-and-suspenders — also concat the AI chunks so a refusal can be read off
|
|
512
|
+
// the FINAL aggregated message's stop/finish reason, not only a per-chunk one. Some
|
|
513
|
+
// providers surface the reason only on the assembled message (or split it across chunks
|
|
514
|
+
// that concat into it); without this fallback such a refusal would be swallowed by the
|
|
515
|
+
// empty-response retry, making the EXT-37 surfacing cosmetic on the DEFAULT streaming
|
|
516
|
+
// surface. Reset at each tool round (below) so a prior round's reason can't concatenate
|
|
517
|
+
// with the final turn's (mirrors processEventStream's per-round reset).
|
|
518
|
+
let aggregatedChunk = null;
|
|
169
519
|
for await (const [chunk, _metadata] of stream) {
|
|
170
520
|
debugLogObject('Stream chunk', { chunk, _metadata });
|
|
521
|
+
// GS2-16: fold every chunk (AIMessageChunk usage/tool_calls, ToolMessage name) into
|
|
522
|
+
// the run tally before the text-only handling below.
|
|
523
|
+
recordRunStats(chunk);
|
|
524
|
+
// EXT-37: first refusal signal wins; keep scanning chunks for text/binary as normal.
|
|
525
|
+
if (!refusalInfo) {
|
|
526
|
+
refusalInfo = detectRefusal(chunk);
|
|
527
|
+
}
|
|
528
|
+
// EXT-41: fold AI chunks into an aggregate for the aggregate-level refusal fallback,
|
|
529
|
+
// resetting at tool-round boundaries so a prior round's stop/finish reason can't bleed
|
|
530
|
+
// into the final turn's aggregate.
|
|
531
|
+
if (AIMessageChunk.isInstance(chunk)) {
|
|
532
|
+
aggregatedChunk = aggregatedChunk ? aggregatedChunk.concat(chunk) : chunk;
|
|
533
|
+
}
|
|
534
|
+
else if (chunk instanceof ToolMessage) {
|
|
535
|
+
aggregatedChunk = null;
|
|
536
|
+
}
|
|
537
|
+
// TUI-C30: fold the chunk into the plain-surface tool indication (renders each
|
|
538
|
+
// completed call when its ToolMessage arrives; a no-op for plain text chunks).
|
|
539
|
+
toolIndication.observe(chunk);
|
|
171
540
|
if (AIMessage.isInstance(chunk)) {
|
|
172
|
-
|
|
541
|
+
// CFG-33: the ANSWER text only. `.text` folds Gemini's `thought: true` blocks into the
|
|
542
|
+
// answer, which would print the model's thinking inline here and in the output file.
|
|
543
|
+
const text = answerTextOf(chunk.content);
|
|
173
544
|
totalChunks++;
|
|
174
545
|
if (text.length > 0) {
|
|
175
546
|
statusUpdate(StatusLevel.STREAM, text);
|
|
@@ -202,6 +573,19 @@ export class GthAbstractAgent {
|
|
|
202
573
|
statusUpdate(StatusLevel.SUCCESS, successMessage);
|
|
203
574
|
}
|
|
204
575
|
}
|
|
576
|
+
// EXT-41: aggregate-level fallback — if no per-chunk metadata flagged a refusal, inspect
|
|
577
|
+
// the FINAL aggregated message's stop/finish reason. Catches providers that expose the
|
|
578
|
+
// reason only on the assembled message (or split across chunks that concat into it).
|
|
579
|
+
if (!refusalInfo && aggregatedChunk) {
|
|
580
|
+
refusalInfo = detectRefusal(aggregatedChunk);
|
|
581
|
+
}
|
|
582
|
+
// EXT-37: surface a captured refusal as the terminal answer. Enqueue the clear message
|
|
583
|
+
// (so the drained result is non-empty and bypasses the empty-response retry) and print it
|
|
584
|
+
// once at WARNING level (surfaceRefusal). Any partial content already streamed is kept;
|
|
585
|
+
// the refusal notice follows it, and its explanation carries any model-provided text.
|
|
586
|
+
if (refusalInfo) {
|
|
587
|
+
controller.enqueue(surfaceRefusal(refusalInfo));
|
|
588
|
+
}
|
|
205
589
|
debugLog(`Stream completed. Total chunks: ${totalChunks}`);
|
|
206
590
|
controller.close();
|
|
207
591
|
}
|
|
@@ -363,6 +747,52 @@ export class GthAbstractAgent {
|
|
|
363
747
|
let aggregatedAIChunk = null;
|
|
364
748
|
let reasoningOpen = false;
|
|
365
749
|
const flushed = new Set();
|
|
750
|
+
// EXT-41: a content-policy refusal on this typed-event path was de-scoped by EXT-37 (there is
|
|
751
|
+
// no empty-response retry here, so no wrong-retry bug), but it still rendered as a SILENT empty
|
|
752
|
+
// turn. Capture it (first per-chunk signal wins; aggregate fallback at stream end) and surface
|
|
753
|
+
// it as a `text` event so every consumer (Ink TUI viewModel, AG-UI SSE) shows a clear notice.
|
|
754
|
+
let refusalInfo = null;
|
|
755
|
+
// TUI-C22 — one splitter for the whole stream so a <think> opened in one chunk and closed
|
|
756
|
+
// several chunks later is tracked across the boundary. Reset at message boundaries via flush().
|
|
757
|
+
const thinkSplitter = createThinkTagSplitter();
|
|
758
|
+
// TUI-C22 — emit ordered answer/reasoning segments, opening/closing the reasoning block as the
|
|
759
|
+
// kind switches. Shares `reasoningOpen` with the reasoning_content path so the two compose
|
|
760
|
+
// (a reasoning_content delta then think-derived reasoning stays one open block; answer text
|
|
761
|
+
// closes it), preserving the exact existing event sequence when no <think> tags are present.
|
|
762
|
+
function* emitSegments(segments) {
|
|
763
|
+
for (const seg of segments) {
|
|
764
|
+
if (seg.text.length === 0)
|
|
765
|
+
continue;
|
|
766
|
+
if (seg.kind === 'reasoning') {
|
|
767
|
+
if (!reasoningOpen) {
|
|
768
|
+
reasoningOpen = true;
|
|
769
|
+
yield { type: 'reasoning_start' };
|
|
770
|
+
}
|
|
771
|
+
yield { type: 'reasoning_delta', delta: seg.text };
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
if (reasoningOpen) {
|
|
775
|
+
reasoningOpen = false;
|
|
776
|
+
yield { type: 'reasoning_end' };
|
|
777
|
+
}
|
|
778
|
+
yield { type: 'text', delta: seg.text };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// CFG-33 — emit a message's content segments in order. A segment already classified as the
|
|
783
|
+
// model's thinking (a Gemini `thought: true` block) goes straight to the reasoning channel;
|
|
784
|
+
// answer text still passes through the TUI-C22 think splitter, so an inline `<think>` tag is
|
|
785
|
+
// peeled exactly as before. With no reasoning block this is the previous `.text` behaviour.
|
|
786
|
+
function* emitContentSegments(segments) {
|
|
787
|
+
for (const segment of segments) {
|
|
788
|
+
if (segment.kind === 'reasoning') {
|
|
789
|
+
yield* emitSegments([segment]);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
yield* emitSegments(thinkSplitter.push(segment.text));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
366
796
|
function* flushAggregated() {
|
|
367
797
|
if (!aggregatedAIChunk)
|
|
368
798
|
return;
|
|
@@ -391,30 +821,41 @@ export class GthAbstractAgent {
|
|
|
391
821
|
}
|
|
392
822
|
for await (const [chunk, _metadata] of stream) {
|
|
393
823
|
debugLogObject('streamWithEvents chunk', { chunk, _metadata });
|
|
824
|
+
// GS2-16: fold every chunk (AIMessageChunk usage/tool_calls, ToolMessage name) into the
|
|
825
|
+
// run tally so the TUI turn can record real token/tool data. Fail-soft.
|
|
826
|
+
this.recordRunStats(chunk);
|
|
827
|
+
// EXT-41: reuse EXT-37's detector (do NOT fork a second one). First per-chunk signal wins;
|
|
828
|
+
// a ToolMessage / normal chunk yields null, so a normal turn never surfaces a false refusal.
|
|
829
|
+
if (!refusalInfo) {
|
|
830
|
+
refusalInfo = detectRefusal(chunk);
|
|
831
|
+
}
|
|
394
832
|
if (AIMessageChunk.isInstance(chunk)) {
|
|
395
833
|
aggregatedAIChunk = aggregatedAIChunk ? aggregatedAIChunk.concat(chunk) : chunk;
|
|
396
|
-
// Reasoning deltas — Ollama (Qwen3, deepseek-r1) and
|
|
397
|
-
// thinking
|
|
398
|
-
// separate event series so clients can render it apart from the answer.
|
|
399
|
-
const reasoningDelta = chunk.additional_kwargs
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
reasoningOpen = true;
|
|
403
|
-
yield { type: 'reasoning_start' };
|
|
404
|
-
}
|
|
405
|
-
yield { type: 'reasoning_delta', delta: reasoningDelta };
|
|
406
|
-
}
|
|
407
|
-
// Yield text incrementally — use this chunk's text (delta), not the
|
|
408
|
-
// aggregated content which is cumulative.
|
|
409
|
-
if (chunk.text) {
|
|
410
|
-
if (reasoningOpen) {
|
|
411
|
-
reasoningOpen = false;
|
|
412
|
-
yield { type: 'reasoning_end' };
|
|
413
|
-
}
|
|
414
|
-
yield { type: 'text', delta: chunk.text };
|
|
834
|
+
// Reasoning deltas — Ollama (Qwen3, deepseek-r1), Anthropic, and OpenRouter surface
|
|
835
|
+
// thinking in additional_kwargs.reasoning_content. Stream
|
|
836
|
+
// it as a separate event series so clients can render it apart from the answer.
|
|
837
|
+
const reasoningDelta = pickReasoningDelta(chunk.additional_kwargs);
|
|
838
|
+
if (reasoningDelta.length > 0) {
|
|
839
|
+
yield* emitSegments([{ kind: 'reasoning', text: reasoningDelta }]);
|
|
415
840
|
}
|
|
841
|
+
// Yield text incrementally — use this chunk's text (delta), not the aggregated content
|
|
842
|
+
// which is cumulative. TUI-C22 routes it through the think splitter so inline
|
|
843
|
+
// <think>...</think> (buffered across chunks) is peeled into the reasoning channel and
|
|
844
|
+
// stripped from the answer; text with no think tags passes straight through unchanged.
|
|
845
|
+
// CFG-33 classifies the chunk's content blocks first, in order, so Gemini's `thought: true`
|
|
846
|
+
// blocks reach the reasoning channel instead of the answer; answer text still goes through
|
|
847
|
+
// the think splitter, thought text does not (it is already classified).
|
|
848
|
+
yield* emitContentSegments(segmentAssistantContent(chunk.content));
|
|
416
849
|
}
|
|
417
850
|
else if (AIMessage.isInstance(chunk)) {
|
|
851
|
+
// Reasoning on a non-chunk AIMessage — a non-streamed / resumed thinking message
|
|
852
|
+
// (e.g. a checkpoint replay) still carries its thinking in
|
|
853
|
+
// additional_kwargs.reasoning_content. Mirror the AIMessageChunk branch and emit the same
|
|
854
|
+
// reasoning event series, otherwise the thought is silently dropped (TUI-C15).
|
|
855
|
+
const reasoningContent = pickReasoningDelta(chunk.additional_kwargs);
|
|
856
|
+
if (reasoningContent.length > 0) {
|
|
857
|
+
yield* emitSegments([{ kind: 'reasoning', text: reasoningContent }]);
|
|
858
|
+
}
|
|
418
859
|
// Non-chunk AIMessage (e.g. on resumed runs) carries final tool_calls
|
|
419
860
|
// directly; merge them into the aggregate so flushAggregated emits them.
|
|
420
861
|
if (chunk.tool_calls && chunk.tool_calls.length > 0) {
|
|
@@ -424,15 +865,17 @@ export class GthAbstractAgent {
|
|
|
424
865
|
});
|
|
425
866
|
aggregatedAIChunk = aggregatedAIChunk ? aggregatedAIChunk.concat(synthetic) : synthetic;
|
|
426
867
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
yield { type: 'text', delta: chunk.text };
|
|
433
|
-
}
|
|
868
|
+
yield* emitContentSegments(segmentAssistantContent(chunk.content));
|
|
869
|
+
// A non-chunk AIMessage is a COMPLETE message, not a delta — drain any residual now
|
|
870
|
+
// (an unterminated <think> becomes reasoning, a dangling partial becomes answer) so its
|
|
871
|
+
// buffered state never leaks into a subsequent message (TUI-C22).
|
|
872
|
+
yield* emitSegments(thinkSplitter.flush());
|
|
434
873
|
}
|
|
435
874
|
if (chunk instanceof ToolMessage) {
|
|
875
|
+
// TUI-C22 — drain buffered think text (emitting its segments, which may open/close
|
|
876
|
+
// reasoning) BEFORE closing the reasoning block, so a trailing reasoning slice can't land
|
|
877
|
+
// after reasoning_end or be dropped. A tool round ends the assistant message, so reset.
|
|
878
|
+
yield* emitSegments(thinkSplitter.flush());
|
|
436
879
|
if (reasoningOpen) {
|
|
437
880
|
reasoningOpen = false;
|
|
438
881
|
yield { type: 'reasoning_end' };
|
|
@@ -444,15 +887,38 @@ export class GthAbstractAgent {
|
|
|
444
887
|
// and end up with empty args.
|
|
445
888
|
aggregatedAIChunk = null;
|
|
446
889
|
const content = typeof chunk.content === 'string' ? chunk.content : JSON.stringify(chunk.content);
|
|
447
|
-
|
|
890
|
+
// Surface the real tool-result error signal (LangChain `ToolMessage.status`) so
|
|
891
|
+
// consumers render the ✗/error affordance from fact, not from sniffing the result
|
|
892
|
+
// text. Only attach the flag on error to keep the success event shape unchanged.
|
|
893
|
+
yield {
|
|
894
|
+
type: 'tool_result',
|
|
895
|
+
id: chunk.tool_call_id,
|
|
896
|
+
content,
|
|
897
|
+
...(chunk.status === 'error' ? { isError: true } : {}),
|
|
898
|
+
};
|
|
448
899
|
}
|
|
449
900
|
}
|
|
901
|
+
// TUI-C22 — drain any buffered think text at stream end (an unterminated <think> surfaces as
|
|
902
|
+
// reasoning, a dangling partial as answer) before closing the reasoning block.
|
|
903
|
+
yield* emitSegments(thinkSplitter.flush());
|
|
450
904
|
// Close any still-open reasoning block before flushing tool calls.
|
|
451
905
|
if (reasoningOpen) {
|
|
452
906
|
yield { type: 'reasoning_end' };
|
|
453
907
|
}
|
|
454
908
|
// Flush any tool calls not followed by a ToolMessage (e.g. terminal tool calls).
|
|
455
909
|
yield* flushAggregated();
|
|
910
|
+
// EXT-41: aggregate-level fallback (I-1's robustness on this path too) — if no per-chunk
|
|
911
|
+
// metadata flagged a refusal, inspect the final aggregated message's stop/finish reason. Then
|
|
912
|
+
// surface any refusal as a `text` event so the user sees a clear notice instead of a silent
|
|
913
|
+
// empty turn. No statusUpdate here: consumers render the typed events, and a WARNING would
|
|
914
|
+
// double-render in the TUI.
|
|
915
|
+
if (!refusalInfo && aggregatedAIChunk) {
|
|
916
|
+
refusalInfo = detectRefusal(aggregatedAIChunk);
|
|
917
|
+
}
|
|
918
|
+
if (refusalInfo) {
|
|
919
|
+
debugLog(`Content-policy refusal detected on typed-event path (provider=${refusalInfo.provider} reason=${refusalInfo.reason})`);
|
|
920
|
+
yield { type: 'text', delta: buildRefusalMessage(refusalInfo) };
|
|
921
|
+
}
|
|
456
922
|
}
|
|
457
923
|
async cleanup() {
|
|
458
924
|
debugLog('Cleaning up agent...');
|