@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
@@ -16,10 +16,11 @@
16
16
  * module; this builder only frames them.
17
17
  * - This harness's: the framing — `## Heading` markdown sections, the house
18
18
  * style for a system prompt — and every text that quotes this engine's
19
- * tools: the `## Skills` activation protocol (`read`, `execute(...)`), the
20
- * response rules, the sub-agent delegation rules, the plan-mode
21
- * read-boundary sentence, and the workspace section (the deepagents
22
- * backend's path resolution is this engine's).
19
+ * tools: the `## Skills` activation protocol, the response rules, the
20
+ * working rules, the plan-mode read-boundary sentence, and the workspace
21
+ * section (the deepagents backend's path resolution is this engine's).
22
+ * Every tool name those texts quote is interpolated from `engine-tools.ts`,
23
+ * the names the engine binds.
23
24
  *
24
25
  * The rendered bytes are load-bearing for the hermetic tests: `ScriptedModel`
25
26
  * tells its scripted roles apart by the system prompt's text, so this
@@ -49,13 +50,15 @@ import {
49
50
  import type { ResolvedAttachment } from "../../shared/attachment-resolver.js";
50
51
  import type { SkillMetadata } from "../../shared/skill-resolver.js";
51
52
  import { STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
53
+ import { DEFAULT_MAX_CONCURRENT } from "../../shared/subagent-gate.js";
54
+ import { ENGINE_TOOL } from "./engine-tools.js";
52
55
 
53
56
  const RESPONSE_RULES = `
54
57
 
55
58
  ## Response rules
56
59
 
57
- - After using the read tool, NEVER reprint, echo, list, or summarize \
58
- file contents in your response. Tool results are already in your \
60
+ - After reading a file with \`${ENGINE_TOOL.readFile}\`, NEVER reprint, echo, \
61
+ list, or summarize file contents in your response. Tool results are already in your \
59
62
  context. Proceed directly to analysis or the task.
60
63
  - Do not begin responses with phrases like \
61
64
  "Below is the complete content", \
@@ -68,53 +71,53 @@ and shell commands (e.g., \`src/main.py\`, \`handleRequest()\`, \
68
71
  code blocks that the user can see in tool results.
69
72
  - Structure complex answers with headings and bullet points.
70
73
  - If you encounter something unexpected that changes the scope, \
71
- explain the issue and propose options before proceeding.
72
- `;
73
-
74
- const SUB_AGENT_RULES = `
75
-
76
- ## Sub-agent delegation rules
77
-
78
- ### Concurrency limit
79
-
80
- Do NOT spawn more than 3 sub-agents concurrently. If you need to \
81
- explore more than 3 areas, batch them: launch the first 3, wait for \
82
- results, then launch more if needed. The runtime enforces this limit — \
83
- excess sub-agents will be rejected.
84
-
85
- ### When NOT to delegate
86
-
87
- - **Reading files.** Use the \`read\` tool yourself. You need raw file \
88
- contents in your own context to reason about them accurately.
89
- - **Single-step lookups.** Use \`grep\`, \`glob\`, \`search\`, or \`read\` \
90
- directly for simple searches across 1-2 files. Only delegate when \
91
- the task requires multi-step exploration.
92
- - **Data you will process yourself.** If you need the output in your \
93
- own context (e.g., to answer a question, write code, compare files), \
94
- do the work directly — do not delegate it.
95
- - **Small tasks (fewer than 3 steps).** The overhead of spawning a \
96
- sub-agent outweighs the benefit for trivial operations.
97
-
98
- ### When TO delegate
99
-
100
- - Multi-step, independent tasks that produce a deliverable (analysis, \
101
- synthesis, generated content) you will incorporate into your response.
102
- - Parallel exploration of genuinely different areas of a codebase or \
103
- knowledge base when context isolation helps.
104
- - Tasks that benefit from a separate context window (e.g., long \
105
- document summarization that would crowd your own context).
106
-
107
- ### Delegation best practices
108
-
109
- - When delegating, specify the **deliverable** you need — not \
110
- "read these files and give me the contents."
111
- - You MUST reference and synthesize sub-agent results in your \
112
- response. If you spawn a sub-agent, its output must visibly \
113
- influence your answer.
114
- - Each sub-agent consumes tokens and time. Prefer doing work \
115
- directly over delegating. Only delegate when context isolation \
116
- or parallelism genuinely helps the user.
117
- `;
74
+ explain the issue and propose options before proceeding.`;
75
+
76
+ // Every section of this prompt opens with its own blank line ("\n\n## ...")
77
+ // and ends on its last line, so sections are one blank line apart (#1127).
78
+
79
+ /**
80
+ * How to work with the tools, and when to delegate: the rules that decide
81
+ * how many model calls a turn takes. Each round re-sends the whole prompt,
82
+ * so a round saved is the turn's largest saving; the benchmark's per-call
83
+ * record (test/conformance src/benchmark/tool-call-facts.ts) showed where
84
+ * they went.
85
+ *
86
+ * - The path model comes first because the file tools speak the virtual
87
+ * root (`/` is the workspace; `middleware/path-normalization.ts`), and a
88
+ * model that is told nothing uses whatever path it saw last. `execute`
89
+ * runs with the workspace as its working directory (deepagents
90
+ * `LocalShellBackend`), so a `cd` into it is a wasted prefix; plan mode
91
+ * binds no shell, so its prompt says nothing about commands.
92
+ * - Edits to different files may go in one response; several edits to ONE
93
+ * file are not asked for in one response, because each is a
94
+ * read-modify-write of the same bytes. Changes that sit close together go
95
+ * in one call instead.
96
+ * - The sub-agent rules keep the one hard fact the model cannot see, the
97
+ * concurrency cap `shared/subagent-gate.ts` enforces by refusal, and leave
98
+ * the delegation mechanics to the `task` tool's own description.
99
+ */
100
+ function workingRules(interactionMode: InteractionMode | undefined): string {
101
+ const commands =
102
+ interactionMode === InteractionMode.PLAN
103
+ ? ""
104
+ : ` Commands run in the workspace, so \`${ENGINE_TOOL.execute}\` needs no \`cd\`.`;
105
+ return `
106
+
107
+ ## Working with tools
108
+
109
+ - Your file tools see the workspace as \`/\`: a file at its top level is \`/README.md\`.${commands}
110
+ - Make independent tool calls together in one response: read every file you need at once, and edit different files at once. Wait for a result only when the next call depends on it.
111
+ - When one file needs changes that sit close together, make them in one \`${ENGINE_TOOL.editFile}\` call.
112
+ - A file you have read stays in your context. Read it again only if something other than your own edit changed it.
113
+ - After your edits, run the checks once. Run them again only after a fix.
114
+
115
+ ## Sub-agents
116
+
117
+ - Do the work yourself unless it is a multi-step, independent task whose result you need only as a summary. Never delegate reading a file you must reason about.
118
+ - At most ${DEFAULT_MAX_CONCURRENT} sub-agents run at once; the runtime rejects more.
119
+ - Tell a sub-agent exactly what to return, and use what it returns.`;
120
+ }
118
121
 
119
122
  export interface PromptBuilderInput {
120
123
  /** The blueprint's instructions, raw; empty reads the built-in assistant's (shared/builtin-assistant-prompt.ts). */
@@ -272,7 +275,7 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
272
275
  }
273
276
 
274
277
  prompt += RESPONSE_RULES;
275
- prompt += SUB_AGENT_RULES;
278
+ prompt += workingRules(input.interactionMode);
276
279
 
277
280
  // Last sections on purpose: these per-execution directives redefine the
278
281
  // turn's deliverable, so they must be the freshest instruction the model
@@ -347,7 +350,7 @@ export function renderSkillsSection(skills: readonly SkillMetadata[]): string {
347
350
  "knowledge or capabilities.",
348
351
  "",
349
352
  "**Activation protocol**: To use a skill, read its SKILL.md file " +
350
- "using the `read` tool. The SKILL.md contains detailed instructions, " +
353
+ `with \`${ENGINE_TOOL.readFile}\`. The SKILL.md contains detailed instructions, ` +
351
354
  "available tools, and usage examples.",
352
355
  "",
353
356
  "**Usage pattern**:",
@@ -356,8 +359,8 @@ export function renderSkillsSection(skills: readonly SkillMetadata[]): string {
356
359
  "2. Read `{location}/SKILL.md` for full instructions",
357
360
  "3. Follow the skill's documented operations:",
358
361
  "",
359
- "`read {location}/references/schema.md`",
360
- '`execute("python3 {location}/scripts/run.py")`',
362
+ `\`${ENGINE_TOOL.readFile}\` on \`{location}/references/schema.md\``,
363
+ `\`${ENGINE_TOOL.execute}\` with \`python3 {location}/scripts/run.py\``,
361
364
  "",
362
365
  ];
363
366
 
@@ -366,7 +369,7 @@ export function renderSkillsSection(skills: readonly SkillMetadata[]): string {
366
369
  lines.push(`### ${skill.name}`);
367
370
  lines.push(`**Description**: ${skill.description || "(no description)"}`);
368
371
  lines.push(`**Location**: \`${skillDir}/\``);
369
- lines.push(`**Activate**: \`read ${skillDir}/SKILL.md\``);
372
+ lines.push(`**Activate**: \`${ENGINE_TOOL.readFile}\` on \`${skillDir}/SKILL.md\``);
370
373
  lines.push("");
371
374
  }
372
375
 
@@ -410,16 +413,16 @@ function buildMultiWorkspaceSection(
410
413
  let section =
411
414
  `\n\n## Workspace\n\n` +
412
415
  `This session has ${results.length} workspace entries.\n\n` +
413
- `**Path resolution**: All tools resolve paths relative to the ` +
414
- `workspace root. Use entry-relative paths ` +
415
- `(e.g., \`${firstLabel}/src/main.py\`). ` +
416
- `Do not use absolute filesystem paths.\n`;
416
+ `**Path resolution**: each entry is a directory at the workspace ` +
417
+ `root; name a file by its entry (e.g., \`/${firstLabel}/src/main.py\`).`;
417
418
 
419
+ // Entries are joined by a blank line, so an entry's file tree never runs
420
+ // into the next entry's heading (#1127).
418
421
  for (let idx = 0; idx < results.length; idx++) {
419
422
  const result = results[idx];
420
423
  const label = result.entryName || `entry-${idx + 1}`;
421
424
  const relPath = workspaceRelativePath(result.rootDir, containerRoot);
422
- section += `\n### ${label} (\`${relPath}\`)\n\n`;
425
+ section += `\n\n### ${label} (\`${relPath}\`)\n\n`;
423
426
  section += formatEntryDescription(result);
424
427
  if (result.fileTree) {
425
428
  section += "\n\n" + result.fileTree;
@@ -445,8 +448,7 @@ function formatEntryDescription(result: ProvisionResult): string {
445
448
 
446
449
  if (result.sourceType === "local_path") {
447
450
  return (
448
- `Workspace entry **${name}** is the user's project directory ` +
449
- `at \`${result.rootDir}\`.\n` +
451
+ `Workspace entry **${name}** is the user's project directory.\n` +
450
452
  "You are operating directly on the user's files — changes are " +
451
453
  "immediate and persistent. Use git to track and verify your changes."
452
454
  );
@@ -483,7 +485,7 @@ function buildReferencedFilesSection(
483
485
  let section =
484
486
  "\n\n## Referenced Files\n\n" +
485
487
  "The user has highlighted the following workspace paths for your " +
486
- "attention. Use `read` to access file contents.\n\n";
488
+ `attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n`;
487
489
 
488
490
  for (const refPath of workspaceFileRefs) {
489
491
  section += `- \`${refPath}\`\n`;
@@ -494,7 +496,7 @@ function buildReferencedFilesSection(
494
496
 
495
497
  /**
496
498
  * The `## Input Files` section: this harness's intro (it names this engine's
497
- * `read` tool) around the shared lines (`prompt-sections.ts` `inputFileLines`
499
+ * file-reading tool) around the shared lines (`prompt-sections.ts` `inputFileLines`
498
500
  * says what each bullet and disclosure carries and why). Each disclosure
499
501
  * group is set off by a blank line, the markdown idiom.
500
502
  */
@@ -508,7 +510,7 @@ function buildInputFilesSection(
508
510
  "The following files have been provided as read-only reference " +
509
511
  "material for your task. They live under `.stigmer/inputs/` and " +
510
512
  "are NOT part of the project source tree.\n\n" +
511
- "Read them using the `read` tool when you need their contents. " +
513
+ `Read them with \`${ENGINE_TOOL.readFile}\` when you need their contents. ` +
512
514
  "Do NOT echo, reprint, or summarize file contents in your response " +
513
515
  "-- they are reference material, not output. " +
514
516
  "Do NOT modify or delete these files.\n\n";
@@ -524,5 +526,7 @@ function buildInputFilesSection(
524
526
  section += "\n" + lines.vision.join("\n") + "\n";
525
527
  }
526
528
 
527
- return section;
529
+ // Every section opens with its own blank line ("\n\n## ..."), so this one
530
+ // ends on its last line rather than adding a second blank (#1127).
531
+ return section.replace(/\n+$/, "");
528
532
  }
@@ -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
@@ -345,6 +345,11 @@ export async function buildEngine(
345
345
  // native-thinking heuristic, and sub-agent inheritance. The credential is
346
346
  // read from the ref at build, once per turn: a LangChain client carries
347
347
  // its headers for its life, so this is the freshest a turn's model can be.
348
+ //
349
+ // No temperature: current Anthropic models refuse one other than their
350
+ // default (claude-opus-4.7 and later and claude-fable-5 inside
351
+ // @langchain/anthropic, before any request; claude-sonnet-5 at the
352
+ // provider), and the Cursor harness sends none (stigmer/stigmer#1341).
348
353
  const buildModelFor = async (name: string) =>
349
354
  (await buildChatModel({
350
355
  modelName: name,
@@ -352,6 +357,7 @@ export async function buildEngine(
352
357
  stigmerToken: config.stigmerTokenRef.current ?? undefined,
353
358
  headerScope: { executionId },
354
359
  serviceTier: input.model.serviceTier,
360
+ temperature: null,
355
361
  })).model;
356
362
  const model = await buildModelFor(modelName);
357
363
  sink.setupTiming.mark("build_model");
@@ -472,13 +478,11 @@ export async function buildEngine(
472
478
  systemPrompt,
473
479
  tools: graphTools,
474
480
  // 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`).
481
+ // installing it by default (1.12), and the product reads it. The
482
+ // framework's own middleware with Stigmer's texts; parent only
483
+ // (`todo-list.ts` says why for both).
480
484
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
481
- middleware: [todoListMiddleware(), ...middleware] as any,
485
+ middleware: [createTodoListMiddleware(), ...middleware] as any,
482
486
  subagents: compiledSubagents ?? undefined,
483
487
  ...(responseFormat ? { responseFormat } : {}),
484
488
  ...(planModePermissions ? { permissions: planModePermissions } : {}),
@@ -74,7 +74,7 @@ import {
74
74
  failedArm,
75
75
  fileReviewResolvedArm,
76
76
  infrastructureCancelArm,
77
- pauseArm,
77
+ orchestratorStopArm,
78
78
  platformStopArm,
79
79
  stallArm,
80
80
  TERMINAL_COPY,
@@ -463,7 +463,7 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
463
463
  * The table for an `interrupted` outcome, in the orchestrator's precedence:
464
464
  * a stall (FAILED, return) outranks everything; a cost cap (TERMINATED,
465
465
  * return); then Temporal's cancellation as the signal's reason reads it —
466
- * the runner's drain, the orchestrator's pause, an infrastructure cancel
466
+ * the runner's drain, the orchestrator's stop, an infrastructure cancel
467
467
  * (all thrown); last a platform stop (COMPLETED, return).
468
468
  */
469
469
  async function settleInterrupted(maxCostUsd: number, accumulator: UsageAccumulator): Promise<Settled> {
@@ -487,9 +487,9 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
487
487
  case "worker-shutdown":
488
488
  console.log(`${activityName} interrupted (worker shutdown): execution=${executionId}`);
489
489
  return settleWith(workerShutdownArm());
490
- case "pause":
491
- console.log(`${activityName} paused: execution=${executionId}`);
492
- return settleWith(pauseArm());
490
+ case "orchestrator-stop":
491
+ console.log(`${activityName} stopped by the orchestrator (pause or cancel): execution=${executionId}`);
492
+ return settleWith(orchestratorStopArm());
493
493
  case "infrastructure":
494
494
  console.log(`${activityName} interrupted (infrastructure cancel): execution=${executionId}`);
495
495
  return settleWith(infrastructureCancelArm());
@@ -659,9 +659,9 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
659
659
  case "worker-shutdown":
660
660
  console.log(`${activityName} cancelled (worker shutdown) for execution ${executionId}`);
661
661
  return workerShutdownArm();
662
- case "pause":
663
- console.log(`${activityName} cancelled (pause) for execution ${executionId}`);
664
- return pauseArm();
662
+ case "orchestrator-stop":
663
+ console.log(`${activityName} cancelled (orchestrator stop) for execution ${executionId}`);
664
+ return orchestratorStopArm();
665
665
  case "infrastructure":
666
666
  console.log(`${activityName} cancelled (infrastructure) for execution ${executionId}`);
667
667
  return infrastructureCancelArm();
@@ -687,11 +687,11 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
687
687
  if (arm !== undefined) {
688
688
  // A non-cancellation error while cancelled was most likely caused by the
689
689
  // cancellation itself (a teardown mid-flight) and must not overwrite the
690
- // state the workflow already expects: a pause stays PAUSED; anything
691
- // else reads as the interruption it was.
692
- if (arm.phase === ExecutionPhase.EXECUTION_PAUSED) {
693
- console.log(`${activityName} error during pause (treating as pause): execution=${executionId}, error=${errDetail}`);
694
- return settleWith(arm, new CancelledFailure(TERMINAL_COPY.pause.throwMessageAfterError));
690
+ // state the workflow already expects: an orchestrator stop stays the
691
+ // workflow's to write; anything else reads as the interruption it was.
692
+ if (interruption() === "orchestrator-stop") {
693
+ console.log(`${activityName} error during an orchestrator stop (treating as the stop): execution=${executionId}, error=${errDetail}`);
694
+ return settleWith(arm, new CancelledFailure(TERMINAL_COPY.orchestratorStop.throwMessageAfterError));
695
695
  }
696
696
  console.log(`${activityName} error during infrastructure cancel: execution=${executionId}, error=${errDetail}`);
697
697
  return settleWith(
@@ -21,6 +21,16 @@
21
21
  * infrastructure cancel it delivered — with the message the control
22
22
  * planes recognise.
23
23
  *
24
+ * One stop has no copy here: the orchestrator's (a Pause or a Cancel, which
25
+ * reach the activity as the same workflow-requested cancellation). The turn
26
+ * cannot tell which it is, so its arm writes no phase and no row, and the
27
+ * invoke workflow, which knows, writes the phase and the stop row that the
28
+ * server's merge keeps last (stigmer#980;
29
+ * `backend/services/stigmer-server/src/domain/agentexecution/platform-rows.ts`).
30
+ * The pause row lived here until then, and a cancelled run ended with an
31
+ * instruction to resume it. The arm's throw messages keep their bytes: they
32
+ * are the wire copy the control planes see on the activity failure.
33
+ *
24
34
  * An arm's rows are appended exactly once, by `applyTerminalArm`, and an arm
25
35
  * is persisted exactly once, by `run-turn.ts`'s `settleWith` — the one
26
36
  * writer of a terminal. The orchestrator this table replaced appended the
@@ -47,6 +57,7 @@ export type TerminalDisposition = { readonly kind: "return" } | { readonly kind:
47
57
 
48
58
  /** One arm of the table: what to write, and how to end. */
49
59
  export interface TerminalArm {
60
+ /** `EXECUTION_PHASE_UNSPECIFIED` writes no phase: the merge applies a phase only when one is sent. */
50
61
  readonly phase: ExecutionPhase;
51
62
  /** `status.error`; absent leaves it as it is (a pause is not an error). */
52
63
  readonly error?: string;
@@ -61,8 +72,7 @@ const RETURN: TerminalDisposition = { kind: "return" };
61
72
 
62
73
  /** The fixed strings of the table, in one place. Every one is pinned by a golden or a hermetic assertion. */
63
74
  export const TERMINAL_COPY = {
64
- pause: {
65
- row: "Execution paused by user. Use resume to continue.",
75
+ orchestratorStop: {
66
76
  throwMessage: "Activity paused by orchestrator",
67
77
  throwMessageAfterError: "Activity paused by orchestrator (error during pause)",
68
78
  },
@@ -138,7 +148,7 @@ export function toolCallLimitArm(): TerminalArm {
138
148
  };
139
149
  }
140
150
 
141
- /** The runner is draining. Not a user pause: FAILED with the shutdown copy, thrown as cancelled. */
151
+ /** The runner is draining. Not a user's stop: FAILED with the shutdown copy, thrown as cancelled. */
142
152
  export function workerShutdownArm(): TerminalArm {
143
153
  return {
144
154
  phase: ExecutionPhase.EXECUTION_FAILED,
@@ -149,13 +159,18 @@ export function workerShutdownArm(): TerminalArm {
149
159
  };
150
160
  }
151
161
 
152
- /** The orchestrator paused the execution. Not an error, not complete; thrown as cancelled. */
153
- export function pauseArm(): TerminalArm {
162
+ /**
163
+ * The orchestrator stopped the turn: a Pause or a Cancel, indistinguishable
164
+ * here (the module header). The transcript is settled and persisted with no
165
+ * phase and no row, because the workflow writes both; not an error, not
166
+ * complete; thrown as cancelled so the workflow sees the activity stop.
167
+ */
168
+ export function orchestratorStopArm(): TerminalArm {
154
169
  return {
155
- phase: ExecutionPhase.EXECUTION_PAUSED,
156
- rows: [TERMINAL_COPY.pause.row],
170
+ phase: ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED,
171
+ rows: [],
157
172
  completes: false,
158
- disposition: { kind: "throw", message: TERMINAL_COPY.pause.throwMessage },
173
+ disposition: { kind: "throw", message: TERMINAL_COPY.orchestratorStop.throwMessage },
159
174
  };
160
175
  }
161
176
 
@@ -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
+ });