@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.
Files changed (78) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/hermetic-activity.d.ts +9 -4
  3. package/dist/__test-utils__/hermetic-activity.js +17 -5
  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 +11 -7
  18. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  19. package/dist/harness/run-turn.js +13 -13
  20. package/dist/harness/run-turn.js.map +1 -1
  21. package/dist/harness/terminal-table.d.ts +20 -5
  22. package/dist/harness/terminal-table.js +22 -8
  23. package/dist/harness/terminal-table.js.map +1 -1
  24. package/dist/middleware/error-hints.js +3 -3
  25. package/dist/middleware/error-hints.js.map +1 -1
  26. package/dist/middleware/path-normalization.d.ts +18 -4
  27. package/dist/middleware/path-normalization.js +42 -13
  28. package/dist/middleware/path-normalization.js.map +1 -1
  29. package/dist/shared/model-client.d.ts +10 -2
  30. package/dist/shared/model-client.js +1 -1
  31. package/dist/shared/model-client.js.map +1 -1
  32. package/dist/shared/subagent-gate.d.ts +6 -0
  33. package/dist/shared/subagent-gate.js +6 -1
  34. package/dist/shared/subagent-gate.js.map +1 -1
  35. package/dist/shared/worker-shutdown.d.ts +10 -5
  36. package/dist/shared/worker-shutdown.js +3 -3
  37. package/dist/shared/worker-shutdown.js.map +1 -1
  38. package/dist/shared/workspace/sources/local-path.js +5 -1
  39. package/dist/shared/workspace/sources/local-path.js.map +1 -1
  40. package/dist/tools/think-tool.d.ts +7 -0
  41. package/dist/tools/think-tool.js +10 -11
  42. package/dist/tools/think-tool.js.map +1 -1
  43. package/package.json +5 -5
  44. package/src/__test-utils__/harness-contract/runtime-contract.ts +27 -16
  45. package/src/__test-utils__/hermetic-activity.ts +16 -5
  46. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +0 -6
  47. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +28 -21
  48. package/src/activities/execute-deep-agent/__tests__/engine-tools.test.ts +96 -0
  49. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +22 -35
  50. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +23 -35
  51. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +11 -23
  52. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +22 -35
  53. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +22 -34
  54. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +0 -6
  55. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +0 -6
  56. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-parallel-gates.test.ts +132 -0
  57. package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +27 -21
  58. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +20 -2
  59. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +9 -0
  60. package/src/activities/execute-deep-agent/engine-tools.ts +33 -0
  61. package/src/activities/execute-deep-agent/prompt-builder.ts +73 -69
  62. package/src/activities/execute-deep-agent/subagent-transformer.ts +28 -13
  63. package/src/activities/execute-deep-agent/todo-list.ts +56 -0
  64. package/src/activities/execute-deep-agent/turn-setup.ts +11 -7
  65. package/src/harness/run-turn.ts +13 -13
  66. package/src/harness/terminal-table.ts +23 -8
  67. package/src/middleware/__tests__/path-normalization.test.ts +32 -0
  68. package/src/middleware/error-hints.ts +3 -3
  69. package/src/middleware/path-normalization.ts +41 -13
  70. package/src/shared/__tests__/model-client-sampling.test.ts +56 -0
  71. package/src/shared/__tests__/model-client.test.ts +15 -0
  72. package/src/shared/__tests__/worker-shutdown.test.ts +2 -2
  73. package/src/shared/model-client.ts +11 -3
  74. package/src/shared/subagent-gate.ts +6 -1
  75. package/src/shared/worker-shutdown.ts +11 -6
  76. package/src/shared/workspace/sources/local-path.ts +5 -1
  77. package/src/tools/__tests__/think-tool.test.ts +3 -2
  78. 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 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
  };
@@ -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 pause", () => {
58
- expect(classifyTurnInterruption({ cancellationReason: "CANCELLED", shutdownSignalAborted: false })).toBe("pause");
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
- /** Defaults to 0. */
57
- readonly temperature?: number;
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
- 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;
@@ -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 pause).
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 pause, and must surface as the
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
- * - `pause`: the workflow asked (`CANCELLED`, the Pause RPC's path);
92
- * PAUSED, thrown.
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" | "pause" | "infrastructure" | "none";
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" ? "pause" : "infrastructure";
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
- `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
  }),