@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* CFG-33 — the bridge between a provider's reasoning SHAPE and gaunt-sloth's reasoning channel, for
|
|
4
|
+
* the providers that carry thinking INSIDE `content` rather than beside it.
|
|
5
|
+
*
|
|
6
|
+
* Most providers hand thinking over out-of-band, in `additional_kwargs.reasoning_content` (or
|
|
7
|
+
* `reasoning`), which {@link pickReasoningDelta} in `GthAbstractAgent` reads. Google Gemini does not:
|
|
8
|
+
* a thought summary arrives as a content BLOCK marked `thought: true` and typed exactly like an
|
|
9
|
+
* answer block (`type: 'text'`). Two consequences follow, and the second is the dangerous one:
|
|
10
|
+
*
|
|
11
|
+
* 1. `additional_kwargs` carries nothing, so the reasoning channel stays empty — the `/reasoning`
|
|
12
|
+
* panel shows nothing at all on `google-genai`/`vertexai`.
|
|
13
|
+
* 2. `BaseMessage.text` maps every `type: 'text'` block, so the thought summary is folded into the
|
|
14
|
+
* ANSWER — it would print inline in the answer on every surface, and into `writeOutputToFile`.
|
|
15
|
+
*
|
|
16
|
+
* These helpers classify content once, so each consumer can take the half it wants. They are pure,
|
|
17
|
+
* shape-driven and provider-agnostic: content that carries no reasoning block is passed through with
|
|
18
|
+
* byte-identical results (and, for {@link stripReasoningBlocks}, the very same array reference), so
|
|
19
|
+
* no other provider's rendering changes.
|
|
20
|
+
*/
|
|
21
|
+
/** One classified slice of assistant output: answer prose vs. the model's thinking. */
|
|
22
|
+
export type ThinkSegment = {
|
|
23
|
+
kind: 'answer' | 'reasoning';
|
|
24
|
+
text: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Is this content block the model's THINKING rather than its answer?
|
|
28
|
+
*
|
|
29
|
+
* Deliberately narrow: ONLY Gemini's `thought: true` marker on an otherwise ordinary text block.
|
|
30
|
+
* Providers whose thinking arrives as a distinct block TYPE (Anthropic's `thinking` blocks) are NOT
|
|
31
|
+
* matched here — `BaseMessage.text` already excludes them from the answer, and their reasoning
|
|
32
|
+
* already reaches the channel via `additional_kwargs.reasoning_content`, so matching them too would
|
|
33
|
+
* emit that provider's thinking TWICE.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isReasoningContentBlock(block: unknown): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Classify an assistant message's `content` into ORDERED answer/reasoning segments, preserving the
|
|
38
|
+
* order the model emitted them in (a thought summary that precedes the answer stays before it).
|
|
39
|
+
*
|
|
40
|
+
* The concatenation of the `answer` segments is EXACTLY what `BaseMessage.text` returns once the
|
|
41
|
+
* reasoning blocks are removed — the same block rules, the same empty join — so for content that
|
|
42
|
+
* carries no reasoning block this is a drop-in for `.text`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function segmentAssistantContent(content: unknown): ThinkSegment[];
|
|
45
|
+
/**
|
|
46
|
+
* The ANSWER text of an assistant message's content — what `BaseMessage.text` would return minus any
|
|
47
|
+
* reasoning block. Used by the surfaces that render answer text directly and have never shown
|
|
48
|
+
* reasoning (the plain console stream), so a thought summary cannot leak into the answer there.
|
|
49
|
+
*/
|
|
50
|
+
export declare function answerTextOf(content: unknown): string;
|
|
51
|
+
/**
|
|
52
|
+
* The same content with its reasoning blocks removed, for consumers that render the block ARRAY
|
|
53
|
+
* rather than its text (`renderAssistantContent` / `materializeBinaryOutputs` on the non-streaming
|
|
54
|
+
* path). Returns the INPUT REFERENCE unchanged whenever nothing matched, so every other provider's
|
|
55
|
+
* content object is untouched and identity comparisons still hold.
|
|
56
|
+
*
|
|
57
|
+
* Only the rendering path strips: the message kept in graph state is left intact, so the thought
|
|
58
|
+
* parts (and any `thoughtSignature` riding with them) still replay to the provider as history.
|
|
59
|
+
*/
|
|
60
|
+
export declare function stripReasoningBlocks(content: unknown): unknown;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* CFG-33 — the bridge between a provider's reasoning SHAPE and gaunt-sloth's reasoning channel, for
|
|
4
|
+
* the providers that carry thinking INSIDE `content` rather than beside it.
|
|
5
|
+
*
|
|
6
|
+
* Most providers hand thinking over out-of-band, in `additional_kwargs.reasoning_content` (or
|
|
7
|
+
* `reasoning`), which {@link pickReasoningDelta} in `GthAbstractAgent` reads. Google Gemini does not:
|
|
8
|
+
* a thought summary arrives as a content BLOCK marked `thought: true` and typed exactly like an
|
|
9
|
+
* answer block (`type: 'text'`). Two consequences follow, and the second is the dangerous one:
|
|
10
|
+
*
|
|
11
|
+
* 1. `additional_kwargs` carries nothing, so the reasoning channel stays empty — the `/reasoning`
|
|
12
|
+
* panel shows nothing at all on `google-genai`/`vertexai`.
|
|
13
|
+
* 2. `BaseMessage.text` maps every `type: 'text'` block, so the thought summary is folded into the
|
|
14
|
+
* ANSWER — it would print inline in the answer on every surface, and into `writeOutputToFile`.
|
|
15
|
+
*
|
|
16
|
+
* These helpers classify content once, so each consumer can take the half it wants. They are pure,
|
|
17
|
+
* shape-driven and provider-agnostic: content that carries no reasoning block is passed through with
|
|
18
|
+
* byte-identical results (and, for {@link stripReasoningBlocks}, the very same array reference), so
|
|
19
|
+
* no other provider's rendering changes.
|
|
20
|
+
*/
|
|
21
|
+
function isContentBlock(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Is this content block the model's THINKING rather than its answer?
|
|
26
|
+
*
|
|
27
|
+
* Deliberately narrow: ONLY Gemini's `thought: true` marker on an otherwise ordinary text block.
|
|
28
|
+
* Providers whose thinking arrives as a distinct block TYPE (Anthropic's `thinking` blocks) are NOT
|
|
29
|
+
* matched here — `BaseMessage.text` already excludes them from the answer, and their reasoning
|
|
30
|
+
* already reaches the channel via `additional_kwargs.reasoning_content`, so matching them too would
|
|
31
|
+
* emit that provider's thinking TWICE.
|
|
32
|
+
*/
|
|
33
|
+
export function isReasoningContentBlock(block) {
|
|
34
|
+
return isContentBlock(block) && block.thought === true && block.type === 'text';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Classify an assistant message's `content` into ORDERED answer/reasoning segments, preserving the
|
|
38
|
+
* order the model emitted them in (a thought summary that precedes the answer stays before it).
|
|
39
|
+
*
|
|
40
|
+
* The concatenation of the `answer` segments is EXACTLY what `BaseMessage.text` returns once the
|
|
41
|
+
* reasoning blocks are removed — the same block rules, the same empty join — so for content that
|
|
42
|
+
* carries no reasoning block this is a drop-in for `.text`.
|
|
43
|
+
*/
|
|
44
|
+
export function segmentAssistantContent(content) {
|
|
45
|
+
if (typeof content === 'string') {
|
|
46
|
+
return content.length > 0 ? [{ kind: 'answer', text: content }] : [];
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(content)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const segments = [];
|
|
52
|
+
for (const block of content) {
|
|
53
|
+
if (typeof block === 'string') {
|
|
54
|
+
if (block.length > 0)
|
|
55
|
+
segments.push({ kind: 'answer', text: block });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!isContentBlock(block))
|
|
59
|
+
continue;
|
|
60
|
+
const text = typeof block.text === 'string' ? block.text : '';
|
|
61
|
+
if (text.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
if (isReasoningContentBlock(block)) {
|
|
64
|
+
segments.push({ kind: 'reasoning', text });
|
|
65
|
+
}
|
|
66
|
+
else if (block.type === 'text') {
|
|
67
|
+
segments.push({ kind: 'answer', text });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return segments;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The ANSWER text of an assistant message's content — what `BaseMessage.text` would return minus any
|
|
74
|
+
* reasoning block. Used by the surfaces that render answer text directly and have never shown
|
|
75
|
+
* reasoning (the plain console stream), so a thought summary cannot leak into the answer there.
|
|
76
|
+
*/
|
|
77
|
+
export function answerTextOf(content) {
|
|
78
|
+
return segmentAssistantContent(content)
|
|
79
|
+
.filter((segment) => segment.kind === 'answer')
|
|
80
|
+
.map((segment) => segment.text)
|
|
81
|
+
.join('');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The same content with its reasoning blocks removed, for consumers that render the block ARRAY
|
|
85
|
+
* rather than its text (`renderAssistantContent` / `materializeBinaryOutputs` on the non-streaming
|
|
86
|
+
* path). Returns the INPUT REFERENCE unchanged whenever nothing matched, so every other provider's
|
|
87
|
+
* content object is untouched and identity comparisons still hold.
|
|
88
|
+
*
|
|
89
|
+
* Only the rendering path strips: the message kept in graph state is left intact, so the thought
|
|
90
|
+
* parts (and any `thoughtSignature` riding with them) still replay to the provider as history.
|
|
91
|
+
*/
|
|
92
|
+
export function stripReasoningBlocks(content) {
|
|
93
|
+
if (!Array.isArray(content))
|
|
94
|
+
return content;
|
|
95
|
+
const kept = content.filter((block) => !isReasoningContentBlock(block));
|
|
96
|
+
return kept.length === content.length ? content : kept;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=reasoningBlocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoningBlocks.js","sourceRoot":"","sources":["../../src/core/reasoningBlocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;AAClF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAgB;IACtD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrE,SAAS;QACX,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;YAAE,SAAS;QACrC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,IAAI,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,OAAO,uBAAuB,CAAC,OAAO,CAAC;SACpC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC;SAC9C,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;SAC9B,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXT-37 — content-policy refusal detection for the agent run loop.
|
|
3
|
+
*
|
|
4
|
+
* A *successful* model response (HTTP 200) can carry a stop/finish reason that means the model — or
|
|
5
|
+
* the provider's safety system — declined to answer. The content is usually empty, so without this
|
|
6
|
+
* detection the response falls through the empty-response retry in {@link GthAgentRunner} and is
|
|
7
|
+
* mis-surfaced as "no content, try again" — burning a second, paid call to reproduce a
|
|
8
|
+
* DETERMINISTIC refusal. This module normalizes the per-provider shapes into one signal so the run
|
|
9
|
+
* loop can surface the refusal clearly and terminate (never retry the same prompt).
|
|
10
|
+
*
|
|
11
|
+
* Prior art: hermes-agent `conversation_loop.py` treats `finish_reason == "content_filter"` as a
|
|
12
|
+
* terminal, non-retryable "content policy blocked" outcome and surfaces the model's explanation.
|
|
13
|
+
*
|
|
14
|
+
* Detection lives here (and is called from {@link GthAbstractAgent}, the invoke/stream loop over
|
|
15
|
+
* messages/chunks) because that is the only layer where a message's `response_metadata` /
|
|
16
|
+
* `additional_kwargs` — where finish/stop reasons live — are visible; `GthAgentRunner` only ever
|
|
17
|
+
* sees the rendered string.
|
|
18
|
+
*/
|
|
19
|
+
/** One detected refusal, normalized across providers. */
|
|
20
|
+
export interface RefusalInfo {
|
|
21
|
+
/** Best-effort provider family the signal came from (for logging / the surfaced message). */
|
|
22
|
+
provider: 'openai' | 'anthropic' | 'bedrock' | 'unknown';
|
|
23
|
+
/** The raw stop/finish reason token that flagged the refusal (e.g. `content_filter`). */
|
|
24
|
+
reason: string;
|
|
25
|
+
/** Any model-provided explanation text (empty string when the refusal carried none). */
|
|
26
|
+
explanation: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Inspect a finished model message (an `AIMessage` / `AIMessageChunk`, or any object exposing
|
|
30
|
+
* `response_metadata` / `additional_kwargs`) and return a {@link RefusalInfo} when its stop/finish
|
|
31
|
+
* reason indicates a content-policy refusal, else `null`. Defensive: any non-message / unexpected
|
|
32
|
+
* shape yields `null`, so a normal turn is never mistaken for a refusal.
|
|
33
|
+
*
|
|
34
|
+
* Covered shapes:
|
|
35
|
+
* - OpenAI-family `finish_reason: 'content_filter'` (also under `additional_kwargs`).
|
|
36
|
+
* - Anthropic `stop_reason: 'refusal'`.
|
|
37
|
+
* - Bedrock Converse guardrail intervention: `stopReason`/`stop_reason`/`finish_reason` ===
|
|
38
|
+
* `'guardrail_intervened'`, or `additional_kwargs['amazon-bedrock-guardrailAction'] ===
|
|
39
|
+
* `'INTERVENED'`.
|
|
40
|
+
* - Bedrock Converse content filter: `stopReason`/`stop_reason`/`finish_reason` ===
|
|
41
|
+
* `'content_filtered'` (EXT-41 — a distinct `StopReason` enum value from `guardrail_intervened`
|
|
42
|
+
* that was previously mapped to `null`, i.e. a silent empty turn / false negative).
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectRefusal(message: unknown): RefusalInfo | null;
|
|
45
|
+
/**
|
|
46
|
+
* Build the clear, user-facing message shown when the model declines. Framed as the model /
|
|
47
|
+
* provider's own policy decision (not a Gaunt Sloth fault) and stated as terminal — a refusal is
|
|
48
|
+
* deterministic for the same input, so retrying as-is will not help. Any model-provided explanation
|
|
49
|
+
* is included verbatim. This string is BOTH surfaced to the console and RETURNED as the turn's
|
|
50
|
+
* answer, so the non-interactive caller writes it to the output file and exits `ok` (it is a
|
|
51
|
+
* successful, if declined, response — not a failure to be re-wrapped as "Failed to get answer").
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildRefusalMessage(info: RefusalInfo): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXT-37 — content-policy refusal detection for the agent run loop.
|
|
3
|
+
*
|
|
4
|
+
* A *successful* model response (HTTP 200) can carry a stop/finish reason that means the model — or
|
|
5
|
+
* the provider's safety system — declined to answer. The content is usually empty, so without this
|
|
6
|
+
* detection the response falls through the empty-response retry in {@link GthAgentRunner} and is
|
|
7
|
+
* mis-surfaced as "no content, try again" — burning a second, paid call to reproduce a
|
|
8
|
+
* DETERMINISTIC refusal. This module normalizes the per-provider shapes into one signal so the run
|
|
9
|
+
* loop can surface the refusal clearly and terminate (never retry the same prompt).
|
|
10
|
+
*
|
|
11
|
+
* Prior art: hermes-agent `conversation_loop.py` treats `finish_reason == "content_filter"` as a
|
|
12
|
+
* terminal, non-retryable "content policy blocked" outcome and surfaces the model's explanation.
|
|
13
|
+
*
|
|
14
|
+
* Detection lives here (and is called from {@link GthAbstractAgent}, the invoke/stream loop over
|
|
15
|
+
* messages/chunks) because that is the only layer where a message's `response_metadata` /
|
|
16
|
+
* `additional_kwargs` — where finish/stop reasons live — are visible; `GthAgentRunner` only ever
|
|
17
|
+
* sees the rendered string.
|
|
18
|
+
*/
|
|
19
|
+
import { stripReasoningBlocks } from '#src/core/reasoningBlocks.js';
|
|
20
|
+
/** Read a nested record field defensively (returns undefined for non-objects / missing keys). */
|
|
21
|
+
function readField(source, key) {
|
|
22
|
+
if (!source || typeof source !== 'object')
|
|
23
|
+
return undefined;
|
|
24
|
+
return source[key];
|
|
25
|
+
}
|
|
26
|
+
/** Pull any human-readable explanation the refusal carried (message content, then reasoning). */
|
|
27
|
+
function extractRefusalText(message) {
|
|
28
|
+
const content = readField(message, 'content');
|
|
29
|
+
if (typeof content === 'string' && content.trim().length > 0)
|
|
30
|
+
return content.trim();
|
|
31
|
+
// Content-block arrays (Anthropic / Bedrock): concatenate any text parts. CFG-33 — minus the
|
|
32
|
+
// model's own thinking: Gemini marks a thought summary `thought: true` and types it exactly like
|
|
33
|
+
// an answer part, so without this it would be pasted verbatim into the refusal notice as the
|
|
34
|
+
// model's "explanation".
|
|
35
|
+
if (Array.isArray(content)) {
|
|
36
|
+
const text = stripReasoningBlocks(content)
|
|
37
|
+
.map((part) => {
|
|
38
|
+
if (typeof part === 'string')
|
|
39
|
+
return part;
|
|
40
|
+
const t = readField(part, 'text');
|
|
41
|
+
return typeof t === 'string' ? t : '';
|
|
42
|
+
})
|
|
43
|
+
.join('')
|
|
44
|
+
.trim();
|
|
45
|
+
if (text.length > 0)
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
// Some refusals put the explanation only in the reasoning channel.
|
|
49
|
+
const kwargs = readField(message, 'additional_kwargs');
|
|
50
|
+
const reasoning = readField(kwargs, 'reasoning_content');
|
|
51
|
+
if (typeof reasoning === 'string' && reasoning.trim().length > 0)
|
|
52
|
+
return reasoning.trim();
|
|
53
|
+
// Anthropic exposes the declined text on a dedicated `refusal` field in some SDK shapes.
|
|
54
|
+
const refusalField = readField(kwargs, 'refusal') ?? readField(message, 'refusal');
|
|
55
|
+
if (typeof refusalField === 'string' && refusalField.trim().length > 0)
|
|
56
|
+
return refusalField.trim();
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Inspect a finished model message (an `AIMessage` / `AIMessageChunk`, or any object exposing
|
|
61
|
+
* `response_metadata` / `additional_kwargs`) and return a {@link RefusalInfo} when its stop/finish
|
|
62
|
+
* reason indicates a content-policy refusal, else `null`. Defensive: any non-message / unexpected
|
|
63
|
+
* shape yields `null`, so a normal turn is never mistaken for a refusal.
|
|
64
|
+
*
|
|
65
|
+
* Covered shapes:
|
|
66
|
+
* - OpenAI-family `finish_reason: 'content_filter'` (also under `additional_kwargs`).
|
|
67
|
+
* - Anthropic `stop_reason: 'refusal'`.
|
|
68
|
+
* - Bedrock Converse guardrail intervention: `stopReason`/`stop_reason`/`finish_reason` ===
|
|
69
|
+
* `'guardrail_intervened'`, or `additional_kwargs['amazon-bedrock-guardrailAction'] ===
|
|
70
|
+
* `'INTERVENED'`.
|
|
71
|
+
* - Bedrock Converse content filter: `stopReason`/`stop_reason`/`finish_reason` ===
|
|
72
|
+
* `'content_filtered'` (EXT-41 — a distinct `StopReason` enum value from `guardrail_intervened`
|
|
73
|
+
* that was previously mapped to `null`, i.e. a silent empty turn / false negative).
|
|
74
|
+
*/
|
|
75
|
+
export function detectRefusal(message) {
|
|
76
|
+
if (!message || typeof message !== 'object')
|
|
77
|
+
return null;
|
|
78
|
+
const meta = readField(message, 'response_metadata');
|
|
79
|
+
const kwargs = readField(message, 'additional_kwargs');
|
|
80
|
+
// Gather the stop/finish reason from every place providers surface it.
|
|
81
|
+
const finishReason = readField(meta, 'finish_reason') ?? readField(kwargs, 'finish_reason') ?? undefined;
|
|
82
|
+
const stopReasonSnake = readField(meta, 'stop_reason') ?? readField(kwargs, 'stop_reason') ?? undefined;
|
|
83
|
+
const stopReasonCamel = readField(meta, 'stopReason') ?? readField(kwargs, 'stopReason') ?? undefined;
|
|
84
|
+
const asString = (v) => (typeof v === 'string' ? v : undefined);
|
|
85
|
+
const finish = asString(finishReason);
|
|
86
|
+
const stopSnake = asString(stopReasonSnake);
|
|
87
|
+
const stopCamel = asString(stopReasonCamel);
|
|
88
|
+
const explanation = extractRefusalText(message);
|
|
89
|
+
// OpenAI-family content filter.
|
|
90
|
+
if (finish === 'content_filter') {
|
|
91
|
+
return { provider: 'openai', reason: 'content_filter', explanation };
|
|
92
|
+
}
|
|
93
|
+
// Anthropic refusal stop reason.
|
|
94
|
+
if (stopSnake === 'refusal' || stopCamel === 'refusal') {
|
|
95
|
+
return { provider: 'anthropic', reason: 'refusal', explanation };
|
|
96
|
+
}
|
|
97
|
+
// Bedrock Converse guardrail intervention (camelCase `stopReason`, or snake / finish variants).
|
|
98
|
+
if (stopCamel === 'guardrail_intervened' ||
|
|
99
|
+
stopSnake === 'guardrail_intervened' ||
|
|
100
|
+
finish === 'guardrail_intervened' ||
|
|
101
|
+
readField(kwargs, 'amazon-bedrock-guardrailAction') === 'INTERVENED' ||
|
|
102
|
+
readField(meta, 'amazon-bedrock-guardrailAction') === 'INTERVENED') {
|
|
103
|
+
return { provider: 'bedrock', reason: 'guardrail_intervened', explanation };
|
|
104
|
+
}
|
|
105
|
+
// EXT-41 — Bedrock Converse content filter. A distinct `StopReason` enum value from
|
|
106
|
+
// `guardrail_intervened` (both live in the same AWS Converse `StopReason` enum); previously
|
|
107
|
+
// unmapped, so a content-filtered turn returned `null` → the silent empty-turn false negative.
|
|
108
|
+
if (stopCamel === 'content_filtered' ||
|
|
109
|
+
stopSnake === 'content_filtered' ||
|
|
110
|
+
finish === 'content_filtered') {
|
|
111
|
+
return { provider: 'bedrock', reason: 'content_filtered', explanation };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build the clear, user-facing message shown when the model declines. Framed as the model /
|
|
117
|
+
* provider's own policy decision (not a Gaunt Sloth fault) and stated as terminal — a refusal is
|
|
118
|
+
* deterministic for the same input, so retrying as-is will not help. Any model-provided explanation
|
|
119
|
+
* is included verbatim. This string is BOTH surfaced to the console and RETURNED as the turn's
|
|
120
|
+
* answer, so the non-interactive caller writes it to the output file and exits `ok` (it is a
|
|
121
|
+
* successful, if declined, response — not a failure to be re-wrapped as "Failed to get answer").
|
|
122
|
+
*/
|
|
123
|
+
export function buildRefusalMessage(info) {
|
|
124
|
+
const head = 'The model declined to respond (safety refusal / content filter) — this is the ' +
|
|
125
|
+
"model/provider's own policy decision, not a Gaunt Sloth error.";
|
|
126
|
+
const detail = info.explanation
|
|
127
|
+
? `Model's explanation: ${info.explanation}`
|
|
128
|
+
: 'The model provided no explanation.';
|
|
129
|
+
const hint = 'A refusal is deterministic for the same input — rephrase the request or try a different ' +
|
|
130
|
+
'model rather than re-running it as-is.';
|
|
131
|
+
return `${head}\n\n${detail}\n\n${hint}`;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/core/refusal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-16 — pure, fail-soft helpers that harvest per-run analytics (token usage + invoked tool
|
|
4
|
+
* names) from LangChain messages, so the opt-in history recorder can populate `gth insights`
|
|
5
|
+
* with real numbers instead of zeros.
|
|
6
|
+
*
|
|
7
|
+
* The extraction is deliberately structural and defensive (duck-typed reads guarded by a
|
|
8
|
+
* try/catch) rather than `instanceof`-based: the same accumulator serves both the non-streaming
|
|
9
|
+
* `invoke` path (a full `messages[]` from graph state) and the streaming paths (individual
|
|
10
|
+
* message chunks / `ToolMessage`s as they arrive), across providers whose message shapes vary.
|
|
11
|
+
* Nothing here may throw into a run — a missing/odd field just means that datum is skipped.
|
|
12
|
+
*/
|
|
13
|
+
import type { GthRunStats, GthToolResult } from '#src/core/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* BATCH-21 — cap on a captured tool-result `content` (characters). Keeps a giant payload (a whole
|
|
16
|
+
* file read, a long shell log) from bloating run stats; anything longer is truncated to this
|
|
17
|
+
* length. Sized so realistic structured payloads (the `gth eval` tool-result-assertion use case)
|
|
18
|
+
* survive intact.
|
|
19
|
+
*/
|
|
20
|
+
export declare const TOOL_RESULT_CONTENT_CAP = 8192;
|
|
21
|
+
/** Mutable tally behind {@link finalizeRunStats}; see {@link createRunStatsAccumulator}. */
|
|
22
|
+
export interface RunStatsAccumulator {
|
|
23
|
+
/** Running sum of input/prompt tokens. */
|
|
24
|
+
input: number;
|
|
25
|
+
/** Running sum of output/completion tokens. */
|
|
26
|
+
output: number;
|
|
27
|
+
/** Whether ANY message reported `usage_metadata` — gates whether tokens are recorded at all. */
|
|
28
|
+
sawUsage: boolean;
|
|
29
|
+
/** Deduplicated set of invoked tool names. */
|
|
30
|
+
tools: Set<string>;
|
|
31
|
+
/** BATCH-21 — one record per executed tool result (`ToolMessage`), in arrival order, un-deduped. */
|
|
32
|
+
toolResults: GthToolResult[];
|
|
33
|
+
}
|
|
34
|
+
/** A fresh, empty accumulator. */
|
|
35
|
+
export declare function createRunStatsAccumulator(): RunStatsAccumulator;
|
|
36
|
+
/**
|
|
37
|
+
* Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
|
|
38
|
+
* shape is swallowed so a run is never affected. Harvests, when present:
|
|
39
|
+
* - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`),
|
|
40
|
+
* - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
|
|
41
|
+
* `.name` (the executed tool), so both "requested" and "executed" tools are captured, and
|
|
42
|
+
* - (BATCH-21) a per-`ToolMessage` result record — `name` + `isError` (from `.status`) + capped
|
|
43
|
+
* `content` — into `acc.toolResults`, so tool-RESULT assertions can grade what a tool returned.
|
|
44
|
+
*/
|
|
45
|
+
export declare function accumulateMessage(acc: RunStatsAccumulator, message: unknown): void;
|
|
46
|
+
/** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
|
|
47
|
+
export declare function finalizeRunStats(acc: RunStatsAccumulator): GthRunStats;
|
|
48
|
+
/**
|
|
49
|
+
* One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
|
|
50
|
+
* state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractRunStats(messages: unknown): GthRunStats;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BATCH-21 — cap on a captured tool-result `content` (characters). Keeps a giant payload (a whole
|
|
3
|
+
* file read, a long shell log) from bloating run stats; anything longer is truncated to this
|
|
4
|
+
* length. Sized so realistic structured payloads (the `gth eval` tool-result-assertion use case)
|
|
5
|
+
* survive intact.
|
|
6
|
+
*/
|
|
7
|
+
export const TOOL_RESULT_CONTENT_CAP = 8192;
|
|
8
|
+
/** A fresh, empty accumulator. */
|
|
9
|
+
export function createRunStatsAccumulator() {
|
|
10
|
+
return { input: 0, output: 0, sawUsage: false, tools: new Set(), toolResults: [] };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* BATCH-21 — derive a tool result's text payload from a `ToolMessage.content`, fail-soft. A string
|
|
14
|
+
* passes through; anything else non-`undefined` is JSON-stringified (the same derivation the
|
|
15
|
+
* `tool_result` stream event uses in `GthAbstractAgent`); the result is capped at
|
|
16
|
+
* {@link TOOL_RESULT_CONTENT_CAP}. Returns `undefined` (payload omitted) when nothing textual can
|
|
17
|
+
* be derived — never throws.
|
|
18
|
+
*/
|
|
19
|
+
function toolResultContentText(content) {
|
|
20
|
+
try {
|
|
21
|
+
let text;
|
|
22
|
+
if (typeof content === 'string') {
|
|
23
|
+
text = content;
|
|
24
|
+
}
|
|
25
|
+
else if (content !== undefined) {
|
|
26
|
+
text = JSON.stringify(content);
|
|
27
|
+
}
|
|
28
|
+
if (text === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
return text.length > TOOL_RESULT_CONTENT_CAP ? text.slice(0, TOOL_RESULT_CONTENT_CAP) : text;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* fail-soft: an unstringifiable payload just means no content is recorded */
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
|
|
39
|
+
* shape is swallowed so a run is never affected. Harvests, when present:
|
|
40
|
+
* - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`),
|
|
41
|
+
* - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
|
|
42
|
+
* `.name` (the executed tool), so both "requested" and "executed" tools are captured, and
|
|
43
|
+
* - (BATCH-21) a per-`ToolMessage` result record — `name` + `isError` (from `.status`) + capped
|
|
44
|
+
* `content` — into `acc.toolResults`, so tool-RESULT assertions can grade what a tool returned.
|
|
45
|
+
*/
|
|
46
|
+
export function accumulateMessage(acc, message) {
|
|
47
|
+
try {
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const m = message;
|
|
50
|
+
if (!m || typeof m !== 'object')
|
|
51
|
+
return;
|
|
52
|
+
const usage = m.usage_metadata;
|
|
53
|
+
if (usage && typeof usage === 'object') {
|
|
54
|
+
acc.sawUsage = true;
|
|
55
|
+
if (typeof usage.input_tokens === 'number' && Number.isFinite(usage.input_tokens)) {
|
|
56
|
+
acc.input += usage.input_tokens;
|
|
57
|
+
}
|
|
58
|
+
if (typeof usage.output_tokens === 'number' && Number.isFinite(usage.output_tokens)) {
|
|
59
|
+
acc.output += usage.output_tokens;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Requested tool calls (AIMessage / AIMessageChunk). Continuation chunks in a streamed
|
|
63
|
+
// tool call carry an empty name, so guard on a non-empty string; the Set dedupes repeats.
|
|
64
|
+
const toolCalls = m.tool_calls;
|
|
65
|
+
if (Array.isArray(toolCalls)) {
|
|
66
|
+
for (const tc of toolCalls) {
|
|
67
|
+
const name = tc?.name;
|
|
68
|
+
if (typeof name === 'string' && name.length > 0)
|
|
69
|
+
acc.tools.add(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Executed tool result (ToolMessage). Its `.name` is the tool that produced the result.
|
|
73
|
+
const type = typeof m.getType === 'function' ? m.getType() : m._getType?.();
|
|
74
|
+
if (type === 'tool' && typeof m.name === 'string' && m.name.length > 0) {
|
|
75
|
+
acc.tools.add(m.name);
|
|
76
|
+
// BATCH-21 — capture the RESULT record too (same capture site, same fail-soft discipline):
|
|
77
|
+
// `.status === 'error'` is LangChain's real tool-error signal, `.content` the returned
|
|
78
|
+
// payload (capped; omitted when no text can be derived). One record per ToolMessage, in
|
|
79
|
+
// arrival order — deliberately NOT deduplicated, unlike the name set above.
|
|
80
|
+
const content = toolResultContentText(m.content);
|
|
81
|
+
acc.toolResults.push({
|
|
82
|
+
name: m.name,
|
|
83
|
+
isError: m.status === 'error',
|
|
84
|
+
...(content !== undefined ? { content } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* fail-soft: never let stats capture affect a run */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
|
|
93
|
+
export function finalizeRunStats(acc) {
|
|
94
|
+
return {
|
|
95
|
+
tokensInput: acc.sawUsage ? acc.input : undefined,
|
|
96
|
+
tokensOutput: acc.sawUsage ? acc.output : undefined,
|
|
97
|
+
tools: [...acc.tools],
|
|
98
|
+
toolResults: [...acc.toolResults],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
|
|
103
|
+
* state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
|
|
104
|
+
*/
|
|
105
|
+
export function extractRunStats(messages) {
|
|
106
|
+
const acc = createRunStatsAccumulator();
|
|
107
|
+
try {
|
|
108
|
+
if (Array.isArray(messages)) {
|
|
109
|
+
for (const m of messages)
|
|
110
|
+
accumulateMessage(acc, m);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* fail-soft */
|
|
115
|
+
}
|
|
116
|
+
return finalizeRunStats(acc);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=runStats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runStats.js","sourceRoot":"","sources":["../../src/core/runStats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/ShellCommandFailedError
|
|
3
|
+
*
|
|
4
|
+
* The typed error a `run_*` shell/dev tool throws when a command did NOT exit cleanly
|
|
5
|
+
* (non-zero exit code, or killed for exceeding the timeout). It carries the FULL model-facing
|
|
6
|
+
* body so a softening middleware can hand the model the exact observation it saw before — the
|
|
7
|
+
* only change being that the tool result's status flips to `'error'` (→ `isError` → the ✗ glyph).
|
|
8
|
+
*
|
|
9
|
+
* Canonical home is **core** so BOTH agents can recognise a shell failure without violating the
|
|
10
|
+
* `agent → core` dependency direction:
|
|
11
|
+
* - the deep agent (`code` mode, `@gaunt-sloth/agent` `GthDeepShellExitSoftening`) and
|
|
12
|
+
* - the lean agent (`exec` / `ask --write`, core `GthLangChainAgent` `GthLeanShellExitSoftening`).
|
|
13
|
+
*
|
|
14
|
+
* The throw site (`GthDevToolkit.executeCommand`) lives in the `agent` package and re-exports this
|
|
15
|
+
* class, so its `throw new ShellCommandFailedError(...)` is one and the same type both agents catch.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A `run_*` command that did NOT exit cleanly (non-zero exit code, or was killed for exceeding the
|
|
19
|
+
* timeout). Carries the FULL model-facing body text ({@link output}) so a softening middleware can
|
|
20
|
+
* hand the model the exact same observation it saw before — the only change is the tool result's
|
|
21
|
+
* status flips to `'error'`, which drives the ✗ (`isError`) glyph.
|
|
22
|
+
*
|
|
23
|
+
* `executeCommand` previously `resolve()`d on a non-zero exit, so the LangChain `ToolMessage` stayed
|
|
24
|
+
* `status: 'success'` and every failure rendered a ✓. Throwing this typed error instead lets each
|
|
25
|
+
* agent's softening middleware convert it into an error `ToolMessage`. A clean exit (`code === 0`)
|
|
26
|
+
* still `resolve()`s; a spawn-level `child.on('error')` still rejects with a plain `Error`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ShellCommandFailedError extends Error {
|
|
29
|
+
/** The full model-facing body (command echo + `<COMMAND_OUTPUT>` + the failure/timeout tail). */
|
|
30
|
+
readonly output: string;
|
|
31
|
+
/** The process exit code; `null` when the command was killed (timeout) and never exited cleanly. */
|
|
32
|
+
readonly exitCode: number | null;
|
|
33
|
+
/** The exact command string that was executed. */
|
|
34
|
+
readonly command: string;
|
|
35
|
+
/** The run_* tool name that invoked the command (e.g. `run_tests`, `run_shell_command`). */
|
|
36
|
+
readonly toolName: string;
|
|
37
|
+
constructor(params: {
|
|
38
|
+
output: string;
|
|
39
|
+
exitCode: number | null;
|
|
40
|
+
command: string;
|
|
41
|
+
toolName: string;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Recognise a {@link ShellCommandFailedError} for the softening middleware in either agent.
|
|
46
|
+
*
|
|
47
|
+
* Prefers a plain `instanceof` (both agents share this one core module, so the class identity is
|
|
48
|
+
* the same), but falls back to a STRUCTURAL check keyed on `name === 'ShellCommandFailedError'`
|
|
49
|
+
* plus the carried fields. The structural arm is deliberate defence against a dual-package /
|
|
50
|
+
* realm-boundary hazard: if the error ever crossed a module boundary that broke `instanceof`, we
|
|
51
|
+
* would otherwise silently rethrow a real shell failure and regress the ✗ signal. Every field the
|
|
52
|
+
* softener reads (`output`) is asserted so a narrowed value is safe to use.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isShellCommandFailedError(e: unknown): e is ShellCommandFailedError;
|