@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.
Files changed (275) hide show
  1. package/.gsloth.review.md +2 -0
  2. package/README.md +71 -20
  3. package/dist/config/colour.d.ts +38 -0
  4. package/dist/config/colour.js +36 -0
  5. package/dist/config/colour.js.map +1 -0
  6. package/dist/config/configDiscovery.d.ts +79 -0
  7. package/dist/config/configDiscovery.js +80 -0
  8. package/dist/config/configDiscovery.js.map +1 -0
  9. package/dist/config/defaults.d.ts +20 -20
  10. package/dist/config/defaults.js +10 -8
  11. package/dist/config/defaults.js.map +1 -1
  12. package/dist/config/filesystem-tools.d.ts +41 -0
  13. package/dist/config/filesystem-tools.js +56 -0
  14. package/dist/config/filesystem-tools.js.map +1 -0
  15. package/dist/config/loader.d.ts +171 -19
  16. package/dist/config/loader.js +1054 -144
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/config/mouse.d.ts +50 -0
  19. package/dist/config/mouse.js +44 -0
  20. package/dist/config/mouse.js.map +1 -0
  21. package/dist/config/profiles.d.ts +68 -0
  22. package/dist/config/profiles.js +93 -0
  23. package/dist/config/profiles.js.map +1 -0
  24. package/dist/config/providerKeys.d.ts +69 -0
  25. package/dist/config/providerKeys.js +69 -0
  26. package/dist/config/providerKeys.js.map +1 -0
  27. package/dist/config/schema.d.ts +2671 -138
  28. package/dist/config/schema.js +1361 -85
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/config/shell-policy.d.ts +899 -111
  31. package/dist/config/shell-policy.js +800 -70
  32. package/dist/config/shell-policy.js.map +1 -1
  33. package/dist/config/tool-descriptions.d.ts +211 -0
  34. package/dist/config/tool-descriptions.js +272 -0
  35. package/dist/config/tool-descriptions.js.map +1 -0
  36. package/dist/config/types.d.ts +352 -41
  37. package/dist/config/types.js +1 -0
  38. package/dist/config/types.js.map +1 -1
  39. package/dist/config.d.ts +35 -1
  40. package/dist/config.js +16 -1
  41. package/dist/config.js.map +1 -1
  42. package/dist/constants.d.ts +28 -1
  43. package/dist/constants.js +28 -1
  44. package/dist/constants.js.map +1 -1
  45. package/dist/core/GthAbstractAgent.d.ts +166 -11
  46. package/dist/core/GthAbstractAgent.js +484 -45
  47. package/dist/core/GthAbstractAgent.js.map +1 -1
  48. package/dist/core/GthAgentRunner.d.ts +543 -57
  49. package/dist/core/GthAgentRunner.js +1494 -140
  50. package/dist/core/GthAgentRunner.js.map +1 -1
  51. package/dist/core/GthLangChainAgent.d.ts +117 -2
  52. package/dist/core/GthLangChainAgent.js +602 -29
  53. package/dist/core/GthLangChainAgent.js.map +1 -1
  54. package/dist/core/approvals/annotations.d.ts +122 -0
  55. package/dist/core/approvals/annotations.js +137 -0
  56. package/dist/core/approvals/annotations.js.map +1 -0
  57. package/dist/core/approvals/grants.d.ts +216 -0
  58. package/dist/core/approvals/grants.js +469 -0
  59. package/dist/core/approvals/grants.js.map +1 -0
  60. package/dist/core/approvals/matcher.d.ts +202 -0
  61. package/dist/core/approvals/matcher.js +267 -0
  62. package/dist/core/approvals/matcher.js.map +1 -0
  63. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  64. package/dist/core/approvals/mcpSubjects.js +99 -0
  65. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  66. package/dist/core/approvals/promptHeader.d.ts +28 -0
  67. package/dist/core/approvals/promptHeader.js +62 -0
  68. package/dist/core/approvals/promptHeader.js.map +1 -0
  69. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  70. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  71. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  72. package/dist/core/approvals/toolHost.d.ts +46 -0
  73. package/dist/core/approvals/toolHost.js +108 -0
  74. package/dist/core/approvals/toolHost.js.map +1 -0
  75. package/dist/core/debugCapture.d.ts +74 -0
  76. package/dist/core/debugCapture.js +100 -0
  77. package/dist/core/debugCapture.js.map +1 -0
  78. package/dist/core/gthLeanAgentFactory.d.ts +4 -4
  79. package/dist/core/gthLeanAgentFactory.js +4 -4
  80. package/dist/core/launchBanner.d.ts +127 -0
  81. package/dist/core/launchBanner.js +414 -0
  82. package/dist/core/launchBanner.js.map +1 -0
  83. package/dist/core/modelLabel.d.ts +19 -0
  84. package/dist/core/modelLabel.js +26 -0
  85. package/dist/core/modelLabel.js.map +1 -0
  86. package/dist/core/plainToolIndication.d.ts +15 -0
  87. package/dist/core/plainToolIndication.js +174 -0
  88. package/dist/core/plainToolIndication.js.map +1 -0
  89. package/dist/core/reasoningBlocks.d.ts +65 -0
  90. package/dist/core/reasoningBlocks.js +103 -0
  91. package/dist/core/reasoningBlocks.js.map +1 -0
  92. package/dist/core/refusal.d.ts +53 -0
  93. package/dist/core/refusal.js +133 -0
  94. package/dist/core/refusal.js.map +1 -0
  95. package/dist/core/runHeader.d.ts +38 -0
  96. package/dist/core/runHeader.js +42 -0
  97. package/dist/core/runHeader.js.map +1 -0
  98. package/dist/core/runStats.d.ts +14 -3
  99. package/dist/core/runStats.js +48 -3
  100. package/dist/core/runStats.js.map +1 -1
  101. package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
  102. package/dist/core/shell/ShellCommandFailedError.js +3 -4
  103. package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
  104. package/dist/core/shell/abstention.d.ts +88 -0
  105. package/dist/core/shell/abstention.js +184 -0
  106. package/dist/core/shell/abstention.js.map +1 -0
  107. package/dist/core/shell/approvalCapture.d.ts +271 -0
  108. package/dist/core/shell/approvalCapture.js +108 -0
  109. package/dist/core/shell/approvalCapture.js.map +1 -0
  110. package/dist/core/shell/approvalStop.d.ts +123 -0
  111. package/dist/core/shell/approvalStop.js +269 -0
  112. package/dist/core/shell/approvalStop.js.map +1 -0
  113. package/dist/core/shell/arity.d.ts +6 -0
  114. package/dist/core/shell/arity.js +20 -6
  115. package/dist/core/shell/arity.js.map +1 -1
  116. package/dist/core/shell/denylist.d.ts +11 -0
  117. package/dist/core/shell/denylist.js +37 -0
  118. package/dist/core/shell/denylist.js.map +1 -0
  119. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  120. package/dist/core/shell/escalationSeverity.js +89 -0
  121. package/dist/core/shell/escalationSeverity.js.map +1 -0
  122. package/dist/core/shell/framing.d.ts +190 -0
  123. package/dist/core/shell/framing.js +633 -0
  124. package/dist/core/shell/framing.js.map +1 -0
  125. package/dist/core/shell/hardline.d.ts +103 -0
  126. package/dist/core/shell/hardline.js +780 -0
  127. package/dist/core/shell/hardline.js.map +1 -0
  128. package/dist/core/shell/negotiation.d.ts +328 -0
  129. package/dist/core/shell/negotiation.js +488 -0
  130. package/dist/core/shell/negotiation.js.map +1 -0
  131. package/dist/core/shell/normalize.d.ts +44 -4
  132. package/dist/core/shell/normalize.js +61 -7
  133. package/dist/core/shell/normalize.js.map +1 -1
  134. package/dist/core/shell/openWorld.d.ts +263 -0
  135. package/dist/core/shell/openWorld.js +1188 -0
  136. package/dist/core/shell/openWorld.js.map +1 -0
  137. package/dist/core/shell/rater.d.ts +873 -0
  138. package/dist/core/shell/rater.js +1454 -0
  139. package/dist/core/shell/rater.js.map +1 -0
  140. package/dist/core/shell/raterModel.d.ts +41 -0
  141. package/dist/core/shell/raterModel.js +51 -0
  142. package/dist/core/shell/raterModel.js.map +1 -0
  143. package/dist/core/shell/raterVocabulary.d.ts +121 -0
  144. package/dist/core/shell/raterVocabulary.js +116 -0
  145. package/dist/core/shell/raterVocabulary.js.map +1 -0
  146. package/dist/core/shell/rejection.d.ts +69 -0
  147. package/dist/core/shell/rejection.js +38 -0
  148. package/dist/core/shell/rejection.js.map +1 -0
  149. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  150. package/dist/core/toolCallRepair/grammar.js +116 -0
  151. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  152. package/dist/core/toolCallRepair/index.d.ts +2 -0
  153. package/dist/core/toolCallRepair/index.js +7 -0
  154. package/dist/core/toolCallRepair/index.js.map +1 -0
  155. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  156. package/dist/core/toolCallRepair/payload.js +341 -0
  157. package/dist/core/toolCallRepair/payload.js.map +1 -0
  158. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  159. package/dist/core/toolCallRepair/promote.js +90 -0
  160. package/dist/core/toolCallRepair/promote.js.map +1 -0
  161. package/dist/core/toolDisplay.d.ts +123 -0
  162. package/dist/core/toolDisplay.js +451 -0
  163. package/dist/core/toolDisplay.js.map +1 -0
  164. package/dist/core/toolOutputChannel.d.ts +95 -0
  165. package/dist/core/toolOutputChannel.js +165 -0
  166. package/dist/core/toolOutputChannel.js.map +1 -0
  167. package/dist/core/types.d.ts +349 -16
  168. package/dist/core/types.js.map +1 -1
  169. package/dist/history/historyFormat.d.ts +12 -3
  170. package/dist/history/historyFormat.js +50 -8
  171. package/dist/history/historyFormat.js.map +1 -1
  172. package/dist/history/historyStore.d.ts +77 -0
  173. package/dist/history/historyStore.js +173 -6
  174. package/dist/history/historyStore.js.map +1 -1
  175. package/dist/history/recordSession.d.ts +10 -1
  176. package/dist/history/recordSession.js +27 -0
  177. package/dist/history/recordSession.js.map +1 -1
  178. package/dist/providers/anthropic.js +12 -0
  179. package/dist/providers/anthropic.js.map +1 -1
  180. package/dist/providers/configurationPassthrough.d.ts +107 -0
  181. package/dist/providers/configurationPassthrough.js +148 -0
  182. package/dist/providers/configurationPassthrough.js.map +1 -0
  183. package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
  184. package/dist/providers/geminiSchemaSanitizer.js +347 -0
  185. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  186. package/dist/providers/geminiThinking.d.ts +60 -0
  187. package/dist/providers/geminiThinking.js +92 -0
  188. package/dist/providers/geminiThinking.js.map +1 -0
  189. package/dist/providers/google-genai.js +18 -1
  190. package/dist/providers/google-genai.js.map +1 -1
  191. package/dist/providers/groq.js +12 -0
  192. package/dist/providers/groq.js.map +1 -1
  193. package/dist/providers/huggingface.d.ts +25 -0
  194. package/dist/providers/huggingface.js +69 -0
  195. package/dist/providers/huggingface.js.map +1 -0
  196. package/dist/providers/modelCatalog.d.ts +109 -0
  197. package/dist/providers/modelCatalog.js +245 -0
  198. package/dist/providers/modelCatalog.js.map +1 -0
  199. package/dist/providers/modelDiscovery.d.ts +99 -5
  200. package/dist/providers/modelDiscovery.js +191 -35
  201. package/dist/providers/modelDiscovery.js.map +1 -1
  202. package/dist/providers/ollama.d.ts +18 -4
  203. package/dist/providers/ollama.js +67 -37
  204. package/dist/providers/ollama.js.map +1 -1
  205. package/dist/providers/openai.js +34 -0
  206. package/dist/providers/openai.js.map +1 -1
  207. package/dist/providers/openrouter.d.ts +26 -4
  208. package/dist/providers/openrouter.js +83 -26
  209. package/dist/providers/openrouter.js.map +1 -1
  210. package/dist/providers/vertexai.js +19 -1
  211. package/dist/providers/vertexai.js.map +1 -1
  212. package/dist/providers/xai.js +20 -0
  213. package/dist/providers/xai.js.map +1 -1
  214. package/dist/runtime/askStructured.d.ts +105 -0
  215. package/dist/runtime/askStructured.js +120 -0
  216. package/dist/runtime/askStructured.js.map +1 -0
  217. package/dist/runtime/conversation.d.ts +64 -0
  218. package/dist/runtime/conversation.js +171 -0
  219. package/dist/runtime/conversation.js.map +1 -0
  220. package/dist/runtime/singleShot.d.ts +37 -6
  221. package/dist/runtime/singleShot.js +113 -67
  222. package/dist/runtime/singleShot.js.map +1 -1
  223. package/dist/runtime/structuredOutput.d.ts +104 -0
  224. package/dist/runtime/structuredOutput.js +393 -0
  225. package/dist/runtime/structuredOutput.js.map +1 -0
  226. package/dist/utils/ProgressIndicator.d.ts +21 -0
  227. package/dist/utils/ProgressIndicator.js +30 -3
  228. package/dist/utils/ProgressIndicator.js.map +1 -1
  229. package/dist/utils/aiignoreUtils.js.map +1 -1
  230. package/dist/utils/binaryOutputUtils.js.map +1 -1
  231. package/dist/utils/consoleUtils.d.ts +95 -0
  232. package/dist/utils/consoleUtils.js +112 -2
  233. package/dist/utils/consoleUtils.js.map +1 -1
  234. package/dist/utils/crashHandler.d.ts +87 -0
  235. package/dist/utils/crashHandler.js +128 -0
  236. package/dist/utils/crashHandler.js.map +1 -0
  237. package/dist/utils/debugDump.d.ts +134 -0
  238. package/dist/utils/debugDump.js +381 -0
  239. package/dist/utils/debugDump.js.map +1 -0
  240. package/dist/utils/debugUtils.d.ts +13 -4
  241. package/dist/utils/debugUtils.js +36 -13
  242. package/dist/utils/debugUtils.js.map +1 -1
  243. package/dist/utils/displayWidth.d.ts +53 -0
  244. package/dist/utils/displayWidth.js +195 -0
  245. package/dist/utils/displayWidth.js.map +1 -0
  246. package/dist/utils/fileUtils.js.map +1 -1
  247. package/dist/utils/globalConfigUtils.d.ts +14 -2
  248. package/dist/utils/globalConfigUtils.js +22 -4
  249. package/dist/utils/globalConfigUtils.js.map +1 -1
  250. package/dist/utils/llmUtils.d.ts +39 -8
  251. package/dist/utils/llmUtils.js +76 -8
  252. package/dist/utils/llmUtils.js.map +1 -1
  253. package/dist/utils/redactSecrets.d.ts +63 -0
  254. package/dist/utils/redactSecrets.js +286 -0
  255. package/dist/utils/redactSecrets.js.map +1 -0
  256. package/dist/utils/systemPromptNotes.d.ts +222 -0
  257. package/dist/utils/systemPromptNotes.js +338 -0
  258. package/dist/utils/systemPromptNotes.js.map +1 -0
  259. package/dist/utils/systemUtils.d.ts +18 -1
  260. package/dist/utils/systemUtils.js +38 -3
  261. package/dist/utils/systemUtils.js.map +1 -1
  262. package/dist/utils/toolMatching.d.ts +30 -0
  263. package/dist/utils/toolMatching.js +44 -0
  264. package/dist/utils/toolMatching.js.map +1 -0
  265. package/dist/utils/untrustedText.d.ts +86 -0
  266. package/dist/utils/untrustedText.js +101 -0
  267. package/dist/utils/untrustedText.js.map +1 -0
  268. package/package.json +21 -7
  269. package/schema/gsloth-config.schema.json +1921 -326
  270. package/dist/core/shell/allowlist.d.ts +0 -75
  271. package/dist/core/shell/allowlist.js +0 -187
  272. package/dist/core/shell/allowlist.js.map +0 -1
  273. package/dist/core/shell/judge.d.ts +0 -161
  274. package/dist/core/shell/judge.js +0 -261
  275. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @module plainToolIndication
