@stigmer/runner 3.36.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/hermetic-activity.d.ts +4 -2
  3. package/dist/__test-utils__/hermetic-activity.js +5 -3
  4. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  5. package/dist/activities/execute-deep-agent/engine-tools.d.ts +32 -0
  6. package/dist/activities/execute-deep-agent/engine-tools.js +32 -0
  7. package/dist/activities/execute-deep-agent/engine-tools.js.map +1 -0
  8. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -4
  9. package/dist/activities/execute-deep-agent/prompt-builder.js +66 -64
  10. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +5 -2
  12. package/dist/activities/execute-deep-agent/subagent-transformer.js +27 -14
  13. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/todo-list.d.ts +40 -0
  15. package/dist/activities/execute-deep-agent/todo-list.js +51 -0
  16. package/dist/activities/execute-deep-agent/todo-list.js.map +1 -0
  17. package/dist/activities/execute-deep-agent/turn-setup.js +5 -7
  18. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  19. package/dist/middleware/error-hints.js +3 -3
  20. package/dist/middleware/error-hints.js.map +1 -1
  21. package/dist/middleware/path-normalization.d.ts +18 -4
  22. package/dist/middleware/path-normalization.js +42 -13
  23. package/dist/middleware/path-normalization.js.map +1 -1
  24. package/dist/shared/subagent-gate.d.ts +6 -0
  25. package/dist/shared/subagent-gate.js +6 -1
  26. package/dist/shared/subagent-gate.js.map +1 -1
  27. package/dist/shared/workspace/sources/local-path.js +5 -1
  28. package/dist/shared/workspace/sources/local-path.js.map +1 -1
  29. package/dist/tools/think-tool.d.ts +7 -0
  30. package/dist/tools/think-tool.js +10 -11
  31. package/dist/tools/think-tool.js.map +1 -1
  32. package/package.json +5 -5
  33. package/src/__test-utils__/hermetic-activity.ts +5 -3
  34. package/src/activities/execute-deep-agent/__tests__/engine-tools.test.ts +96 -0
  35. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +22 -35
  36. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +23 -35
  37. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +11 -23
  38. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +22 -35
  39. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +22 -34
  40. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-parallel-gates.test.ts +132 -0
  41. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +20 -2
  42. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +9 -0
  43. package/src/activities/execute-deep-agent/engine-tools.ts +33 -0
  44. package/src/activities/execute-deep-agent/prompt-builder.ts +73 -69
  45. package/src/activities/execute-deep-agent/subagent-transformer.ts +28 -13
  46. package/src/activities/execute-deep-agent/todo-list.ts +56 -0
  47. package/src/activities/execute-deep-agent/turn-setup.ts +5 -7
  48. package/src/middleware/__tests__/path-normalization.test.ts +32 -0
  49. package/src/middleware/error-hints.ts +3 -3
  50. package/src/middleware/path-normalization.ts +41 -13
  51. package/src/shared/subagent-gate.ts +6 -1
  52. package/src/shared/workspace/sources/local-path.ts +5 -1
  53. package/src/tools/__tests__/think-tool.test.ts +3 -2
  54. package/src/tools/think-tool.ts +10 -11
@@ -22,7 +22,7 @@
22
22
  * - Empty subagent list returns null (no subagents configured)
23
23
  */
24
24
 
25
- import { createDeepAgent, FilesystemBackend, LocalShellBackend, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT } from "deepagents";
25
+ import { createDeepAgent, FilesystemBackend, LocalShellBackend, DEFAULT_SUBAGENT_PROMPT } from "deepagents";
26
26
  import type { AnyBackendProtocol, CompiledSubAgent, FilesystemPermission } from "deepagents";
27
27
  import type { StructuredTool } from "@langchain/core/tools";
28
28
  import type { RunnableConfig } from "@langchain/core/runnables";
@@ -42,6 +42,7 @@ import { SubAgentGate } from "../../shared/subagent-gate.js";
42
42
  import { isModelRegistered } from "../../shared/model-registry.js";
