@gaunt-sloth/core 2.0.0-alpha.2 → 2.0.0-alpha.21

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 (158) hide show
  1. package/.gsloth.code.md +10 -0
  2. package/README.md +6 -6
  3. package/dist/config/defaults.d.ts +85 -0
  4. package/dist/config/defaults.js +103 -0
  5. package/dist/config/defaults.js.map +1 -0
  6. package/dist/config/jsonc.d.ts +12 -0
  7. package/dist/config/jsonc.js +41 -0
  8. package/dist/config/jsonc.js.map +1 -0
  9. package/dist/config/loader.d.ts +169 -0
  10. package/dist/config/loader.js +872 -0
  11. package/dist/config/loader.js.map +1 -0
  12. package/dist/config/schema.d.ts +673 -0
  13. package/dist/config/schema.js +524 -0
  14. package/dist/config/schema.js.map +1 -0
  15. package/dist/config/shell-policy.d.ts +331 -0
  16. package/dist/config/shell-policy.js +238 -0
  17. package/dist/config/shell-policy.js.map +1 -0
  18. package/dist/config/types.d.ts +596 -0
  19. package/dist/config/types.js +13 -0
  20. package/dist/config/types.js.map +1 -0
  21. package/dist/config.d.ts +18 -647
  22. package/dist/config.js +15 -516
  23. package/dist/config.js.map +1 -1
  24. package/dist/constants.d.ts +42 -0
  25. package/dist/constants.js +42 -0
  26. package/dist/constants.js.map +1 -1
  27. package/dist/core/GthAbstractAgent.d.ts +85 -2
  28. package/dist/core/GthAbstractAgent.js +398 -28
  29. package/dist/core/GthAbstractAgent.js.map +1 -1
  30. package/dist/core/GthAgentRunner.d.ts +158 -1
  31. package/dist/core/GthAgentRunner.js +367 -4
  32. package/dist/core/GthAgentRunner.js.map +1 -1
  33. package/dist/core/GthLangChainAgent.js +193 -16
  34. package/dist/core/GthLangChainAgent.js.map +1 -1
  35. package/dist/core/debugCapture.d.ts +59 -0
  36. package/dist/core/debugCapture.js +101 -0
  37. package/dist/core/debugCapture.js.map +1 -0
  38. package/dist/core/gthLeanAgentFactory.d.ts +9 -0
  39. package/dist/core/gthLeanAgentFactory.js +10 -0
  40. package/dist/core/gthLeanAgentFactory.js.map +1 -0
  41. package/dist/core/plainToolIndication.d.ts +15 -0
  42. package/dist/core/plainToolIndication.js +148 -0
  43. package/dist/core/plainToolIndication.js.map +1 -0
  44. package/dist/core/runStats.d.ts +41 -0
  45. package/dist/core/runStats.js +73 -0
  46. package/dist/core/runStats.js.map +1 -0
  47. package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
  48. package/dist/core/shell/ShellCommandFailedError.js +68 -0
  49. package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
  50. package/dist/core/shell/allowlist.d.ts +75 -0
  51. package/dist/core/shell/allowlist.js +187 -0
  52. package/dist/core/shell/allowlist.js.map +1 -0
  53. package/dist/core/shell/arity.d.ts +75 -0
  54. package/dist/core/shell/arity.js +313 -0
  55. package/dist/core/shell/arity.js.map +1 -0
  56. package/dist/core/shell/judge.d.ts +161 -0
  57. package/dist/core/shell/judge.js +261 -0
  58. package/dist/core/shell/judge.js.map +1 -0
  59. package/dist/core/shell/normalize.d.ts +27 -0
  60. package/dist/core/shell/normalize.js +53 -0
  61. package/dist/core/shell/normalize.js.map +1 -0
  62. package/dist/core/toolDisplay.d.ts +101 -0
  63. package/dist/core/toolDisplay.js +374 -0
  64. package/dist/core/toolDisplay.js.map +1 -0
  65. package/dist/core/toolOutputChannel.d.ts +68 -0
  66. package/dist/core/toolOutputChannel.js +112 -0
  67. package/dist/core/toolOutputChannel.js.map +1 -0
  68. package/dist/core/types.d.ts +173 -0
  69. package/dist/core/types.js.map +1 -1
  70. package/dist/history/historyFormat.d.ts +28 -0
  71. package/dist/history/historyFormat.js +127 -0
  72. package/dist/history/historyFormat.js.map +1 -0
  73. package/dist/history/historyStore.d.ts +198 -0
  74. package/dist/history/historyStore.js +482 -0
  75. package/dist/history/historyStore.js.map +1 -0
  76. package/dist/history/recordSession.d.ts +37 -0
  77. package/dist/history/recordSession.js +56 -0
  78. package/dist/history/recordSession.js.map +1 -0
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/index.js.map +1 -1
  82. package/dist/providers/anthropic.d.ts +1 -1
  83. package/dist/providers/anthropic.js +5 -10
  84. package/dist/providers/anthropic.js.map +1 -1
  85. package/dist/providers/deepseek.d.ts +1 -1
  86. package/dist/providers/deepseek.js +5 -10
  87. package/dist/providers/deepseek.js.map +1 -1
  88. package/dist/providers/geminiSchemaSanitizer.d.ts +52 -0
  89. package/dist/providers/geminiSchemaSanitizer.js +201 -0
  90. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  91. package/dist/providers/google-genai.d.ts +1 -1
  92. package/dist/providers/google-genai.js +9 -11
  93. package/dist/providers/google-genai.js.map +1 -1
  94. package/dist/providers/groq.d.ts +1 -1
  95. package/dist/providers/groq.js +5 -10
  96. package/dist/providers/groq.js.map +1 -1
  97. package/dist/providers/huggingface.d.ts +25 -0
  98. package/dist/providers/huggingface.js +69 -0
  99. package/dist/providers/huggingface.js.map +1 -0
  100. package/dist/providers/modelCatalog.d.ts +109 -0
  101. package/dist/providers/modelCatalog.js +245 -0
  102. package/dist/providers/modelCatalog.js.map +1 -0
  103. package/dist/providers/modelDiscovery.d.ts +111 -3
  104. package/dist/providers/modelDiscovery.js +183 -27
  105. package/dist/providers/modelDiscovery.js.map +1 -1
  106. package/dist/providers/ollama.d.ts +19 -5
  107. package/dist/providers/ollama.js +60 -52
  108. package/dist/providers/ollama.js.map +1 -1
  109. package/dist/providers/openai.d.ts +1 -1
  110. package/dist/providers/openai.js +5 -10
  111. package/dist/providers/openai.js.map +1 -1
  112. package/dist/providers/openrouter.d.ts +1 -1
  113. package/dist/providers/openrouter.js +12 -12
  114. package/dist/providers/openrouter.js.map +1 -1
  115. package/dist/providers/vertexai.d.ts +1 -1
  116. package/dist/providers/vertexai.js +9 -11
  117. package/dist/providers/vertexai.js.map +1 -1
  118. package/dist/providers/xai.d.ts +1 -1
  119. package/dist/providers/xai.js +5 -10
  120. package/dist/providers/xai.js.map +1 -1
  121. package/dist/runtime/askStructured.d.ts +62 -0
  122. package/dist/runtime/askStructured.js +76 -0
  123. package/dist/runtime/askStructured.js.map +1 -0
  124. package/dist/runtime/conversation.d.ts +59 -0
  125. package/dist/runtime/conversation.js +137 -0
  126. package/dist/runtime/conversation.js.map +1 -0
  127. package/dist/runtime/singleShot.d.ts +24 -5
  128. package/dist/runtime/singleShot.js +56 -9
  129. package/dist/runtime/singleShot.js.map +1 -1
  130. package/dist/utils/aiignoreUtils.js.map +1 -1
  131. package/dist/utils/binaryOutputUtils.js.map +1 -1
  132. package/dist/utils/consoleUtils.d.ts +22 -0
  133. package/dist/utils/consoleUtils.js +45 -0
  134. package/dist/utils/consoleUtils.js.map +1 -1
  135. package/dist/utils/debugDump.d.ts +57 -0
  136. package/dist/utils/debugDump.js +236 -0
  137. package/dist/utils/debugDump.js.map +1 -0
  138. package/dist/utils/debugUtils.d.ts +13 -4
  139. package/dist/utils/debugUtils.js +36 -13
  140. package/dist/utils/debugUtils.js.map +1 -1
  141. package/dist/utils/fileUtils.d.ts +24 -2
  142. package/dist/utils/fileUtils.js +54 -12
  143. package/dist/utils/fileUtils.js.map +1 -1
  144. package/dist/utils/llmUtils.js.map +1 -1
  145. package/dist/utils/redactSecrets.d.ts +63 -0
  146. package/dist/utils/redactSecrets.js +238 -0
  147. package/dist/utils/redactSecrets.js.map +1 -0
  148. package/dist/utils/systemPromptNotes.d.ts +181 -0
  149. package/dist/utils/systemPromptNotes.js +298 -0
  150. package/dist/utils/systemPromptNotes.js.map +1 -0
  151. package/dist/utils/systemUtils.d.ts +32 -1
  152. package/dist/utils/systemUtils.js +50 -3
  153. package/dist/utils/systemUtils.js.map +1 -1
  154. package/dist/utils/toolMatching.d.ts +30 -0
  155. package/dist/utils/toolMatching.js +44 -0
  156. package/dist/utils/toolMatching.js.map +1 -0
  157. package/package.json +17 -8
  158. package/schema/gsloth-config.schema.json +2111 -0
