@gaunt-sloth/agent 2.0.0-alpha.34 → 2.0.0-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/cli-acp.js +20 -24
- package/dist/builtInToolsConfig.d.ts +2 -2
- package/dist/builtInToolsConfig.js +2 -2
- package/dist/core/debugCapture.d.ts +4 -5
- package/dist/core/debugCapture.js.map +1 -1
- package/dist/core/resolveAgentFactory.d.ts +10 -9
- package/dist/core/resolveAgentFactory.js +11 -12
- package/dist/core/resolveAgentFactory.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/frontendImageInjectionMiddleware.d.ts +3 -2
- package/dist/middleware/frontendImageInjectionMiddleware.js +3 -2
- package/dist/middleware/frontendImageInjectionMiddleware.js.map +1 -1
- package/dist/middleware/registry.js +16 -3
- package/dist/middleware/registry.js.map +1 -1
- package/dist/modules/acp/acpAgentApp.d.ts +93 -0
- package/dist/modules/acp/acpAgentApp.js +593 -0
- package/dist/modules/acp/acpAgentApp.js.map +1 -0
- package/dist/modules/acp/acpPermissions.d.ts +60 -0
- package/dist/modules/acp/acpPermissions.js +158 -0
- package/dist/modules/acp/acpPermissions.js.map +1 -0
- package/dist/modules/acp/acpStdio.d.ts +41 -0
- package/dist/modules/acp/acpStdio.js +65 -0
- package/dist/modules/acp/acpStdio.js.map +1 -0
- package/dist/modules/acp/acpUpdates.d.ts +101 -0
- package/dist/modules/acp/acpUpdates.js +287 -0
- package/dist/modules/acp/acpUpdates.js.map +1 -0
- package/dist/modules/apiAgUiModule.js +8 -16
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.js +39 -6
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/tools/GthCustomToolkit.js +1 -1
- package/dist/tools/GthCustomToolkit.js.map +1 -1
- package/dist/tools/GthDevToolkit.d.ts +2 -2
- package/dist/tools/GthDevToolkit.js +8 -13
- package/dist/tools/GthDevToolkit.js.map +1 -1
- package/dist/tools/gthChecklistTool.js +4 -5
- package/dist/tools/gthChecklistTool.js.map +1 -1
- package/dist/tools/gthGrepTool.js +6 -6
- package/dist/tools/shell/workDir.d.ts +1 -10
- package/dist/tools/shell/workDir.js +10 -36
- package/dist/tools/shell/workDir.js.map +1 -1
- package/package.json +8 -9
- package/dist/core/GthDeepAgent.d.ts +0 -128
- package/dist/core/GthDeepAgent.js +0 -712
- package/dist/core/GthDeepAgent.js.map +0 -1
- package/dist/core/deepAgentPermissions.d.ts +0 -138
- package/dist/core/deepAgentPermissions.js +0 -309
- package/dist/core/deepAgentPermissions.js.map +0 -1
- package/dist/core/gthAcpServer.d.ts +0 -27
- package/dist/core/gthAcpServer.js +0 -82
- package/dist/core/gthAcpServer.js.map +0 -1
- package/dist/core/gthDeepAgentFactory.d.ts +0 -11
- package/dist/core/gthDeepAgentFactory.js +0 -17
- package/dist/core/gthDeepAgentFactory.js.map +0 -1
- package/dist/core/subagentProfiles.d.ts +0 -50
- package/dist/core/subagentProfiles.js +0 -76
- package/dist/core/subagentProfiles.js.map +0 -1
- package/dist/modules/acpModule.d.ts +0 -40
- package/dist/modules/acpModule.js +0 -86
- package/dist/modules/acpModule.js.map +0 -1
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
import { SHELL_TOOL_NAME, commandAnswersApprovals, resolveApprovals, resolveGatedToolNames, resolveInterruptToolNames, resolveShellApprovalGate, } from '@gaunt-sloth/core/config.js';
|
|
2
|
-
import { GthAbstractAgent } from '@gaunt-sloth/core/core/GthAbstractAgent.js';
|
|
3
|
-
import { StatusLevel } from '@gaunt-sloth/core/core/types.js';
|
|
4
|
-
import { debugLog, debugLogObject } from '@gaunt-sloth/core/utils/debugUtils.js';
|
|
5
|
-
import { buildSystemMessages, formatToolCalls, readModePrompt, } from '@gaunt-sloth/core/utils/llmUtils.js';
|
|
6
|
-
import { getCurrentWorkDir, getProjectDir, getUseColour, setProjectDir, setUseColour, } from '@gaunt-sloth/core/utils/systemUtils.js';
|
|
7
|
-
import { getConsoleLevel, setConsoleLevel } from '@gaunt-sloth/core/utils/consoleUtils.js';
|
|
8
|
-
import { buildProfileSubagents } from '#src/core/subagentProfiles.js';
|
|
9
|
-
import { isToolAllowed } from '@gaunt-sloth/core/utils/toolMatching.js';
|
|
10
|
-
// GS2-27: the OS/shell-dialect and real-cwd notes are backend-agnostic (both backends expose
|
|
11
|
-
// run_shell_command and run on the real-fs cwd), so their canonical source moved to core so the
|
|
12
|
-
// lean backend composes them too. Imported here for GthDeepAgent.init()'s code-mode composition and
|
|
13
|
-
// re-exported so existing importers of this module keep working.
|
|
14
|
-
import { appendOsShellNote, appendCwdNote, appendCommitCoAuthorNote, appendModelContextNote, appendMcpServerInstructionsNote, resolveModelIdentity, } from '@gaunt-sloth/core/utils/systemPromptNotes.js';
|
|
15
|
-
export { appendOsShellNote, appendCwdNote, appendCommitCoAuthorNote, appendModelContextNote, resolveModelIdentity, OS_SHELL_GUIDANCE, } from '@gaunt-sloth/core/utils/systemPromptNotes.js';
|
|
16
|
-
import { AIMessage, ToolMessage } from '@langchain/core/messages';
|
|
17
|
-
import { GraphInterrupt } from '@langchain/langgraph';
|
|
18
|
-
import { createMiddleware } from 'langchain';
|
|
19
|
-
import { createDeepAgent, FilesystemBackend } from 'deepagents';
|
|
20
|
-
import { buildPermissions, DEEP_AGENT_BUILT_IN_TOOL_NAMES, FILESYSTEM_TOOL_NAMES, guardFilesystemBackend, } from '#src/core/deepAgentPermissions.js';
|
|
21
|
-
import { extractDebugRequestExtras } from '#src/core/debugCapture.js';
|
|
22
|
-
// Re-export so existing importers of this module (extractDebugRequestExtras.spec) keep working
|
|
23
|
-
// now that the implementation lives in @gaunt-sloth/core.
|
|
24
|
-
export { extractDebugRequestExtras } from '#src/core/debugCapture.js';
|
|
25
|
-
import { ShellCommandFailedError } from '#src/tools/GthDevToolkit.js';
|
|
26
|
-
/**
|
|
27
|
-
* EXT-16: decide whether the deepagents filesystem backend must run in virtualMode.
|
|
28
|
-
*
|
|
29
|
-
* deepagents' permission layer (`validatePath`) requires POSIX `/`-rooted glob paths, and its
|
|
30
|
-
* fs tools hand the SAME model-supplied path string to both the permission check and the native
|
|
31
|
-
* `path.resolve`/`fs` backend. On Windows a real cwd is `D:\...`, which can satisfy neither side
|
|
32
|
-
* as one string, so the EXT-13 real-path sandbox throws `Error: path must be absolute` on every
|
|
33
|
-
* turn and the agent hangs. The precise trigger is "the real cwd is not POSIX-rooted", so we key
|
|
34
|
-
* off that directly (not just `win32`): when true, run virtualMode (cwd→`/`) with virtual
|
|
35
|
-
* permissions — the pre-EXT-13 known-good behavior. POSIX keeps the EXT-13 real-path namespace.
|
|
36
|
-
*/
|
|
37
|
-
function shouldUseVirtualFs() {
|
|
38
|
-
return !getCurrentWorkDir().startsWith('/');
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Deep agent: builds a `createDeepAgent` graph (deepagents). All run/stream/event
|
|
42
|
-
* plumbing lives in {@link GthAbstractAgent}; this class only knows how to construct
|
|
43
|
-
* the graph in {@link init}.
|
|
44
|
-
*
|
|
45
|
-
* Differences from the lean {@link GthLangChainAgent}:
|
|
46
|
-
* - deepagents provides the filesystem tools (`read_file`/`write_file`/`edit_file`/
|
|
47
|
-
* `ls`/`glob`/`grep`/`execute`) via its own middleware, backed by a
|
|
48
|
-
* {@link FilesystemBackend}. gsloth's `.aiignore` + `filesystem` config are mapped
|
|
49
|
-
* onto deepagents `permissions` (see {@link buildPermissions}). Any resolved tool
|
|
50
|
-
* that reuses a deepagents filesystem-tool name is therefore superseded and dropped
|
|
51
|
-
* (`createDeepAgent` would otherwise throw on the collision). EXT-14: the
|
|
52
|
-
* `FilesystemBackend` itself is wrapped with {@link guardFilesystemBackend} before it
|
|
53
|
-
* reaches `createDeepAgent`, adding a realpath (symlink-resolved) containment check the
|
|
54
|
-
* permission globs alone can't provide.
|
|
55
|
-
* - todos / subagents / summarization come from deepagents' standard middleware.
|
|
56
|
-
*
|
|
57
|
-
* The transport-agnostic param assembly lives in {@link buildDeepAgentParams} so the ACP
|
|
58
|
-
* entry (`deepagents-acp`) can reuse the exact same tool resolution, permission mapping and
|
|
59
|
-
* middleware hardening without re-running `createDeepAgent` locally.
|
|
60
|
-
*/
|
|
61
|
-
export class GthDeepAgent extends GthAbstractAgent {
|
|
62
|
-
// `debugCapture` (the opt-in TUI `/debug` sink) now lives on the shared GthAbstractAgent base
|
|
63
|
-
// so the lean backend supports it too; the wrapModelCall capture middleware below reads it.
|
|
64
|
-
async init(command, configIn, checkpointer) {
|
|
65
|
-
const params = await this.buildDeepAgentParams(command, configIn);
|
|
66
|
-
// Runner-path only: surface requested tool calls to the console. This is intentionally
|
|
67
|
-
// NOT part of buildDeepAgentParams — it writes via statusUpdate (stdout) and the ACP
|
|
68
|
-
// server renders tool calls itself over its own protocol channel.
|
|
69
|
-
const statusUpdate = this.statusUpdate;
|
|
70
|
-
const toolCallStatusMiddleware = createMiddleware({
|
|
71
|
-
name: 'GthMiddlewareToolCallStatusUpdate',
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
-
afterModel: (state) => {
|
|
74
|
-
debugLogObject('postModel state', state);
|
|
75
|
-
const lastMessage = state.messages[state.messages.length - 1];
|
|
76
|
-
if (AIMessage.isInstance(lastMessage) &&
|
|
77
|
-
lastMessage.tool_calls &&
|
|
78
|
-
lastMessage.tool_calls?.length > 0) {
|
|
79
|
-
statusUpdate(StatusLevel.INFO, `\nRequested tools: ${formatToolCalls(lastMessage.tool_calls)}\n`);
|
|
80
|
-
}
|
|
81
|
-
return state;
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
// Debug-capture middleware (TUI `/debug` panel). Always installed but lazy: it reads
|
|
85
|
-
// `this.debugCapture` per call, so until the TUI attaches a sink it is a transparent
|
|
86
|
-
// pass-through (one extra await around the handler — the normal path pays nothing).
|
|
87
|
-
// `request.messages` is the real history at call time (post-summarization/middleware),
|
|
88
|
-
// and `handler(request)` resolves to the AIMessage response (decision (a): whole
|
|
89
|
-
// resolved message, not per-chunk — the streaming core stays untouched).
|
|
90
|
-
const getDebugCapture = () => this.debugCapture;
|
|
91
|
-
const debugCaptureMiddleware = createMiddleware({
|
|
92
|
-
name: 'GthMiddlewareDebugCapture',
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
-
wrapModelCall: async (request, handler) => {
|
|
95
|
-
// GS2-56: stash the always-on last-model-request snapshot (extras + as-sent messages)
|
|
96
|
-
// UNCONDITIONALLY — before the `capture` short-circuit — so `/debug-dump` has the full
|
|
97
|
-
// model input even when no TUI `/debug` sink is attached (a non-TUI surface, or `/debug`
|
|
98
|
-
// never opened). Guarded: snapshotting must never break the run. The computed extras are
|
|
99
|
-
// reused for the sink below so extraction runs once. Symmetric with the lean backend.
|
|
100
|
-
let extras;
|
|
101
|
-
try {
|
|
102
|
-
extras = extractDebugRequestExtras(request);
|
|
103
|
-
this.setLastModelRequest(request.messages, extras);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
/* the always-on snapshot must never break the run */
|
|
107
|
-
}
|
|
108
|
-
const capture = getDebugCapture();
|
|
109
|
-
if (!capture)
|
|
110
|
-
return handler(request);
|
|
111
|
-
try {
|
|
112
|
-
capture.onRequest?.(request.messages, extras);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
/* a debug sink must never break the run */
|
|
116
|
-
}
|
|
117
|
-
const response = await handler(request);
|
|
118
|
-
try {
|
|
119
|
-
capture.onResponse?.(response);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
/* a debug sink must never break the run */
|
|
123
|
-
}
|
|
124
|
-
return response;
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
// EXT-16: whether the deepagents fs backend runs in virtualMode. deepagents' permission layer
|
|
128
|
-
// requires POSIX `/`-rooted paths, so a Windows real cwd (`D:\...`) can't be expressed as a
|
|
129
|
-
// permission glob and the EXT-13 real-path mode hangs there (`Error: path must be absolute`).
|
|
130
|
-
// When the real cwd isn't POSIX-rooted, fall back to virtualMode (cwd→`/`) with virtual
|
|
131
|
-
// permissions — the pre-EXT-13 known-good Windows behavior. Computed here because the EXT-22 S1
|
|
132
|
-
// middleware (below), the backend, and the systemPrompt gate (further down) all key off it.
|
|
133
|
-
const useVirtualFs = shouldUseVirtualFs();
|
|
134
|
-
// EXT-22 (S1): last-word path-namespace correction. Appends the shared guidance as a trailing
|
|
135
|
-
// system-message block ONLY in code + virtualMode (where the fs virtual `/` root and the
|
|
136
|
-
// shell's real-OS paths diverge); a transparent pass-through otherwise. Added LAST in the
|
|
137
|
-
// middleware array so, being the innermost wrapModelCall, its block lands AFTER deepagents'
|
|
138
|
-
// "All file paths must start with a /." line (see handoff/spike-systemmessage-ordering.md).
|
|
139
|
-
const pathNamespaceCorrectionMiddleware = createPathNamespaceCorrectionMiddleware(this.command === 'code' && useVirtualFs);
|
|
140
|
-
const middleware = [
|
|
141
|
-
...params.middleware,
|
|
142
|
-
toolCallStatusMiddleware,
|
|
143
|
-
debugCaptureMiddleware,
|
|
144
|
-
pathNamespaceCorrectionMiddleware,
|
|
145
|
-
];
|
|
146
|
-
this.headerStatus(`Loaded middleware: ${middleware.map((m) => m.name).join(', ')}`);
|
|
147
|
-
// EXT-13: the backend always runs in REAL-path mode (virtualMode off) so the deepagents fs
|
|
148
|
-
// tools and the EXT-9 run_shell_command tool share ONE path namespace — real absolute paths
|
|
149
|
-
// rooted at cwd. Containment is enforced by the permission allow/deny globs built in
|
|
150
|
-
// buildDeepAgentParams (default: allow cwd/**, deny /**), which match what virtualMode used to
|
|
151
|
-
// give for free (see deepAgentPermissions + the EXT-13 symlink/`..` parity tests), PLUS the
|
|
152
|
-
// EXT-14 realpath guard wrapped around the backend below (closes the intermediate-symlinked-
|
|
153
|
-
// directory gap those lexical globs alone can't catch).
|
|
154
|
-
// `--allow-dir` (config.allowDirs) further widens those allow-rules to reach extra real dirs;
|
|
155
|
-
// it removes a guardrail, so it is announced loudly by the exec command and surfaced here.
|
|
156
|
-
const allowDirs = this.config?.allowDirs;
|
|
157
|
-
const widenFs = Array.isArray(allowDirs) && allowDirs.length > 0;
|
|
158
|
-
if (widenFs) {
|
|
159
|
-
this.statusUpdate(StatusLevel.WARNING, `Filesystem sandbox widened beyond cwd (--allow-dir): ${allowDirs.join(', ')}` +
|
|
160
|
-
(useVirtualFs
|
|
161
|
-
? ' — note: on this platform the sandbox runs in virtual mode, so widening beyond cwd is not applied.'
|
|
162
|
-
: ''));
|
|
163
|
-
}
|
|
164
|
-
// EXT-14: layer the realpath containment guard around the backend deepagents' fs middleware
|
|
165
|
-
// (main agent AND every subagent — they all share this one `backend` reference, see
|
|
166
|
-
// guardFilesystemBackend's doc comment) reads/writes through. Closes the intermediate-
|
|
167
|
-
// symlinked-directory escape that the lexical allow/deny globs alone cannot catch.
|
|
168
|
-
const backend = guardFilesystemBackend(new FilesystemBackend({
|
|
169
|
-
rootDir: getCurrentWorkDir(),
|
|
170
|
-
virtualMode: useVirtualFs,
|
|
171
|
-
}), {
|
|
172
|
-
cwd: getCurrentWorkDir(),
|
|
173
|
-
virtual: useVirtualFs,
|
|
174
|
-
allowDirs: widenFs ? allowDirs : undefined,
|
|
175
|
-
});
|
|
176
|
-
// EXT-13 (part b): on the local-runner code path the model used to be told nothing about
|
|
177
|
-
// where it is, so it assumed `/` was cwd and fed `/`-rooted paths to the real-fs shell. Now
|
|
178
|
-
// the backend uses real absolute paths (above), so inject the dynamic real cwd + path model
|
|
179
|
-
// into the prompt the model actually receives. Code mode only — the surface with full fs +
|
|
180
|
-
// shell access; the ACP transport keeps virtualMode and re-roots per session, so this
|
|
181
|
-
// real-path note must NOT leak there (which is why it lives in init(), not the
|
|
182
|
-
// transport-agnostic buildDeepAgentParams).
|
|
183
|
-
// In virtualMode (EXT-16, Windows) the real-cwd note must NOT be injected — it would mislabel
|
|
184
|
-
// the namespace (the fs tools' `/` is the virtual root, not the real cwd). Instead, EXT-22 (S2)
|
|
185
|
-
// injects the virtualMode path-namespace note so the model is told EARLY that the fs virtual
|
|
186
|
-
// `/` root and run_shell_command's real-OS paths differ (the S1 middleware repeats it as the
|
|
187
|
-
// authoritative last word after deepagents' `/`-rooted line). Non-code paths get neither.
|
|
188
|
-
// EXT-26: after the cwd/virtual-cwd note, append the OS + shell-dialect note so the model is
|
|
189
|
-
// told its host OS and which shell run_shell_command spawns (cmd.exe on Windows, /bin/sh on
|
|
190
|
-
// POSIX). This is ORTHOGONAL to the path-namespace notes above (those say WHERE it is; this
|
|
191
|
-
// says WHAT shell it speaks) and applies in BOTH code-mode branches, independent of
|
|
192
|
-
// virtualMode — the shell dialect matters on every platform. Non-code paths get nothing new.
|
|
193
|
-
// GS2-34/EXT-83: resolve the active model identity ONCE, honouring the `injectModelContext`
|
|
194
|
-
// opt-out (default ON) at this single read site. Both consumers below take this same value, so
|
|
195
|
-
// the commit trailer and the model-context note can never disagree about which model is serving
|
|
196
|
-
// the session — and the opt-out means "my model identity stays out of the prompt", which covers
|
|
197
|
-
// the trailer as much as the identity line. Mirrors the lean backend seam.
|
|
198
|
-
const modelIdentity = this.config?.injectModelContext !== false ? resolveModelIdentity(this.config) : undefined;
|
|
199
|
-
// GS2-35: append the commit co-authoring rule (config `commit.coAuthor`, defaulting to the Gaunt
|
|
200
|
-
// Sloth account) so agent-authored commits credit Gaunt Sloth in the `Co-Authored-By` trailer,
|
|
201
|
-
// and the EXT-83 commit-message rules (plain English, and passed by file — never inline, where
|
|
202
|
-
// the shell would expand the message before git runs). Mirrors the lean backend seam so both
|
|
203
|
-
// compose the same shared note (GS2-27 parity); same code-mode gate as the shell/cwd notes and
|
|
204
|
-
// independent of virtualMode.
|
|
205
|
-
// EXT-84: the effective `filesystem` is threaded in so the note names the writing tool only
|
|
206
|
-
// where that tool is registered. It is the command-merged value set by buildDeepAgentParams
|
|
207
|
-
// (getEffectiveConfig) — NOT the `filesystem: 'none'` override used purely to keep gsloth's
|
|
208
|
-
// filesystem toolkit out of tool resolution, which says nothing about the fs access deepagents
|
|
209
|
-
// itself grants from the same value. Mirrors the lean backend seam.
|
|
210
|
-
const codeNotesPrompt = this.command === 'code'
|
|
211
|
-
? appendCommitCoAuthorNote(appendOsShellNote(useVirtualFs
|
|
212
|
-
? appendVirtualCwdNote(params.systemPrompt)
|
|
213
|
-
: appendCwdNote(params.systemPrompt, getCurrentWorkDir())), this.config?.commit?.coAuthor, modelIdentity, this.config?.filesystem)
|
|
214
|
-
: params.systemPrompt;
|
|
215
|
-
// GS2-34: inject the resolved provider:model identity (mirrors the lean GthLangChainAgent seam so
|
|
216
|
-
// both backends compose the same shared note — GS2-27 parity). Composed OUTSIDE the code-mode
|
|
217
|
-
// gate above: "which model are you?" can be asked in ANY mode (chat/ask/code/exec), so the
|
|
218
|
-
// identity is visible everywhere, unlike the code-only cwd/os-shell/commit notes. The
|
|
219
|
-
// `injectModelContext` opt-out is applied at the single read site above; when it is off — or
|
|
220
|
-
// when no model resolves — `modelIdentity` is undefined, nothing is appended, and the prompt is
|
|
221
|
-
// exactly as before. Lives in init() like the cwd note (not buildDeepAgentParams), so the
|
|
222
|
-
// deepagents-acp transport is unaffected, consistent with every other init()-composed note.
|
|
223
|
-
// GS2-6 capability note deferred.
|
|
224
|
-
const modelContextPrompt = appendModelContextNote(codeNotesPrompt, modelIdentity);
|
|
225
|
-
// EXT-32: inject the connected MCP servers' discovery `instructions` (captured by the resolver
|
|
226
|
-
// during buildDeepAgentParams' resolveTools call, above) into the prompt — fenced + per-server-
|
|
227
|
-
// labelled as untrusted server-provided context. Mode-independent (MCP tools load in every
|
|
228
|
-
// mode). This mirrors the lean backend's GthLangChainAgent seam so both backends compose the
|
|
229
|
-
// same shared note (GS2-27 shared-path parity). Lives in init() like the cwd note, so the ACP
|
|
230
|
-
// buildDeepAgentParams entry is intentionally unaffected. Empty capture adds nothing.
|
|
231
|
-
// When tools are disabled, buildDeepAgentParams skips resolveTools (no MCP contact), so a REUSED
|
|
232
|
-
// resolver could still hold a prior run's capture — gate on toolsDisabled (recomputed from the
|
|
233
|
-
// effective config buildDeepAgentParams set on `this.config`) so no stale instructions leak.
|
|
234
|
-
const deepToolsDisabled = Array.isArray(this.config?.allowedTools) && this.config.allowedTools.length === 0;
|
|
235
|
-
const mcpInstructions = deepToolsDisabled
|
|
236
|
-
? []
|
|
237
|
-
: (this.resolvers?.getMcpServerInstructions?.() ?? []);
|
|
238
|
-
const systemPrompt = appendMcpServerInstructionsNote(modelContextPrompt, mcpInstructions);
|
|
239
|
-
// GS2-33 — resolve profile-backed subagents (config `subagents`). Runner-only, like the
|
|
240
|
-
// cwd/model-context notes above: it lives in init() (NOT the transport-agnostic
|
|
241
|
-
// buildDeepAgentParams), so the deepagents-acp path is unaffected and the child-config
|
|
242
|
-
// resolution's process-global side effects stay on the local-runner path. Each declared
|
|
243
|
-
// subagent's CHILD resolves its named profile through the GS2-1 cascade, so the deepagents `task`
|
|
244
|
-
// tool can dispatch it under that profile's own model + tools + prompt (e.g. a cheap flash-lite
|
|
245
|
-
// profile for recall/search while the parent runs on a strong model). Because resolving a child
|
|
246
|
-
// goes through initConfig — which mutates projectDir/consoleLevel/useColour exactly as the parent
|
|
247
|
-
// run's own initConfig did — snapshot and restore those globals so a child profile's console
|
|
248
|
-
// level / colour can never leak into the parent run.
|
|
249
|
-
let subagents;
|
|
250
|
-
const subagentSpecs = this.config?.subagents;
|
|
251
|
-
if (Array.isArray(subagentSpecs) && subagentSpecs.length > 0) {
|
|
252
|
-
const savedProjectDir = getProjectDir();
|
|
253
|
-
const savedConsoleLevel = getConsoleLevel();
|
|
254
|
-
const savedUseColour = getUseColour();
|
|
255
|
-
try {
|
|
256
|
-
subagents = await buildProfileSubagents(subagentSpecs, {
|
|
257
|
-
command: this.command,
|
|
258
|
-
resolveTools: this.resolvers?.resolveTools?.bind(this.resolvers),
|
|
259
|
-
});
|
|
260
|
-
this.headerStatus(`Loaded profile subagents: ${subagents.map((s) => s.name).join(', ')}`);
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
setProjectDir(savedProjectDir);
|
|
264
|
-
setConsoleLevel(savedConsoleLevel);
|
|
265
|
-
setUseColour(savedUseColour);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
this.agent = createDeepAgent({
|
|
269
|
-
model: params.model,
|
|
270
|
-
tools: params.tools,
|
|
271
|
-
// GS2-33 — profile-backed subagents (undefined when none configured → deepagents' default
|
|
272
|
-
// general-purpose subagent only, unchanged behaviour).
|
|
273
|
-
subagents,
|
|
274
|
-
// gsloth's composed prompt, combined ADDITIVELY by deepagents with its base + fs prompts
|
|
275
|
-
// into a single system message (avoids the two-system-message Anthropic rejection).
|
|
276
|
-
systemPrompt,
|
|
277
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
278
|
-
middleware: middleware,
|
|
279
|
-
backend,
|
|
280
|
-
permissions: params.permissions,
|
|
281
|
-
// Per-tool human-in-the-loop gating (e.g. run_shell_command confirmation). When set,
|
|
282
|
-
// deepagents installs humanInTheLoopMiddleware so a matching tool call suspends the graph
|
|
283
|
-
// for approval; `undefined` (the default, and under bypass) leaves every tool ungated.
|
|
284
|
-
interruptOn: params.interruptOn,
|
|
285
|
-
checkpointer,
|
|
286
|
-
});
|
|
287
|
-
debugLog('Deep agent created successfully');
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Assemble the transport-agnostic {@link GthDeepAgentParams}: resolve tools (with the
|
|
291
|
-
* filesystem disabled so deepagents owns fs access), apply the allowedTools allow-list and
|
|
292
|
-
* the deepagents fs-name supersession safety-net, map `.aiignore` + filesystem mode onto
|
|
293
|
-
* deepagents permissions, and build the fs-denial-softening middleware. Shared by the local
|
|
294
|
-
* runner ({@link init}) and the `deepagents-acp` ACP entry.
|
|
295
|
-
*/
|
|
296
|
-
async buildDeepAgentParams(command, configIn) {
|
|
297
|
-
this.command = command;
|
|
298
|
-
debugLog(`GthDeepAgent.buildDeepAgentParams called with command: ${command || 'default'}`);
|
|
299
|
-
// Merge command-specific filesystem config if provided
|
|
300
|
-
this.config = this.getEffectiveConfig(configIn, command);
|
|
301
|
-
debugLogObject('Effective Config', {
|
|
302
|
-
filesystem: this.config.filesystem,
|
|
303
|
-
builtInTools: this.config.builtInTools,
|
|
304
|
-
streamOutput: this.config.streamOutput,
|
|
305
|
-
debugLog: this.config.debugLog,
|
|
306
|
-
});
|
|
307
|
-
this.headerStatus(`Workdir: ${getCurrentWorkDir()}`);
|
|
308
|
-
if (this.config.modelDisplayName) {
|
|
309
|
-
this.headerStatus(`Model: ${this.config.modelDisplayName}`);
|
|
310
|
-
}
|
|
311
|
-
// An empty allowedTools allow-list disables every tool. Skip resolution entirely so we
|
|
312
|
-
// don't contact MCP servers (and trigger OAuth) just to discard the result.
|
|
313
|
-
const allowedTools = this.config.allowedTools;
|
|
314
|
-
const toolsDisabled = Array.isArray(allowedTools) && allowedTools.length === 0;
|
|
315
|
-
if (toolsDisabled) {
|
|
316
|
-
this.headerStatus('Tool loading disabled by allowedTools: []; MCP/A2A servers will not be contacted. Omit allowedTools for no filtering.');
|
|
317
|
-
}
|
|
318
|
-
// Resolve tools with filesystem access disabled. deepagents OWNS the filesystem
|
|
319
|
-
// (its fs middleware + the `permissions` built below); gsloth's filesystem toolkit
|
|
320
|
-
// must NOT be loaded here, because its non-colliding tools (read_multiple_files,
|
|
321
|
-
// delete_file, search_files, …) would otherwise bypass deepagents' permission
|
|
322
|
-
// enforcement entirely — a model could read an .aiignore-protected file through them.
|
|
323
|
-
debugLog('Resolving tools (filesystem disabled; deepagents provides fs)...');
|
|
324
|
-
const toolResolutionConfig = { ...this.config, filesystem: 'none' };
|
|
325
|
-
const resolvedTools = !toolsDisabled && this.resolvers?.resolveTools
|
|
326
|
-
? await this.resolvers.resolveTools(toolResolutionConfig, command)
|
|
327
|
-
: [];
|
|
328
|
-
debugLog(`Resolved tools loaded: ${resolvedTools.length}`);
|
|
329
|
-
// Get user config tools (toolkit-flattened; client tools get interrupt() stubs)
|
|
330
|
-
const flattenedConfigTools = toolsDisabled
|
|
331
|
-
? []
|
|
332
|
-
: this.extractAndFlattenTools(this.config.tools || []);
|
|
333
|
-
debugLog(`User config tools loaded: ${flattenedConfigTools.length}`);
|
|
334
|
-
// Combine all tools, then apply the allowedTools name allow-list when configured. Entries
|
|
335
|
-
// match by exact name, or glob-style when they contain `*` (e.g. `mcp__unimarket__*`) — see
|
|
336
|
-
// isToolAllowed. Nameless ServerTools are retained (they can't be named in the allow-list).
|
|
337
|
-
let tools = [...resolvedTools, ...flattenedConfigTools];
|
|
338
|
-
if (Array.isArray(allowedTools)) {
|
|
339
|
-
tools = tools.filter((tool) => !tool.name || isToolAllowed(tool.name, allowedTools));
|
|
340
|
-
}
|
|
341
|
-
// Safety net: a custom/dev/MCP tool may still reuse a deepagents filesystem-tool
|
|
342
|
-
// name (createDeepAgent throws on such a collision). Drop the colliding tool — the
|
|
343
|
-
// deep agent's built-in fs tool wins. With filesystem disabled above this is normally
|
|
344
|
-
// empty; it only fires for a genuine user/MCP name clash.
|
|
345
|
-
const reserved = new Set(FILESYSTEM_TOOL_NAMES);
|
|
346
|
-
const superseded = tools.filter((tool) => tool.name && reserved.has(tool.name));
|
|
347
|
-
const passThroughTools = tools.filter((tool) => !tool.name || !reserved.has(tool.name));
|
|
348
|
-
if (superseded.length > 0) {
|
|
349
|
-
const names = superseded.map((tool) => tool.name).join(', ');
|
|
350
|
-
this.statusUpdate(StatusLevel.WARNING, `Dropping tool(s) that collide with deepagents built-in filesystem tools: ${names}`);
|
|
351
|
-
}
|
|
352
|
-
if (passThroughTools.length > 0) {
|
|
353
|
-
const toolNames = passThroughTools
|
|
354
|
-
.map((tool) => tool.name)
|
|
355
|
-
.filter((name) => name)
|
|
356
|
-
.join(', ');
|
|
357
|
-
this.headerStatus(`Loaded tools: ${toolNames}`);
|
|
358
|
-
debugLog(`Total tools available: ${passThroughTools.length}`);
|
|
359
|
-
debugLogObject('All Tools', toolNames.split(', '));
|
|
360
|
-
}
|
|
361
|
-
debugLog('Creating deep agent...');
|
|
362
|
-
// Resolve middleware via resolver or fall back to empty. These are applied AFTER
|
|
363
|
-
// deepagents' standard middleware (todos, subagents, summarization, filesystem).
|
|
364
|
-
const resolvedMiddleware = this.resolvers?.resolveMiddleware
|
|
365
|
-
? await this.resolvers.resolveMiddleware(
|
|
366
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367
|
-
this.config.middleware, this.config)
|
|
368
|
-
: [];
|
|
369
|
-
// deepagents' standard middleware already summarizes long conversations; drop any
|
|
370
|
-
// gsloth-configured summarization middleware so the deep agent doesn't summarize twice.
|
|
371
|
-
const configuredMiddleware = resolvedMiddleware.filter((m) => {
|
|
372
|
-
const name = m.name ?? '';
|
|
373
|
-
if (/summar/i.test(name)) {
|
|
374
|
-
debugLog(`Dropping duplicate summarization middleware '${name}' (deepagents provides it)`);
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
return true;
|
|
378
|
-
});
|
|
379
|
-
// Soften deepagents' fail-hard filesystem tool throws. By default the permission layer
|
|
380
|
-
// THROWS on both a denied read/write AND on a malformed path the model supplied — a relative
|
|
381
|
-
// path, a `..`/`~` segment, or an empty string (deepagents' validatePath, run BEFORE the
|
|
382
|
-
// permission check in enforcePermission). Any of these aborts the WHOLE run. On the AG-UI
|
|
383
|
-
// transport that throw propagates out of streamWithEvents into the run handler's catch, which
|
|
384
|
-
// emits RUN_ERROR and ends the response WITHOUT a terminal RUN_FINISHED — and since AG-UI's
|
|
385
|
-
// protocol makes RUN_ERROR terminal ("no further events can be sent"), a consumer waiting for
|
|
386
|
-
// RUN_FINISHED hangs (EXT-24). Wrap tool calls so each of these becomes a recoverable error
|
|
387
|
-
// ToolMessage instead, letting the model observe the mistake, retry with a good path, and
|
|
388
|
-
// finish the run normally (reaching RUN_FINISHED). This preserves gsloth's recoverable-denial
|
|
389
|
-
// UX (the old GthFileSystemToolkit returned a message rather than throwing). Only these known
|
|
390
|
-
// fs path/permission messages are caught; every other throw (GraphInterrupt from a client-tool
|
|
391
|
-
// interrupt stub, AbortError on client disconnect, unexpected errors) is rethrown untouched so
|
|
392
|
-
// control-flow and genuine failures still surface.
|
|
393
|
-
const fsDenialSoftening = createMiddleware({
|
|
394
|
-
name: 'GthDeepFsDenialSoftening',
|
|
395
|
-
wrapToolCall: async (request, handler) => {
|
|
396
|
-
try {
|
|
397
|
-
return await handler(request);
|
|
398
|
-
}
|
|
399
|
-
catch (e) {
|
|
400
|
-
// EXT-25: rethrow control-flow throws BY TYPE, BEFORE the message regex below. A
|
|
401
|
-
// GraphInterrupt (a client-tool interrupt() suspending the graph for HITL tool
|
|
402
|
-
// approval) and an AbortError (caller cancellation) must ALWAYS propagate so the graph
|
|
403
|
-
// suspends / cancels — never be converted into a benign ToolMessage. Mirrors the guard
|
|
404
|
-
// in GthAbstractAgent (error.name checks + GraphInterrupt instanceof). Today these
|
|
405
|
-
// survive only because their messages happen not to match the regex; guarding by type
|
|
406
|
-
// stops a future regex broadening from silently swallowing the HITL suspend.
|
|
407
|
-
if (e instanceof GraphInterrupt ||
|
|
408
|
-
e?.name === 'GraphInterrupt' ||
|
|
409
|
-
e?.name === 'AbortError') {
|
|
410
|
-
throw e;
|
|
411
|
-
}
|
|
412
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
413
|
-
// A permission denial, or a path that is relative / contains ".." or "~" / is empty —
|
|
414
|
-
// all recoverable model-input errors. The live source of these throws is gsloth's own
|
|
415
|
-
// realpath guard (guardFilesystemBackend); deepagents' enforcement used to throw the
|
|
416
|
-
// same messages too, but since 1.11.1 it returns the equivalent error ToolMessage
|
|
417
|
-
// itself and so never reaches this catch. Keep matching both: the observation the model
|
|
418
|
-
// sees is identical either way, and the guard still depends on this softening.
|
|
419
|
-
if (/permission denied for (read|write)|path must (be absolute|not contain|be a non-empty string)/i.test(message)) {
|
|
420
|
-
debugLog(`Softened fs tool throw into a ToolMessage: ${message}`);
|
|
421
|
-
return new ToolMessage({
|
|
422
|
-
content: message,
|
|
423
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
|
-
tool_call_id: request.toolCall?.id ?? '',
|
|
425
|
-
status: 'error',
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
throw e;
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
// EXT-20: sibling of fsDenialSoftening for the run_* (dev/shell) tools. GthDevToolkit's
|
|
433
|
-
// executeCommand now THROWS a ShellCommandFailedError on a non-zero exit or a timeout-kill
|
|
434
|
-
// (instead of resolving with the failure text), so the tool result no longer misreports
|
|
435
|
-
// status:'success' (✓). Catch it here and return an error ToolMessage that PRESERVES the full
|
|
436
|
-
// stdout/stderr body — the model's observation is unchanged except that status flips to
|
|
437
|
-
// 'error', which drives the ✗ (isError) glyph (GthAbstractAgent maps status==='error' →
|
|
438
|
-
// isError). Returning a ToolMessage (rather than rethrowing) also means the approved-then-failed
|
|
439
|
-
// command does NOT trigger a retry loop — it is a normal, observed tool result.
|
|
440
|
-
const shellExitSoftening = createMiddleware({
|
|
441
|
-
name: 'GthDeepShellExitSoftening',
|
|
442
|
-
wrapToolCall: async (request, handler) => {
|
|
443
|
-
try {
|
|
444
|
-
return await handler(request);
|
|
445
|
-
}
|
|
446
|
-
catch (e) {
|
|
447
|
-
if (e instanceof ShellCommandFailedError) {
|
|
448
|
-
debugLog(`Softened shell/dev command failure (exit ${e.exitCode ?? 'timeout'}) into an ` +
|
|
449
|
-
`error ToolMessage for '${e.command}'`);
|
|
450
|
-
return new ToolMessage({
|
|
451
|
-
content: e.output,
|
|
452
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
-
tool_call_id: request.toolCall?.id ?? '',
|
|
454
|
-
status: 'error',
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
throw e;
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
// MCP tool-execution errors are spec-compliant RESULTS, not fatal faults. Per the MCP spec
|
|
462
|
-
// (2025-11-25 & draft, "Server › Tools › Error Handling"), a tool that hits an API failure, an
|
|
463
|
-
// input-validation problem, or a business-logic error (e.g. a disabled capability) returns a
|
|
464
|
-
// normal tools/call result with `isError: true`, and the CLIENT *SHOULD* hand that error to the
|
|
465
|
-
// model so it can self-correct. `@langchain/mcp-adapters` instead surfaces such a result by
|
|
466
|
-
// THROWING a ToolException at call time (its `_convertCallToolResult`). Because we install a
|
|
467
|
-
// wrapToolCall middleware, langchain's ToolNode treats any error a middleware rethrows as a fatal
|
|
468
|
-
// "middleware error" (`errorFromMiddleware && handleToolErrors !== true` → throw) and aborts the
|
|
469
|
-
// whole turn instead of relaying the error to the model — the opposite of the spec's client
|
|
470
|
-
// SHOULD. This middleware closes that gap: it catches a thrown ToolException and RETURNS it as a
|
|
471
|
-
// status:'error' ToolMessage (→ isError → ✗), so the model observes the error and can retry or
|
|
472
|
-
// explain (matching the non-stream invoke path's ToolException handling). Scope & safety: matched
|
|
473
|
-
// by name === 'ToolException' (the adapter's marker), so GraphInterrupt and every non-MCP throw
|
|
474
|
-
// fall through the final rethrow untouched. The adapter ALSO wraps a call-time AbortError into a
|
|
475
|
-
// ToolException (its `_callTool` catch-all), so we RETHROW when the run's abort signal is set —
|
|
476
|
-
// otherwise softening here would swallow user cancellation that ToolNode's own `signal?.aborted`
|
|
477
|
-
// guard normally enforces (bypassed once we handle the error in middleware). MCP connect/auth
|
|
478
|
-
// (401/403) and load failures are handled at CONNECT time (resolvers.ts throwOnLoadError +
|
|
479
|
-
// onConnectionError), not here, so they stay fatal as intended.
|
|
480
|
-
const mcpToolErrorSoftening = createMiddleware({
|
|
481
|
-
name: 'GthMcpToolErrorSoftening',
|
|
482
|
-
wrapToolCall: async (request, handler) => {
|
|
483
|
-
try {
|
|
484
|
-
return await handler(request);
|
|
485
|
-
}
|
|
486
|
-
catch (e) {
|
|
487
|
-
if (e instanceof Error &&
|
|
488
|
-
e.name === 'ToolException' &&
|
|
489
|
-
!request.runtime?.signal?.aborted) {
|
|
490
|
-
debugLog(`Softened MCP tool error into an error ToolMessage: ${e.message}`);
|
|
491
|
-
return new ToolMessage({
|
|
492
|
-
content: e.message,
|
|
493
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
494
|
-
tool_call_id: request.toolCall?.id ?? '',
|
|
495
|
-
status: 'error',
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
throw e;
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
});
|
|
502
|
-
// fsDenialSoftening first so it is the outermost wrapToolCall — it must see the throw from
|
|
503
|
-
// deepagents' permission-enforcing fs tools. shellExitSoftening sits right after it (still
|
|
504
|
-
// outboard of any user-configured middleware, so it always sees the raw ShellCommandFailedError
|
|
505
|
-
// throw before a user wrapToolCall could transform it). Order between the two softeners is not
|
|
506
|
-
// load-bearing: they catch DISJOINT conditions (a permission-denied regex vs an
|
|
507
|
-
// `instanceof ShellCommandFailedError`) and each rethrows what it doesn't recognize, so neither
|
|
508
|
-
// can swallow the other. The console-bound tool-call-status middleware is NOT added here (see
|
|
509
|
-
// GthDeepAgentParams.middleware); the runner appends it. mcpToolErrorSoftening sits alongside the
|
|
510
|
-
// other two softeners (still outboard of user middleware); it catches a DISJOINT condition
|
|
511
|
-
// (name==='ToolException') and rethrows everything else, so ordering among the three is not
|
|
512
|
-
// load-bearing.
|
|
513
|
-
const middleware = [
|
|
514
|
-
fsDenialSoftening,
|
|
515
|
-
shellExitSoftening,
|
|
516
|
-
mcpToolErrorSoftening,
|
|
517
|
-
...configuredMiddleware,
|
|
518
|
-
];
|
|
519
|
-
// Map gsloth's .aiignore + filesystem mode onto deepagents permission rules. When
|
|
520
|
-
// `--allow-dir` widens the sandbox, the backend runs without virtualMode, so paths are REAL
|
|
521
|
-
// absolute paths: constrain read+write to cwd + the allowed dirs (everything else denied),
|
|
522
|
-
// layered under the .aiignore deny rules.
|
|
523
|
-
const permissions = buildPermissions({
|
|
524
|
-
filesystem: this.config.filesystem,
|
|
525
|
-
aiignore: this.config.aiignore,
|
|
526
|
-
allowDirs: Array.isArray(this.config.allowDirs) && this.config.allowDirs.length > 0
|
|
527
|
-
? this.config.allowDirs
|
|
528
|
-
: undefined,
|
|
529
|
-
},
|
|
530
|
-
// EXT-16: build virtual (`/`-rooted) permission rules when the backend will run in
|
|
531
|
-
// virtualMode (Windows), matching the FilesystemBackend created in init().
|
|
532
|
-
shouldUseVirtualFs());
|
|
533
|
-
debugLogObject('Filesystem permissions', permissions);
|
|
534
|
-
// Compose gsloth's system prompt (backstory + guidelines + per-command mode prompt +
|
|
535
|
-
// system prompt) so identity profiles (Gaunt Sloth, sorcerer, fisher-alt, …) and
|
|
536
|
-
// `.gsloth.*.md` are honored. This is passed to createDeepAgent as `systemPrompt` — combined
|
|
537
|
-
// additively with deepagents' base + fs prompts into ONE system message — rather than injected
|
|
538
|
-
// as a separate SystemMessage per turn (which produced a non-first system message that
|
|
539
|
-
// Anthropic rejects). GS2-79: which mode prompt a command gets is decided ONCE, in core's
|
|
540
|
-
// `readModePrompt` — 'code' the code-mode prompt, 'exec' the prompt-as-script exec-mode prompt,
|
|
541
|
-
// 'review'/'pr' the REVIEW INSTRUCTIONS, chat/api/others the chat prompt — so this backend and
|
|
542
|
-
// the lean one cannot disagree, and a command left out of the selection can no longer be served
|
|
543
|
-
// the chat prompt by silent default.
|
|
544
|
-
const modePrompt = readModePrompt(this.command, this.config);
|
|
545
|
-
const systemMessages = buildSystemMessages(this.config, modePrompt);
|
|
546
|
-
const systemPrompt = typeof systemMessages[0]?.content === 'string' ? systemMessages[0].content : undefined;
|
|
547
|
-
// Gate the opt-in run_shell_command tool behind a per-command approval interrupt. The gate
|
|
548
|
-
// condition and its user-facing notices are the SHARED core policy (`resolveShellApprovalGate`,
|
|
549
|
-
// EXT-12 semantics documented there), so this backend and the lean one can never drift; here it
|
|
550
|
-
// is installed through deepagents' `interruptOn` rather than as middleware. The resolver reads
|
|
551
|
-
// the same `builtInTools` registry GthDevToolkit emits the tool from (CFG-18), so the wiring
|
|
552
|
-
// stays in lockstep with where the tool actually exists.
|
|
553
|
-
const { gateShell, notice: shellGateNotice } = resolveShellApprovalGate(this.config ?? undefined, this.command);
|
|
554
|
-
//
|
|
555
|
-
// EXT-80: at `manual` and `write` every bound tool the rung's access class does not
|
|
556
|
-
// auto-grant must reach the human. Both sets below come from core's shared policy — the same one
|
|
557
|
-
// the lean backend and `GthAgentRunner` call — so the three cannot disagree about what is gated.
|
|
558
|
-
//
|
|
559
|
-
// **The interrupt is wired rung-INDEPENDENTLY, over every tool any rung could gate.** It is
|
|
560
|
-
// built once, here, while `/approvals <rung>` moves the rung for the rest of the session without
|
|
561
|
-
// rebuilding the graph; a set that carried the rung would be frozen at the rung the session
|
|
562
|
-
// started on, and since the default is `assisted`, typing `/approvals manual` would leave
|
|
563
|
-
// exactly the write tools ungated. `GthAgentRunner.decideToolApproval` decides on the rung in
|
|
564
|
-
// force, so wiring wider does not gate wider.
|
|
565
|
-
//
|
|
566
|
-
// **`boundToolNames` must include deepagents' OWN tools.** deepagents registers its filesystem
|
|
567
|
-
// tools, `execute`, `task` and `write_todos` itself (this backend resolves with
|
|
568
|
-
// `filesystem: 'none'`), so they never appear in `passThroughTools`; deriving the set from that
|
|
569
|
-
// array alone would leave `write_file`, `edit_file`, `execute`, `task` and `write_todos` ungated
|
|
570
|
-
// at `manual` on this backend — precisely the defect this change exists to remove. Gating
|
|
571
|
-
// them by name works because deepagents installs the very same langchain
|
|
572
|
-
// `humanInTheLoopMiddleware`, which matches the model's tool CALLS by name in `afterModel` and so
|
|
573
|
-
// does not care which party registered the tool.
|
|
574
|
-
//
|
|
575
|
-
// Note this union keeps `execute`, unlike `additionalToolNames` below: that list answers "what
|
|
576
|
-
// may the rater offer as a granted alternative", a different question from "what must the gate
|
|
577
|
-
// stop". `execute` is deepagents' shell and has no access class, so it must be gated at both
|
|
578
|
-
// deterministic rungs and offered as an alternative at none.
|
|
579
|
-
const rung = resolveApprovals(this.config ?? undefined, this.command).rung;
|
|
580
|
-
const boundToolNames = [
|
|
581
|
-
...passThroughTools
|
|
582
|
-
.map((tool) => tool?.name)
|
|
583
|
-
.filter((name) => typeof name === 'string' && name.length > 0),
|
|
584
|
-
...DEEP_AGENT_BUILT_IN_TOOL_NAMES,
|
|
585
|
-
];
|
|
586
|
-
// **What a surface that answers no approval gets — for BOTH sets below.** An interrupt nobody
|
|
587
|
-
// can answer suspends the graph forever: the tool never runs and the client is never asked. So
|
|
588
|
-
// such a surface is wired with exactly what the shell gate itself requires and nothing more,
|
|
589
|
-
// and is not TOLD it will be asked either. This backend is ONE of the two the AG-UI server can
|
|
590
|
-
// run on — `apiAgUiModule.createConfiguredAgent` builds a `GthDeepAgent` only when
|
|
591
|
-
// `agent.backend === 'deep'` (and warns that it is experimental); every other configuration,
|
|
592
|
-
// the default included, gets the lean `GthLangChainAgent`, which carries the same wiring for
|
|
593
|
-
// the same reason. It matters on both: at `manual` and `write` the live set is non-empty, so a
|
|
594
|
-
// write, an MCP call, `task` or deepagents' own `write_todos` bookkeeping would simply vanish —
|
|
595
|
-
// or be announced to the model as approvable when nothing will ever approve it.
|
|
596
|
-
const answersApprovals = commandAnswersApprovals(this.command);
|
|
597
|
-
const noDrainTools = gateShell ? [SHELL_TOOL_NAME] : [];
|
|
598
|
-
// The LIVE gated set — what THIS rung gates — for the §4.5 tool descriptions below.
|
|
599
|
-
const gatedTools = answersApprovals
|
|
600
|
-
? resolveGatedToolNames({ rung, gateShell, boundToolNames })
|
|
601
|
-
: noDrainTools;
|
|
602
|
-
// Rung-independent ONLY where something answers the interrupt.
|
|
603
|
-
const interruptTools = answersApprovals
|
|
604
|
-
? resolveInterruptToolNames({ gateShell, boundToolNames })
|
|
605
|
-
: noDrainTools;
|
|
606
|
-
// Keyed off the interrupt SET, not `gateShell`: at a deterministic rung there is a gate to
|
|
607
|
-
// install even when the shell tool is disabled, and deepagents installs no HITL middleware at
|
|
608
|
-
// all when `interruptOn` is undefined.
|
|
609
|
-
const interruptOn = interruptTools.length > 0
|
|
610
|
-
? Object.fromEntries(interruptTools.map((name) => [name, { allowedDecisions: ['approve', 'reject'] }]))
|
|
611
|
-
: undefined;
|
|
612
|
-
if (shellGateNotice) {
|
|
613
|
-
this.statusUpdate(shellGateNotice.level, shellGateNotice.message);
|
|
614
|
-
}
|
|
615
|
-
// EXT-58 (spec §4.5) — the same tool-registration hook the lean backend calls, on the same LIVE
|
|
616
|
-
// gated set, so neither backend's descriptions can disagree with its own rung. That set is
|
|
617
|
-
// narrower than the `interruptOn` set above, which covers every rung so the rung can still move
|
|
618
|
-
// mid-session; describing from the wider one would promise approvals this rung does not ask
|
|
619
|
-
// for. deepagents registers its OWN filesystem tools (this backend
|
|
620
|
-
// resolves with `filesystem: 'none'`), so they never appear in `passThroughTools` and their
|
|
621
|
-
// descriptions are deepagents' rather than ours, which is why they cannot be suffixed here even
|
|
622
|
-
// though the gated set above does gate them. They are declared as additional registered names so
|
|
623
|
-
// the rater's granted-alternative list (§4.4) reflects the tools a deep session actually has.
|
|
624
|
-
// `execute` is deliberately excluded from THAT list: it is deepagents' shell, not a filesystem
|
|
625
|
-
// tool, and must never be offered as a granted alternative to a shell command.
|
|
626
|
-
this.registerApprovalsAwareTools(passThroughTools, {
|
|
627
|
-
rung,
|
|
628
|
-
gatedTools,
|
|
629
|
-
additionalToolNames: FILESYSTEM_TOOL_NAMES.filter((name) => name !== 'execute'),
|
|
630
|
-
});
|
|
631
|
-
return {
|
|
632
|
-
model: this.config.llm,
|
|
633
|
-
tools: passThroughTools,
|
|
634
|
-
permissions,
|
|
635
|
-
middleware,
|
|
636
|
-
systemPrompt,
|
|
637
|
-
interruptOn,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* EXT-22: shared virtualMode path-namespace guidance — ONE source of truth used by BOTH the S2
|
|
643
|
-
* early-framing note ({@link appendVirtualCwdNote}, injected into gsloth's composed systemPrompt =
|
|
644
|
-
* block 0) and the S1 last-word correction middleware
|
|
645
|
-
* ({@link createPathNamespaceCorrectionMiddleware}, appended after deepagents' `/`-rooted line).
|
|
646
|
-
*
|
|
647
|
-
* In a virtualMode `code` session (EXT-16, e.g. Windows) the deepagents filesystem tools use a
|
|
648
|
-
* VIRTUAL `/` root (= the working dir) while `run_shell_command` uses REAL native OS paths; the
|
|
649
|
-
* model conflates the two forms. This text draws the distinction and steers toward cwd-relative
|
|
650
|
-
* paths (the one form both tool families read alike).
|
|
651
|
-
*
|
|
652
|
-
* It deliberately does NOT equate the virtual root with a specific real path (no "`/` = D:\\work"):
|
|
653
|
-
* virtualMode withholds the real cwd on purpose (see the systemPrompt gate in {@link
|
|
654
|
-
* GthDeepAgent.init}), so the guidance is about the DISTINCTION between the two namespaces and the
|
|
655
|
-
* safety of relative paths, not a mapping between them.
|
|
656
|
-
*/
|
|
657
|
-
export const PATH_NAMESPACE_GUIDANCE = 'The filesystem tools (ls, read_file, write_file, edit_file, glob, grep) use a VIRTUAL root in ' +
|
|
658
|
-
'this session: a leading `/` means your working directory, and their paths are written ' +
|
|
659
|
-
'`/`-rooted relative to it (this is what "all file paths must start with a /" refers to). That ' +
|
|
660
|
-
'`/` is NOT the real operating-system filesystem root. run_shell_command is different: it runs ' +
|
|
661
|
-
'in the real operating system and uses real native paths (on Windows, e.g. ' +
|
|
662
|
-
'`C:\\Users\\...\\project`, with backslashes), never the virtual `/` root. A `/`-rooted path ' +
|
|
663
|
-
'from the filesystem tools is NOT a valid shell path and must never be passed to ' +
|
|
664
|
-
'run_shell_command. The one form that means the same thing to both tool families is a path ' +
|
|
665
|
-
'RELATIVE to the working directory (e.g. `src/index.ts`); prefer relative paths for both. When ' +
|
|
666
|
-
'you must be absolute, use `/`-rooted form ONLY for the filesystem tools and real native form ' +
|
|
667
|
-
'ONLY for run_shell_command.';
|
|
668
|
-
/**
|
|
669
|
-
* EXT-22 (S2): virtualMode variant of {@link appendCwdNote}. On the `code` path when the fs
|
|
670
|
-
* backend runs in virtualMode (EXT-16), inject the shared path-namespace guidance EARLY in
|
|
671
|
-
* gsloth's composed systemPrompt (block 0) so the model is framed before deepagents' own prompt.
|
|
672
|
-
*
|
|
673
|
-
* This is early framing only: deepagents' hardcoded `/`-rooted line lands in a LATER block and can
|
|
674
|
-
* partially override block 0, so the authoritative last word is delivered by the S1 middleware
|
|
675
|
-
* ({@link createPathNamespaceCorrectionMiddleware}); see handoff/spike-systemmessage-ordering.md.
|
|
676
|
-
* Returns the note alone when there is no base prompt.
|
|
677
|
-
*/
|
|
678
|
-
export function appendVirtualCwdNote(systemPrompt) {
|
|
679
|
-
const note = `Filesystem vs shell path namespaces: ${PATH_NAMESPACE_GUIDANCE}`;
|
|
680
|
-
return systemPrompt ? `${systemPrompt}\n\n${note}` : note;
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* EXT-22 (S1): the load-bearing path-namespace correction. A gsloth `wrapModelCall` middleware
|
|
684
|
-
* runs INNERMOST (inside deepagents' filesystem middleware), so appending a trailing block to
|
|
685
|
-
* `request.systemMessage` lands AFTER deepagents' hardcoded "All file paths must start with a /."
|
|
686
|
-
* line — giving gsloth the last word on path semantics (empirically verified; see
|
|
687
|
-
* handoff/spike-systemmessage-ordering.md). It APPENDS via `request.systemMessage.concat(...)`
|
|
688
|
-
* (mirroring how deepagents appends its own fs prompt), never string-splices, and returns a NEW
|
|
689
|
-
* request so it never mutates persisted state (no compounding across turns).
|
|
690
|
-
*
|
|
691
|
-
* `appendCorrection` gates it to `code` + virtualMode: only there do the fs virtual `/` root and
|
|
692
|
-
* the shell's real-OS paths diverge. On POSIX real-path mode deepagents' "start with /" is
|
|
693
|
-
* literally true, so the middleware is a transparent pass-through (like the debug-capture
|
|
694
|
-
* middleware when no sink is attached).
|
|
695
|
-
*/
|
|
696
|
-
export function createPathNamespaceCorrectionMiddleware(appendCorrection) {
|
|
697
|
-
return createMiddleware({
|
|
698
|
-
name: 'GthDeepPathNamespaceCorrection',
|
|
699
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
700
|
-
wrapModelCall: async (request, handler) => {
|
|
701
|
-
if (!appendCorrection || !request?.systemMessage)
|
|
702
|
-
return handler(request);
|
|
703
|
-
const correction = 'IMPORTANT — path namespaces (authoritative; this overrides any earlier instruction that ' +
|
|
704
|
-
`implies every path is a simple \`/\`-rooted filesystem path): ${PATH_NAMESPACE_GUIDANCE}`;
|
|
705
|
-
return handler({ ...request, systemMessage: request.systemMessage.concat(correction) });
|
|
706
|
-
},
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
// The `/debug` request-extras extraction (extractDebugRequestExtras + its model-param / tool-def
|
|
710
|
-
// allowlist helpers) now lives in @gaunt-sloth/core (`core/debugCapture.ts`) so the lean backend
|
|
711
|
-
// shares it. Imported at the top of this module and re-exported for back-compat.
|
|
712
|
-
//# sourceMappingURL=GthDeepAgent.js.map
|