43
43
  import type { SkillMetadata } from "../../shared/skill-resolver.js";
44
44
  import { renderSkillsSection } from "./prompt-builder.js";
45
+ import { ENGINE_TOOL } from "./engine-tools.js";
45
46
 
46
47
  // =========================================================================
47
48
  // Built-in subagent types and prompts
@@ -53,33 +54,44 @@ export const BUILTIN_SUBAGENT_TYPES: ReadonlySet<string> = new Set([
53
54
  "general-purpose",
54
55
  ]);
55
56
 
57
+ // The task arrives as the sub-agent's first message (deepagents' `task`
58
+ // tool), so a prompt carries no slot for it. Tool names come from the one
59
+ // table of bound names (`engine-tools.ts`).
56
60
  const EXPLORE_SYSTEM_PROMPT = `\
57
61
  You are an exploration specialist. Your ONLY job is to explore codebases \
58
62
  and report findings back to the parent agent.
59
63
 
60
64
  STRICT BOUNDARIES:
61
- - Use ONLY the read-only tools provided (read_file, list_dir, glob, grep, search)
65
+ - Use ONLY the read-only tools (${ENGINE_TOOL.readFile}, ${ENGINE_TOOL.ls}, ${ENGINE_TOOL.glob}, ${ENGINE_TOOL.grep})
62
66
  - Do NOT write files, create files, or modify anything
63
67
  - Do NOT execute shell commands
64
68
  - Do NOT follow skill activation instructions from any context
65
69
  - Do NOT create deliverables, scaffolds, or run initialization scripts
66
- - Report your findings concisely — the parent agent has direct file access
67
-
68
- Your task: {description}`;
70
+ - Report your findings concisely — the parent agent has direct file access`;
69
71
 
70
72
  const SHELL_SYSTEM_PROMPT = `\
71
73
  You are a command execution specialist. Your ONLY job is to run shell \
72
74
  commands and report the results back to the parent agent.
73
75
 
74
76
  STRICT BOUNDARIES:
75
- - Use ONLY the tools provided (execute, read_file, list_dir)
77
+ - Use ONLY ${ENGINE_TOOL.execute}, ${ENGINE_TOOL.readFile} and ${ENGINE_TOOL.ls}
76
78
  - Do NOT write or modify files directly — use shell commands if needed
77
79
  - Do NOT search extensively or explore the codebase beyond what is needed
78
80
  - Do NOT follow skill activation instructions from any context
79
81
  - Do NOT create deliverables, scaffolds, or run initialization scripts
80
- - Report command output concisely — the parent agent will interpret results
82
+ - Report command output concisely — the parent agent will interpret results`;
81
83
 
82
- Your task: {description}`;
84
+ /**
85
+ * The general-purpose sub-agent's line in the `task` tool. Stigmer's own
86
+ * words rather than upstream's `DEFAULT_GENERAL_PURPOSE_DESCRIPTION`, which
87
+ * invites delegating a search ("use this agent to perform the search for
88
+ * you") that the parent's working rules tell it to do itself
89
+ * (`prompt-builder.ts`): each delegation is a sub-agent's own rounds on top
90
+ * of the parent's.
91
+ */
92
+ const GENERAL_PURPOSE_DESCRIPTION =
93
+ "Runs a multi-step task with the same tools as you, in its own context, and returns one report. " +
94
+ "Use it only for independent work you need as a summary.";
83
95
 
