@gaunt-sloth/core 2.0.0-alpha.8 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gsloth.review.md +2 -0
- package/README.md +71 -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/configDiscovery.d.ts +79 -0
- package/dist/config/configDiscovery.js +80 -0
- package/dist/config/configDiscovery.js.map +1 -0
- package/dist/config/defaults.d.ts +20 -20
- package/dist/config/defaults.js +10 -8
- package/dist/config/defaults.js.map +1 -1
- 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/loader.d.ts +171 -19
- package/dist/config/loader.js +1054 -144
- package/dist/config/loader.js.map +1 -1
- 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 +2671 -138
- package/dist/config/schema.js +1361 -85
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +899 -111
- package/dist/config/shell-policy.js +800 -70
- package/dist/config/shell-policy.js.map +1 -1
- 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 +352 -41
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +35 -1
- package/dist/config.js +16 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +28 -1
- package/dist/constants.js +28 -1
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +166 -11
- package/dist/core/GthAbstractAgent.js +484 -45
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +543 -57
- package/dist/core/GthAgentRunner.js +1494 -140
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +117 -2
- package/dist/core/GthLangChainAgent.js +602 -29
- 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/promptHeader.d.ts +28 -0
- package/dist/core/approvals/promptHeader.js +62 -0
- package/dist/core/approvals/promptHeader.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 +74 -0
- package/dist/core/debugCapture.js +100 -0
- package/dist/core/debugCapture.js.map +1 -0
- package/dist/core/gthLeanAgentFactory.d.ts +4 -4
- package/dist/core/gthLeanAgentFactory.js +4 -4
- package/dist/core/launchBanner.d.ts +127 -0
- package/dist/core/launchBanner.js +414 -0
- package/dist/core/launchBanner.js.map +1 -0
- package/dist/core/modelLabel.d.ts +19 -0
- package/dist/core/modelLabel.js +26 -0
- package/dist/core/modelLabel.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 +65 -0
- package/dist/core/reasoningBlocks.js +103 -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/runHeader.d.ts +38 -0
- package/dist/core/runHeader.js +42 -0
- package/dist/core/runHeader.js.map +1 -0
- package/dist/core/runStats.d.ts +14 -3
- package/dist/core/runStats.js +48 -3
- package/dist/core/runStats.js.map +1 -1
- package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
- package/dist/core/shell/ShellCommandFailedError.js +3 -4
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
- 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 +123 -0
- package/dist/core/shell/approvalStop.js +269 -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 +103 -0
- package/dist/core/shell/hardline.js +780 -0
- package/dist/core/shell/hardline.js.map +1 -0
- package/dist/core/shell/negotiation.d.ts +328 -0
- package/dist/core/shell/negotiation.js +488 -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 +873 -0
- package/dist/core/shell/rater.js +1454 -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/raterVocabulary.d.ts +121 -0
- package/dist/core/shell/raterVocabulary.js +116 -0
- package/dist/core/shell/raterVocabulary.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 +349 -16
- package/dist/core/types.js.map +1 -1
- package/dist/history/historyFormat.d.ts +12 -3
- package/dist/history/historyFormat.js +50 -8
- package/dist/history/historyFormat.js.map +1 -1
- package/dist/history/historyStore.d.ts +77 -0
- package/dist/history/historyStore.js +173 -6
- package/dist/history/historyStore.js.map +1 -1
- package/dist/history/recordSession.d.ts +10 -1
- package/dist/history/recordSession.js +27 -0
- package/dist/history/recordSession.js.map +1 -1
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +107 -0
- package/dist/providers/configurationPassthrough.js +148 -0
- package/dist/providers/configurationPassthrough.js.map +1 -0
- 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 +60 -0
- package/dist/providers/geminiThinking.js +92 -0
- package/dist/providers/geminiThinking.js.map +1 -0
- package/dist/providers/google-genai.js +18 -1
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.js +12 -0
- 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 +99 -5
- package/dist/providers/modelDiscovery.js +191 -35
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.d.ts +18 -4
- package/dist/providers/ollama.js +67 -37
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +34 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +26 -4
- package/dist/providers/openrouter.js +83 -26
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +19 -1
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.js +20 -0
- 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 +64 -0
- package/dist/runtime/conversation.js +171 -0
- package/dist/runtime/conversation.js.map +1 -0
- package/dist/runtime/singleShot.d.ts +37 -6
- package/dist/runtime/singleShot.js +113 -67
- 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 +95 -0
- package/dist/utils/consoleUtils.js +112 -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.js.map +1 -1
- package/dist/utils/globalConfigUtils.d.ts +14 -2
- package/dist/utils/globalConfigUtils.js +22 -4
- package/dist/utils/globalConfigUtils.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 +222 -0
- package/dist/utils/systemPromptNotes.js +338 -0
- package/dist/utils/systemPromptNotes.js.map +1 -0
- package/dist/utils/systemUtils.d.ts +18 -1
- package/dist/utils/systemUtils.js +38 -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 +86 -0
- package/dist/utils/untrustedText.js +101 -0
- package/dist/utils/untrustedText.js.map +1 -0
- package/package.json +21 -7
- package/schema/gsloth-config.schema.json +1921 -326
- 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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// EXT-35 — promote a text-emitted tool call to a native LangChain tool_call.
|
|
2
|
+
//
|
|
3
|
+
// The LangChain-message adaptation of the openclaw `@openclaw/tool-call-repair` reference
|
|
4
|
+
// (`packages/tool-call-repair/src/promote.ts`'s `promoteStandalonePlainTextToolCallMessage`).
|
|
5
|
+
// Small/local models (Gemma, lmstudio, gpt-oss) often serialise a tool call as assistant TEXT
|
|
6
|
+
// rather than a native `tool_call`; the ReAct graph then sees no tool_calls and ends the turn
|
|
7
|
+
// ("no tool calls = done"), stalling the loop. This module rescues a STANDALONE text-emitted call
|
|
8
|
+
// by rewriting the assistant message into one carrying a native `tool_call`, so the graph routes to
|
|
9
|
+
// the tools node and the loop continues.
|
|
10
|
+
//
|
|
11
|
+
// Three hard gates keep prose from ever being misread as a call:
|
|
12
|
+
// 1. name allow-list — only a call whose tool name is in the bound toolset promotes (an empty
|
|
13
|
+
// allow-list promotes nothing, the prose-safe default);
|
|
14
|
+
// 2. payload-size cap — an oversized JSON blob is not treated as a call;
|
|
15
|
+
// 3. standalone-only — the message content must be ESSENTIALLY JUST the call (enforced by
|
|
16
|
+
// {@link parseStandalonePlainTextToolCallBlocks}'s whole-input walk).
|
|
17
|
+
//
|
|
18
|
+
// Unlike the reference we do NOT set a `stopReason`/`toolUse` flag: langchain's ReAct router keys
|
|
19
|
+
// purely off the presence of `tool_calls` on the last AIMessage, so promoting the tool_calls array
|
|
20
|
+
// is sufficient to continue the loop.
|
|
21
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES, parseStandalonePlainTextToolCallBlocks, } from './payload.js';
|
|
24
|
+
/**
|
|
25
|
+
* A sane default cap on a single repaired call's serialized payload. A text blob larger than this
|
|
26
|
+
* is never treated as a tool call. Overridable per call so a test can assert the cap with a small
|
|
27
|
+
* value; production leaves it at the default. The single source of truth lives in `./payload.js`
|
|
28
|
+
* and is re-exported here under its public name (EXT-43 unified the former duplicated literal).
|
|
29
|
+
*/
|
|
30
|
+
export { MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES } from './payload.js';
|
|
31
|
+
/**
|
|
32
|
+
* Parse assistant `text` as one-or-more STANDALONE text-emitted tool calls and return them as
|
|
33
|
+
* native LangChain tool_calls, or `undefined` when the text is not a standalone call, names a tool
|
|
34
|
+
* outside the allow-list, or exceeds the payload cap. Fresh `id`s are minted per call (the model
|
|
35
|
+
* emitted none). Empty allow-list ⇒ `undefined` (never promote).
|
|
36
|
+
*/
|
|
37
|
+
export function textToNativeToolCalls(text, options) {
|
|
38
|
+
const allowedToolNames = new Set(options.allowedToolNames);
|
|
39
|
+
// Prose-safety default: with no bound tools nothing can be a call.
|
|
40
|
+
if (allowedToolNames.size === 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const trimmed = (text ?? '').trim();
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const parseOptions = {
|
|
48
|
+
allowedToolNames,
|
|
49
|
+
maxPayloadBytes: options.maxPayloadBytes ?? MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES,
|
|
50
|
+
};
|
|
51
|
+
const blocks = parseStandalonePlainTextToolCallBlocks(trimmed, parseOptions);
|
|
52
|
+
if (!blocks) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return blocks.map((block) => ({
|
|
56
|
+
id: randomUUID(),
|
|
57
|
+
name: block.name,
|
|
58
|
+
args: block.arguments,
|
|
59
|
+
type: 'tool_call',
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Repair one assistant {@link AIMessage}: if it has NO native tool_calls but its content is a
|
|
64
|
+
* standalone text-emitted call (allow-listed, within the payload cap), return a NEW AIMessage that
|
|
65
|
+
* carries the promoted native tool_calls; otherwise return `undefined` (the caller keeps the
|
|
66
|
+
* original, so the native happy path is untouched).
|
|
67
|
+
*
|
|
68
|
+
* The returned message PRESERVES the original `id`. That is load-bearing: LangGraph's message-state
|
|
69
|
+
* reducer merges by id, so a same-id message REPLACES the original in graph state (rather than
|
|
70
|
+
* appending a duplicate/dangling assistant message). Content is cleared to '' since the raw
|
|
71
|
+
* text-call is now represented natively as tool_calls.
|
|
72
|
+
*/
|
|
73
|
+
export function promoteTextEmittedToolCallMessage(message, options) {
|
|
74
|
+
// Happy path untouched: repair engages ONLY when there are no native tool_calls.
|
|
75
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const toolCalls = textToNativeToolCalls(message.text, options);
|
|
79
|
+
if (!toolCalls) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return new AIMessage({
|
|
83
|
+
id: message.id,
|
|
84
|
+
content: '',
|
|
85
|
+
tool_calls: toolCalls,
|
|
86
|
+
additional_kwargs: message.additional_kwargs,
|
|
87
|
+
response_metadata: message.response_metadata,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=promote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promote.js","sourceRoot":"","sources":["../../../src/core/toolCallRepair/promote.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,0FAA0F;AAC1F,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,kGAAkG;AAClG,oGAAoG;AACpG,yCAAyC;AACzC,EAAE;AACF,iEAAiE;AACjE,gGAAgG;AAChG,6DAA6D;AAC7D,2EAA2E;AAC3E,4FAA4F;AAC5F,2EAA2E;AAC3E,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,sCAAsC;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,wCAAwC,EACxC,sCAAsC,GAEvC,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,OAAO,EAAE,wCAAwC,EAAE,MAAM,cAAc,CAAC;AAsBxE;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAwB,EACxB,OAAkC;IAElC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3D,mEAAmE;IACnE,IAAI,gBAAgB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,YAAY,GAAkC;QAClD,gBAAgB;QAChB,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,wCAAwC;KACrF,CAAC;IACF,MAAM,MAAM,GAAG,sCAAsC,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,EAAE,EAAE,UAAU,EAAE;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,SAAS;QACrB,IAAI,EAAE,WAAoB;KAC3B,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iCAAiC,CAC/C,OAAkB,EAClB,OAAkC;IAElC,iFAAiF;IACjF,IAAI,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,qBAAqB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,IAAI,SAAS,CAAC;QACnB,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,OAAO,EAAE,EAAE;QACX,UAAU,EAAE,SAAS;QACrB,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;KAC7C,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** The single canonical output-preview cap (lines). No other preview length exists anywhere. */
|
|
2
|
+
export declare const TOOL_OUTPUT_PREVIEW_LINES = 10;
|
|
3
|
+
/**
|
|
4
|
+
* Per-line cap for preview lines, in terminal COLUMNS (a one-line minified bundle must not flood a
|
|
5
|
+
* row). The `CHARS` in the name is historical; every cap here is a column budget, because a
|
|
6
|
+
* character count is not a width — see `#src/utils/displayWidth.js`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const TOOL_PREVIEW_LINE_MAX_CHARS = 200;
|
|
9
|
+
/** Per-value cap inside a params summary, in terminal COLUMNS. */
|
|
10
|
+
export declare const TOOL_PARAM_VALUE_MAX_CHARS = 48;
|
|
11
|
+
/** Whole params-summary cap (everything inside the parentheses), in terminal COLUMNS. */
|
|
12
|
+
export declare const TOOL_SUMMARY_MAX_CHARS = 120;
|
|
13
|
+
/** The overflow/truncation marker used everywhere in this module. */
|
|
14
|
+
export declare const ELLIPSIS = "\u2026";
|
|
15
|
+
/**
|
|
16
|
+
* Columns {@link ELLIPSIS} itself occupies, which every truncator has to reserve out of its budget
|
|
17
|
+
* before slicing. Derived rather than written as `1` so it stays true of whatever marker is used.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ELLIPSIS_WIDTH: number;
|
|
20
|
+
/**
|
|
21
|
+
* Style tag for one rendered line. The two surfaces map these to their own colour systems
|
|
22
|
+
* (Ink `<Text>` props / raw ANSI): `dim` = greyed preview text, `added`/`removed` = diff
|
|
23
|
+
* green/red. DL-8 colour semantics; monochrome surfaces render the raw text unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export type ToolDisplayStyle = 'dim' | 'added' | 'removed';
|
|
26
|
+
/** One line of a tool call's rendered body/preview. */
|
|
27
|
+
export interface ToolDisplayLine {
|
|
28
|
+
text: string;
|
|
29
|
+
style: ToolDisplayStyle;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Everything a formatter may look at for one tool call. Both surfaces build this from their
|
|
33
|
+
* own state (the TUI from `ToolCallViewModel`, the plain surface from the message stream).
|
|
34
|
+
*/
|
|
35
|
+
export interface ToolCallDisplayInput {
|
|
36
|
+
/** The gth tool name (`read_file`, `run_shell_command`, a custom tool's name). */
|
|
37
|
+
name: string;
|
|
38
|
+
/** The raw streamed args JSON (possibly partial mid-stream, possibly invalid). */
|
|
39
|
+
argsText?: string;
|
|
40
|
+
/** The final model-facing tool result, when it has arrived. */
|
|
41
|
+
result?: string;
|
|
42
|
+
/** Live streamed child output (TUI only — the `tool_output` channel's accumulation). */
|
|
43
|
+
output?: string;
|
|
44
|
+
/** The real `ToolMessage.status === 'error'` signal (TUI-C7) — never sniffed from text. */
|
|
45
|
+
isError?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* True when the live child output ALREADY streamed raw to the user's terminal (the plain
|
|
48
|
+
* surface's default sink). Formatters then suppress the duplicated output body and render
|
|
49
|
+
* only the closing status — the TUI-C17 "output AND result repeat each other" dedupe.
|
|
50
|
+
*/
|
|
51
|
+
liveOutputAlreadyShown?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Tolerant parse of a (possibly partial) streamed args JSON. Mirrors the view-model's
|
|
55
|
+
* defensive posture: a half-streamed or malformed buffer never throws — it returns `null`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseToolArgsSafe(argsText: string | undefined): Record<string, unknown> | null;
|
|
58
|
+
/**
|
|
59
|
+
* Register the live config for inline-secret collection (TUI-C32 residual a). Resets the secret
|
|
60
|
+
* cache so the next {@link getDefaultSecrets} recomputes with the config's inline literals folded
|
|
61
|
+
* in. Idempotent; a later call with a fresh config supersedes the previous one.
|
|
62
|
+
*/
|
|
63
|
+
export declare function setToolDisplayConfig(config: unknown): void;
|
|
64
|
+
/** Test seam: drop the cached secret literals + registered config so specs can vary both. */
|
|
65
|
+
export declare function resetToolDisplaySecretsCacheForTests(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Parse the shared shell-result shape (`Executing '…'…\n\n<COMMAND_OUTPUT>…</COMMAND_OUTPUT>\n\n
|
|
68
|
+
* <status line>`). Returns the captured output body and the trailing status text, or `null`
|
|
69
|
+
* when the result is not shell-shaped.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseCommandOutputResult(result: string | undefined): {
|
|
72
|
+
body: string;
|
|
73
|
+
tail: string;
|
|
74
|
+
} | null;
|
|
75
|
+
/**
|
|
76
|
+
* TUI-C32 residual c — is THIS tool call genuinely a shell-shaped result (name allows it AND the
|
|
77
|
+
* result actually carries the `<COMMAND_OUTPUT>` shape)? Used by the plain surface to decide
|
|
78
|
+
* `liveOutputAlreadyShown` (the child streamed live via the tool-output channel's default sink),
|
|
79
|
+
* so a non-shell tool whose result merely quotes the marker no longer has its body suppressed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isShellShapedResult(name: string, result: string | undefined): boolean;
|
|
82
|
+
/** The registry glyph for a tool name (generic `⚙` when unknown). */
|
|
83
|
+
export declare function getToolGlyph(name: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* One-line call summary: `name(arg=val, other=…)`. Key args only (per the registry entry, or
|
|
86
|
+
* all args for unknown tools), each value inlined + truncated, the whole parenthesised part
|
|
87
|
+
* capped at {@link TOOL_SUMMARY_MAX_CHARS}, and everything secret-redacted (literals +
|
|
88
|
+
* provider patterns). Unparsable (mid-stream/malformed) args render as `name(…)` — never a
|
|
89
|
+
* raw JSON dump. `secrets` defaults to the env-derived literals; pass explicitly for tests.
|
|
90
|
+
*
|
|
91
|
+
* Redaction runs BEFORE every truncation step (per value in {@link formatParamValue}, and again
|
|
92
|
+
* before the whole-summary cap): truncating first would bisect a literal secret longer than a
|
|
93
|
+
* cap so it no longer literal-matches, leaking its head into the rendered summary
|
|
94
|
+
* (fix-cycle-1 finding). The final pass over the assembled string is defense in depth only —
|
|
95
|
+
* `redactText` is idempotent, so re-redacting already-marked text is safe.
|
|
96
|
+
*/
|
|
97
|
+
export declare function summariseToolCall(name: string, argsText: string | undefined, secrets?: readonly string[]): string;
|
|
98
|
+
/**
|
|
99
|
+
* The FULL (uncapped) body lines for a call: the registry formatter when one applies, else the
|
|
100
|
+
* shape-based shell formatter, else the generic fallback (live output lines, then the final
|
|
101
|
+
* result — both dim). Every line is secret-redacted. Used by the TUI's EXPANDED panel; cap it
|
|
102
|
+
* with {@link capToolDisplayLines} for the collapsed preview.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildToolBodyLines(input: ToolCallDisplayInput, secrets?: readonly string[]): ToolDisplayLine[];
|
|
105
|
+
/**
|
|
106
|
+
* Apply the canonical render cap: at most `maxLines` lines (each char-capped at
|
|
107
|
+
* {@link TOOL_PREVIEW_LINE_MAX_CHARS} with `…`), plus a dim `… (+N more lines)` overflow
|
|
108
|
+
* marker when anything was cut. The marker line is IN ADDITION to the cap so exactly how much
|
|
109
|
+
* was hidden is always stated (DL-4 transparency).
|
|
110
|
+
*/
|
|
111
|
+
export declare function capToolDisplayLines(lines: ToolDisplayLine[], maxLines?: number): ToolDisplayLine[];
|
|
112
|
+
/**
|
|
113
|
+
* The collapsed inline preview: {@link buildToolBodyLines} capped at the canonical
|
|
114
|
+
* {@link TOOL_OUTPUT_PREVIEW_LINES}.
|
|
115
|
+
*/
|
|
116
|
+
export declare function buildToolPreviewLines(input: ToolCallDisplayInput, secrets?: readonly string[]): ToolDisplayLine[];
|
|
117
|
+
/**
|
|
118
|
+
* Render one styled line as a raw string for the plain surface. Each line is SELF-STYLED
|
|
119
|
+
* (its own SGR open + reset) so lines compose safely regardless of surrounding styling.
|
|
120
|
+
* With `colour` false the raw text is returned unchanged — the clean monochrome degradation
|
|
121
|
+
* for non-TTY/piped output (DL-7); diff lines still read via their `+`/`-` prefixes.
|
|
122
|
+
*/
|
|
123
|
+
export declare function renderToolLineAnsi(line: ToolDisplayLine, colour: boolean): string;
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module toolDisplay
|
|
3
|
+
* TUI-C30 — the surface-agnostic tool-display registry: how a tool CALL is summarised
|
|
4
|
+
* (`name(arg=val, …)`) and how its OUTPUT is previewed (up to {@link TOOL_OUTPUT_PREVIEW_LINES}
|
|
5
|
+
* greyed lines, diff-coloured for `write_file`/`edit_file`), shared by BOTH render surfaces:
|
|
6
|
+
*
|
|
7
|
+
* - the Ink TUI's `ToolCallPanel` (`packages/app/src/tui/components/LiveTurn.tsx`), and
|
|
8
|
+
* - the plain/readline (`--no-tui`, piped, single-shot) surface
|
|
9
|
+
* (`core/plainToolIndication.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Everything here is PURE STRINGS plus a style tag per line ({@link ToolDisplayLine}), so the
|
|
12
|
+
* module knows nothing about Ink or ANSI: the TUI maps styles to `<Text>` props and the plain
|
|
13
|
+
* surface maps them to raw ANSI via {@link renderToolLineAnsi}. The shape mirrors vue-ui's
|
|
14
|
+
* PLAT-17 `toolDisplay.ts` (name → glyph + which args to summarise + optional result formatter,
|
|
15
|
+
* with a generic fallback — cf. openclaw's `TOOL_DISPLAY_CONFIG` and opencode's
|
|
16
|
+
* `collapseToolOutput`) so the surfaces stay analogous without sharing code.
|
|
17
|
+
*
|
|
18
|
+
* Design rules (from the node spec):
|
|
19
|
+
* - **One canonical output cap: {@link TOOL_OUTPUT_PREVIEW_LINES} lines** (and a per-line char
|
|
20
|
+
* cap), applied by {@link capToolDisplayLines} with a `… (+N more lines)` overflow marker.
|
|
21
|
+
* This is a RENDER-time cap only — the model-facing `OutputBuffer`/EXT-9 head-tail caps are a
|
|
22
|
+
* separate layer beneath it and are never touched here.
|
|
23
|
+
* - **Secret redaction reuses the GS2-47 `redactSecrets` lineage** (literal env/config secrets +
|
|
24
|
+
* provider key patterns) — no new redactor. Applied to the params summary and every
|
|
25
|
+
* preview/body line, fail-safe (redact MORE on any error, never leak).
|
|
26
|
+
* - **`write_file`/`edit_file` render the change as a diff derived from the tool's ARGS**
|
|
27
|
+
* (added = `added` style/green, removed = `removed` style/red); monochrome keeps the `+`/`-`
|
|
28
|
+
* prefixes so the diff still reads without colour (DL-7 graceful degradation).
|
|
29
|
+
*/
|
|
30
|
+
import { displayWidth, sliceToWidth } from '#src/utils/displayWidth.js';
|
|
31
|
+
import { collectSecretValues, redactText } from '#src/utils/redactSecrets.js';
|
|
32
|
+
import { env } from '#src/utils/systemUtils.js';
|
|
33
|
+
/** The single canonical output-preview cap (lines). No other preview length exists anywhere. */
|
|
34
|
+
export const TOOL_OUTPUT_PREVIEW_LINES = 10;
|
|
35
|
+
/**
|
|
36
|
+
* Per-line cap for preview lines, in terminal COLUMNS (a one-line minified bundle must not flood a
|
|
37
|
+
* row). The `CHARS` in the name is historical; every cap here is a column budget, because a
|
|
38
|
+
* character count is not a width — see `#src/utils/displayWidth.js`.
|
|
39
|
+
*/
|
|
40
|
+
export const TOOL_PREVIEW_LINE_MAX_CHARS = 200;
|
|
41
|
+
/** Per-value cap inside a params summary, in terminal COLUMNS. */
|
|
42
|
+
export const TOOL_PARAM_VALUE_MAX_CHARS = 48;
|
|
43
|
+
/** Whole params-summary cap (everything inside the parentheses), in terminal COLUMNS. */
|
|
44
|
+
export const TOOL_SUMMARY_MAX_CHARS = 120;
|
|
45
|
+
/** The overflow/truncation marker used everywhere in this module. */
|
|
46
|
+
export const ELLIPSIS = '…';
|
|
47
|
+
/**
|
|
48
|
+
* Columns {@link ELLIPSIS} itself occupies, which every truncator has to reserve out of its budget
|
|
49
|
+
* before slicing. Derived rather than written as `1` so it stays true of whatever marker is used.
|
|
50
|
+
*/
|
|
51
|
+
export const ELLIPSIS_WIDTH = displayWidth(ELLIPSIS);
|
|
52
|
+
const FALLBACK_GLYPH = '⚙';
|
|
53
|
+
const FILE_GLYPH = '📁';
|
|
54
|
+
const SHELL_GLYPH = '🔧';
|
|
55
|
+
/* ------------------------------------------------------------------------- *
|
|
56
|
+
* Small shared helpers *
|
|
57
|
+
* ------------------------------------------------------------------------- */
|
|
58
|
+
/**
|
|
59
|
+
* Tolerant parse of a (possibly partial) streamed args JSON. Mirrors the view-model's
|
|
60
|
+
* defensive posture: a half-streamed or malformed buffer never throws — it returns `null`.
|
|
61
|
+
*/
|
|
62
|
+
export function parseToolArgsSafe(argsText) {
|
|
63
|
+
if (!argsText || !argsText.trim())
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(argsText);
|
|
67
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The live gth config, registered once per run so {@link getDefaultSecrets} can harvest INLINE
|
|
78
|
+
* config secrets (a pasted `apiKey`/`token` value) via the GS2-47 config walk — not only the
|
|
79
|
+
* env-derived literals. `GthAgentRunner.init` sets this next to the crash-context hand-off, so
|
|
80
|
+
* both render surfaces (the plain observer and the Ink TUI) see it. `undefined` (no config yet)
|
|
81
|
+
* degrades to env-only collection + the provider patterns, exactly as before.
|
|
82
|
+
*/
|
|
83
|
+
let displayConfig = undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Register the live config for inline-secret collection (TUI-C32 residual a). Resets the secret
|
|
86
|
+
* cache so the next {@link getDefaultSecrets} recomputes with the config's inline literals folded
|
|
87
|
+
* in. Idempotent; a later call with a fresh config supersedes the previous one.
|
|
88
|
+
*/
|
|
89
|
+
export function setToolDisplayConfig(config) {
|
|
90
|
+
displayConfig = config;
|
|
91
|
+
cachedSecrets = null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Lazily-computed default secret literals for redaction (GS2-47 technique 1), harvested from the
|
|
95
|
+
* process env AND the registered config (inline `apiKey`/`token` values) the same way
|
|
96
|
+
* `/debug-dump` does. Cached because `collectSecretValues` walks the whole env + config; the set
|
|
97
|
+
* cannot change mid-process in any way this render path must react to (a new config resets it via
|
|
98
|
+
* {@link setToolDisplayConfig}). `redactText` additionally always applies the provider-key
|
|
99
|
+
* patterns (technique 2).
|
|
100
|
+
*/
|
|
101
|
+
let cachedSecrets = null;
|
|
102
|
+
function getDefaultSecrets() {
|
|
103
|
+
if (cachedSecrets === null) {
|
|
104
|
+
try {
|
|
105
|
+
cachedSecrets = collectSecretValues(displayConfig, env ?? {});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
cachedSecrets = []; // patterns still apply via redactText
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return cachedSecrets;
|
|
112
|
+
}
|
|
113
|
+
/** Test seam: drop the cached secret literals + registered config so specs can vary both. */
|
|
114
|
+
export function resetToolDisplaySecretsCacheForTests() {
|
|
115
|
+
cachedSecrets = null;
|
|
116
|
+
displayConfig = undefined;
|
|
117
|
+
}
|
|
118
|
+
/** Collapse whitespace runs (incl. newlines) so a value stays a one-line token. */
|
|
119
|
+
function inline(value) {
|
|
120
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Truncate to `max` terminal COLUMNS with the {@link ELLIPSIS} marker, reserving the marker's own
|
|
124
|
+
* width out of the budget. Slicing goes through the shared width primitive: a code-point count
|
|
125
|
+
* would read a CJK or emoji value as shorter than it renders and hand it back whole, over-running
|
|
126
|
+
* the row it is drawn on.
|
|
127
|
+
*
|
|
128
|
+
* Whether the value fits is put to the SLICE rather than to the ruler, because the slice stops at
|
|
129
|
+
* the budget: a value that over-runs is recognised from its first `max` columns, where measuring
|
|
130
|
+
* it reads all of it — and the values arriving here are whole tool-output lines, which
|
|
131
|
+
* {@link TOOL_PREVIEW_LINE_MAX_CHARS} exists precisely because they can be a megabyte long.
|
|
132
|
+
*/
|
|
133
|
+
function truncate(value, max) {
|
|
134
|
+
const fitted = sliceToWidth(value, max);
|
|
135
|
+
if (fitted === value)
|
|
136
|
+
return value;
|
|
137
|
+
return sliceToWidth(value, max - ELLIPSIS_WIDTH) + ELLIPSIS;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Render one arg value for the summary: strings inline+truncated, the rest JSON-ish.
|
|
141
|
+
*
|
|
142
|
+
* ORDER MATTERS (fix-cycle-1 finding): redaction runs BEFORE any transformation. Truncating
|
|
143
|
+
* first would bisect a literal secret longer than the value cap so it no longer
|
|
144
|
+
* literal-matches — its head would render in the call summary on both surfaces (a partial
|
|
145
|
+
* leak); whitespace-collapsing first could likewise alter a literal out of matching. So:
|
|
146
|
+
* redact the RAW text, then inline, then truncate (`<redacted>` contains no whitespace and is
|
|
147
|
+
* shorter than the cap, so the later steps can never damage the marker).
|
|
148
|
+
*/
|
|
149
|
+
function formatParamValue(value, secrets) {
|
|
150
|
+
let text;
|
|
151
|
+
if (typeof value === 'string') {
|
|
152
|
+
text = value;
|
|
153
|
+
}
|
|
154
|
+
else if (value === undefined) {
|
|
155
|
+
text = 'undefined';
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
try {
|
|
159
|
+
text = JSON.stringify(value) ?? String(value);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
text = String(value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return truncate(inline(redactText(text, secrets)), TOOL_PARAM_VALUE_MAX_CHARS);
|
|
166
|
+
}
|
|
167
|
+
/** Split into lines, dropping a single trailing newline's phantom empty last element. */
|
|
168
|
+
function toLines(text) {
|
|
169
|
+
const lines = text.split('\n');
|
|
170
|
+
if (lines.length > 1 && lines[lines.length - 1] === '')
|
|
171
|
+
lines.pop();
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
/** Map raw text lines to styled lines. */
|
|
175
|
+
function styled(lines, style) {
|
|
176
|
+
return lines.map((text) => ({ text, style }));
|
|
177
|
+
}
|
|
178
|
+
/* ------------------------------------------------------------------------- *
|
|
179
|
+
* Shell-shaped results (<COMMAND_OUTPUT>) — run_* / run_shell_command / *
|
|
180
|
+
* custom toolkit tools all share this body shape, so it is detected by *
|
|
181
|
+
* SHAPE, not by name (custom tool names are user-defined). *
|
|
182
|
+
* ------------------------------------------------------------------------- */
|
|
183
|
+
const COMMAND_OUTPUT_RE = /<COMMAND_OUTPUT>\n?([\s\S]*?)<\/COMMAND_OUTPUT>\n?([\s\S]*)$/;
|
|
184
|
+
/**
|
|
185
|
+
* Parse the shared shell-result shape (`Executing '…'…\n\n<COMMAND_OUTPUT>…</COMMAND_OUTPUT>\n\n
|
|
186
|
+
* <status line>`). Returns the captured output body and the trailing status text, or `null`
|
|
187
|
+
* when the result is not shell-shaped.
|
|
188
|
+
*/
|
|
189
|
+
export function parseCommandOutputResult(result) {
|
|
190
|
+
if (!result)
|
|
191
|
+
return null;
|
|
192
|
+
const match = COMMAND_OUTPUT_RE.exec(result);
|
|
193
|
+
if (!match)
|
|
194
|
+
return null;
|
|
195
|
+
return { body: match[1] ?? '', tail: (match[2] ?? '').trim() };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* TUI-C32 residual c — may this tool NAME be treated as shell-shaped at all? A registered entry
|
|
199
|
+
* must be explicitly {@link ToolDisplayEntry.shellShaped}; an unregistered/custom name falls back
|
|
200
|
+
* to shape-detection (custom toolkit tools own user-defined names yet share the result shape). A
|
|
201
|
+
* registered non-shell tool (`read_file`, `list_directory`, …) is therefore NEVER shell-parsed
|
|
202
|
+
* just because its result happens to contain the `<COMMAND_OUTPUT>` marker.
|
|
203
|
+
*/
|
|
204
|
+
function nameAllowsShellShape(name) {
|
|
205
|
+
const entry = TOOL_DISPLAY_REGISTRY[name];
|
|
206
|
+
return entry ? entry.shellShaped === true : true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* TUI-C32 residual c — is THIS tool call genuinely a shell-shaped result (name allows it AND the
|
|
210
|
+
* result actually carries the `<COMMAND_OUTPUT>` shape)? Used by the plain surface to decide
|
|
211
|
+
* `liveOutputAlreadyShown` (the child streamed live via the tool-output channel's default sink),
|
|
212
|
+
* so a non-shell tool whose result merely quotes the marker no longer has its body suppressed.
|
|
213
|
+
*/
|
|
214
|
+
export function isShellShapedResult(name, result) {
|
|
215
|
+
return nameAllowsShellShape(name) && parseCommandOutputResult(result) !== null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Body formatter for shell-shaped calls: the child's output (preferring the LIVE streamed
|
|
219
|
+
* output — verbatim what the child printed — over the result's `<COMMAND_OUTPUT>` copy of it,
|
|
220
|
+
* which is the TUI-C17 dedupe) followed by the closing status line. When the live output
|
|
221
|
+
* already streamed raw to the terminal (plain surface), only the status tail is rendered so
|
|
222
|
+
* nothing the user just watched is repeated.
|
|
223
|
+
*/
|
|
224
|
+
function formatShellBody(input) {
|
|
225
|
+
const parsed = parseCommandOutputResult(input.result);
|
|
226
|
+
// Not shell-shaped: only handle the still-running live-output case (a named run_* entry with
|
|
227
|
+
// no result yet). Anything with a non-shell result falls through to the generic rendering so
|
|
228
|
+
// e.g. a hardline refusal text is never dropped.
|
|
229
|
+
if (!parsed && (input.result || !input.output))
|
|
230
|
+
return null;
|
|
231
|
+
const lines = [];
|
|
232
|
+
if (!input.liveOutputAlreadyShown) {
|
|
233
|
+
// TUI-C32 residual d — the streamed live output is normally the verbatim, most-complete copy,
|
|
234
|
+
// so it is preferred (TUI-C17 dedupe). But if the live channel dropped straggler/tail chunks
|
|
235
|
+
// it can be a strict PREFIX of the result's `<COMMAND_OUTPUT>` copy (which the model always
|
|
236
|
+
// receives in full) — in that case fall back to the fuller copy so the expanded view can still
|
|
237
|
+
// recover what the stream missed. The common case (live === result, an independent/capped
|
|
238
|
+
// result, or an empty live output) is unchanged: only a genuine dropped-tail prefix overrides.
|
|
239
|
+
const live = input.output ?? '';
|
|
240
|
+
const resultBody = parsed?.body ?? '';
|
|
241
|
+
const body = resultBody.length > live.length && resultBody.startsWith(live) ? resultBody : live;
|
|
242
|
+
if (body.trim().length > 0)
|
|
243
|
+
lines.push(...styled(toLines(body), 'dim'));
|
|
244
|
+
}
|
|
245
|
+
if (parsed && parsed.tail.length > 0) {
|
|
246
|
+
lines.push(...styled(toLines(parsed.tail), 'dim'));
|
|
247
|
+
}
|
|
248
|
+
return lines.length > 0 ? lines : null;
|
|
249
|
+
}
|
|
250
|
+
/* ------------------------------------------------------------------------- *
|
|
251
|
+
* write_file / edit_file — diff-coloured rendering derived from the ARGS *
|
|
252
|
+
* ------------------------------------------------------------------------- */
|
|
253
|
+
/** `write_file` body: every content line is an ADDED diff line (there is no old content). */
|
|
254
|
+
function formatWriteFileBody(input, args) {
|
|
255
|
+
if (input.isError)
|
|
256
|
+
return null; // fall through: the error text is the story, not the diff
|
|
257
|
+
const content = args?.content;
|
|
258
|
+
if (typeof content !== 'string')
|
|
259
|
+
return null;
|
|
260
|
+
const lines = styled(toLines(content).map((l) => `+ ${l}`), 'added');
|
|
261
|
+
if (input.result && input.result.trim().length > 0) {
|
|
262
|
+
lines.push(...styled(toLines(input.result.trim()), 'dim'));
|
|
263
|
+
}
|
|
264
|
+
return lines;
|
|
265
|
+
}
|
|
266
|
+
/** `edit_file` body: per edit, the removed oldText lines then the added newText lines. */
|
|
267
|
+
function formatEditFileBody(input, args) {
|
|
268
|
+
if (input.isError)
|
|
269
|
+
return null; // fall through: show the recoverable error text instead
|
|
270
|
+
const edits = args?.edits;
|
|
271
|
+
if (!Array.isArray(edits) || edits.length === 0)
|
|
272
|
+
return null;
|
|
273
|
+
const lines = [];
|
|
274
|
+
let rendered = 0;
|
|
275
|
+
for (const raw of edits) {
|
|
276
|
+
const oldText = raw?.oldText;
|
|
277
|
+
const newText = raw?.newText;
|
|
278
|
+
if (typeof oldText !== 'string' || typeof newText !== 'string')
|
|
279
|
+
continue;
|
|
280
|
+
if (rendered > 0)
|
|
281
|
+
lines.push({ text: `${ELLIPSIS}`, style: 'dim' }); // hunk separator
|
|
282
|
+
rendered += 1;
|
|
283
|
+
lines.push(...styled(toLines(oldText).map((l) => `- ${l}`), 'removed'));
|
|
284
|
+
lines.push(...styled(toLines(newText).map((l) => `+ ${l}`), 'added'));
|
|
285
|
+
}
|
|
286
|
+
return rendered > 0 ? lines : null;
|
|
287
|
+
}
|
|
288
|
+
/* ------------------------------------------------------------------------- *
|
|
289
|
+
* The registry *
|
|
290
|
+
* ------------------------------------------------------------------------- */
|
|
291
|
+
/**
|
|
292
|
+
* Named entries. Anything not listed uses the generic fallback (all args summarised,
|
|
293
|
+
* output+result previewed dim) — plus the SHAPE-based shell formatter, which also covers
|
|
294
|
+
* user-named custom toolkit tools (they share the `<COMMAND_OUTPUT>` result shape).
|
|
295
|
+
*/
|
|
296
|
+
const TOOL_DISPLAY_REGISTRY = {
|
|
297
|
+
read_file: { glyph: FILE_GLYPH, summariseArgs: ['path', 'offset', 'limit', 'head', 'tail'] },
|
|
298
|
+
read_multiple_files: { glyph: FILE_GLYPH, summariseArgs: ['paths'] },
|
|
299
|
+
gth_read_binary: { glyph: FILE_GLYPH, summariseArgs: ['path'] },
|
|
300
|
+
write_file: { glyph: FILE_GLYPH, summariseArgs: ['path'], formatBody: formatWriteFileBody },
|
|
301
|
+
edit_file: {
|
|
302
|
+
glyph: FILE_GLYPH,
|
|
303
|
+
summariseArgs: ['path', 'dryRun'],
|
|
304
|
+
formatBody: formatEditFileBody,
|
|
305
|
+
},
|
|
306
|
+
create_directory: { glyph: FILE_GLYPH },
|
|
307
|
+
list_directory: { glyph: FILE_GLYPH },
|
|
308
|
+
list_directory_with_sizes: { glyph: FILE_GLYPH },
|
|
309
|
+
directory_tree: { glyph: FILE_GLYPH },
|
|
310
|
+
move_file: { glyph: FILE_GLYPH },
|
|
311
|
+
search_files: { glyph: FILE_GLYPH },
|
|
312
|
+
get_file_info: { glyph: FILE_GLYPH },
|
|
313
|
+
delete_file: { glyph: FILE_GLYPH },
|
|
314
|
+
delete_directory: { glyph: FILE_GLYPH },
|
|
315
|
+
list_allowed_directories: { glyph: FILE_GLYPH },
|
|
316
|
+
run_shell_command: {
|
|
317
|
+
glyph: SHELL_GLYPH,
|
|
318
|
+
summariseArgs: ['command'],
|
|
319
|
+
formatBody: formatShellBody,
|
|
320
|
+
shellShaped: true,
|
|
321
|
+
},
|
|
322
|
+
run_tests: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
|
|
323
|
+
run_single_test: {
|
|
324
|
+
glyph: SHELL_GLYPH,
|
|
325
|
+
summariseArgs: ['testPath'],
|
|
326
|
+
formatBody: formatShellBody,
|
|
327
|
+
shellShaped: true,
|
|
328
|
+
},
|
|
329
|
+
run_lint: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
|
|
330
|
+
run_build: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
|
|
331
|
+
task: { glyph: '🤖', summariseArgs: ['subagent_type', 'description'] },
|
|
332
|
+
};
|
|
333
|
+
/** The registry glyph for a tool name (generic `⚙` when unknown). */
|
|
334
|
+
export function getToolGlyph(name) {
|
|
335
|
+
return TOOL_DISPLAY_REGISTRY[name]?.glyph ?? FALLBACK_GLYPH;
|
|
336
|
+
}
|
|
337
|
+
/* ------------------------------------------------------------------------- *
|
|
338
|
+
* Params summary *
|
|
339
|
+
* ------------------------------------------------------------------------- */
|
|
340
|
+
/**
|
|
341
|
+
* One-line call summary: `name(arg=val, other=…)`. Key args only (per the registry entry, or
|
|
342
|
+
* all args for unknown tools), each value inlined + truncated, the whole parenthesised part
|
|
343
|
+
* capped at {@link TOOL_SUMMARY_MAX_CHARS}, and everything secret-redacted (literals +
|
|
344
|
+
* provider patterns). Unparsable (mid-stream/malformed) args render as `name(…)` — never a
|
|
345
|
+
* raw JSON dump. `secrets` defaults to the env-derived literals; pass explicitly for tests.
|
|
346
|
+
*
|
|
347
|
+
* Redaction runs BEFORE every truncation step (per value in {@link formatParamValue}, and again
|
|
348
|
+
* before the whole-summary cap): truncating first would bisect a literal secret longer than a
|
|
349
|
+
* cap so it no longer literal-matches, leaking its head into the rendered summary
|
|
350
|
+
* (fix-cycle-1 finding). The final pass over the assembled string is defense in depth only —
|
|
351
|
+
* `redactText` is idempotent, so re-redacting already-marked text is safe.
|
|
352
|
+
*/
|
|
353
|
+
export function summariseToolCall(name, argsText, secrets = getDefaultSecrets()) {
|
|
354
|
+
const label = name || '(tool)';
|
|
355
|
+
const args = parseToolArgsSafe(argsText);
|
|
356
|
+
if (args === null) {
|
|
357
|
+
const hasRawArgs = !!argsText && argsText.trim().length > 0 && argsText.trim() !== '{}';
|
|
358
|
+
return hasRawArgs ? `${label}(${ELLIPSIS})` : `${label}()`;
|
|
359
|
+
}
|
|
360
|
+
const entry = TOOL_DISPLAY_REGISTRY[name];
|
|
361
|
+
const keys = entry?.summariseArgs !== undefined
|
|
362
|
+
? entry.summariseArgs.filter((k) => args[k] !== undefined)
|
|
363
|
+
: Object.keys(args);
|
|
364
|
+
const parts = keys.map((k) => `${k}=${formatParamValue(args[k], secrets)}`);
|
|
365
|
+
// Anything parsed but not summarised (write_file's content, unlisted keys) is signalled with
|
|
366
|
+
// a trailing ellipsis so the summary never silently pretends to be the whole call.
|
|
367
|
+
const hasHiddenArgs = entry?.summariseArgs !== undefined && Object.keys(args).some((k) => !keys.includes(k));
|
|
368
|
+
if (hasHiddenArgs)
|
|
369
|
+
parts.push(ELLIPSIS);
|
|
370
|
+
// Redact before the whole-summary cap too, so this truncation can no more bisect a secret
|
|
371
|
+
// out of literal-matching than the per-value one can.
|
|
372
|
+
const inner = truncate(redactText(parts.join(', '), secrets), TOOL_SUMMARY_MAX_CHARS);
|
|
373
|
+
return redactText(`${label}(${inner})`, secrets);
|
|
374
|
+
}
|
|
375
|
+
/* ------------------------------------------------------------------------- *
|
|
376
|
+
* Body + preview *
|
|
377
|
+
* ------------------------------------------------------------------------- */
|
|
378
|
+
/**
|
|
379
|
+
* The FULL (uncapped) body lines for a call: the registry formatter when one applies, else the
|
|
380
|
+
* shape-based shell formatter, else the generic fallback (live output lines, then the final
|
|
381
|
+
* result — both dim). Every line is secret-redacted. Used by the TUI's EXPANDED panel; cap it
|
|
382
|
+
* with {@link capToolDisplayLines} for the collapsed preview.
|
|
383
|
+
*/
|
|
384
|
+
export function buildToolBodyLines(input, secrets = getDefaultSecrets()) {
|
|
385
|
+
const args = parseToolArgsSafe(input.argsText);
|
|
386
|
+
const entry = TOOL_DISPLAY_REGISTRY[input.name];
|
|
387
|
+
let lines = entry?.formatBody?.(input, args) ?? null;
|
|
388
|
+
// TUI-C32 residual c — only fall back to the shape-based shell formatter for names that may be
|
|
389
|
+
// shell-shaped (flagged registry entries + unregistered custom tools), so a registered non-shell
|
|
390
|
+
// tool whose result merely contains `<COMMAND_OUTPUT>` is rendered generically, not shell-parsed.
|
|
391
|
+
if (lines === null && nameAllowsShellShape(input.name))
|
|
392
|
+
lines = formatShellBody(input);
|
|
393
|
+
if (lines === null) {
|
|
394
|
+
lines = [];
|
|
395
|
+
if (input.output && input.output.trim().length > 0 && !input.liveOutputAlreadyShown) {
|
|
396
|
+
lines.push(...styled(toLines(input.output), 'dim'));
|
|
397
|
+
}
|
|
398
|
+
if (input.result && input.result.trim().length > 0) {
|
|
399
|
+
lines.push(...styled(toLines(input.result), 'dim'));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return lines.map((l) => ({ ...l, text: redactText(l.text, secrets) }));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Apply the canonical render cap: at most `maxLines` lines (each char-capped at
|
|
406
|
+
* {@link TOOL_PREVIEW_LINE_MAX_CHARS} with `…`), plus a dim `… (+N more lines)` overflow
|
|
407
|
+
* marker when anything was cut. The marker line is IN ADDITION to the cap so exactly how much
|
|
408
|
+
* was hidden is always stated (DL-4 transparency).
|
|
409
|
+
*/
|
|
410
|
+
export function capToolDisplayLines(lines, maxLines = TOOL_OUTPUT_PREVIEW_LINES) {
|
|
411
|
+
const capped = lines.slice(0, maxLines).map((l) => ({
|
|
412
|
+
...l,
|
|
413
|
+
text: truncate(l.text, TOOL_PREVIEW_LINE_MAX_CHARS),
|
|
414
|
+
}));
|
|
415
|
+
const hidden = lines.length - capped.length;
|
|
416
|
+
if (hidden > 0) {
|
|
417
|
+
capped.push({
|
|
418
|
+
text: `${ELLIPSIS} (+${hidden} more line${hidden === 1 ? '' : 's'})`,
|
|
419
|
+
style: 'dim',
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return capped;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The collapsed inline preview: {@link buildToolBodyLines} capped at the canonical
|
|
426
|
+
* {@link TOOL_OUTPUT_PREVIEW_LINES}.
|
|
427
|
+
*/
|
|
428
|
+
export function buildToolPreviewLines(input, secrets = getDefaultSecrets()) {
|
|
429
|
+
return capToolDisplayLines(buildToolBodyLines(input, secrets));
|
|
430
|
+
}
|
|
431
|
+
/* ------------------------------------------------------------------------- *
|
|
432
|
+
* ANSI adapter (plain surface) *
|
|
433
|
+
* ------------------------------------------------------------------------- */
|
|
434
|
+
const ANSI_BY_STYLE = {
|
|
435
|
+
dim: '\x1b[2m',
|
|
436
|
+
added: '\x1b[32m',
|
|
437
|
+
removed: '\x1b[31m',
|
|
438
|
+
};
|
|
439
|
+
const ANSI_RESET = '\x1b[0m';
|
|
440
|
+
/**
|
|
441
|
+
* Render one styled line as a raw string for the plain surface. Each line is SELF-STYLED
|
|
442
|
+
* (its own SGR open + reset) so lines compose safely regardless of surrounding styling.
|
|
443
|
+
* With `colour` false the raw text is returned unchanged — the clean monochrome degradation
|
|
444
|
+
* for non-TTY/piped output (DL-7); diff lines still read via their `+`/`-` prefixes.
|
|
445
|
+
*/
|
|
446
|
+
export function renderToolLineAnsi(line, colour) {
|
|
447
|
+
if (!colour)
|
|
448
|
+
return line.text;
|
|
449
|
+
return `${ANSI_BY_STYLE[line.style]}${line.text}${ANSI_RESET}`;
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=toolDisplay.js.map
|