@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.
- package/.gsloth.code.md +10 -0
- package/README.md +6 -6
- package/dist/config/defaults.d.ts +85 -0
- package/dist/config/defaults.js +103 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/jsonc.d.ts +12 -0
- package/dist/config/jsonc.js +41 -0
- package/dist/config/jsonc.js.map +1 -0
- package/dist/config/loader.d.ts +169 -0
- package/dist/config/loader.js +872 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +673 -0
- package/dist/config/schema.js +524 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/shell-policy.d.ts +331 -0
- package/dist/config/shell-policy.js +238 -0
- package/dist/config/shell-policy.js.map +1 -0
- package/dist/config/types.d.ts +596 -0
- package/dist/config/types.js +13 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config.d.ts +18 -647
- package/dist/config.js +15 -516
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +42 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +85 -2
- package/dist/core/GthAbstractAgent.js +398 -28
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +158 -1
- package/dist/core/GthAgentRunner.js +367 -4
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.js +193 -16
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/debugCapture.d.ts +59 -0
- package/dist/core/debugCapture.js +101 -0
- package/dist/core/debugCapture.js.map +1 -0
- package/dist/core/gthLeanAgentFactory.d.ts +9 -0
- package/dist/core/gthLeanAgentFactory.js +10 -0
- package/dist/core/gthLeanAgentFactory.js.map +1 -0
- package/dist/core/plainToolIndication.d.ts +15 -0
- package/dist/core/plainToolIndication.js +148 -0
- package/dist/core/plainToolIndication.js.map +1 -0
- package/dist/core/runStats.d.ts +41 -0
- package/dist/core/runStats.js +73 -0
- package/dist/core/runStats.js.map +1 -0
- package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
- package/dist/core/shell/ShellCommandFailedError.js +68 -0
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
- package/dist/core/shell/allowlist.d.ts +75 -0
- package/dist/core/shell/allowlist.js +187 -0
- package/dist/core/shell/allowlist.js.map +1 -0
- package/dist/core/shell/arity.d.ts +75 -0
- package/dist/core/shell/arity.js +313 -0
- package/dist/core/shell/arity.js.map +1 -0
- package/dist/core/shell/judge.d.ts +161 -0
- package/dist/core/shell/judge.js +261 -0
- package/dist/core/shell/judge.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +27 -0
- package/dist/core/shell/normalize.js +53 -0
- package/dist/core/shell/normalize.js.map +1 -0
- package/dist/core/toolDisplay.d.ts +101 -0
- package/dist/core/toolDisplay.js +374 -0
- package/dist/core/toolDisplay.js.map +1 -0
- package/dist/core/toolOutputChannel.d.ts +68 -0
- package/dist/core/toolOutputChannel.js +112 -0
- package/dist/core/toolOutputChannel.js.map +1 -0
- package/dist/core/types.d.ts +173 -0
- package/dist/core/types.js.map +1 -1
- package/dist/history/historyFormat.d.ts +28 -0
- package/dist/history/historyFormat.js +127 -0
- package/dist/history/historyFormat.js.map +1 -0
- package/dist/history/historyStore.d.ts +198 -0
- package/dist/history/historyStore.js +482 -0
- package/dist/history/historyStore.js.map +1 -0
- package/dist/history/recordSession.d.ts +37 -0
- package/dist/history/recordSession.js +56 -0
- package/dist/history/recordSession.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +5 -10
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/deepseek.d.ts +1 -1
- package/dist/providers/deepseek.js +5 -10
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/geminiSchemaSanitizer.d.ts +52 -0
- package/dist/providers/geminiSchemaSanitizer.js +201 -0
- package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
- package/dist/providers/google-genai.d.ts +1 -1
- package/dist/providers/google-genai.js +9 -11
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +5 -10
- package/dist/providers/groq.js.map +1 -1
- package/dist/providers/huggingface.d.ts +25 -0
- package/dist/providers/huggingface.js +69 -0
- package/dist/providers/huggingface.js.map +1 -0
- package/dist/providers/modelCatalog.d.ts +109 -0
- package/dist/providers/modelCatalog.js +245 -0
- package/dist/providers/modelCatalog.js.map +1 -0
- package/dist/providers/modelDiscovery.d.ts +111 -3
- package/dist/providers/modelDiscovery.js +183 -27
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.d.ts +19 -5
- package/dist/providers/ollama.js +60 -52
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +5 -10
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +12 -12
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.d.ts +1 -1
- package/dist/providers/vertexai.js +9 -11
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/xai.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +62 -0
- package/dist/runtime/askStructured.js +76 -0
- package/dist/runtime/askStructured.js.map +1 -0
- package/dist/runtime/conversation.d.ts +59 -0
- package/dist/runtime/conversation.js +137 -0
- package/dist/runtime/conversation.js.map +1 -0
- package/dist/runtime/singleShot.d.ts +24 -5
- package/dist/runtime/singleShot.js +56 -9
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/aiignoreUtils.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +22 -0
- package/dist/utils/consoleUtils.js +45 -0
- package/dist/utils/consoleUtils.js.map +1 -1
- package/dist/utils/debugDump.d.ts +57 -0
- package/dist/utils/debugDump.js +236 -0
- package/dist/utils/debugDump.js.map +1 -0
- package/dist/utils/debugUtils.d.ts +13 -4
- package/dist/utils/debugUtils.js +36 -13
- package/dist/utils/debugUtils.js.map +1 -1
- package/dist/utils/fileUtils.d.ts +24 -2
- package/dist/utils/fileUtils.js +54 -12
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/llmUtils.js.map +1 -1
- package/dist/utils/redactSecrets.d.ts +63 -0
- package/dist/utils/redactSecrets.js +238 -0
- package/dist/utils/redactSecrets.js.map +1 -0
- package/dist/utils/systemPromptNotes.d.ts +181 -0
- package/dist/utils/systemPromptNotes.js +298 -0
- package/dist/utils/systemPromptNotes.js.map +1 -0
- package/dist/utils/systemUtils.d.ts +32 -1
- package/dist/utils/systemUtils.js +50 -3
- package/dist/utils/systemUtils.js.map +1 -1
- package/dist/utils/toolMatching.d.ts +30 -0
- package/dist/utils/toolMatching.js +44 -0
- package/dist/utils/toolMatching.js.map +1 -0
- package/package.json +17 -8
- package/schema/gsloth-config.schema.json +2111 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GthConfig } from '#src/config.js';
|
|
2
2
|
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
3
|
-
import { AgentResolvers, AgentStreamEvent, GthAgentFactory, GthAgentInterface, GthCommand, Message, StatusUpdateCallback } from '#src/core/types.js';
|
|
3
|
+
import { AgentResolvers, AgentStreamEvent, GthAgentFactory, GthAgentInterface, GthCommand, GthRunStats, Message, StatusUpdateCallback, ToolApprovalCallback } from '#src/core/types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Agent simplifies interaction with LLM and reduces it to calling a few methods
|
|
6
6
|
* {@link GthAgentRunner#init} and {@link GthAgentRunner#processMessages}.
|
|
@@ -12,6 +12,47 @@ export declare class GthAgentRunner {
|
|
|
12
12
|
private config;
|
|
13
13
|
private runConfig;
|
|
14
14
|
private agentFactory;
|
|
15
|
+
/**
|
|
16
|
+
* Consumer hook invoked when a run suspends on a tool-approval interrupt (e.g. the opt-in
|
|
17
|
+
* `run_shell_command` confirmation). Set via {@link setToolApprovalCallback}; when unset the
|
|
18
|
+
* runner REJECTS pending tool calls rather than hanging or auto-approving — the safe default
|
|
19
|
+
* for non-interactive entrypoints (a scripted `exec` run with no TTY to prompt on).
|
|
20
|
+
*/
|
|
21
|
+
private toolApprovalCallback;
|
|
22
|
+
/** The command the runner was initialized for; selects which `devTools` config applies. */
|
|
23
|
+
private command;
|
|
24
|
+
/**
|
|
25
|
+
* GS2-16 — snapshot of the last turn's analytics (token usage + invoked tools), captured from
|
|
26
|
+
* the agent at {@link cleanup} time. Needed because {@link runSingleShot} reads stats AFTER it
|
|
27
|
+
* has already called `cleanup()` (which nulls the agent); interactive callers read live via
|
|
28
|
+
* {@link getRunStats} before cleanup. Defaults to an empty tally.
|
|
29
|
+
*/
|
|
30
|
+
private lastRunStats;
|
|
31
|
+
/**
|
|
32
|
+
* EXT-12 — runtime, session-scoped auto-approve flag driven by the `/auto-approve` (a.k.a.
|
|
33
|
+
* `/yolo`) slash command. Because the shell tool stays gated (in `interruptOn`) in the
|
|
34
|
+
* interactive `code` mode, this flag is consulted at the TOP of {@link decideToolApproval}:
|
|
35
|
+
* when ON, a gated `run_shell_command` is auto-approved WITHOUT prompting for the rest of this
|
|
36
|
+
* runner's life. Never persisted, but INITIALIZED at {@link init} from the static
|
|
37
|
+
* `run_shell_command.yolo` config knob — so a config that pre-enables auto-approval still keeps the
|
|
38
|
+
* tool gated and therefore toggleable (`/auto-approve off` restores the per-command prompt).
|
|
39
|
+
* It does NOT disable the hardline floor — catastrophic commands are still refused at exec time
|
|
40
|
+
* in `GthDevToolkit.executeCommand`.
|
|
41
|
+
*/
|
|
42
|
+
private sessionYolo;
|
|
43
|
+
/**
|
|
44
|
+
* EXT-9 Tier-2 session allow-list — approved command prefixes that auto-approve for the
|
|
45
|
+
* life of THIS runner instance. Instance-scoped (not module-global) so concurrent
|
|
46
|
+
* sessions (ACP / AG-UI multi-session) cannot stomp each other's approvals.
|
|
47
|
+
*/
|
|
48
|
+
private readonly sessionAllowlist;
|
|
49
|
+
/**
|
|
50
|
+
* EXT-9 Tier-2 persisted (`always`) allow-list, loaded lazily on first use from
|
|
51
|
+
* `.gsloth/.gsloth-settings/shell-allowlist.json`. Null until the shell tool is gated
|
|
52
|
+
* and the allow-list is enabled; null also when persistence is disabled by config.
|
|
53
|
+
*/
|
|
54
|
+
private persistedAllowlist;
|
|
55
|
+
private persistedAllowlistLoaded;
|
|
15
56
|
/**
|
|
16
57
|
* @param agentFactory Produces the {@link GthAgentInterface} the runner drives.
|
|
17
58
|
* Defaults to the lean {@link GthLangChainAgent} (core). `@gaunt-sloth/agent`
|
|
@@ -19,6 +60,28 @@ export declare class GthAgentRunner {
|
|
|
19
60
|
* `createDeepAgent` graph without core depending on deepagents.
|
|
20
61
|
*/
|
|
21
62
|
constructor(statusUpdate: StatusUpdateCallback, resolvers?: AgentResolvers, agentFactory?: GthAgentFactory);
|
|
63
|
+
/**
|
|
64
|
+
* Register the tool-approval handler the runner calls when a run suspends on a tool-approval
|
|
65
|
+
* interrupt (the interactive readline session wires a y/n prompt here). Pass `null` to clear.
|
|
66
|
+
* Without a handler the runner rejects pending tool calls (see {@link toolApprovalCallback}).
|
|
67
|
+
*/
|
|
68
|
+
setToolApprovalCallback(callback: ToolApprovalCallback | null): void;
|
|
69
|
+
/**
|
|
70
|
+
* EXT-12 — flip the runtime, session-scoped auto-approve flag (the `/auto-approve` /
|
|
71
|
+
* `/yolo` slash command with no argument). When ON, gated `run_shell_command` calls
|
|
72
|
+
* auto-approve without prompting for the rest of this session; the hardline floor still applies
|
|
73
|
+
* at exec time. Returns the NEW state so the caller can render a notice. Session-scoped only —
|
|
74
|
+
* nothing is written to config.
|
|
75
|
+
*/
|
|
76
|
+
toggleSessionYolo(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* EXT-12 — set the session-scoped auto-approve flag explicitly (the `/auto-approve on|off`
|
|
79
|
+
* slash command). Idempotent; returns the NEW state so the caller can render a notice.
|
|
80
|
+
* Session-scoped only — nothing is written to config.
|
|
81
|
+
*/
|
|
82
|
+
setSessionYolo(on: boolean): boolean;
|
|
83
|
+
/** EXT-12 — current state of the runtime session-scoped auto-approve flag (see {@link toggleSessionYolo}). */
|
|
84
|
+
isSessionYolo(): boolean;
|
|
22
85
|
/**
|
|
23
86
|
* Init is split into a separate method. This may create a number of connections,
|
|
24
87
|
* and we'd better have an instance by that moment, for the case things will go wrong,
|
|
@@ -29,6 +92,59 @@ export declare class GthAgentRunner {
|
|
|
29
92
|
* processMessages deals with both streaming and non-streaming approaches.
|
|
30
93
|
*/
|
|
31
94
|
processMessages(messages: Message[]): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Accumulate a text stream into a single string. Extracted so {@link processMessages} and
|
|
97
|
+
* the interrupt-resume loop ({@link resolveToolInterrupts}) drain streams identically.
|
|
98
|
+
*/
|
|
99
|
+
private drainTextStream;
|
|
100
|
+
/**
|
|
101
|
+
* After a streamed run ends, resolve any tool-approval interrupts it suspended on. For each
|
|
102
|
+
* pending tool call the {@link toolApprovalCallback} is consulted (defaulting to REJECT when
|
|
103
|
+
* no handler is wired, so a non-interactive run never hangs or auto-approves); the collected
|
|
104
|
+
* decisions are then sent back via the agent's `streamResume` as a LangChain HITL resume
|
|
105
|
+
* (`{ decisions }`). Because a resumed run can suspend again on the next gated tool call, this
|
|
106
|
+
* loops until the graph completes with no pending interrupts. Returns the concatenated text
|
|
107
|
+
* streamed across all resume turns (empty when nothing was resumed).
|
|
108
|
+
*
|
|
109
|
+
* No-ops (returns '') when the agent does not support interrupts (`getPendingToolInterrupts`/
|
|
110
|
+
* `streamResume` absent), so the lean agent and non-HITL configs are unaffected.
|
|
111
|
+
*/
|
|
112
|
+
private resolveToolInterrupts;
|
|
113
|
+
/**
|
|
114
|
+
* Decide a single pending tool call (EXT-9 Tier-2). For the opt-in `run_shell_command`,
|
|
115
|
+
* consult the scoped allow-list FIRST: if the command's classified prefix is already
|
|
116
|
+
* approved (session or persisted `always`) and survives the safe-bin anti-widening
|
|
117
|
+
* re-validation, auto-approve SILENTLY (no human prompt). Otherwise fall through to the
|
|
118
|
+
* human callback; when the human grants `session`/`always` scope, record the command's
|
|
119
|
+
* classified prefix into the matching store so future flag-variants stop re-prompting.
|
|
120
|
+
*
|
|
121
|
+
* When no human callback is wired (non-TTY exec run) and nothing is allow-listed, reject —
|
|
122
|
+
* never auto-approve. Non-shell tools (or any tool when the allow-list is disabled) skip the
|
|
123
|
+
* allow-list and go straight to the human callback / default-reject, preserving prior behaviour.
|
|
124
|
+
*
|
|
125
|
+
* Hardline catastrophic commands remain refused at exec time regardless of any approval here
|
|
126
|
+
* (defense in depth in `GthDevToolkit.executeCommand`), so an allow-listed `rm -rf /` still
|
|
127
|
+
* cannot run.
|
|
128
|
+
*/
|
|
129
|
+
private decideToolApproval;
|
|
130
|
+
/** Whether the EXT-10 LLM-as-judge safety gate is enabled for the active command's config. */
|
|
131
|
+
private isShellJudgeOn;
|
|
132
|
+
/** Whether the EXT-9 Tier-2 allow-list is enabled for the active command's devTools config. */
|
|
133
|
+
private isShellAllowlistOn;
|
|
134
|
+
/**
|
|
135
|
+
* Lazily load (once per instance) the persisted `always` allow-list, unless persistence is
|
|
136
|
+
* disabled by config. Returns null when persistence is off so `always` grants behave as
|
|
137
|
+
* `session` (in-memory only).
|
|
138
|
+
*/
|
|
139
|
+
private getPersistedAllowlist;
|
|
140
|
+
/** Check the command against the session + persisted stores (with anti-widening re-validation). */
|
|
141
|
+
private isApprovedByAllowlist;
|
|
142
|
+
/**
|
|
143
|
+
* Record a human-granted approval at the given scope. `once` persists nothing. `session`
|
|
144
|
+
* adds the classified prefix to the in-memory store. `always` additionally persists it (or
|
|
145
|
+
* falls back to session-only when persistence is disabled).
|
|
146
|
+
*/
|
|
147
|
+
private recordApproval;
|
|
32
148
|
/**
|
|
33
149
|
* Event-stream counterpart to {@link processMessages}: drives the agent's typed
|
|
34
150
|
* {@link AgentStreamEvent} path using the runner's own thread-bound `runConfig`, so a
|
|
@@ -39,9 +155,50 @@ export declare class GthAgentRunner {
|
|
|
39
155
|
* underlying `streamWithEvents` ends cleanly on abort or `interrupt()`. The string
|
|
40
156
|
* path's empty-stream retry/`invoke` fallback is intentionally NOT duplicated here — the
|
|
41
157
|
* TUI renders the live event stream directly; revisit if empty-stream retries are needed.
|
|
158
|
+
*
|
|
159
|
+
* Tool-approval round-trip (EXT-11): after the stream ends, a gated `run_shell_command`
|
|
160
|
+
* leaves the graph suspended on a `humanInTheLoopMiddleware` interrupt rather than
|
|
161
|
+
* completing. This is the event-stream counterpart to the readline path's
|
|
162
|
+
* {@link resolveToolInterrupts}: it drains any pending interrupts through
|
|
163
|
+
* {@link decideToolApproval} (allow-list → judge → bridged human prompt), resumes via
|
|
164
|
+
* `streamWithEventsResume({ decisions })`, and loops until the graph completes with no
|
|
165
|
+
* pending interrupts — so the executed command's output renders into the TUI. Without
|
|
166
|
+
* this the TUI silently finalized an empty turn (approval gate was dead code on the
|
|
167
|
+
* event-stream path).
|
|
42
168
|
*/
|
|
43
169
|
processMessagesWithEvents(messages: Message[], signal?: AbortSignal): AsyncGenerator<AgentStreamEvent>;
|
|
170
|
+
/**
|
|
171
|
+
* Event-stream counterpart to {@link resolveToolInterrupts}: after a streamed run ends,
|
|
172
|
+
* resolve any tool-approval interrupts it suspended on, yielding the resumed run's typed
|
|
173
|
+
* {@link AgentStreamEvent}s so the renderer (the Ink TUI) shows the executed command's
|
|
174
|
+
* output. Each pending tool call is consulted via {@link decideToolApproval} — the SAME
|
|
175
|
+
* three-layer gate the readline path uses (allow-list auto-approve → EXT-10 judge →
|
|
176
|
+
* bridged human callback, defaulting to REJECT when no handler is wired) — and the
|
|
177
|
+
* collected decisions are sent back via `streamWithEventsResume` as a LangChain HITL
|
|
178
|
+
* resume (`{ decisions }`). Because a resumed run can suspend again on the next gated
|
|
179
|
+
* tool call, this loops until the graph completes with no pending interrupts.
|
|
180
|
+
*
|
|
181
|
+
* No-ops (yields nothing) when the agent does not support interrupts
|
|
182
|
+
* (`getPendingToolInterrupts`/`streamWithEventsResume` absent), so the lean agent and
|
|
183
|
+
* non-HITL configs are unaffected. Aborts (`signal`) propagate through the resumed stream.
|
|
184
|
+
*/
|
|
185
|
+
private resolveToolInterruptsWithEvents;
|
|
44
186
|
getAgent(): GthAgentInterface | null;
|
|
187
|
+
/**
|
|
188
|
+
* GS2-16 — reset the current turn's analytics tally on both the live agent and the runner's
|
|
189
|
+
* cached snapshot, so a new turn starts clean. Fail-soft (an agent without stats support is a
|
|
190
|
+
* no-op). Called at the top of each `processMessages` / `processMessagesWithEvents`.
|
|
191
|
+
*/
|
|
192
|
+
private resetRunStats;
|
|
193
|
+
/** GS2-16 — read the live agent's run stats (fail-soft; empty tally if unavailable). */
|
|
194
|
+
private captureRunStats;
|
|
195
|
+
/**
|
|
196
|
+
* GS2-16 — the analytics harvested from the just-finished turn (token usage + invoked tools),
|
|
197
|
+
* to thread into the opt-in history recorder. Reads live from the agent when one is present,
|
|
198
|
+
* otherwise the snapshot captured at {@link cleanup} (the single-shot path reads post-cleanup).
|
|
199
|
+
* Never throws.
|
|
200
|
+
*/
|
|
201
|
+
getRunStats(): GthRunStats;
|
|
45
202
|
/**
|
|
46
203
|
* Rotate the thread the runner drives by minting a fresh `runConfig` (new `thread_id`),
|
|
47
204
|
* so subsequent turns start from an empty checkpointer thread rather than retrieving the
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { getEffectiveDevToolsConfig, getShellJudgeSettings, isShellAllowlistEnabled, isShellAllowlistPersisted, isShellJudgeEnabled, } from '#src/config.js';
|
|
1
2
|
import { GthLangChainAgent } from '#src/core/GthLangChainAgent.js';
|
|
3
|
+
import { AllowlistStore, PersistedAllowlist, matchesApproval, } from '#src/core/shell/allowlist.js';
|
|
4
|
+
import { classifyCommand } from '#src/core/shell/arity.js';
|
|
5
|
+
import { normalizeCommand } from '#src/core/shell/normalize.js';
|
|
6
|
+
import { judgeShellCommand, mapVerdictToAction, } from '#src/core/shell/judge.js';
|
|
7
|
+
import { env } from '#src/utils/systemUtils.js';
|
|
8
|
+
import { getGslothConfigWritePath } from '#src/utils/fileUtils.js';
|
|
9
|
+
import { SHELL_ALLOWLIST_FILE } from '#src/constants.js';
|
|
2
10
|
import { enhanceVertexUnauthorizedMessage } from '#src/utils/vertexaiUtils.js';
|
|
3
11
|
import { getNewRunnableConfig } from '#src/utils/llmUtils.js';
|
|
4
12
|
import { initDebugLogging, debugLog, debugLogError, debugLogObject, } from '#src/utils/debugUtils.js';
|
|
@@ -13,6 +21,47 @@ export class GthAgentRunner {
|
|
|
13
21
|
config = null;
|
|
14
22
|
runConfig = null;
|
|
15
23
|
agentFactory;
|
|
24
|
+
/**
|
|
25
|
+
* Consumer hook invoked when a run suspends on a tool-approval interrupt (e.g. the opt-in
|
|
26
|
+
* `run_shell_command` confirmation). Set via {@link setToolApprovalCallback}; when unset the
|
|
27
|
+
* runner REJECTS pending tool calls rather than hanging or auto-approving — the safe default
|
|
28
|
+
* for non-interactive entrypoints (a scripted `exec` run with no TTY to prompt on).
|
|
29
|
+
*/
|
|
30
|
+
toolApprovalCallback = null;
|
|
31
|
+
/** The command the runner was initialized for; selects which `devTools` config applies. */
|
|
32
|
+
command = undefined;
|
|
33
|
+
/**
|
|
34
|
+
* GS2-16 — snapshot of the last turn's analytics (token usage + invoked tools), captured from
|
|
35
|
+
* the agent at {@link cleanup} time. Needed because {@link runSingleShot} reads stats AFTER it
|
|
36
|
+
* has already called `cleanup()` (which nulls the agent); interactive callers read live via
|
|
37
|
+
* {@link getRunStats} before cleanup. Defaults to an empty tally.
|
|
38
|
+
*/
|
|
39
|
+
lastRunStats = { tools: [] };
|
|
40
|
+
/**
|
|
41
|
+
* EXT-12 — runtime, session-scoped auto-approve flag driven by the `/auto-approve` (a.k.a.
|
|
42
|
+
* `/yolo`) slash command. Because the shell tool stays gated (in `interruptOn`) in the
|
|
43
|
+
* interactive `code` mode, this flag is consulted at the TOP of {@link decideToolApproval}:
|
|
44
|
+
* when ON, a gated `run_shell_command` is auto-approved WITHOUT prompting for the rest of this
|
|
45
|
+
* runner's life. Never persisted, but INITIALIZED at {@link init} from the static
|
|
46
|
+
* `run_shell_command.yolo` config knob — so a config that pre-enables auto-approval still keeps the
|
|
47
|
+
* tool gated and therefore toggleable (`/auto-approve off` restores the per-command prompt).
|
|
48
|
+
* It does NOT disable the hardline floor — catastrophic commands are still refused at exec time
|
|
49
|
+
* in `GthDevToolkit.executeCommand`.
|
|
50
|
+
*/
|
|
51
|
+
sessionYolo = false;
|
|
52
|
+
/**
|
|
53
|
+
* EXT-9 Tier-2 session allow-list — approved command prefixes that auto-approve for the
|
|
54
|
+
* life of THIS runner instance. Instance-scoped (not module-global) so concurrent
|
|
55
|
+
* sessions (ACP / AG-UI multi-session) cannot stomp each other's approvals.
|
|
56
|
+
*/
|
|
57
|
+
sessionAllowlist = new AllowlistStore();
|
|
58
|
+
/**
|
|
59
|
+
* EXT-9 Tier-2 persisted (`always`) allow-list, loaded lazily on first use from
|
|
60
|
+
* `.gsloth/.gsloth-settings/shell-allowlist.json`. Null until the shell tool is gated
|
|
61
|
+
* and the allow-list is enabled; null also when persistence is disabled by config.
|
|
62
|
+
*/
|
|
63
|
+
persistedAllowlist = null;
|
|
64
|
+
persistedAllowlistLoaded = false;
|
|
16
65
|
/**
|
|
17
66
|
* @param agentFactory Produces the {@link GthAgentInterface} the runner drives.
|
|
18
67
|
* Defaults to the lean {@link GthLangChainAgent} (core). `@gaunt-sloth/agent`
|
|
@@ -25,6 +74,38 @@ export class GthAgentRunner {
|
|
|
25
74
|
this.agentFactory =
|
|
26
75
|
agentFactory ?? ((status, agentResolvers) => new GthLangChainAgent(status, agentResolvers));
|
|
27
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Register the tool-approval handler the runner calls when a run suspends on a tool-approval
|
|
79
|
+
* interrupt (the interactive readline session wires a y/n prompt here). Pass `null` to clear.
|
|
80
|
+
* Without a handler the runner rejects pending tool calls (see {@link toolApprovalCallback}).
|
|
81
|
+
*/
|
|
82
|
+
setToolApprovalCallback(callback) {
|
|
83
|
+
this.toolApprovalCallback = callback;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* EXT-12 — flip the runtime, session-scoped auto-approve flag (the `/auto-approve` /
|
|
87
|
+
* `/yolo` slash command with no argument). When ON, gated `run_shell_command` calls
|
|
88
|
+
* auto-approve without prompting for the rest of this session; the hardline floor still applies
|
|
89
|
+
* at exec time. Returns the NEW state so the caller can render a notice. Session-scoped only —
|
|
90
|
+
* nothing is written to config.
|
|
91
|
+
*/
|
|
92
|
+
toggleSessionYolo() {
|
|
93
|
+
this.sessionYolo = !this.sessionYolo;
|
|
94
|
+
return this.sessionYolo;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* EXT-12 — set the session-scoped auto-approve flag explicitly (the `/auto-approve on|off`
|
|
98
|
+
* slash command). Idempotent; returns the NEW state so the caller can render a notice.
|
|
99
|
+
* Session-scoped only — nothing is written to config.
|
|
100
|
+
*/
|
|
101
|
+
setSessionYolo(on) {
|
|
102
|
+
this.sessionYolo = on;
|
|
103
|
+
return this.sessionYolo;
|
|
104
|
+
}
|
|
105
|
+
/** EXT-12 — current state of the runtime session-scoped auto-approve flag (see {@link toggleSessionYolo}). */
|
|
106
|
+
isSessionYolo() {
|
|
107
|
+
return this.sessionYolo;
|
|
108
|
+
}
|
|
28
109
|
/**
|
|
29
110
|
* Init is split into a separate method. This may create a number of connections,
|
|
30
111
|
* and we'd better have an instance by that moment, for the case things will go wrong,
|
|
@@ -32,6 +113,12 @@ export class GthAgentRunner {
|
|
|
32
113
|
*/
|
|
33
114
|
async init(command, configIn, checkpointSaver) {
|
|
34
115
|
this.config = configIn;
|
|
116
|
+
this.command = command;
|
|
117
|
+
// EXT-12 — seed the runtime auto-approve flag from the static `run_shell_command.yolo` config so a
|
|
118
|
+
// config that pre-enables auto-approval starts ON, while the shell tool stays gated (see
|
|
119
|
+
// GthDeepAgent) and therefore remains toggleable (`/auto-approve off`). Resolved per-command,
|
|
120
|
+
// mirroring where the shell tool is actually emitted; no effect where the tool is ungated.
|
|
121
|
+
this.sessionYolo = getEffectiveDevToolsConfig(configIn, command)?.shellYolo === true;
|
|
35
122
|
// Initialize debug logging
|
|
36
123
|
initDebugLogging(configIn.debugLog ?? false);
|
|
37
124
|
debugLog(`Initializing GthAgentRunner with command: ${command || 'default'}`);
|
|
@@ -50,6 +137,8 @@ export class GthAgentRunner {
|
|
|
50
137
|
if (!this.agent || !this.config || !this.runConfig) {
|
|
51
138
|
throw new Error('AgentRunner not initialized. Call init() first.');
|
|
52
139
|
}
|
|
140
|
+
// GS2-16: start this turn's analytics tally from zero (the runner is reused across turns).
|
|
141
|
+
this.resetRunStats();
|
|
53
142
|
debugLog('Processing messages...');
|
|
54
143
|
debugLogObject('Input Messages', messages);
|
|
55
144
|
try {
|
|
@@ -60,10 +149,11 @@ export class GthAgentRunner {
|
|
|
60
149
|
const stream = await this.agent.stream(messages, this.runConfig);
|
|
61
150
|
let result = '';
|
|
62
151
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
152
|
+
result = await this.drainTextStream(stream);
|
|
153
|
+
// A run may suspend on one or more tool-approval interrupts (run_shell_command).
|
|
154
|
+
// Resolve them in a loop: each resume can itself suspend again on the next gated
|
|
155
|
+
// tool call, so keep going until the graph completes with no pending interrupts.
|
|
156
|
+
result += await this.resolveToolInterrupts();
|
|
67
157
|
}
|
|
68
158
|
catch (streamError) {
|
|
69
159
|
// Handle streaming-specific errors
|
|
@@ -101,6 +191,189 @@ export class GthAgentRunner {
|
|
|
101
191
|
throw new Error(`Agent processing failed: ${enhancedMessage}`, error instanceof Error ? { cause: error } : undefined);
|
|
102
192
|
}
|
|
103
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Accumulate a text stream into a single string. Extracted so {@link processMessages} and
|
|
196
|
+
* the interrupt-resume loop ({@link resolveToolInterrupts}) drain streams identically.
|
|
197
|
+
*/
|
|
198
|
+
async drainTextStream(stream) {
|
|
199
|
+
let result = '';
|
|
200
|
+
for await (const chunk of stream) {
|
|
201
|
+
debugLogObject('Stream chunk', chunk);
|
|
202
|
+
result += chunk;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* After a streamed run ends, resolve any tool-approval interrupts it suspended on. For each
|
|
208
|
+
* pending tool call the {@link toolApprovalCallback} is consulted (defaulting to REJECT when
|
|
209
|
+
* no handler is wired, so a non-interactive run never hangs or auto-approves); the collected
|
|
210
|
+
* decisions are then sent back via the agent's `streamResume` as a LangChain HITL resume
|
|
211
|
+
* (`{ decisions }`). Because a resumed run can suspend again on the next gated tool call, this
|
|
212
|
+
* loops until the graph completes with no pending interrupts. Returns the concatenated text
|
|
213
|
+
* streamed across all resume turns (empty when nothing was resumed).
|
|
214
|
+
*
|
|
215
|
+
* No-ops (returns '') when the agent does not support interrupts (`getPendingToolInterrupts`/
|
|
216
|
+
* `streamResume` absent), so the lean agent and non-HITL configs are unaffected.
|
|
217
|
+
*/
|
|
218
|
+
async resolveToolInterrupts() {
|
|
219
|
+
const agent = this.agent;
|
|
220
|
+
const runConfig = this.runConfig;
|
|
221
|
+
if (!agent || !runConfig)
|
|
222
|
+
return '';
|
|
223
|
+
if (!agent.getPendingToolInterrupts || !agent.streamResume)
|
|
224
|
+
return '';
|
|
225
|
+
let resumedText = '';
|
|
226
|
+
// Bound the loop defensively so a misbehaving graph that re-suspends forever cannot spin.
|
|
227
|
+
for (let guard = 0; guard < 100; guard++) {
|
|
228
|
+
const pending = await agent.getPendingToolInterrupts(runConfig);
|
|
229
|
+
if (pending.length === 0)
|
|
230
|
+
break;
|
|
231
|
+
const decisions = [];
|
|
232
|
+
for (const tool of pending) {
|
|
233
|
+
decisions.push(await this.decideToolApproval(tool));
|
|
234
|
+
}
|
|
235
|
+
const stream = await agent.streamResume({ decisions }, runConfig);
|
|
236
|
+
resumedText += await this.drainTextStream(stream);
|
|
237
|
+
}
|
|
238
|
+
return resumedText;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Decide a single pending tool call (EXT-9 Tier-2). For the opt-in `run_shell_command`,
|
|
242
|
+
* consult the scoped allow-list FIRST: if the command's classified prefix is already
|
|
243
|
+
* approved (session or persisted `always`) and survives the safe-bin anti-widening
|
|
244
|
+
* re-validation, auto-approve SILENTLY (no human prompt). Otherwise fall through to the
|
|
245
|
+
* human callback; when the human grants `session`/`always` scope, record the command's
|
|
246
|
+
* classified prefix into the matching store so future flag-variants stop re-prompting.
|
|
247
|
+
*
|
|
248
|
+
* When no human callback is wired (non-TTY exec run) and nothing is allow-listed, reject —
|
|
249
|
+
* never auto-approve. Non-shell tools (or any tool when the allow-list is disabled) skip the
|
|
250
|
+
* allow-list and go straight to the human callback / default-reject, preserving prior behaviour.
|
|
251
|
+
*
|
|
252
|
+
* Hardline catastrophic commands remain refused at exec time regardless of any approval here
|
|
253
|
+
* (defense in depth in `GthDevToolkit.executeCommand`), so an allow-listed `rm -rf /` still
|
|
254
|
+
* cannot run.
|
|
255
|
+
*/
|
|
256
|
+
async decideToolApproval(tool) {
|
|
257
|
+
const command = typeof tool.args?.command === 'string' ? tool.args.command : null;
|
|
258
|
+
const isShellCommand = tool.name === 'run_shell_command' && command !== null;
|
|
259
|
+
const allowlistApplies = isShellCommand && this.isShellAllowlistOn();
|
|
260
|
+
// EXT-12 — runtime session yolo (`/yolo`): when ON, auto-approve a gated shell command WITHOUT
|
|
261
|
+
// prompting, judging, or persisting. Scope `once` so nothing is written to the allow-list (the
|
|
262
|
+
// bypass is intentionally ephemeral and reversible). The hardline floor is NOT bypassed here —
|
|
263
|
+
// it is enforced at exec time in GthDevToolkit.executeCommand regardless of this decision, so a
|
|
264
|
+
// catastrophic command is still refused even under yolo.
|
|
265
|
+
if (isShellCommand && this.sessionYolo) {
|
|
266
|
+
return { type: 'approve', scope: 'once' };
|
|
267
|
+
}
|
|
268
|
+
// Auto-approve from the allow-list without prompting. The allow-list ALWAYS wins over the
|
|
269
|
+
// judge: a human-trusted prefix shouldn't pay for an LLM call on every variant.
|
|
270
|
+
if (allowlistApplies && this.isApprovedByAllowlist(command)) {
|
|
271
|
+
return { type: 'approve', scope: 'session' };
|
|
272
|
+
}
|
|
273
|
+
// EXT-10 — LLM-as-judge safety gate (default OFF). Runs BEFORE the human callback for a
|
|
274
|
+
// `run_shell_command` not already allow-listed: auto-approve clearly-safe (fatigue reducer),
|
|
275
|
+
// reject clearly-catastrophic (only when blockHigh), otherwise fall through to the human with
|
|
276
|
+
// the verdict attached. When disabled this is a no-op and behaviour is exactly EXT-9.
|
|
277
|
+
let safetyVerdict;
|
|
278
|
+
if (isShellCommand && command !== null && this.isShellJudgeOn()) {
|
|
279
|
+
const settings = getShellJudgeSettings(getEffectiveDevToolsConfig(this.config ?? undefined, this.command));
|
|
280
|
+
const verdict = await judgeShellCommand(command, this.config, {
|
|
281
|
+
home: env?.HOME,
|
|
282
|
+
});
|
|
283
|
+
const action = mapVerdictToAction(command, verdict, {
|
|
284
|
+
autoApproveLow: settings.autoApproveLow,
|
|
285
|
+
blockHigh: settings.blockHigh,
|
|
286
|
+
});
|
|
287
|
+
if (action === 'auto-approve') {
|
|
288
|
+
// Scope `once`: judge approvals are NEVER persisted to the allow-list.
|
|
289
|
+
return { type: 'approve', scope: 'once' };
|
|
290
|
+
}
|
|
291
|
+
if (action === 'reject') {
|
|
292
|
+
return {
|
|
293
|
+
type: 'reject',
|
|
294
|
+
message: `Safety judge blocked the command: ${verdict.reason}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Escalate: carry the verdict to the human approval surface.
|
|
298
|
+
safetyVerdict = verdict;
|
|
299
|
+
}
|
|
300
|
+
if (!this.toolApprovalCallback) {
|
|
301
|
+
// No interactive handler (e.g. non-TTY exec run): reject rather than auto-approve.
|
|
302
|
+
return {
|
|
303
|
+
type: 'reject',
|
|
304
|
+
message: 'Tool call rejected: no interactive approval handler available.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Surface the judge's verdict to the human prompt (if the judge escalated) without mutating
|
|
308
|
+
// the original interrupt object the caller holds.
|
|
309
|
+
const pending = safetyVerdict ? { ...tool, safetyVerdict } : tool;
|
|
310
|
+
const decision = await this.toolApprovalCallback(pending);
|
|
311
|
+
// Persist the human's scoped grant so future variants of the same operation skip the prompt.
|
|
312
|
+
if (decision.type === 'approve' && allowlistApplies && command) {
|
|
313
|
+
this.recordApproval(command, decision.scope ?? 'once');
|
|
314
|
+
}
|
|
315
|
+
return decision;
|
|
316
|
+
}
|
|
317
|
+
/** Whether the EXT-10 LLM-as-judge safety gate is enabled for the active command's config. */
|
|
318
|
+
isShellJudgeOn() {
|
|
319
|
+
if (!this.config)
|
|
320
|
+
return false;
|
|
321
|
+
const devTools = getEffectiveDevToolsConfig(this.config, this.command);
|
|
322
|
+
return isShellJudgeEnabled(devTools);
|
|
323
|
+
}
|
|
324
|
+
/** Whether the EXT-9 Tier-2 allow-list is enabled for the active command's devTools config. */
|
|
325
|
+
isShellAllowlistOn() {
|
|
326
|
+
const devTools = getEffectiveDevToolsConfig(this.config ?? undefined, this.command);
|
|
327
|
+
return isShellAllowlistEnabled(devTools);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Lazily load (once per instance) the persisted `always` allow-list, unless persistence is
|
|
331
|
+
* disabled by config. Returns null when persistence is off so `always` grants behave as
|
|
332
|
+
* `session` (in-memory only).
|
|
333
|
+
*/
|
|
334
|
+
getPersistedAllowlist() {
|
|
335
|
+
if (this.persistedAllowlistLoaded)
|
|
336
|
+
return this.persistedAllowlist;
|
|
337
|
+
this.persistedAllowlistLoaded = true;
|
|
338
|
+
const devTools = getEffectiveDevToolsConfig(this.config ?? undefined, this.command);
|
|
339
|
+
if (!isShellAllowlistPersisted(devTools)) {
|
|
340
|
+
this.persistedAllowlist = null;
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const filePath = getGslothConfigWritePath(SHELL_ALLOWLIST_FILE);
|
|
345
|
+
this.persistedAllowlist = new PersistedAllowlist(filePath);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
// Path/IO failure → behave as no persisted store (still safe: just prompts more).
|
|
349
|
+
debugLogError('Loading persisted shell allow-list', e);
|
|
350
|
+
this.persistedAllowlist = null;
|
|
351
|
+
}
|
|
352
|
+
return this.persistedAllowlist;
|
|
353
|
+
}
|
|
354
|
+
/** Check the command against the session + persisted stores (with anti-widening re-validation). */
|
|
355
|
+
isApprovedByAllowlist(command) {
|
|
356
|
+
return matchesApproval(command, {
|
|
357
|
+
session: this.sessionAllowlist,
|
|
358
|
+
always: this.getPersistedAllowlist() ?? undefined,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Record a human-granted approval at the given scope. `once` persists nothing. `session`
|
|
363
|
+
* adds the classified prefix to the in-memory store. `always` additionally persists it (or
|
|
364
|
+
* falls back to session-only when persistence is disabled).
|
|
365
|
+
*/
|
|
366
|
+
recordApproval(command, scope) {
|
|
367
|
+
if (scope === 'once')
|
|
368
|
+
return;
|
|
369
|
+
const classification = classifyCommand(command, normalizeCommand);
|
|
370
|
+
if (!classification)
|
|
371
|
+
return; // unclassifiable (composition/redirection) → never remember.
|
|
372
|
+
this.sessionAllowlist.add(classification.prefix);
|
|
373
|
+
if (scope === 'always') {
|
|
374
|
+
this.getPersistedAllowlist()?.add(classification.prefix);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
104
377
|
/**
|
|
105
378
|
* Event-stream counterpart to {@link processMessages}: drives the agent's typed
|
|
106
379
|
* {@link AgentStreamEvent} path using the runner's own thread-bound `runConfig`, so a
|
|
@@ -111,19 +384,106 @@ export class GthAgentRunner {
|
|
|
111
384
|
* underlying `streamWithEvents` ends cleanly on abort or `interrupt()`. The string
|
|
112
385
|
* path's empty-stream retry/`invoke` fallback is intentionally NOT duplicated here — the
|
|
113
386
|
* TUI renders the live event stream directly; revisit if empty-stream retries are needed.
|
|
387
|
+
*
|
|
388
|
+
* Tool-approval round-trip (EXT-11): after the stream ends, a gated `run_shell_command`
|
|
389
|
+
* leaves the graph suspended on a `humanInTheLoopMiddleware` interrupt rather than
|
|
390
|
+
* completing. This is the event-stream counterpart to the readline path's
|
|
391
|
+
* {@link resolveToolInterrupts}: it drains any pending interrupts through
|
|
392
|
+
* {@link decideToolApproval} (allow-list → judge → bridged human prompt), resumes via
|
|
393
|
+
* `streamWithEventsResume({ decisions })`, and loops until the graph completes with no
|
|
394
|
+
* pending interrupts — so the executed command's output renders into the TUI. Without
|
|
395
|
+
* this the TUI silently finalized an empty turn (approval gate was dead code on the
|
|
396
|
+
* event-stream path).
|
|
114
397
|
*/
|
|
115
398
|
async *processMessagesWithEvents(messages, signal) {
|
|
116
399
|
if (!this.agent || !this.config || !this.runConfig) {
|
|
117
400
|
throw new Error('AgentRunner not initialized. Call init() first.');
|
|
118
401
|
}
|
|
402
|
+
// GS2-16: start this turn's analytics tally from zero (the runner is reused across turns).
|
|
403
|
+
this.resetRunStats();
|
|
119
404
|
debugLog('Processing messages (event stream)...');
|
|
120
405
|
debugLogObject('Input Messages', messages);
|
|
121
406
|
yield* this.agent.streamWithEvents(messages, this.runConfig, signal);
|
|
407
|
+
yield* this.resolveToolInterruptsWithEvents(signal);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Event-stream counterpart to {@link resolveToolInterrupts}: after a streamed run ends,
|
|
411
|
+
* resolve any tool-approval interrupts it suspended on, yielding the resumed run's typed
|
|
412
|
+
* {@link AgentStreamEvent}s so the renderer (the Ink TUI) shows the executed command's
|
|
413
|
+
* output. Each pending tool call is consulted via {@link decideToolApproval} — the SAME
|
|
414
|
+
* three-layer gate the readline path uses (allow-list auto-approve → EXT-10 judge →
|
|
415
|
+
* bridged human callback, defaulting to REJECT when no handler is wired) — and the
|
|
416
|
+
* collected decisions are sent back via `streamWithEventsResume` as a LangChain HITL
|
|
417
|
+
* resume (`{ decisions }`). Because a resumed run can suspend again on the next gated
|
|
418
|
+
* tool call, this loops until the graph completes with no pending interrupts.
|
|
419
|
+
*
|
|
420
|
+
* No-ops (yields nothing) when the agent does not support interrupts
|
|
421
|
+
* (`getPendingToolInterrupts`/`streamWithEventsResume` absent), so the lean agent and
|
|
422
|
+
* non-HITL configs are unaffected. Aborts (`signal`) propagate through the resumed stream.
|
|
423
|
+
*/
|
|
424
|
+
async *resolveToolInterruptsWithEvents(signal) {
|
|
425
|
+
const agent = this.agent;
|
|
426
|
+
const runConfig = this.runConfig;
|
|
427
|
+
if (!agent || !runConfig)
|
|
428
|
+
return;
|
|
429
|
+
if (!agent.getPendingToolInterrupts || !agent.streamWithEventsResume)
|
|
430
|
+
return;
|
|
431
|
+
// Bound the loop defensively so a misbehaving graph that re-suspends forever cannot spin.
|
|
432
|
+
for (let guard = 0; guard < 100; guard++) {
|
|
433
|
+
if (signal?.aborted)
|
|
434
|
+
return;
|
|
435
|
+
const pending = await agent.getPendingToolInterrupts(runConfig);
|
|
436
|
+
if (pending.length === 0)
|
|
437
|
+
break;
|
|
438
|
+
const decisions = [];
|
|
439
|
+
for (const tool of pending) {
|
|
440
|
+
decisions.push(await this.decideToolApproval(tool));
|
|
441
|
+
}
|
|
442
|
+
yield* agent.streamWithEventsResume({ decisions }, runConfig, [], signal);
|
|
443
|
+
}
|
|
122
444
|
}
|
|
123
445
|
// noinspection JSUnusedGlobalSymbols
|
|
124
446
|
getAgent() {
|
|
125
447
|
return this.agent;
|
|
126
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* GS2-16 — reset the current turn's analytics tally on both the live agent and the runner's
|
|
451
|
+
* cached snapshot, so a new turn starts clean. Fail-soft (an agent without stats support is a
|
|
452
|
+
* no-op). Called at the top of each `processMessages` / `processMessagesWithEvents`.
|
|
453
|
+
*/
|
|
454
|
+
resetRunStats() {
|
|
455
|
+
this.lastRunStats = { tools: [] };
|
|
456
|
+
try {
|
|
457
|
+
this.agent?.resetRunStats?.();
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
/* fail-soft: analytics must never affect a run */
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** GS2-16 — read the live agent's run stats (fail-soft; empty tally if unavailable). */
|
|
464
|
+
captureRunStats() {
|
|
465
|
+
try {
|
|
466
|
+
const stats = this.agent?.getRunStats?.();
|
|
467
|
+
if (stats)
|
|
468
|
+
return stats;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
/* fail-soft */
|
|
472
|
+
}
|
|
473
|
+
return { tools: [] };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* GS2-16 — the analytics harvested from the just-finished turn (token usage + invoked tools),
|
|
477
|
+
* to thread into the opt-in history recorder. Reads live from the agent when one is present,
|
|
478
|
+
* otherwise the snapshot captured at {@link cleanup} (the single-shot path reads post-cleanup).
|
|
479
|
+
* Never throws.
|
|
480
|
+
*/
|
|
481
|
+
getRunStats() {
|
|
482
|
+
if (this.agent) {
|
|
483
|
+
this.lastRunStats = this.captureRunStats();
|
|
484
|
+
}
|
|
485
|
+
return this.lastRunStats;
|
|
486
|
+
}
|
|
127
487
|
/**
|
|
128
488
|
* Rotate the thread the runner drives by minting a fresh `runConfig` (new `thread_id`),
|
|
129
489
|
* so subsequent turns start from an empty checkpointer thread rather than retrieving the
|
|
@@ -139,6 +499,9 @@ export class GthAgentRunner {
|
|
|
139
499
|
}
|
|
140
500
|
async cleanup() {
|
|
141
501
|
debugLog('Cleaning up GthAgentRunner...');
|
|
502
|
+
// GS2-16: snapshot the agent's run stats BEFORE nulling it, so a post-cleanup reader
|
|
503
|
+
// (runSingleShot records history after calling cleanup) still gets this turn's analytics.
|
|
504
|
+
this.lastRunStats = this.captureRunStats();
|
|
142
505
|
if (this.agent && 'cleanup' in this.agent && typeof this.agent.cleanup === 'function') {
|
|
143
506
|
await this.agent.cleanup();
|
|
144
507
|
}
|