3
+ * TUI-C30 — compact tool-call indication for the PLAIN surface (`--no-tui` readline sessions,
4
+ * piped/single-shot `ask`/`exec`/`review`/`pr`). The Ink TUI renders tool calls from the typed
5
+ * event stream; the plain surface streams strings, so until now a tool call only surfaced
6
+ * through the tools' own transient notices (`📁 Reading file: …`, `🔧 Executing …` + raw child
7
+ * output via the tool-output channel's default sink). This module watches the SAME LangGraph
8
+ * message stream the string path already iterates and, when each `ToolMessage` lands, prints
9
+ * one compact indication built from the shared {@link toolDisplay} registry:
10
+ *
11
+ * ✓ 📁 read_file(path=README.md)
12
+ * # Readme ← up to the canonical 10 preview lines, dim
13
+ * … (+42 more lines)
14
+ *
15
+ * Stream discipline (matches how the plain surface prints tool activity today): the block is
16
+ * emitted at INFO level through `displayToolIndication` — same stdout channel, same
17
+ * `consoleLevel` gate and session-log treatment as the existing tool notices — so scripted
18
+ * consumers that already silence INFO chatter silence this too. Colour is used exactly when the
19
+ * resolved `useColour` (the CFG-30 ladder in `config/colour.ts`) says so — TUI-C35 removed the
20
+ * local `&& stdout.isTTY` narrowing this module used to apply on top, which was redundant against
21
+ * the ladder's own rung-4 TTY auto-detection everywhere except `FORCE_COLOR` on a pipe, the one
22
+ * case that variable exists to serve. An ordinary piped run is therefore still clean monochrome
23
+ * (DL-7) — rung 4 decides that — with diff lines readable via their `+`/`-` prefixes.
24
+ *
25
+ * Live-output dedupe: shell-shaped results (`<COMMAND_OUTPUT>`) belong to tools whose child
26
+ * output ALREADY streamed raw via the channel's default sink, so those render with
27
+ * `liveOutputAlreadyShown` and show only the closing status line — never a repeat of output
28
+ * the user just watched.
29
+ */
30
+ import { AIMessage, AIMessageChunk, ToolMessage } from '@langchain/core/messages';
31
+ import { buildToolPreviewLines, getToolGlyph, isShellShapedResult, renderToolLineAnsi, summariseToolCall, } from '#src/core/toolDisplay.js';
32
+ import { displayToolIndication } from '#src/utils/consoleUtils.js';
33
+ import { getUseColour } from '#src/utils/systemUtils.js';
34
+ const INDENT = ' ';
35
+ /**
36
+ * Create the per-stream observer. State is scoped to one stream (one `agent.stream()` call);
37
+ * tool_call deltas are accumulated from `tool_call_chunks` (keyed by the provider's chunk
38
+ * `index`, which restarts per LLM round — the map is flushed into the by-id map whenever a
39
+ * `ToolMessage` arrives, mirroring `processEventStream`'s reset-per-round). Deliberately does
40
+ * NOT `concat()` whole `AIMessageChunk`s: only the tool-call slices are needed, which also
41
+ * sidesteps the TUI-C29 `__raw_response` aggregation-growth trap entirely.
42
+ *
43
+ * `emit` is injectable for tests; production uses the INFO-level `displayToolIndication`.
44
+ */
45
+ export function createPlainToolIndication(emit = displayToolIndication) {
46
+ /** Streaming tool-call deltas for the CURRENT round, keyed by tool_call_chunk index. */
47
+ const streaming = new Map();
48
+ /** Completed calls awaiting their ToolMessage, keyed by tool call id. */
49
+ const byId = new Map();
50
+ const flushStreamingIntoById = () => {
51
+ for (const call of streaming.values()) {
52
+ if (call.id)
53
+ byId.set(call.id, call);
54
+ }
55
+ streaming.clear();
56
+ };
57
+ const renderToolMessage = (message) => {
58
+ const id = typeof message.tool_call_id === 'string' ? message.tool_call_id : '';
59
+ const tracked = id ? byId.get(id) : undefined;
60
+ if (id)
61
+ byId.delete(id);
62
+ const name = tracked?.name || (typeof message.name === 'string' ? message.name : '') || '';
63
+ const result = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
64
+ const isError = message.status === 'error';
65
+ // TUI-C35 — colour is exactly what the resolved ladder says, with no local narrowing.
66
+ // This used to AND in `stdout.isTTY`, which was redundant in every case but one: rung 4 of
67
+ // `config/colour.ts` already auto-detects from stdout's TTY status, so an unconfigured piped
68
+ // run is monochrome either way. The one case it changed was `FORCE_COLOR` into a pipe — which
69
+ // it suppressed, defeating the only thing that variable is for.
70
+ const colour = getUseColour();
71
+ const statusGlyph = isError
72
+ ? colour
73
+ ? '\x1b[31m✗\x1b[0m'
74
+ : '✗'
75
+ : colour
76
+ ? '\x1b[32m✓\x1b[0m'
77
+ : '✓';
78
+ const summary = summariseToolCall(name, tracked?.argsText);
79
+ const summaryText = colour ? `\x1b[2m${summary}\x1b[0m` : summary;
80
+ const head = `${statusGlyph} ${getToolGlyph(name)} ${summaryText}`;
81
+ const preview = buildToolPreviewLines({
82
+ name,
83
+ argsText: tracked?.argsText,
84
+ result,
85
+ isError,
86
+ // Shell-shaped results stream their child output live through the channel's default
87
+ // sink on this surface — suppress the duplicated body, keep the status tail. TUI-C32
88
+ // residual c: gate on the tool NAME + shape (not shape alone), so a non-shell tool whose
89
+ // result merely quotes `<COMMAND_OUTPUT>` keeps its preview body instead of being suppressed.
90
+ liveOutputAlreadyShown: isShellShapedResult(name, result),
91
+ });
92
+ const body = preview.map((line) => INDENT + renderToolLineAnsi(line, colour));
93
+ // Leading newline mirrors the historical notice framing (the model text stream may have
94
+ // left the cursor mid-line).
95
+ emit(['', head, ...body].join('\n'));
96
+ };
97
+ return {
98
+ observe(chunk) {
99
+ // Order matters: AIMessageChunk extends AIMessage, so test the chunk shape first
100
+ // (mirrors processEventStream).
101
+ if (AIMessageChunk.isInstance(chunk)) {
102
+ // TUI-C32 residual e — fail-soft, matching the ToolMessage branch: accumulating tool-call
103
+ // deltas (`JSON.stringify(tc.args)` can throw on an unserialisable arg, e.g. a BigInt) must
104
+ // never break the run's stream loop. On any error we simply skip this chunk's tracking.
105
+ try {
106
+ const c = chunk;
107
+ const deltas = c.tool_call_chunks ?? [];
108
+ if (deltas.length > 0) {
109
+ for (const delta of deltas) {
110
+ const index = typeof delta.index === 'number' ? delta.index : 0;
111
+ const entry = streaming.get(index) ?? { name: '', argsText: '' };
112
+ if (delta.id)
113
+ entry.id = delta.id;
114
+ if (delta.name)
115
+ entry.name = entry.name || delta.name;
116
+ if (delta.args)
117
+ entry.argsText += delta.args;
118
+ streaming.set(index, entry);
119
+ }
120
+ }
121
+ else {
122
+ // Some providers surface COMPLETE tool_calls on a chunk instead of deltas.
123
+ for (const tc of c.tool_calls ?? []) {
124
+ if (tc.id) {
125
+ byId.set(tc.id, {
126
+ id: tc.id,
127
+ name: tc.name,
128
+ argsText: JSON.stringify(tc.args ?? {}),
129
+ });
130
+ }
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ /* indication is best-effort; the model-facing stream is untouched */
136
+ }
137
+ return;
138
+ }
139
+ if (AIMessage.isInstance(chunk)) {
140
+ // A non-chunk AIMessage (resumed/checkpoint-replayed runs) carries final tool_calls.
141
+ // TUI-C32 residual e — same fail-soft wrap as above/the ToolMessage branch.
142
+ try {
143
+ const m = chunk;
144
+ for (const tc of m.tool_calls ?? []) {
145
+ if (tc.id) {
146
+ byId.set(tc.id, {
147
+ id: tc.id,
148
+ name: tc.name,
149
+ argsText: JSON.stringify(tc.args ?? {}),
150
+ });
151
+ }
152
+ }
153
+ }
154
+ catch {
155
+ /* indication is best-effort; the model-facing stream is untouched */
156
+ }
157
+ return;
158
+ }
159
+ if (chunk instanceof ToolMessage) {
160
+ // The round is over: park any streamed calls under their ids (chunk indexes restart
161
+ // next round), then render the arrived result. Fail-soft — rendering must never break
162
+ // the run.
163
+ try {
164
+ flushStreamingIntoById();
165
+ renderToolMessage(chunk);
166
+ }
167
+ catch {
168
+ /* indication is best-effort; the model-facing stream is untouched */
169
+ }
170
+ }
171
+ },
172
+ };
173
+ }
174
+ //# sourceMappingURL=plainToolIndication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plainToolIndication.js","sourceRoot":"","sources":["../../src/core/plainToolIndication.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
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
+ * A pure content transform with no opinion about graph state — what a caller does with the result is
58
+ * the caller's argument to make. Every caller in this package only READS the result — rendering it,
59
+ * or pulling a refusal's explanation out of it — so the message kept in state stays whole and its
60
+ * thought parts (and any `thoughtSignature` riding with them) still replay to the provider as
61
+ * history. The one caller that rewrites state instead is the subagent thought
62
+ * redaction in `@gaunt-sloth/agent`, which strips a FINISHED subagent's own messages; why that costs
63
+ * nothing is argued there.
64
+ */
65
+ export declare function stripReasoningBlocks(content: unknown): unknown;
@@ -0,0 +1,103 @@
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
+ * A pure content transform with no opinion about graph state — what a caller does with the result is
90
+ * the caller's argument to make. Every caller in this package only READS the result — rendering it,
91
+ * or pulling a refusal's explanation out of it — so the message kept in state stays whole and its
92
+ * thought parts (and any `thoughtSignature` riding with them) still replay to the provider as
93
+ * history. The one caller that rewrites state instead is the subagent thought
94
+ * redaction in `@gaunt-sloth/agent`, which strips a FINISHED subagent's own messages; why that costs
95
+ * nothing is argued there.
96
+ */
97
+ export function stripReasoningBlocks(content) {
98
+ if (!Array.isArray(content))
99
+ return content;
100
+ const kept = content.filter((block) => !isReasoningContentBlock(block));
101
+ return kept.length === content.length ? content : kept;
102
+ }
103
+ //# 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;;;;;;;;;;;;;GAaG;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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAYpE,iGAAiG;AACjG,SAAS,SAAS,CAAC,MAAe,EAAE,GAAW;IAC7C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC5D,OAAQ,MAAkC,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,iGAAiG;AACjG,SAAS,kBAAkB,CAAC,OAAgB;IAC1C,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC9C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IACpF,6FAA6F;IAC7F,iGAAiG;IACjG,6FAA6F;IAC7F,yBAAyB;IACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAI,oBAAoB,CAAC,OAAO,CAAe;aACtD,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACZ,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1C,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAClC,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACxC,CAAC,CAAC;aACD,IAAI,CAAC,EAAE,CAAC;aACR,IAAI,EAAE,CAAC;QACV,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACnC,CAAC;IACD,mEAAmE;IACnE,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACzD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC;IAC1F,yFAAyF;IACzF,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACnF,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QACpE,OAAO,YAAY,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzD,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAEvD,uEAAuE;IACvE,MAAM,YAAY,GAChB,SAAS,CAAC,IAAI,EAAE,eAAe,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,eAAe,CAAC,IAAI,SAAS,CAAC;IACtF,MAAM,eAAe,GACnB,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,SAAS,CAAC;IAClF,MAAM,eAAe,GACnB,SAAS,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,SAAS,CAAC;IAEhF,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC7F,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IAE5C,MAAM,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAEhD,gCAAgC;IAChC,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAC;IACvE,CAAC;IACD,iCAAiC;IACjC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;IACnE,CAAC;IACD,gGAAgG;IAChG,IACE,SAAS,KAAK,sBAAsB;QACpC,SAAS,KAAK,sBAAsB;QACpC,MAAM,KAAK,sBAAsB;QACjC,SAAS,CAAC,MAAM,EAAE,gCAAgC,CAAC,KAAK,YAAY;QACpE,SAAS,CAAC,IAAI,EAAE,gCAAgC,CAAC,KAAK,YAAY,EAClE,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,sBAAsB,EAAE,WAAW,EAAE,CAAC;IAC9E,CAAC;IACD,oFAAoF;IACpF,4FAA4F;IAC5F,+FAA+F;IAC/F,IACE,SAAS,KAAK,kBAAkB;QAChC,SAAS,KAAK,kBAAkB;QAChC,MAAM,KAAK,kBAAkB,EAC7B,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,WAAW,EAAE,CAAC;IAC1E,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAiB;IACnD,MAAM,IAAI,GACR,gFAAgF;QAChF,gEAAgE,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW;QAC7B,CAAC,CAAC,wBAAwB,IAAI,CAAC,WAAW,EAAE;QAC5C,CAAC,CAAC,oCAAoC,CAAC;IACzC,MAAM,IAAI,GACR,0FAA0F;QAC1F,wCAAwC,CAAC;IAC3C,OAAO,GAAG,IAAI,OAAO,MAAM,OAAO,IAAI,EAAE,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @module runHeader
3
+ * GS2-95 — the ONE spelling of the run header, the single line every command opens with.
4
+ *
5
+ * Two unrelated writers render it: the agent's `compact` rung
6
+ * (`GthAbstractAgent#compactHeaderStatus`) and the review document's opening line
7
+ * (`@gaunt-sloth/review`'s `reviewHeadingBlock`). They emit at different moments, through different
8
+ * helpers, into different sinks — so the only thing that can keep them saying the same thing is a
9
+ * shared builder rather than two templates that happen to match today.
10
+ *
11
+ * It is a leaf with **no imports at all**, for the reason `modelLabel` is: the agent reaches it
12
+ * during module load, and pulling console/filesystem helpers in to format one line would drag that
13
+ * graph along.
14
+ *
15
+ * The header carries no markdown, in any mode. The honest condition for a `##` prefix is not "this
16
+ * run is under GitHub Actions" but "the consumer renders markdown", and those differ in both
17
+ * directions — the Ink TUI renders markdown, a GHA *job log* does not while a GHA *step summary*
18
+ * does. That property belongs to the SINK, and one run has several at once (the TUI on screen, a
19
+ * posted PR comment, a session log on disk), so no single flag can be right for all of them.
20
+ * Formatting a header is not worth a per-sink capability, so there is no prefix anywhere.
21
+ */
22
+ /** The product name the header opens with — the part a reader is meant to recognise across runs. */
23
+ export declare const RUN_HEADER_NAME = "Gaunt Sloth";
24
+ /** Separates the header's parts: U+00B7 MIDDLE DOT, padded with a space on each side. */
25
+ export declare const RUN_HEADER_SEPARATOR = " \u00B7 ";
26
+ /**
27
+ * Build the run header: `Gaunt Sloth · <command> · <model> (<provider>)`.
28
+ *
29
+ * `command` is the name of the command the USER typed (`review`, `exec`, `eval`), never the init
30
+ * verb that selects the agent's mode prompt — the two differ for every command that runs its work
31
+ * through another verb's prompt, and a header naming the verb misnames the run.
32
+ *
33
+ * `label` is the caller's already-resolved `model (provider)` half — `modelProviderLabel`'s one
34
+ * spelling of "which model served this run" (DL-6). It is resolved by the caller rather than here
35
+ * because the two writers gate it differently, and it is **dropped rather than faked** when nothing
36
+ * resolves (DL-7): the line then ends after the command, with no placeholder and no empty `()`.
37
+ */
38
+ export declare function runHeaderLine(command: string, label?: string): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module runHeader
3
+ * GS2-95 — the ONE spelling of the run header, the single line every command opens with.
4
+ *
5
+ * Two unrelated writers render it: the agent's `compact` rung
6
+ * (`GthAbstractAgent#compactHeaderStatus`) and the review document's opening line
7
+ * (`@gaunt-sloth/review`'s `reviewHeadingBlock`). They emit at different moments, through different
8
+ * helpers, into different sinks — so the only thing that can keep them saying the same thing is a
9
+ * shared builder rather than two templates that happen to match today.
10
+ *
11
+ * It is a leaf with **no imports at all**, for the reason `modelLabel` is: the agent reaches it
12
+ * during module load, and pulling console/filesystem helpers in to format one line would drag that
13
+ * graph along.
14
+ *
15
+ * The header carries no markdown, in any mode. The honest condition for a `##` prefix is not "this
16
+ * run is under GitHub Actions" but "the consumer renders markdown", and those differ in both
17
+ * directions — the Ink TUI renders markdown, a GHA *job log* does not while a GHA *step summary*
18
+ * does. That property belongs to the SINK, and one run has several at once (the TUI on screen, a
19
+ * posted PR comment, a session log on disk), so no single flag can be right for all of them.
20
+ * Formatting a header is not worth a per-sink capability, so there is no prefix anywhere.
21
+ */
22
+ /** The product name the header opens with — the part a reader is meant to recognise across runs. */
23
+ export const RUN_HEADER_NAME = 'Gaunt Sloth';
24
+ /** Separates the header's parts: U+00B7 MIDDLE DOT, padded with a space on each side. */
25
+ export const RUN_HEADER_SEPARATOR = ' · ';
26
+ /**
27
+ * Build the run header: `Gaunt Sloth · <command> · <model> (<provider>)`.
28
+ *
29
+ * `command` is the name of the command the USER typed (`review`, `exec`, `eval`), never the init
30
+ * verb that selects the agent's mode prompt — the two differ for every command that runs its work
31
+ * through another verb's prompt, and a header naming the verb misnames the run.
32
+ *
33
+ * `label` is the caller's already-resolved `model (provider)` half — `modelProviderLabel`'s one
34
+ * spelling of "which model served this run" (DL-6). It is resolved by the caller rather than here
35
+ * because the two writers gate it differently, and it is **dropped rather than faked** when nothing
36
+ * resolves (DL-7): the line then ends after the command, with no placeholder and no empty `()`.
37
+ */
38
+ export function runHeaderLine(command, label) {
39
+ const head = RUN_HEADER_NAME + RUN_HEADER_SEPARATOR + command;
40
+ return label ? head + RUN_HEADER_SEPARATOR + label : head;
41
+ }
42
+ //# sourceMappingURL=runHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runHeader.js","sourceRoot":"","sources":["../../src/core/runHeader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,oGAAoG;AACpG,MAAM,CAAC,MAAM,eAAe,GAAG,aAAa,CAAC;AAE7C,yFAAyF;AACzF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAE1C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,KAAc;IAC3D,MAAM,IAAI,GAAG,eAAe,GAAG,oBAAoB,GAAG,OAAO,CAAC;IAC9D,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,oBAAoB,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC"}