@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
|
@@ -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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
the
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
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 +=
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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**: \`
|
|
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**:
|
|
414
|
-
`
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
@@ -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
|
|
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`).
|
|
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: [
|
|
485
|
+
middleware: [createTodoListMiddleware(), ...middleware] as any,
|
|
482
486
|
subagents: compiledSubagents ?? undefined,
|
|
483
487
|
...(responseFormat ? { responseFormat } : {}),
|
|
484
488
|
...(planModePermissions ? { permissions: planModePermissions } : {}),
|
package/src/harness/run-turn.ts
CHANGED
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
failedArm,
|
|
75
75
|
fileReviewResolvedArm,
|
|
76
76
|
infrastructureCancelArm,
|
|
77
|
-
|
|
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
|
|
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 "
|
|
491
|
-
console.log(`${activityName}
|
|
492
|
-
return settleWith(
|
|
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 "
|
|
663
|
-
console.log(`${activityName} cancelled (
|
|
664
|
-
return
|
|
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:
|
|
691
|
-
// else reads as the interruption it was.
|
|
692
|
-
if (
|
|
693
|
-
console.log(`${activityName} error during
|
|
694
|
-
return settleWith(arm, new CancelledFailure(TERMINAL_COPY.
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
153
|
-
|
|
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.
|
|
156
|
-
rows: [
|
|
170
|
+
phase: ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED,
|
|
171
|
+
rows: [],
|
|
157
172
|
completes: false,
|
|
158
|
-
disposition: { kind: "throw", message: TERMINAL_COPY.
|
|
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
|
+
});
|