@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.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/hermetic-activity.d.ts +4 -2
- package/dist/__test-utils__/hermetic-activity.js +5 -3
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- package/dist/activities/execute-deep-agent/engine-tools.d.ts +32 -0
- package/dist/activities/execute-deep-agent/engine-tools.js +32 -0
- package/dist/activities/execute-deep-agent/engine-tools.js.map +1 -0
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -4
- package/dist/activities/execute-deep-agent/prompt-builder.js +66 -64
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +5 -2
- package/dist/activities/execute-deep-agent/subagent-transformer.js +27 -14
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/todo-list.d.ts +40 -0
- package/dist/activities/execute-deep-agent/todo-list.js +51 -0
- package/dist/activities/execute-deep-agent/todo-list.js.map +1 -0
- package/dist/activities/execute-deep-agent/turn-setup.js +5 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/middleware/error-hints.js +3 -3
- package/dist/middleware/error-hints.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +18 -4
- package/dist/middleware/path-normalization.js +42 -13
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/shared/subagent-gate.d.ts +6 -0
- package/dist/shared/subagent-gate.js +6 -1
- package/dist/shared/subagent-gate.js.map +1 -1
- package/dist/shared/workspace/sources/local-path.js +5 -1
- package/dist/shared/workspace/sources/local-path.js.map +1 -1
- package/dist/tools/think-tool.d.ts +7 -0
- package/dist/tools/think-tool.js +10 -11
- package/dist/tools/think-tool.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/hermetic-activity.ts +5 -3
- package/src/activities/execute-deep-agent/__tests__/engine-tools.test.ts +96 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +22 -35
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +23 -35
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +11 -23
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +22 -35
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +22 -34
- package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-parallel-gates.test.ts +132 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +20 -2
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +9 -0
- package/src/activities/execute-deep-agent/engine-tools.ts +33 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +73 -69
- package/src/activities/execute-deep-agent/subagent-transformer.ts +28 -13
- package/src/activities/execute-deep-agent/todo-list.ts +56 -0
- package/src/activities/execute-deep-agent/turn-setup.ts +5 -7
- package/src/middleware/__tests__/path-normalization.test.ts +32 -0
- package/src/middleware/error-hints.ts +3 -3
- package/src/middleware/path-normalization.ts +41 -13
- package/src/shared/subagent-gate.ts +6 -1
- package/src/shared/workspace/sources/local-path.ts +5 -1
- package/src/tools/__tests__/think-tool.test.ts +3 -2
- 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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
219
|
-
*
|
|
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:
|
|
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
|
|
476
|
-
//
|
|
477
|
-
//
|
|
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: [
|
|
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
|
|
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,
|
|
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
|
|
42
|
-
*
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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).
|
|
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 () => {
|
package/src/tools/think-tool.ts
CHANGED
|
@@ -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
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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
|
}),
|