84
96
  const BUILTIN_DESCRIPTIONS: ReadonlyMap<string, string> = new Map([
85
97
  ["explore", (
@@ -102,8 +114,8 @@ const RESPONSE_RULES = `\
102
114
 
103
115
  ## Response rules
104
116
 
105
- - After using the read tool, NEVER reprint, echo, list, or \
106
- summarize file contents in your response. Tool results are \
117
+ - After reading a file with ${ENGINE_TOOL.readFile}, NEVER reprint, echo, \
118
+ list, or summarize file contents in your response. Tool results are \
107
119
  already in your context. Proceed directly to the task.
108
120
  - Your response is returned to the parent agent as a task \
109
121
  result. Return concise findings and actionable results — not \
@@ -215,8 +227,11 @@ export interface SubagentTransformOptions {
215
227
  * general-purpose is the gated replacement for the sub-agent deepagents would
216
228
  * otherwise auto-inject WITHOUT our approval gate (suppressed process-wide in
217
229
  * deepagents-profiles.ts; supplying our own under the same name is the
218
- * documented override path). It uses deepagents' own description/prompt so the
219
- * model's delegation behavior is unchanged, and receives the parent's MCP tools
230
+ * documented override path). It keeps deepagents' own prompt, but its line in
231
+ * the `task` tool is Stigmer's ({@link GENERAL_PURPOSE_DESCRIPTION}): it was
232
+ * upstream's while the goal was parity with the suppressed original, and
233
+ * upstream's invites the delegation the parent's rules advise against. It
234
+ * receives the parent's MCP tools
220
235
  * for capability parity with the injected original. Conscious simplification:
221
236
  * the parent's skills prompt is NOT inherited — a sub-agent needing skills
222
237
  * should be declared explicitly in the Agent spec.
@@ -239,7 +254,7 @@ export function createBuiltinSubagents(
239
254
  if (subagentType === "general-purpose") {
240
255
  result.push({
241
256
  name: "general-purpose",
242
- description: DEFAULT_GENERAL_PURPOSE_DESCRIPTION,
257
+ description: GENERAL_PURPOSE_DESCRIPTION,
243
258
  systemPrompt: DEFAULT_SUBAGENT_PROMPT + RESPONSE_RULES,
244
259
  // Parent-parity tool set: MCP tools plus the native web_fetch the
245
260
  // parent always carries. explore/shell stay web-less on purpose —
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The parent's to-do list: langchain's `todoListMiddleware`, with Stigmer's
3
+ * words for its tool and its system line.
4
+ *
5
+ * Why the middleware and not our own: it owns the `todos` state channel, the
6
+ * tool, and the refusal of two list updates in one response, and the
7
+ * product reads what it writes (the parent's `write_todos` is what
8
+ * `status.todos` projects and what the clients' progress card renders,
9
+ * `harness/transcript/builder.ts`, `shared/todos.ts`). Parent only: a
10
+ * sub-agent's list never reaches the execution.
11
+ *
12
+ * Why our own words: langchain's defaults are an 11.4 KB tool description
13
+ * and a 1 KB system block, sent on every model call of every turn whether
14
+ * or not a list is kept, and they ask for the list to be updated "as soon
15
+ * as you are done with a step. Do not batch", which the benchmark's
16
+ * per-call record showed as one model call per update, five of seventeen on
17
+ * a multi-part task. These texts keep what the product relies on (a list for
18
+ * work of several parts, the three states the card shows, one item in
19
+ * progress) and make each update ride with another tool call.
20
+ *
21
+ * The threshold is "several separate parts", not a step count: a first cut
22
+ * said "three or more distinct steps", and the model then kept a list on a
23
+ * one-change edit (read, edit, test) with every update in a round of its
24
+ * own. A soft "update with your next action" was not followed either, so
25
+ * the rule names the call's company outright.
26
+ *
27
+ * Plan mode's build progress names no tool: its directive says "your to-do
28
+ * list" (`shared/implement-plan-prompt.ts`) and leaves each harness's tool
29
+ * to say what it is, so the description opens by saying exactly that.
30
+ *
31
+ * The system line cannot be empty: the middleware appends it as a block of
32
+ * its own, and an empty option would still send a whitespace block.
33
+ */
34
+
35
+ import { todoListMiddleware } from "langchain";
36
+ import { ENGINE_TOOL } from "./engine-tools.js";
37
+
38
+ /** The to-do tool's description, as the model reads it. */
39
+ export const TODO_TOOL_DESCRIPTION =
40
+ "Your to-do list, shown to the user as your progress. Use it when a request has several separate parts, " +
41
+ "or when the user asks for one. Skip it for a single change, however many steps it takes, and for questions.\n" +
42
+ "- Send the whole list each time. Each item is pending, in_progress or completed; keep exactly one " +
43
+ "in_progress while you work.\n" +
44
+ `- A \`${ENGINE_TOOL.writeTodos}\` call always shares its response with another tool call: update the list ` +
45
+ "alongside your next action, never in a response of its own, and send the last update with your last tool call.\n" +
46
+ "- Call this tool at most once per response.";
47
+
48
+ /** The line the middleware adds to the system prompt. */
49
+ export const TODO_SYSTEM_PROMPT =
50
+ `Keep a to-do list with \`${ENGINE_TOOL.writeTodos}\` when a request has several separate parts; ` +
51
+ "the user sees it as your progress.";
52
+
53
+ /** The parent graph's to-do middleware, with Stigmer's texts. */
54
+ export function createTodoListMiddleware(): ReturnType<typeof todoListMiddleware> {
55
+ return todoListMiddleware({ toolDescription: TODO_TOOL_DESCRIPTION, systemPrompt: TODO_SYSTEM_PROMPT });
56
+ }
@@ -40,7 +40,6 @@ import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
40
40
  import type { DynamicStructuredTool } from "@langchain/core/tools";
41
41
  import type { Command } from "@langchain/langgraph";
42
42
  import { createDeepAgent } from "deepagents";
43
- import { todoListMiddleware } from "langchain";
44
43
  import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
45
44
 
46
45
  import type { Config } from "../../config.js";
@@ -77,6 +76,7 @@ import { resolveResumeInput, type GraphStateSnapshot } from "./hitl.js";
77
76
  import { buildEnhancedSystemPrompt, composeUserMessage, renderSkillsSection } from "./prompt-builder.js";
78
77
  import { buildShellEnv } from "./shell-env.js";
79
78
  import { modelHasNativeThinking, transformAndCompileSubagents } from "./subagent-transformer.js";
79
+ import { createTodoListMiddleware } from "./todo-list.js";
80
80
 
81
81
  /**
82
82
  * The runner config this harness reads per turn, as a named slice
@@ -472,13 +472,11 @@ export async function buildEngine(
472
472
  systemPrompt,
473
473
  tools: graphTools,
474
474
  // The to-do list rides ahead of Stigmer's stack: deepagents stopped
475
- // installing it by default (1.12), and the product reads it — the parent's
476
- // `write_todos` is what `status.todos` projects and what plan mode's
477
- // build progress is instructed through (`shared/implement-plan-prompt.ts`).
478
- // The framework's own middleware, with its default texts. Parent only: a
479
- // sub-agent's list never reaches the execution (`harness/transcript/builder.ts`).
475
+ // installing it by default (1.12), and the product reads it. The
476
+ // framework's own middleware with Stigmer's texts; parent only
477
+ // (`todo-list.ts` says why for both).
480
478
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
481
- middleware: [todoListMiddleware(), ...middleware] as any,
479
+ middleware: [createTodoListMiddleware(), ...middleware] as any,
482
480
  subagents: compiledSubagents ?? undefined,
483
481
  ...(responseFormat ? { responseFormat } : {}),
484
482
  ...(planModePermissions ? { permissions: planModePermissions } : {}),
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest";
2
2
  import { ToolMessage } from "@langchain/core/messages";
3
3
  import {
4
4
  createPathNormalizationMiddleware,
5
+ normalizeGlobPattern,
5
6
  normalizeWorkspacePathArg,
6
7
  } from "../path-normalization.js";
7
8
  import type { ToolCallRequest } from "../types.js";
@@ -110,6 +111,23 @@ describe("createPathNormalizationMiddleware", () => {
110
111
  }
111
112
  });
112
113
 
114
+ it("maps a glob pattern that opens with the real root to its virtual form", async () => {
115
+ const args = await argsSeenByHandler("glob", { pattern: `${ROOT}/duration/*_test.go` });
116
+ expect(args.pattern).toBe("/duration/*_test.go");
117
+ });
118
+
119
+ it("repairs the glob pattern and the base path of one call together", async () => {
120
+ const args = await argsSeenByHandler("glob", { path: `${ROOT}/src`, pattern: `${ROOT}/src/**/*.go` });
121
+ expect(args.path).toBe("/src");
122
+ expect(args.pattern).toBe("/src/**/*.go");
123
+ });
124
+
125
+ it("rewrites a real-root prefix in glob's pattern only, never in grep's", async () => {
126
+ // grep's pattern is literal text to find, which may well be a path.
127
+ const args = await argsSeenByHandler("grep", { pattern: `${ROOT}/config` });
128
+ expect(args.pattern).toBe(`${ROOT}/config`);
129
+ });
130
+
113
131
  it("passes canonical virtual paths through byte-untouched", async () => {
114
132
  const args = await argsSeenByHandler("read_file", { file_path: "/etc/hosts" });
115
133
  expect(args.file_path).toBe("/etc/hosts");
@@ -164,3 +182,17 @@ describe("createPathNormalizationMiddleware", () => {
164
182
  expect(original.toolCall.args.file_path).toBe("notes.md");
165
183
  });
166
184
  });
185
+
186
+ describe("normalizeGlobPattern", () => {
187
+ it("replaces a whole leading root, keeping every wildcard", () => {
188
+ expect(normalizeGlobPattern(`${ROOT}/**/*.md`, ROOT)).toBe("/**/*.md");
189
+ expect(normalizeGlobPattern(`${ROOT}/a/*.go`, `${ROOT}/`)).toBe("/a/*.go");
190
+ });
191
+
192
+ it("leaves a pattern that only resembles the root, or is the root itself", () => {
193
+ expect(normalizeGlobPattern("**/*.go", ROOT)).toBeUndefined();
194
+ expect(normalizeGlobPattern(`${ROOT}-other/*.go`, ROOT)).toBeUndefined();
195
+ expect(normalizeGlobPattern(`${ROOT}/`, ROOT)).toBeUndefined();
196
+ expect(normalizeGlobPattern(`/x${ROOT}/a`, ROOT)).toBeUndefined();
197
+ });
198
+ });
@@ -37,10 +37,10 @@ export function enrichErrorMessage(toolName: string, error: string): string {
37
37
  }