@@ -0,0 +1,148 @@
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 only when
19
+ * `useColour` is on AND stdout is a TTY; otherwise the block degrades to clean monochrome
20
+ * (DL-7), with diff lines still readable via their `+`/`-` prefixes.
21
+ *
22
+ * Live-output dedupe: shell-shaped results (`<COMMAND_OUTPUT>`) belong to tools whose child
23
+ * output ALREADY streamed raw via the channel's default sink, so those render with
24
+ * `liveOutputAlreadyShown` and show only the closing status line — never a repeat of output
25
+ * the user just watched.
26
+ */
27
+ import { AIMessage, AIMessageChunk, ToolMessage } from '@langchain/core/messages';
28
+ import { buildToolPreviewLines, getToolGlyph, parseCommandOutputResult, renderToolLineAnsi, summariseToolCall, } from '#src/core/toolDisplay.js';
29
+ import { displayToolIndication } from '#src/utils/consoleUtils.js';
30
+ import { getUseColour, stdout } from '#src/utils/systemUtils.js';
31
+ const INDENT = ' ';
32
+ /**
33
+ * Create the per-stream observer. State is scoped to one stream (one `agent.stream()` call);
34
+ * tool_call deltas are accumulated from `tool_call_chunks` (keyed by the provider's chunk
35
+ * `index`, which restarts per LLM round — the map is flushed into the by-id map whenever a
36
+ * `ToolMessage` arrives, mirroring `processEventStream`'s reset-per-round). Deliberately does
37
+ * NOT `concat()` whole `AIMessageChunk`s: only the tool-call slices are needed, which also
38
+ * sidesteps the TUI-C29 `__raw_response` aggregation-growth trap entirely.
39
+ *
40
+ * `emit` is injectable for tests; production uses the INFO-level `displayToolIndication`.
41
+ */
42
+ export function createPlainToolIndication(emit = displayToolIndication) {
43
+ /** Streaming tool-call deltas for the CURRENT round, keyed by tool_call_chunk index. */
44
+ const streaming = new Map();
45
+ /** Completed calls awaiting their ToolMessage, keyed by tool call id. */
46
+ const byId = new Map();
47
+ const flushStreamingIntoById = () => {
48
+ for (const call of streaming.values()) {
49
+ if (call.id)
50
+ byId.set(call.id, call);
51
+ }
52
+ streaming.clear();
53
+ };
54
+ const renderToolMessage = (message) => {
55
+ const id = typeof message.tool_call_id === 'string' ? message.tool_call_id : '';
56
+ const tracked = id ? byId.get(id) : undefined;
57
+ if (id)
58
+ byId.delete(id);
59
+ const name = tracked?.name || (typeof message.name === 'string' ? message.name : '') || '';
60
+ const result = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
61
+ const isError = message.status === 'error';
62
+ // Colour only when configured AND stdout is a real terminal — piped output stays clean
63
+ // monochrome (unlike consoleUtils' blanket dim, this block is asserted non-TTY-clean).
64
+ const colour = getUseColour() && !!stdout.isTTY;
65
+ const statusGlyph = isError
66
+ ? colour
67
+ ? '\x1b[31m✗\x1b[0m'
68
+ : '✗'
69
+ : colour
70
+ ? '\x1b[32m✓\x1b[0m'
71
+ : '✓';
72
+ const summary = summariseToolCall(name, tracked?.argsText);
73
+ const summaryText = colour ? `\x1b[2m${summary}\x1b[0m` : summary;
74
+ const head = `${statusGlyph} ${getToolGlyph(name)} ${summaryText}`;
75
+ const preview = buildToolPreviewLines({
76
+ name,
77
+ argsText: tracked?.argsText,
78
+ result,
79
+ isError,
80
+ // Shell-shaped results stream their child output live through the channel's default
81
+ // sink on this surface — suppress the duplicated body, keep the status tail.
82
+ liveOutputAlreadyShown: parseCommandOutputResult(result) !== null,
83
+ });
84
+ const body = preview.map((line) => INDENT + renderToolLineAnsi(line, colour));
85
+ // Leading newline mirrors the historical notice framing (the model text stream may have
86
+ // left the cursor mid-line).
87
+ emit(['', head, ...body].join('\n'));
88
+ };
89
+ return {
90
+ observe(chunk) {
91
+ // Order matters: AIMessageChunk extends AIMessage, so test the chunk shape first
92
+ // (mirrors processEventStream).
93
+ if (AIMessageChunk.isInstance(chunk)) {
94
+ const c = chunk;
95
+ const deltas = c.tool_call_chunks ?? [];
96
+ if (deltas.length > 0) {
97
+ for (const delta of deltas) {
98
+ const index = typeof delta.index === 'number' ? delta.index : 0;
99
+ const entry = streaming.get(index) ?? { name: '', argsText: '' };
100
+ if (delta.id)
101
+ entry.id = delta.id;
102
+ if (delta.name)
103
+ entry.name = entry.name || delta.name;
104
+ if (delta.args)
105
+ entry.argsText += delta.args;
106
+ streaming.set(index, entry);
107
+ }
108
+ }
109
+ else {
110
+ // Some providers surface COMPLETE tool_calls on a chunk instead of deltas.
111
+ for (const tc of c.tool_calls ?? []) {
112
+ if (tc.id) {
113
+ byId.set(tc.id, {
114
+ id: tc.id,
115
+ name: tc.name,
116
+ argsText: JSON.stringify(tc.args ?? {}),
117
+ });
118
+ }
119
+ }
120
+ }
121
+ return;
122
+ }
123
+ if (AIMessage.isInstance(chunk)) {
124
+ // A non-chunk AIMessage (resumed/checkpoint-replayed runs) carries final tool_calls.
125
+ const m = chunk;
126
+ for (const tc of m.tool_calls ?? []) {
127
+ if (tc.id) {
128
+ byId.set(tc.id, { id: tc.id, name: tc.name, argsText: JSON.stringify(tc.args ?? {}) });
129
+ }
130
+ }
131
+ return;
132
+ }
133
+ if (chunk instanceof ToolMessage) {
134
+ // The round is over: park any streamed calls under their ids (chunk indexes restart
135
+ // next round), then render the arrived result. Fail-soft — rendering must never break
136
+ // the run.
137
+ try {
138
+ flushStreamingIntoById();
139
+ renderToolMessage(chunk);
140
+ }
141
+ catch {
142
+ /* indication is best-effort; the model-facing stream is untouched */
143
+ }
144
+ }
145
+ },
146
+ };
147
+ }
148
+ //# sourceMappingURL=plainToolIndication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plainToolIndication.js","sourceRoot":"","sources":["../../src/core/plainToolIndication.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAElF,OAAO,EACL,qBAAqB,EACrB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEjE,MAAM,MAAM,GAAG,MAAM,CAAC;AActB;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CACvC,IAAI,GAA2B,qBAAqB;IAEpD,wFAAwF;IACxF,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;IACrD,yEAAyE;IACzE,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEhD,MAAM,sBAAsB,GAAG,GAAS,EAAE;QACxC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,EAAE;gBAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;QACD,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,CAAC,OAAoB,EAAQ,EAAE;QACvD,MAAM,EAAE,GAAG,OAAO,OAAO,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,MAAM,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9C,IAAI,EAAE;YAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,CAAC,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;QAC3F,MAAM,MAAM,GACV,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1F,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC;QAC3C,uFAAuF;QACvF,uFAAuF;QACvF,MAAM,MAAM,GAAG,YAAY,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAEhD,MAAM,WAAW,GAAG,OAAO;YACzB,CAAC,CAAC,MAAM;gBACN,CAAC,CAAC,kBAAkB;gBACpB,CAAC,CAAC,GAAG;YACP,CAAC,CAAC,MAAM;gBACN,CAAC,CAAC,kBAAkB;gBACpB,CAAC,CAAC,GAAG,CAAC;QACV,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC3D,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,OAAO,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;QAClE,MAAM,IAAI,GAAG,GAAG,WAAW,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC;QAEnE,MAAM,OAAO,GAAG,qBAAqB,CAAC;YACpC,IAAI;YACJ,QAAQ,EAAE,OAAO,EAAE,QAAQ;YAC3B,MAAM;YACN,OAAO;YACP,oFAAoF;YACpF,6EAA6E;YAC7E,sBAAsB,EAAE,wBAAwB,CAAC,MAAM,CAAC,KAAK,IAAI;SAClE,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC9E,wFAAwF;QACxF,6BAA6B;QAC7B,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC;IAEF,OAAO;QACL,OAAO,CAAC,KAAc;YACpB,iFAAiF;YACjF,gCAAgC;YAChC,IAAI,cAAc,CAAC,UAAU,CAAC,KAAoB,CAAC,EAAE,CAAC;gBACpD,MAAM,CAAC,GAAG,KAAuB,CAAC;gBAClC,MAAM,MAAM,GAAG,CAAC,CAAC,gBAAgB,IAAI,EAAE,CAAC;gBACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACtB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;wBAC3B,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;wBAChE,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;wBACjE,IAAI,KAAK,CAAC,EAAE;4BAAE,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;wBAClC,IAAI,KAAK,CAAC,IAAI;4BAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;wBACtD,IAAI,KAAK,CAAC,IAAI;4BAAE,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC;wBAC7C,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;oBAC9B,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,2EAA2E;oBAC3E,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;wBACpC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;4BACV,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE;gCACd,EAAE,EAAE,EAAE,CAAC,EAAE;gCACT,IAAI,EAAE,EAAE,CAAC,IAAI;gCACb,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;6BACxC,CAAC,CAAC;wBACL,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,OAAO;YACT,CAAC;YACD,IAAI,SAAS,CAAC,UAAU,CAAC,KAAoB,CAAC,EAAE,CAAC;gBAC/C,qFAAqF;gBACrF,MAAM,CAAC,GAAG,KAAkB,CAAC;gBAC7B,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;oBACpC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;wBACV,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;oBACzF,CAAC;gBACH,CAAC;gBACD,OAAO;YACT,CAAC;YACD,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;gBACjC,oFAAoF;gBACpF,sFAAsF;gBACtF,WAAW;gBACX,IAAI,CAAC;oBACH,sBAAsB,EAAE,CAAC;oBACzB,iBAAiB,CAAC,KAAK,CAAC,CAAC;gBAC3B,CAAC;gBAAC,MAAM,CAAC;oBACP,qEAAqE;gBACvE,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,41 @@
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 } from '#src/core/types.js';
14
+ /** Mutable tally behind {@link finalizeRunStats}; see {@link createRunStatsAccumulator}. */
15
+ export interface RunStatsAccumulator {
16
+ /** Running sum of input/prompt tokens. */
17
+ input: number;
18
+ /** Running sum of output/completion tokens. */
19
+ output: number;
20
+ /** Whether ANY message reported `usage_metadata` — gates whether tokens are recorded at all. */
21
+ sawUsage: boolean;
22
+ /** Deduplicated set of invoked tool names. */
23
+ tools: Set<string>;
24
+ }
25
+ /** A fresh, empty accumulator. */
26
+ export declare function createRunStatsAccumulator(): RunStatsAccumulator;
27
+ /**
28
+ * Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
29
+ * shape is swallowed so a run is never affected. Harvests, when present:
30
+ * - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`), and
31
+ * - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
32
+ * `.name` (the executed tool), so both "requested" and "executed" tools are captured.
33
+ */
34
+ export declare function accumulateMessage(acc: RunStatsAccumulator, message: unknown): void;
35
+ /** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
36
+ export declare function finalizeRunStats(acc: RunStatsAccumulator): GthRunStats;
37
+ /**
38
+ * One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
39
+ * state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
40
+ */
41
+ export declare function extractRunStats(messages: unknown): GthRunStats;
@@ -0,0 +1,73 @@
1
+ /** A fresh, empty accumulator. */
2
+ export function createRunStatsAccumulator() {
3
+ return { input: 0, output: 0, sawUsage: false, tools: new Set() };
4
+ }
5
+ /**
6
+ * Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
7
+ * shape is swallowed so a run is never affected. Harvests, when present:
8
+ * - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`), and
9
+ * - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
10
+ * `.name` (the executed tool), so both "requested" and "executed" tools are captured.
11
+ */
12
+ export function accumulateMessage(acc, message) {
13
+ try {
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ const m = message;
16
+ if (!m || typeof m !== 'object')
17
+ return;
18
+ const usage = m.usage_metadata;
19
+ if (usage && typeof usage === 'object') {
20
+ acc.sawUsage = true;
21
+ if (typeof usage.input_tokens === 'number' && Number.isFinite(usage.input_tokens)) {
22
+ acc.input += usage.input_tokens;
23
+ }
24
+ if (typeof usage.output_tokens === 'number' && Number.isFinite(usage.output_tokens)) {
25
+ acc.output += usage.output_tokens;
26
+ }
27
+ }
28
+ // Requested tool calls (AIMessage / AIMessageChunk). Continuation chunks in a streamed
29
+ // tool call carry an empty name, so guard on a non-empty string; the Set dedupes repeats.
30
+ const toolCalls = m.tool_calls;
31
+ if (Array.isArray(toolCalls)) {
32
+ for (const tc of toolCalls) {
33
+ const name = tc?.name;
34
+ if (typeof name === 'string' && name.length > 0)
35
+ acc.tools.add(name);
36
+ }
37
+ }
38
+ // Executed tool result (ToolMessage). Its `.name` is the tool that produced the result.
39
+ const type = typeof m.getType === 'function' ? m.getType() : m._getType?.();
40
+ if (type === 'tool' && typeof m.name === 'string' && m.name.length > 0) {
41
+ acc.tools.add(m.name);
42
+ }
43
+ }
44
+ catch {
45
+ /* fail-soft: never let stats capture affect a run */
46
+ }
47
+ }
48
+ /** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
49
+ export function finalizeRunStats(acc) {
50
+ return {
51
+ tokensInput: acc.sawUsage ? acc.input : undefined,
52
+ tokensOutput: acc.sawUsage ? acc.output : undefined,
53
+ tools: [...acc.tools],
54
+ };
55
+ }
56
+ /**
57
+ * One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
58
+ * state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
59
+ */
60
+ export function extractRunStats(messages) {
61
+ const acc = createRunStatsAccumulator();
62
+ try {
63
+ if (Array.isArray(messages)) {
64
+ for (const m of messages)
65
+ accumulateMessage(acc, m);
66
+ }
67
+ }
68
+ catch {
69
+ /* fail-soft */
70
+ }
71
+ return finalizeRunStats(acc);
72
+ }
73
+ //# 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;
@@ -0,0 +1,68 @@
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 class ShellCommandFailedError extends Error {
29
+ /** The full model-facing body (command echo + `<COMMAND_OUTPUT>` + the failure/timeout tail). */
30
+ output;
31
+ /** The process exit code; `null` when the command was killed (timeout) and never exited cleanly. */
32
+ exitCode;
33
+ /** The exact command string that was executed. */
34
+ command;
35
+ /** The run_* tool name that invoked the command (e.g. `run_tests`, `run_shell_command`). */
36
+ toolName;
37
+ constructor(params) {
38
+ // Use the full body as the Error message so any generic logger/handler still surfaces the
39
+ // real command output rather than an opaque wrapper string.
40
+ super(params.output);
41
+ this.name = 'ShellCommandFailedError';
42
+ this.output = params.output;
43
+ this.exitCode = params.exitCode;
44
+ this.command = params.command;
45
+ this.toolName = params.toolName;
46
+ }
47
+ }
48
+ /**
49
+ * Recognise a {@link ShellCommandFailedError} for the softening middleware in either agent.
50
+ *
51
+ * Prefers a plain `instanceof` (both agents share this one core module, so the class identity is
52
+ * the same), but falls back to a STRUCTURAL check keyed on `name === 'ShellCommandFailedError'`
53
+ * plus the carried fields. The structural arm is deliberate defence against a dual-package /
54
+ * realm-boundary hazard: if the error ever crossed a module boundary that broke `instanceof`, we
55
+ * would otherwise silently rethrow a real shell failure and regress the ✗ signal. Every field the
56
+ * softener reads (`output`) is asserted so a narrowed value is safe to use.
57
+ */
58
+ export function isShellCommandFailedError(e) {
59
+ if (e instanceof ShellCommandFailedError)
60
+ return true;
61
+ return (typeof e === 'object' &&
62
+ e !== null &&
63
+ e.name === 'ShellCommandFailedError' &&
64
+ typeof e.output === 'string' &&
65
+ typeof e.command === 'string' &&
66
+ typeof e.toolName === 'string');
67
+ }
68
+ //# sourceMappingURL=ShellCommandFailedError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShellCommandFailedError.js","sourceRoot":"","sources":["../../../src/core/shell/ShellCommandFailedError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,iGAAiG;IACxF,MAAM,CAAS;IACxB,oGAAoG;IAC3F,QAAQ,CAAgB;IACjC,kDAAkD;IACzC,OAAO,CAAS;IACzB,4FAA4F;IACnF,QAAQ,CAAS;IAE1B,YAAY,MAKX;QACC,0FAA0F;QAC1F,4DAA4D;QAC5D,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,CAAU;IAClD,IAAI,CAAC,YAAY,uBAAuB;QAAE,OAAO,IAAI,CAAC;IACtD,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACT,CAAwB,CAAC,IAAI,KAAK,yBAAyB;QAC5D,OAAQ,CAA0B,CAAC,MAAM,KAAK,QAAQ;QACtD,OAAQ,CAA2B,CAAC,OAAO,KAAK,QAAQ;QACxD,OAAQ,CAA4B,CAAC,QAAQ,KAAK,QAAQ,CAC3D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,75 @@
1
+ import type { ToolApprovalScope } from '#src/core/types.js';
2
+ /**
3
+ * Approval persistence scopes, widest-lived last. Alias of the canonical
4
+ * {@link ToolApprovalScope} so the allow-list engine and the decision vocabulary stay in sync.
5
+ */
6
+ export type ApprovalScope = ToolApprovalScope;
7
+ /**
8
+ * Decide whether a candidate argv contains a flag that *widens* the approved operation.
9
+ *
10
+ * Rule implemented (documented for the coordinator):
11
+ * - Re-derive the meaningful prefix from the candidate's actual argv.
12
+ * - Inspect every token of the candidate that is a flag (starts with `-`).
13
+ * - If any flag token — normalized by stripping a trailing `=value` and lowercasing —
14
+ * is in {@link WIDENING_FLAGS}, the command is considered a widening of the approved
15
+ * operation and the match is REFUSED (returns true).
16
+ *
17
+ * This is purposely a deny-list of operation-changing flags rather than an allow-list of
18
+ * benign flags: benign flag variants (`-b`, `--oneline`, `-la`) are exactly what we WANT
19
+ * to keep auto-approving, while the handful of "run-an-arbitrary-program / redirect-the-
20
+ * transport" flags are what an injected approval must never silently enable.
21
+ */
22
+ export declare function hasWideningFlag(argv: string[]): boolean;
23
+ /**
24
+ * A holder of approved prefixes with set semantics. Used for both the in-memory session
25
+ * store and the loaded persisted store. Pure data + membership; persistence is layered on
26
+ * top by {@link PersistedAllowlist}.
27
+ */
28
+ export declare class AllowlistStore {
29
+ private readonly prefixes;
30
+ constructor(initial?: Iterable<string>);
31
+ has(prefix: string): boolean;
32
+ add(prefix: string): void;
33
+ list(): string[];
34
+ }
35
+ /**
36
+ * The persisted (`always`) allow-list, backed by a JSON file. The path is injected (the
37
+ * runner resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json`) so
38
+ * tests can point it at a temp dir. Loads lazily/defensively: a missing or malformed file
39
+ * yields an empty store rather than throwing (fail-open on READ is safe — an empty
40
+ * allow-list just means "prompt"; it never auto-approves anything).
41
+ */
42
+ export declare class PersistedAllowlist {
43
+ private readonly store;
44
+ private readonly filePath;
45
+ constructor(filePath: string);
46
+ private static load;
47
+ has(prefix: string): boolean;
48
+ list(): string[];
49
+ /** Add a prefix and persist the whole set to disk. */
50
+ add(prefix: string): void;
51
+ private persist;
52
+ }
53
+ /**
54
+ * Read-only view over the stores consulted for an auto-approval check. The runner passes
55
+ * its per-instance session store and (optionally) the persisted store.
56
+ */
57
+ export interface ApprovalStores {
58
+ session: Pick<AllowlistStore, 'has'>;
59
+ always?: Pick<PersistedAllowlist, 'has'>;
60
+ }
61
+ /**
62
+ * Decide whether `command` is already approved by the given stores — the gate the runner
63
+ * consults BEFORE prompting the human.
64
+ *
65
+ * Returns true ONLY when ALL of the following hold:
66
+ * 1. {@link classifyCommand} returns a non-null classification (so composition /
67
+ * substitution / redirection commands can never match — anti-injection layer 1);
68
+ * 2. the classified prefix is present in the session OR persisted (`always`) store; AND
69
+ * 3. the safe-bin re-validation passes: the candidate's actual argv contains no
70
+ * operation-widening flag (anti-injection layer 2 — see {@link hasWideningFlag}).
71
+ *
72
+ * Anything else (unclassifiable, unknown prefix, or a widening flag) → false → the human
73
+ * is prompted. Fail-closed by construction.
74
+ */
75
+ export declare function matchesApproval(command: string, stores: ApprovalStores): boolean;