@stigmer/runner 3.36.0 → 3.37.1
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 +9 -4
- package/dist/__test-utils__/hermetic-activity.js +17 -5
- 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 +11 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/harness/run-turn.js +13 -13
- package/dist/harness/run-turn.js.map +1 -1
- package/dist/harness/terminal-table.d.ts +20 -5
- package/dist/harness/terminal-table.js +22 -8
- package/dist/harness/terminal-table.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/model-client.d.ts +10 -2
- package/dist/shared/model-client.js +1 -1
- package/dist/shared/model-client.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/worker-shutdown.d.ts +10 -5
- package/dist/shared/worker-shutdown.js +3 -3
- package/dist/shared/worker-shutdown.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__/harness-contract/runtime-contract.ts +27 -16
- package/src/__test-utils__/hermetic-activity.ts +16 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +0 -6
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +28 -21
- 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/goldens/pause.loop.after-tool.status.json +0 -6
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +0 -6
- package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-parallel-gates.test.ts +132 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +27 -21
- 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 +11 -7
- package/src/harness/run-turn.ts +13 -13
- package/src/harness/terminal-table.ts +23 -8
- 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/__tests__/model-client-sampling.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +15 -0
- package/src/shared/__tests__/worker-shutdown.test.ts +2 -2
- package/src/shared/model-client.ts +11 -3
- package/src/shared/subagent-gate.ts +6 -1
- package/src/shared/worker-shutdown.ts +11 -6
- 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
|
@@ -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
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native execution turn's request against the REAL @langchain/anthropic
|
|
3
|
+
* (stigmer/stigmer#1341): the unit suite in model-client.test.ts mocks the
|
|
4
|
+
* wrapper, so it cannot see the wrapper's own parameter validation. That
|
|
5
|
+
* validation is what failed every native turn on claude-opus-4.7,
|
|
6
|
+
* claude-opus-4.8 and claude-fable-5 while the runner pinned temperature 0.
|
|
7
|
+
* These tests build the client the way the execution turn does and read the
|
|
8
|
+
* request parameters offline; no request leaves the process.
|
|
9
|
+
*/
|
|
10
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { buildChatModel } from "../model-client.js";
|
|
14
|
+
import { _resetRegistryCache } from "../model-registry.js";
|
|
15
|
+
|
|
16
|
+
// Ids the wrapper treats as adaptive-only and refuses a sampling override
|
|
17
|
+
// for, and ids it lets one through for (the provider itself refuses it on
|
|
18
|
+
// claude-sonnet-5); the registry is unreachable in these tests, so each id
|
|
19
|
+
// reaches the wrapper unchanged.
|
|
20
|
+
const ADAPTIVE_ONLY_IDS = ["claude-opus-4-7", "claude-opus-4-8", "claude-fable-5"];
|
|
21
|
+
const SAMPLING_TOLERANT_IDS = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-sonnet-5", "claude-haiku-4-5-20251001"];
|
|
22
|
+
|
|
23
|
+
async function executionTurnParams(modelName: string, temperature: number | null | undefined) {
|
|
24
|
+
const { model } = await buildChatModel({
|
|
25
|
+
modelName,
|
|
26
|
+
proxyEndpoint: "http://127.0.0.1:9",
|
|
27
|
+
stigmerToken: "sampling-test",
|
|
28
|
+
temperature,
|
|
29
|
+
});
|
|
30
|
+
return (model as ChatAnthropic).invocationParams({});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("the native execution turn's sampling parameters (real @langchain/anthropic)", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
_resetRegistryCache();
|
|
36
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry unreachable in this test"));
|
|
37
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
_resetRegistryCache();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each(ADAPTIVE_ONLY_IDS)("%s: the pinned temperature 0 is refused before any request", async (id) => {
|
|
46
|
+
await expect(executionTurnParams(id, undefined)).rejects.toThrow(/temperature is not supported/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each([...ADAPTIVE_ONLY_IDS, ...SAMPLING_TOLERANT_IDS])(
|
|
50
|
+
"%s: with no temperature the request builds and carries no sampling override",
|
|
51
|
+
async (id) => {
|
|
52
|
+
const params = await executionTurnParams(id, null);
|
|
53
|
+
expect(params).not.toHaveProperty("temperature");
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -156,6 +156,21 @@ describe("buildChatModel", () => {
|
|
|
156
156
|
expect(lastAnthropicArgs()).toMatchObject({ maxTokens: 4096 });
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
it("pins temperature 0 by default, and sends none when the caller passes null", async () => {
|
|
160
|
+
mockRegistryResponse([
|
|
161
|
+
{ id: "claude-opus-4.8", apiModelId: "claude-opus-4-8", provider: "anthropic" },
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
await buildChatModel({ modelName: "claude-opus-4.8" });
|
|
165
|
+
expect(lastAnthropicArgs()).toMatchObject({ temperature: 0 });
|
|
166
|
+
|
|
167
|
+
await buildChatModel({ modelName: "claude-opus-4.8", temperature: 0.3 });
|
|
168
|
+
expect(lastAnthropicArgs()).toMatchObject({ temperature: 0.3 });
|
|
169
|
+
|
|
170
|
+
await buildChatModel({ modelName: "claude-opus-4.8", temperature: null });
|
|
171
|
+
expect(lastAnthropicArgs()).not.toHaveProperty("temperature");
|
|
172
|
+
});
|
|
173
|
+
|
|
159
174
|
it("passes the model id through unchanged when the registry is unavailable", async () => {
|
|
160
175
|
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("network down"));
|
|
161
176
|
|
|
@@ -54,8 +54,8 @@ describe("classifyTurnInterruption", () => {
|
|
|
54
54
|
expect(classifyTurnInterruption({ cancellationReason: undefined, shutdownSignalAborted: true })).toBe("none");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("a workflow-requested cancellation without a shutdown signal is the orchestrator's
|
|
58
|
-
expect(classifyTurnInterruption({ cancellationReason: "CANCELLED", shutdownSignalAborted: false })).toBe("
|
|
57
|
+
it("a workflow-requested cancellation without a shutdown signal is the orchestrator's stop (a Pause or a Cancel, indistinguishable here)", () => {
|
|
58
|
+
expect(classifyTurnInterruption({ cancellationReason: "CANCELLED", shutdownSignalAborted: false })).toBe("orchestrator-stop");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it("a delivered cancellation WITH the shutdown signal aborted is a worker shutdown, not a pause (the #776 misclassification)", () => {
|
|
@@ -53,8 +53,16 @@ export interface BuildChatModelOptions {
|
|
|
53
53
|
mcpServerId?: string;
|
|
54
54
|
workflowExecutionId?: string;
|
|
55
55
|
};
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Defaults to 0. `null` sends no sampling override at all, so the
|
|
58
|
+
* provider's default applies: the native execution turn passes it,
|
|
59
|
+
* because current Anthropic models refuse a temperature other than their
|
|
60
|
+
* default (claude-opus-4.7 and later, claude-sonnet-5, claude-fable-5;
|
|
61
|
+
* @langchain/anthropic throws before the request for some of them) and
|
|
62
|
+
* the Cursor harness sends none either (stigmer/stigmer#1341). Utility
|
|
63
|
+
* calls keep their deterministic 0.
|
|
64
|
+
*/
|
|
65
|
+
readonly temperature?: number | null;
|
|
58
66
|
/**
|
|
59
67
|
* Pass-through only; intentionally no default. Anthropic requires a value,
|
|
60
68
|
* but callers differ (setup omits it, call-llm uses 4096), so the default
|
|
@@ -258,7 +266,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
|
|
|
258
266
|
? { maxRetries: opts.maxRetries ?? 0 }
|
|
259
267
|
: {};
|
|
260
268
|
const common = {
|
|
261
|
-
temperature: opts.temperature ?? 0,
|
|
269
|
+
...(opts.temperature === null ? {} : { temperature: opts.temperature ?? 0 }),
|
|
262
270
|
apiKey,
|
|
263
271
|
...(maxTokens ? { maxTokens } : {}),
|
|
264
272
|
...maxRetries,
|
|
@@ -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;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-task-queue worker-shutdown signals — the classification channel that
|
|
3
3
|
* lets an in-flight activity distinguish "my worker is shutting down" from
|
|
4
|
-
* "the orchestrator cancelled me" (a user
|
|
4
|
+
* "the orchestrator cancelled me" (a user's Pause or Cancel).
|
|
5
5
|
*
|
|
6
6
|
* The signal carries NO lifecycle authority: aborting it never stops a
|
|
7
7
|
* worker or an activity (Temporal's own drain does that). It exists purely
|
|
8
8
|
* so the activity's cancellation handling can classify the interruption
|
|
9
|
-
* honestly — a shutdown is not a
|
|
9
|
+
* honestly — a shutdown is not a user's stop, and must surface as the
|
|
10
10
|
* worker-shutdown failure shape the control planes recognize (issue #776).
|
|
11
11
|
*
|
|
12
12
|
* Ownership contract:
|
|
@@ -88,13 +88,18 @@ export interface TurnInterruptionEvidence {
|
|
|
88
88
|
* How Temporal's cancellation reads to the turn:
|
|
89
89
|
* - `worker-shutdown`: the runner is draining (the queue's signal, or the
|
|
90
90
|
* SDK's own `WORKER_SHUTDOWN`); FAILED with the shutdown copy, thrown.
|
|
91
|
-
* - `
|
|
92
|
-
*
|
|
91
|
+
* - `orchestrator-stop`: the workflow asked (`CANCELLED`). That is a Pause
|
|
92
|
+
* or a Cancel, and the turn cannot tell which: both reach the activity
|
|
93
|
+
* as the same workflow-requested cancellation, and both RPCs signal the
|
|
94
|
+
* engine before they persist the phase, so no stored fact tells them
|
|
95
|
+
* apart in time. The turn settles its transcript with no phase and no
|
|
96
|
+
* row, thrown; the workflow, which knows which stop it runs, writes the
|
|
97
|
+
* phase and the stop row (stigmer#980).
|
|
93
98
|
* - `infrastructure`: the platform cancelled for its own reason (a heartbeat
|
|
94
99
|
* timeout, a closed run); FAILED with the unresponsive copy, thrown.
|
|
95
100
|
* - `none`: nothing was delivered.
|
|
96
101
|
*/
|
|
97
|
-
export type TurnInterruption = "worker-shutdown" | "
|
|
102
|
+
export type TurnInterruption = "worker-shutdown" | "orchestrator-stop" | "infrastructure" | "none";
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
105
|
* Classify how (whether) Temporal interrupted the turn, from the two live
|
|
@@ -125,5 +130,5 @@ export function classifyTurnInterruption(evidence: TurnInterruptionEvidence): Tu
|
|
|
125
130
|
if (shutdownSignalAborted || cancellationReason === "WORKER_SHUTDOWN") {
|
|
126
131
|
return "worker-shutdown";
|
|
127
132
|
}
|
|
128
|
-
return cancellationReason === "CANCELLED" ? "
|
|
133
|
+
return cancellationReason === "CANCELLED" ? "orchestrator-stop" : "infrastructure";
|
|
129
134
|
}
|
|
@@ -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
|
}),
|