38
38
 
39
39
  if (errorLower.includes("text to replace not found")) {
40
- hints.push("Re-read the file with the read tool to see its current contents");
41
- hints.push("The file content may have changed — use the actual text from read output");
40
+ hints.push("Re-read the file to see its current contents");
41
+ hints.push("The file content may have changed — use the actual text from the read output");
42
42
  hints.push("Check for whitespace differences (tabs vs spaces, trailing newlines)");
43
- hints.push("If the exact text cannot be matched, use write to replace the entire file");
43
+ hints.push("If the exact text cannot be matched, write the whole file instead");
44
44
  }
45
45
 
46
46
  if (toolLower.includes("edit") || toolLower.includes("write")) {
@@ -19,7 +19,13 @@
19
19
  * filesystem paths that a model may still echo back;
20
20
  * - interior `..` segments that stay inside the root ("src/../notes.md" →
21
21
  * "/notes.md") — the virtual resolver rejects `..` outright, so a safe
22
- * interior collapse is repaired here rather than burning a tool round.
22
+ * interior collapse is repaired here rather than burning a tool round;
23
+ * - a `glob` PATTERN that opens with the real root ("{root}/src/*.go") to
24
+ * its virtual form ("/src/*.go"). deepagents strips a pattern's leading
25
+ * "/" and matches the rest under the search path, so a real-root pattern
26
+ * silently matches nothing and the model goes looking with `ls`; the
27
+ * benchmark's per-call record showed it doing so for three rounds. Only
28
+ * that exact prefix is rewritten, never the pattern's wildcards.
23
29
  *
24
30
  * Nothing becomes newly reachable: escaping relatives ("../x") and
25
31
  * `~`-carrying paths are left raw so the upstream refusal keeps speaking,
@@ -38,9 +44,9 @@
38
44
  *
39
45
  * Tool matching is by bare built-in name, the house doctrine (an MCP server
40
46
  * is not expected to shadow a built-in name — see shared/tool-kind.ts); the
41
- * rewrite touches only the tool's path-bearing argument, never glob/grep
42
- * patterns. Install FIRST in the stack so every downstream middleware
43
- * observes canonical paths.
47
+ * rewrite touches the tool's path-bearing argument and, for `glob`, a
48
+ * real-root prefix of the pattern; nothing else in a pattern. Install FIRST
49
+ * in the stack so every downstream middleware observes canonical paths.
44
50
  */
45
51
 
46
52
  import { isAbsolute, relative, posix } from "node:path";
@@ -60,6 +66,10 @@ const PATH_ARG_BY_TOOL: ReadonlyMap<string, string> = new Map([
60
66
  ["edit_file", "file_path"],
61
67
  ]);
62
68
 
69
+ /** The one built-in whose pattern can carry a path prefix, and its argument. */
70
+ const GLOB_TOOL = "glob";
71
+ const GLOB_PATTERN_ARG = "pattern";
72
+
63
73
  export interface PathNormalizationConfig {
64
74
  /**
65
75
  * The REAL workspace root the graph's backend is rooted at — used only for
@@ -116,6 +126,19 @@ export function normalizeWorkspacePathArg(
116
126
  return virtual === raw ? undefined : virtual;
117
127
  }
118
128
 
129
+ /**
130
+ * Rewrite a `glob` pattern that opens with the real workspace root to its
131
+ * virtual form, or return undefined to leave it untouched. Only a whole
132
+ * leading `{root}/` is replaced; a pattern that merely contains the root's
133
+ * text elsewhere, or is the root itself, is left as the model wrote it.
134
+ * Exported for direct unit testing.
135
+ */
136
+ export function normalizeGlobPattern(pattern: string, rootDir: string): string | undefined {
137
+ const prefix = rootDir.endsWith("/") ? rootDir : `${rootDir}/`;
138
+ if (!pattern.startsWith(prefix) || pattern.length === prefix.length) return undefined;
139
+ return `/${pattern.slice(prefix.length)}`;
140
+ }
141
+
119
142
  /**
120
143
  * Create the dialect-repair middleware (see module header). Stateless per
121
144
  * request; safe on every graph.
@@ -132,18 +155,23 @@ export function createPathNormalizationMiddleware(
132
155
  const argKey = PATH_ARG_BY_TOOL.get(request.toolCall.name);
133
156
  if (!argKey) return handler(request);
134
157
 
135
- const raw = request.toolCall.args[argKey];
136
- if (typeof raw !== "string") return handler(request);
137
-
138
- const normalized = normalizeWorkspacePathArg(raw, rootDir);
139
- if (normalized === undefined) return handler(request);
158
+ const args = request.toolCall.args;
159
+ const rewritten: Record<string, string> = {};
160
+ const raw = args[argKey];
161
+ if (typeof raw === "string") {
162
+ const normalized = normalizeWorkspacePathArg(raw, rootDir);
163
+ if (normalized !== undefined) rewritten[argKey] = normalized;
164
+ }
165
+ const pattern = args[GLOB_PATTERN_ARG];
166
+ if (request.toolCall.name === GLOB_TOOL && typeof pattern === "string") {
167
+ const normalized = normalizeGlobPattern(pattern, rootDir);
168
+ if (normalized !== undefined) rewritten[GLOB_PATTERN_ARG] = normalized;
169
+ }
170
+ if (Object.keys(rewritten).length === 0) return handler(request);
140
171
 
141
172
  return handler({
142
173
  ...request,
143
- toolCall: {
144
- ...request.toolCall,
145
- args: { ...request.toolCall.args, [argKey]: normalized },
146
- },
174
+ toolCall: { ...request.toolCall, args: { ...args, ...rewritten } },
147
175
  });
148
176
  },
149
177
  };
@@ -13,7 +13,12 @@
13
13
 
14
14
  import { RunnableLambda, type RunnableConfig } from "@langchain/core/runnables";
15
15
 
16
- const DEFAULT_MAX_CONCURRENT = 3;
16
+ /**
17
+ * How many sub-agents run at once by default. The native prompt states this
18
+ * number to the model (`execute-deep-agent/prompt-builder.ts`), so it has one
19
+ * home: this constant.
20
+ */
21
+ export const DEFAULT_MAX_CONCURRENT = 3;
17
22
 
18
23
  export interface SubAgentGateOptions {
19
24
  readonly maxConcurrent?: number;
@@ -63,8 +63,12 @@ export function provisionLocalPath(options: LocalPathProvisionOptions): Provisio
63
63
  rootDir: path,
64
64
  sourceType: "local_path",
65
65
  consumedKeys: [],
66
+ // No host path in the model's text: the native file tools see this
67
+ // directory as `/` and its commands run in it, so the path would only be
68
+ // echoed back where it cannot resolve (a glob pattern, a mistyped
69
+ // prefix). The directory is `rootDir`'s to carry.
66
70
  workspaceDescription:
67
- `Your workspace is the user's project directory: ${path}\n` +
71
+ "Your workspace is the user's project directory.\n" +
68
72
  "IMPORTANT: You are operating directly on the user's files. " +
69
73
  "Changes are immediate and persistent.\n" +
70
74
  "Use git to track and verify your changes before finalizing.",
@@ -7,9 +7,10 @@ describe("createThinkTool", () => {
7
7
  expect(thinkTool.name).toBe("think");
8
8
  });
9
9
 
10
- it("has a description explaining its purpose", () => {
10
+ it("keeps its description short: it rides on every model call, and every use is a round", () => {
11
11
  const thinkTool = createThinkTool();
12
- expect(thinkTool.description).toContain("think through a problem");
12
+ expect(thinkTool.description.length).toBeGreaterThan(0);
13
+ expect(thinkTool.description.length).toBeLessThan(400);
13
14
  });
14
15
 
15
16
  it("returns 'ok' for any thought input", async () => {
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * Follows the Anthropic "think tool" pattern:
10
10
  * https://www.anthropic.com/engineering/claude-think-tool
11
+ *
12
+ * The description says when a call is worth it, because every call is a
13
+ * model round of its own: the whole prompt re-sent for a thought. The
14
+ * earlier text suggested one "after reading files or tool output", and the
15
+ * benchmark's per-call record showed think rounds right after reads and
16
+ * right before the reply. Whether the tool is bound at all is the thinking
17
+ * mode's question, not this module's.
11
18
  */
12
19
 
13
20
  import { tool } from "@langchain/core/tools";
@@ -19,17 +26,9 @@ export function createThinkTool() {
19
26
  {
20
27
  name: "think",
21
28
  description:
22
- "Use this tool to think through a problem step-by-step. " +
23
- "The think tool does not read files, execute commands, or make any changes — " +
24
- "it simply records your reasoning. Call it when you need to pause and work " +
25
- "something out before acting.\n\n" +
26
- "Good times to use think:\n" +
27
- "- After reading files or tool output, to analyse what you learned\n" +
28
- "- Before a complex or multi-step operation, to plan your approach\n" +
29
- "- When you need to choose between several possible strategies\n" +
30
- "- When debugging — to reason about what might have gone wrong\n\n" +
31
- "You do NOT need to use think for every step — only when genuine " +
32
- "reasoning will improve the quality of your next action.",
29
+ "Record your reasoning when a decision is genuinely hard: choosing between approaches, " +
30
+ "or working out why something failed. It reads nothing and changes nothing. Do not use it " +
31
+ "to restate a tool result or to announce your next step; take the step.",
33
32
  schema: z.object({
34
33
  thought: z.string().describe("Your reasoning, analysis, or plan."),
35
34
  }),