@stigmer/runner 3.39.1-dev.20260928194002 → 3.40.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/README.md +1 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +35 -29
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/package.json +5 -5
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/turn-setup.ts +36 -31
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/sources/git.ts +14 -8
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The native harness's system prompt
|
|
3
|
-
* `systemPrompt`,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The native harness's prompt: the system prompt `createDeepAgent` receives
|
|
3
|
+
* as `systemPrompt`, and the turn's user message.
|
|
4
|
+
*
|
|
5
|
+
* The system prompt is rebuilt on EVERY invocation (never checkpointed with
|
|
6
|
+
* the message history), so every standing fact is injected on every turn by
|
|
7
|
+
* design — a first-turn-only injection would vanish from turn 2 onward. It is
|
|
8
|
+
* a function of the session and of the execution's settings (its mode), never
|
|
9
|
+
* of the turn. The provider caches tools, system and messages as one prefix,
|
|
10
|
+
* so a system byte that follows the message re-writes the cached prompt and
|
|
11
|
+
* the whole conversation after it, on every turn, at the cache-write price.
|
|
12
|
+
* What belongs to one turn — its input files, the workspace paths it
|
|
13
|
+
* references, the conversation catchup — rides that turn's user message
|
|
14
|
+
* (`composeUserMessage`), which enters the checkpointer with the turn and
|
|
15
|
+
* stays in history. That is the Cursor harness's resumed-turn placement
|
|
16
|
+
* (`execute-cursor/prompt-builder.ts` `buildPrompt`) and the runner's rule
|
|
17
|
+
* that user content goes in user messages, never the system prompt.
|
|
6
18
|
*
|
|
7
19
|
* Pure functions: no side effects, no I/O. `turn-setup.ts` gathers the
|
|
8
20
|
* inputs from the runtime's resolved record and this module renders them.
|
|
9
21
|
*
|
|
10
22
|
* What is this harness's and what is shared (since #1096):
|
|
11
23
|
* - Shared through `shared/prompt-sections.ts`: WHICH standing sections
|
|
12
|
-
* render and in WHAT order (`standingContextSections`)
|
|
13
|
-
* bullet and disclosure lines (`inputFileLines`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
24
|
+
* render and in WHAT order (`standingContextSections`) and the
|
|
25
|
+
* input-files bullet and disclosure lines (`inputFileLines`). Their WORDS
|
|
26
|
+
* come from each fact's own module; this builder only frames them. Every
|
|
27
|
+
* mounted skill is described on every turn, as the Cursor harness does:
|
|
28
|
+
* choosing skills by the message made the system prompt follow the
|
|
29
|
+
* message.
|
|
17
30
|
* - This harness's: the framing — `## Heading` markdown sections, the house
|
|
18
31
|
* style for a system prompt — and every text that quotes this engine's
|
|
19
32
|
* tools: the `## Skills` activation protocol, the response rules, the
|
|
@@ -24,7 +37,8 @@
|
|
|
24
37
|
*
|
|
25
38
|
* The rendered bytes are load-bearing for the hermetic tests: `ScriptedModel`
|
|
26
39
|
* tells its scripted roles apart by the system prompt's text, so this
|
|
27
|
-
* module's whole output
|
|
40
|
+
* module's whole output — the system prompt and the turn's message — is
|
|
41
|
+
* pinned by `__tests__/prompt-goldens.test.ts`.
|
|
28
42
|
*/
|
|
29
43
|
|
|
30
44
|
import { relative } from "node:path";
|
|
@@ -43,10 +57,7 @@ import {
|
|
|
43
57
|
} from "../../shared/prompt-sections.js";
|
|
44
58
|
import { effectiveInstructions } from "../../shared/builtin-assistant-prompt.js";
|
|
45
59
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
46
|
-
import {
|
|
47
|
-
buildImplementPlanDirective,
|
|
48
|
-
findApprovedPlanPath,
|
|
49
|
-
} from "../../shared/implement-plan-prompt.js";
|
|
60
|
+
import { buildImplementPlanDirective } from "../../shared/implement-plan-prompt.js";
|
|
50
61
|
import type { ResolvedAttachment } from "../../shared/attachment-resolver.js";
|
|
51
62
|
import type { SkillMetadata } from "../../shared/skill-resolver.js";
|
|
52
63
|
import { STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
|
|
@@ -131,22 +142,6 @@ export interface PromptBuilderInput {
|
|
|
131
142
|
* when the agent serves no proactive channel or nothing is sendable.
|
|
132
143
|
*/
|
|
133
144
|
channelTemplatesPromptSection?: string;
|
|
134
|
-
workspaceFileRefs: string[];
|
|
135
|
-
workspaceRoot: string;
|
|
136
|
-
/** The turn's resolved input files, as the runtime's attachment phase returns them. */
|
|
137
|
-
inputFiles: readonly ResolvedAttachment[];
|
|
138
|
-
/**
|
|
139
|
-
* Vision facts about this turn's attachments (T04): which images the model
|
|
140
|
-
* sees inline in the user message and which degraded to path-only.
|
|
141
|
-
* Rendered inside the Input Files section.
|
|
142
|
-
*/
|
|
143
|
-
vision?: VisionPromptInfo;
|
|
144
|
-
/**
|
|
145
|
-
* What kind of URL the turn's storage backend mints (issue #532) — keys
|
|
146
|
-
* the Input Files section's hand-off wording (attachment-download-urls.ts).
|
|
147
|
-
* One turn-level fact: all attachments ride the one configured storage.
|
|
148
|
-
*/
|
|
149
|
-
downloadUrlKind?: DownloadUrlKind;
|
|
150
145
|
/**
|
|
151
146
|
* The execution's interaction mode. PLAN appends the shared plan-mode
|
|
152
147
|
* directive so the model knows the turn's deliverable is a plan document.
|
|
@@ -163,6 +158,14 @@ export interface PromptBuilderInput {
|
|
|
163
158
|
* message itself is just a short label ("Build from plan").
|
|
164
159
|
*/
|
|
165
160
|
buildFromPlan?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* The approved plan's path among the turn's input files
|
|
163
|
+
* (`implement-plan-prompt.ts` `findApprovedPlanPath`), read only under
|
|
164
|
+
* `buildFromPlan`. Absent when the plan did not materialize, which selects
|
|
165
|
+
* the directive's conversation-plan variant. The Input Files section
|
|
166
|
+
* itself rides the turn's message (`composeUserMessage`).
|
|
167
|
+
*/
|
|
168
|
+
approvedPlanPath?: string;
|
|
166
169
|
/**
|
|
167
170
|
* Rollover context bridge (cloud DD-013): a digest of the previous
|
|
168
171
|
* session's conversation, read from `SessionSpec.metadata`. Injected on
|
|
@@ -252,22 +255,6 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
252
255
|
prompt += "\n\n" + input.channelTemplatesPromptSection;
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
if (input.workspaceFileRefs.length > 0) {
|
|
256
|
-
const refSection = buildReferencedFilesSection(
|
|
257
|
-
input.workspaceFileRefs,
|
|
258
|
-
input.workspaceRoot,
|
|
259
|
-
);
|
|
260
|
-
if (refSection) {
|
|
261
|
-
prompt += refSection;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (input.inputFiles.length > 0) {
|
|
266
|
-
prompt += buildInputFilesSection(
|
|
267
|
-
input.inputFiles, input.vision, input.downloadUrlKind,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
258
|
// The standing context, in the shared order (the doctrine is stated once,
|
|
272
259
|
// on `standingContextSections`); this harness only frames each section.
|
|
273
260
|
for (const section of standingContextSections(input)) {
|
|
@@ -296,34 +283,67 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
296
283
|
}
|
|
297
284
|
|
|
298
285
|
if (input.buildFromPlan) {
|
|
299
|
-
const planPath = findApprovedPlanPath(
|
|
300
|
-
input.inputFiles.map((f) => f.relativePath),
|
|
301
|
-
);
|
|
302
286
|
prompt +=
|
|
303
287
|
"\n\n## Implement the approved plan\n\n" +
|
|
304
|
-
buildImplementPlanDirective(
|
|
288
|
+
buildImplementPlanDirective(input.approvedPlanPath);
|
|
305
289
|
}
|
|
306
290
|
|
|
307
291
|
return prompt;
|
|
308
292
|
}
|
|
309
293
|
|
|
294
|
+
/** The turn's own content, composed around what the user typed. */
|
|
295
|
+
export interface TurnMessageInput {
|
|
296
|
+
/** What the user typed (`spec.message`); never mutated. */
|
|
297
|
+
readonly message: string;
|
|
298
|
+
/** The conversation catchup digest (cloud DD-006), when the turn carries one. */
|
|
299
|
+
readonly conversationCatchup?: string;
|
|
300
|
+
/** This turn's resolved input files, as the runtime's attachment phase returns them. */
|
|
301
|
+
readonly inputFiles: readonly ResolvedAttachment[];
|
|
302
|
+
/**
|
|
303
|
+
* Vision facts about this turn's attachments (T04): which images the model
|
|
304
|
+
* sees inline in this same message and which degraded to path-only.
|
|
305
|
+
* Rendered inside the Input Files section.
|
|
306
|
+
*/
|
|
307
|
+
readonly vision?: VisionPromptInfo;
|
|
308
|
+
/**
|
|
309
|
+
* What kind of URL the turn's storage backend mints (issue #532) — keys
|
|
310
|
+
* the Input Files section's hand-off wording (attachment-download-urls.ts).
|
|
311
|
+
* One turn-level fact: all attachments ride the one configured storage.
|
|
312
|
+
*/
|
|
313
|
+
readonly downloadUrlKind?: DownloadUrlKind;
|
|
314
|
+
/** The workspace paths the user highlighted for this message (`spec.workspace_file_refs`). */
|
|
315
|
+
readonly workspaceFileRefs: readonly string[];
|
|
316
|
+
}
|
|
317
|
+
|
|
310
318
|
/**
|
|
311
|
-
* Compose the turn's USER MESSAGE for the graph invocation:
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
+
* Compose the turn's USER MESSAGE for the graph invocation: this turn's
|
|
320
|
+
* payload (its input files, then the workspace paths it references), then the
|
|
321
|
+
* framed conversation catchup (cloud DD-006), then what the user typed, each
|
|
322
|
+
* set off by a horizontal rule. Payload first and context closest to the task
|
|
323
|
+
* is the Cursor harness's resumed-turn order.
|
|
324
|
+
*
|
|
325
|
+
* In the user message and never the system prompt (A27, and the module
|
|
326
|
+
* header): the system prompt is rebuilt per invocation, so a turn's content
|
|
327
|
+
* there would vanish one turn later and, while it lasted, change the cached
|
|
328
|
+
* prefix; a message enters the checkpointer with the turn and persists in
|
|
329
|
+
* history, so a later turn still knows which files an earlier one carried.
|
|
330
|
+
* An approval resume sends no new message and needs none: the payload is
|
|
331
|
+
* already in the checkpointed one. The caller's `spec.message` is never
|
|
332
|
+
* mutated; the composition exists only in the graph input.
|
|
319
333
|
*/
|
|
320
|
-
export function composeUserMessage(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
export function composeUserMessage(input: TurnMessageInput): string {
|
|
335
|
+
const parts: string[] = [];
|
|
336
|
+
if (input.inputFiles.length > 0) {
|
|
337
|
+
parts.push(buildInputFilesSection(input.inputFiles, input.vision, input.downloadUrlKind));
|
|
338
|
+
}
|
|
339
|
+
if (input.workspaceFileRefs.length > 0) {
|
|
340
|
+
parts.push(buildReferencedFilesSection(input.workspaceFileRefs));
|
|
341
|
+
}
|
|
342
|
+
if (input.conversationCatchup) {
|
|
343
|
+
parts.push(formatConversationCatchupText(input.conversationCatchup));
|
|
344
|
+
}
|
|
345
|
+
parts.push(input.message);
|
|
346
|
+
return parts.join("\n\n---\n\n");
|
|
327
347
|
}
|
|
328
348
|
|
|
329
349
|
/**
|
|
@@ -454,14 +474,14 @@ function formatEntryDescription(result: ProvisionResult): string {
|
|
|
454
474
|
);
|
|
455
475
|
}
|
|
456
476
|
|
|
477
|
+
// No commit: the clone's HEAD moves with every write-back commit, and a
|
|
478
|
+
// system prompt that names it would change on every such turn
|
|
479
|
+
// (`shared/workspace/sources/git.ts` says the same for one entry).
|
|
457
480
|
if (result.sourceType === "git_repo" && result.gitMetadata) {
|
|
458
481
|
const meta: GitMetadata = result.gitMetadata;
|
|
459
|
-
const
|
|
460
|
-
? meta.baseCommit.slice(0, 7)
|
|
461
|
-
: meta.baseCommit;
|
|
482
|
+
const branch = meta.branch ? ` (branch: ${meta.branch})` : "";
|
|
462
483
|
return (
|
|
463
|
-
`Workspace entry **${name}** was initialized from ` +
|
|
464
|
-
`${meta.repoUrl} (branch: ${meta.branch}, commit: ${shortSha}).\n` +
|
|
484
|
+
`Workspace entry **${name}** was initialized from ${meta.repoUrl}${branch}.\n` +
|
|
465
485
|
"Changes you make will be captured as artifacts when execution completes."
|
|
466
486
|
);
|
|
467
487
|
}
|
|
@@ -476,36 +496,29 @@ function formatEntryDescription(result: ProvisionResult): string {
|
|
|
476
496
|
return result.workspaceDescription;
|
|
477
497
|
}
|
|
478
498
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (workspaceFileRefs.length === 0) return "";
|
|
484
|
-
|
|
485
|
-
let section =
|
|
486
|
-
"\n\n## Referenced Files\n\n" +
|
|
499
|
+
/** The `## Referenced Files` part of the turn's message: the paths the user highlighted, one bullet each. */
|
|
500
|
+
function buildReferencedFilesSection(workspaceFileRefs: readonly string[]): string {
|
|
501
|
+
return (
|
|
502
|
+
"## Referenced Files\n\n" +
|
|
487
503
|
"The user has highlighted the following workspace paths for your " +
|
|
488
|
-
`attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
section += `- \`${refPath}\`\n`;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return section;
|
|
504
|
+
`attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n` +
|
|
505
|
+
workspaceFileRefs.map((refPath) => `- \`${refPath}\``).join("\n")
|
|
506
|
+
);
|
|
495
507
|
}
|
|
496
508
|
|
|
497
509
|
/**
|
|
498
|
-
* The `## Input Files`
|
|
499
|
-
* file-reading tool) around the shared lines
|
|
500
|
-
* says what each bullet and disclosure
|
|
501
|
-
* group is set off by a blank line, the
|
|
510
|
+
* The `## Input Files` part of the turn's message: this harness's intro (it
|
|
511
|
+
* names this engine's file-reading tool) around the shared lines
|
|
512
|
+
* (`prompt-sections.ts` `inputFileLines` says what each bullet and disclosure
|
|
513
|
+
* carries and why). Each disclosure group is set off by a blank line, the
|
|
514
|
+
* markdown idiom.
|
|
502
515
|
*/
|
|
503
516
|
function buildInputFilesSection(
|
|
504
517
|
files: readonly ResolvedAttachment[],
|
|
505
518
|
vision?: VisionPromptInfo,
|
|
506
519
|
downloadUrlKind?: DownloadUrlKind,
|
|
507
520
|
): string {
|
|
508
|
-
let section = "
|
|
521
|
+
let section = "## Input Files\n\n";
|
|
509
522
|
section +=
|
|
510
523
|
"The following files have been provided as read-only reference " +
|
|
511
524
|
"material for your task. They live under `.stigmer/inputs/` and " +
|
|
@@ -526,7 +539,6 @@ function buildInputFilesSection(
|
|
|
526
539
|
section += "\n" + lines.vision.join("\n") + "\n";
|
|
527
540
|
}
|
|
528
541
|
|
|
529
|
-
//
|
|
530
|
-
// ends on its last line rather than adding a second blank (#1127).
|
|
542
|
+
// The part ends on its last line; the message's rule sets it off (#1127).
|
|
531
543
|
return section.replace(/\n+$/, "");
|
|
532
544
|
}
|
|
@@ -67,7 +67,8 @@ import { buildChatModel } from "../../shared/model-client.js";
|
|
|
67
67
|
import { graphThinks, toAnthropicThinking } from "../../shared/thinking-mode.js";
|
|
68
68
|
import { isUnattendedApprovalMode, type MergedToolPolicy } from "../../shared/approval-policy.js";
|
|
69
69
|
import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
|
|
70
|
-
import {
|
|
70
|
+
import { visionPromptInfoOf } from "../../shared/prompt-sections.js";
|
|
71
|
+
import { findApprovedPlanPath } from "../../shared/implement-plan-prompt.js";
|
|
71
72
|
import type { RecalledMemoriesContent } from "../../shared/recalled-memories.js";
|
|
72
73
|
import { toLangChainImageBlocks } from "../../shared/attachment-vision.js";
|
|
73
74
|
import { resolveRecursionLimit, UNBOUNDED_ADVISORY_RECURSION_LIMIT } from "../../shared/tool-rounds.js";
|
|
@@ -260,25 +261,6 @@ export function readGateState(input: TurnInput, tools: DeepAgentTools): DeepAgen
|
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
/**
|
|
264
|
-
* The `## Skills` section of the root prompt: the skills the shared selection
|
|
265
|
-
* highlights for this turn's message (`shared/prompt-sections.ts`
|
|
266
|
-
* `selectSkillsForPrompt`: every skill below the threshold, the relevant ones
|
|
267
|
-
* at and above it), rendered with this engine's activation protocol, plus —
|
|
268
|
-
* when the selection left any out — an `### Also Available` note naming them
|
|
269
|
-
* (the sentence is the shared, tool-neutral one; the heading is this
|
|
270
|
-
* harness's markdown framing).
|
|
271
|
-
*/
|
|
272
|
-
export function renderRootSkillsSection(input: TurnInput): string {
|
|
273
|
-
const { highlighted, alsoAvailable } = selectSkillsForPrompt(input.execution.spec?.message ?? "", input.skills.root);
|
|
274
|
-
if (alsoAvailable.length === 0) return renderSkillsSection(highlighted);
|
|
275
|
-
console.log(
|
|
276
|
-
`[turn-setup] Skill relevance filter: ${highlighted.length} included, ` +
|
|
277
|
-
`${alsoAvailable.length} excluded: ${alsoAvailable.join(", ")}`,
|
|
278
|
-
);
|
|
279
|
-
return renderSkillsSection(highlighted) + "\n### Also Available\n\n" + alsoAvailableSkillsNote(alsoAvailable) + "\n";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
264
|
/**
|
|
283
265
|
* The system prompt over the runtime's resolved record: every input the
|
|
284
266
|
* builder renders is read from `TurnInput` here, in one place, so the mapping
|
|
@@ -287,6 +269,12 @@ export function renderRootSkillsSection(input: TurnInput): string {
|
|
|
287
269
|
* that is not on the record is the memoized memory selection, which the
|
|
288
270
|
* caller awaits (the runtime stamps the report on the status the first time
|
|
289
271
|
* it is pulled, before this turn's first persist).
|
|
272
|
+
*
|
|
273
|
+
* Nothing here reads the turn's message or payload: the system prompt is the
|
|
274
|
+
* session's (`prompt-builder.ts` says why), so every mounted skill is
|
|
275
|
+
* described and the turn's files ride its message (`composeTurnMessage`).
|
|
276
|
+
* The approved plan's path is the one fact taken from the attachments, and
|
|
277
|
+
* only on a build-from-plan turn, where the mode already changes the prompt.
|
|
290
278
|
*/
|
|
291
279
|
export function composeSystemPrompt(input: TurnInput, recalledMemories: RecalledMemoriesContent | undefined): string {
|
|
292
280
|
const spec = input.execution.spec!;
|
|
@@ -296,19 +284,17 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
|
|
|
296
284
|
instructions: input.blueprint.instructions,
|
|
297
285
|
provisionResults: input.workspace.provision.provisionResults,
|
|
298
286
|
containerRoot: primaryDir,
|
|
299
|
-
skillsPromptSection:
|
|
287
|
+
skillsPromptSection: renderSkillsSection(input.skills.root),
|
|
300
288
|
// "" (nothing sendable) threads as undefined: the tool alone still serves
|
|
301
289
|
// text sends inside a 24-hour window (DD-006 D6).
|
|
302
290
|
channelTemplatesPromptSection: input.mcp.channelMessaging.length > 0
|
|
303
291
|
? formatChannelTemplatesSection(input.mcp.channelMessaging) || undefined
|
|
304
292
|
: undefined,
|
|
305
|
-
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
306
|
-
workspaceRoot: primaryDir,
|
|
307
|
-
inputFiles: input.attachments.results,
|
|
308
|
-
vision: visionPromptInfoOf(input.attachments),
|
|
309
|
-
downloadUrlKind: input.artifactStorage?.downloadUrlKind,
|
|
310
293
|
interactionMode: execConfig?.interactionMode,
|
|
311
294
|
buildFromPlan: execConfig?.buildFromPlan,
|
|
295
|
+
...(execConfig?.buildFromPlan
|
|
296
|
+
? { approvedPlanPath: findApprovedPlanPath(input.attachments.results.map((f) => f.relativePath)) }
|
|
297
|
+
: {}),
|
|
312
298
|
contextBridge: input.standing.contextBridge,
|
|
313
299
|
senderIdentity: input.standing.senderIdentity,
|
|
314
300
|
sessionContext: input.standing.sessionContext,
|
|
@@ -317,6 +303,25 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
|
|
|
317
303
|
});
|
|
318
304
|
}
|
|
319
305
|
|
|
306
|
+
/**
|
|
307
|
+
* The turn's user message over the runtime's resolved record: what the user
|
|
308
|
+
* typed, with this turn's payload (its input files and referenced paths) and
|
|
309
|
+
* the conversation catchup composed around it (`prompt-builder.ts`
|
|
310
|
+
* `composeUserMessage`). The twin of `composeSystemPrompt` for the turn's
|
|
311
|
+
* half, so the goldens pin this mapping and not a copy of it.
|
|
312
|
+
*/
|
|
313
|
+
export function composeTurnMessage(input: TurnInput): string {
|
|
314
|
+
const spec = input.execution.spec!;
|
|
315
|
+
return composeUserMessage({
|
|
316
|
+
message: spec.message,
|
|
317
|
+
conversationCatchup: input.standing.conversationCatchup,
|
|
318
|
+
inputFiles: input.attachments.results,
|
|
319
|
+
vision: visionPromptInfoOf(input.attachments),
|
|
320
|
+
downloadUrlKind: input.artifactStorage?.downloadUrlKind,
|
|
321
|
+
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
320
325
|
/**
|
|
321
326
|
* Build the engine: the model, the middleware stack with the gate, the
|
|
322
327
|
* tools, the compiled sub-agents, the system prompt and `createDeepAgent`.
|
|
@@ -577,9 +582,10 @@ export async function buildEngine(
|
|
|
577
582
|
* What the graph is invoked with. The checkpoint is read once (on the
|
|
578
583
|
* durable http saver an extra `getState` is a network round-trip): pending
|
|
579
584
|
* interrupts that the runtime's decisions answer make a `Command(resume)`;
|
|
580
|
-
* anything else is the turn's user message —
|
|
581
|
-
* framed before it (
|
|
582
|
-
* checkpointer with the turn
|
|
585
|
+
* anything else is the turn's user message — this turn's payload and the
|
|
586
|
+
* conversation catchup framed before it (`composeTurnMessage`; in the USER
|
|
587
|
+
* MESSAGE so it enters the checkpointer with the turn and the system prompt
|
|
588
|
+
* stays the session's), the structured-output contract appended, and
|
|
583
589
|
* the inline images FIRST as content blocks when the turn carries any (per
|
|
584
590
|
* Anthropic's images-before-text guidance; a plain string otherwise).
|
|
585
591
|
*/
|
|
@@ -590,8 +596,7 @@ export async function composeGraphInput(input: TurnInput, engine: DeepAgentEngin
|
|
|
590
596
|
return { kind: "resume", command: resume.graphInput };
|
|
591
597
|
}
|
|
592
598
|
|
|
593
|
-
|
|
594
|
-
let userMessage = composeUserMessage(spec.message, input.standing.conversationCatchup);
|
|
599
|
+
let userMessage = composeTurnMessage(input);
|
|
595
600
|
if (engine.hasStructuredOutput) {
|
|
596
601
|
userMessage += `\n\n---\nIMPORTANT: When your analysis is complete, provide your findings as structured output matching the required schema. The system will capture your structured response automatically.`;
|
|
597
602
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared prompt glue (`shared/prompt-sections.ts`): the standing order
|
|
3
|
-
* the input-files lines
|
|
4
|
-
* (the sentence's arms came from `skill-writer.test.ts`, retired with its
|
|
5
|
-
* module in #1096). What each harness renders AROUND these is pinned whole by
|
|
2
|
+
* The shared prompt glue (`shared/prompt-sections.ts`): the standing order
|
|
3
|
+
* and the input-files lines. What each harness renders AROUND these is pinned whole by
|
|
6
4
|
* the two builders' prompt goldens (`execute-deep-agent/__tests__/
|
|
7
5
|
* prompt-goldens.test.ts`, `execute-cursor/__tests__/prompt-goldens.test.ts`);
|
|
8
6
|
* this file pins the glue's own contract, harness-free.
|
|
@@ -10,16 +8,12 @@
|
|
|
10
8
|
|
|
11
9
|
import { describe, it, expect } from "vitest";
|
|
12
10
|
import {
|
|
13
|
-
alsoAvailableSkillsNote,
|
|
14
11
|
inputFileLines,
|
|
15
|
-
selectSkillsForPrompt,
|
|
16
12
|
standingContextSections,
|
|
17
13
|
visionPromptInfoOf,
|
|
18
14
|
type StandingSectionKind,
|
|
19
15
|
} from "../prompt-sections.js";
|
|
20
16
|
import type { ResolvedAttachment } from "../attachment-resolver.js";
|
|
21
|
-
import type { SkillMetadata } from "../skill-resolver.js";
|
|
22
|
-
import { SKILL_COUNT_THRESHOLD } from "../skill-relevance.js";
|
|
23
17
|
import { formatDeclaredPreferencesText } from "../declared-preferences.js";
|
|
24
18
|
|
|
25
19
|
describe("standingContextSections", () => {
|
|
@@ -119,49 +113,3 @@ describe("visionPromptInfoOf", () => {
|
|
|
119
113
|
});
|
|
120
114
|
});
|
|
121
115
|
});
|
|
122
|
-
|
|
123
|
-
describe("selectSkillsForPrompt", () => {
|
|
124
|
-
const skill = (name: string, description: string): SkillMetadata => ({ name, description, path: `.stigmer/skills/${name}/SKILL.md` });
|
|
125
|
-
|
|
126
|
-
it("highlights every skill below the threshold, in mount order, and names none as also-available", () => {
|
|
127
|
-
const skills = [skill("alpha", "Deploy to kubernetes"), skill("beta", "Reshape CSV exports")];
|
|
128
|
-
const selection = selectSkillsForPrompt("deploy the service", skills);
|
|
129
|
-
expect(selection.highlighted).toEqual(skills);
|
|
130
|
-
expect(selection.alsoAvailable).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("at the threshold, highlights the skills the message reaches and names the rest", () => {
|
|
134
|
-
const skills = Array.from({ length: SKILL_COUNT_THRESHOLD }, (_, i) =>
|
|
135
|
-
i === 0 ? skill("k8s-deploy", "Deploy services to kubernetes clusters") : skill(`other-${i}`, `Unrelated thing number ${i}`),
|
|
136
|
-
);
|
|
137
|
-
const selection = selectSkillsForPrompt("Deploy the payments service to kubernetes", skills);
|
|
138
|
-
expect(selection.highlighted.map((s) => s.name)).toContain("k8s-deploy");
|
|
139
|
-
expect(selection.alsoAvailable.length).toBeGreaterThan(0);
|
|
140
|
-
expect(selection.highlighted.length + selection.alsoAvailable.length).toBe(SKILL_COUNT_THRESHOLD);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("selects nothing from nothing", () => {
|
|
144
|
-
expect(selectSkillsForPrompt("anything", [])).toEqual({ highlighted: [], alsoAvailable: [] });
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("alsoAvailableSkillsNote", () => {
|
|
149
|
-
it("returns empty for no excluded names", () => {
|
|
150
|
-
expect(alsoAvailableSkillsNote([])).toBe("");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("lists excluded skills with backtick formatting", () => {
|
|
154
|
-
const note = alsoAvailableSkillsNote(["alpha", "beta", "gamma"]);
|
|
155
|
-
expect(note).toContain("`alpha`");
|
|
156
|
-
expect(note).toContain("`beta`");
|
|
157
|
-
expect(note).toContain("`gamma`");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("includes activation instructions that name no tool, so any harness can carry it", () => {
|
|
161
|
-
const note = alsoAvailableSkillsNote(["some-skill"]);
|
|
162
|
-
expect(note).toContain(".stigmer/skills/<name>/SKILL.md");
|
|
163
|
-
expect(note).toContain("relevant to your task");
|
|
164
|
-
expect(note).not.toMatch(/\b(read|Read) tool\b/);
|
|
165
|
-
expect(note.startsWith("#")).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* `attachment-download-urls.ts`). What the two prompt builders duplicated
|
|
10
10
|
* until #1096 was the glue around those bodies: the order
|
|
11
11
|
* the five standing sections read in, with the same doctrine comments written
|
|
12
|
-
* twice; the input-files bullet and disclosure lines
|
|
13
|
-
*
|
|
12
|
+
* twice; the input-files bullet and disclosure lines. This module is that
|
|
13
|
+
* glue, written once. It also chose which skills a prompt highlighted when
|
|
14
|
+
* there were many, until the native harness stopped choosing skills by the
|
|
15
|
+
* turn's message: a system prompt that follows the message misses the
|
|
16
|
+
* provider's prompt cache on every turn, so every harness now describes every
|
|
17
|
+
* mounted skill.
|
|
14
18
|
*
|
|
15
19
|
* What stays with each harness, on purpose:
|
|
16
20
|
* - Framing and placement. The native harness renders `## Heading` sections
|
|
@@ -43,9 +47,6 @@ import { formatDeclaredPreferencesText, type DeclaredPreferencesContent } from "
|
|
|
43
47
|
import { formatRecalledMemoriesText, type RecalledMemoriesContent } from "./recalled-memories.js";
|
|
44
48
|
import { formatSenderIdentityText, type SenderIdentity } from "./sender-identity.js";
|
|
45
49
|
import { formatSessionContextText } from "./session-context.js";
|
|
46
|
-
import { filterSkills } from "./skill-relevance.js";
|
|
47
|
-
import type { SkillMetadata } from "./skill-resolver.js";
|
|
48
|
-
import { STIGMER_LOCAL_STATE_DIR } from "./workspace/stigmer-link.js";
|
|
49
50
|
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
51
52
|
// The standing context: five sections, one order
|
|
@@ -204,53 +205,3 @@ export function inputFileLines(
|
|
|
204
205
|
vision: vision ? visionDisclosureLines(vision.inlineFilenames, vision.notViewable) : [],
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// The skills: which to highlight
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
/** Which mounted skills a prompt highlights, and which it only names. */
|
|
213
|
-
export interface SkillSelection {
|
|
214
|
-
/** The skills the prompt describes in full, in the filter's order. */
|
|
215
|
-
readonly highlighted: readonly SkillMetadata[];
|
|
216
|
-
/** The names of the skills left out of the highlighted set; empty below the threshold. */
|
|
217
|
-
readonly alsoAvailable: readonly string[];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* The relevance selection over the turn's message (`skill-relevance.ts`):
|
|
222
|
-
* below `SKILL_COUNT_THRESHOLD` every skill is highlighted; at and above it,
|
|
223
|
-
* the skills the message's terms reach are highlighted and the rest are
|
|
224
|
-
* named, so the agent can still activate one on its own judgement (the Agent
|
|
225
|
-
* Skills spec's progressive disclosure: metadata up front, instructions on
|
|
226
|
-
* demand). The native harness calls this on every turn; the Cursor harness
|
|
227
|
-
* lists every skill today; adopting the selection there is an open item.
|
|
228
|
-
*/
|
|
229
|
-
export function selectSkillsForPrompt(userMessage: string, skills: readonly SkillMetadata[]): SkillSelection {
|
|
230
|
-
const filter = filterSkills(
|
|
231
|
-
userMessage,
|
|
232
|
-
skills.map((s) => s.name),
|
|
233
|
-
skills.map((s) => s.description),
|
|
234
|
-
);
|
|
235
|
-
return {
|
|
236
|
-
highlighted: filter.includedIndices.map((i) => skills[i]),
|
|
237
|
-
alsoAvailable: filter.excludedNames,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* The tool-neutral sentence that names the skills a prompt did not highlight
|
|
243
|
-
* (`SkillSelection.alsoAvailable`), so the agent can activate one by reading
|
|
244
|
-
* its SKILL.md at the mounted path. The sentence names no tool, so it serves
|
|
245
|
-
* any harness; the heading above it is the builder's framing. Empty for no
|
|
246
|
-
* names.
|
|
247
|
-
*/
|
|
248
|
-
export function alsoAvailableSkillsNote(excludedNames: readonly string[]): string {
|
|
249
|
-
if (excludedNames.length === 0) return "";
|
|
250
|
-
const names = excludedNames.map((n) => `\`${n}\``).join(", ");
|
|
251
|
-
return (
|
|
252
|
-
`These skills are installed but were not highlighted above: ${names}. ` +
|
|
253
|
-
"If you determine one of them is relevant to your task, " +
|
|
254
|
-
`read its SKILL.md at \`${STIGMER_LOCAL_STATE_DIR}/skills/<name>/SKILL.md\` to activate it.`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
* into its prompt in its own placement. Moved from
|
|
15
15
|
* `activities/execute-cursor/` in #1070; the native harness's twin
|
|
16
16
|
* (`skill-writer.ts` `fetchSkillsByRefs` + `mountSkills`) retired in #1096,
|
|
17
|
-
* and the module itself with it
|
|
18
|
-
*
|
|
19
|
-
* skills path for every harness.
|
|
17
|
+
* and the module itself with it. This is the one skills path for every
|
|
18
|
+
* harness.
|
|
20
19
|
*
|
|
21
20
|
* The mount is cached by the skill's content-addressed version hash
|
|
22
21
|
* (stigmer/stigmer#672): metadata is fetched on every execution (that keeps
|
|
@@ -73,14 +73,17 @@ describe("provisionGit", () => {
|
|
|
73
73
|
expect(cmds.some((c) => c.includes("git checkout 'develop'"))).toBe(true);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it("
|
|
76
|
+
it("describes the clone by repo URL and branch, never by its commit", async () => {
|
|
77
77
|
const backend = mockWorkspaceBackend({
|
|
78
78
|
execute: routingExecute({ branch: "main\n", sha: "sha789\n" }),
|
|
79
79
|
});
|
|
80
80
|
const result = await provisionGit(makeOptions({ backend }));
|
|
81
81
|
expect(result.workspaceDescription).toContain("github.com/org/repo.git");
|
|
82
|
-
expect(result.workspaceDescription).toContain("main");
|
|
83
|
-
|
|
82
|
+
expect(result.workspaceDescription).toContain("(branch: main)");
|
|
83
|
+
// The native system prompt carries this text on every turn; write-back
|
|
84
|
+
// commits move HEAD every approved turn, so a commit here would change it.
|
|
85
|
+
expect(result.workspaceDescription).not.toContain("sha789");
|
|
86
|
+
expect(result.gitMetadata?.baseCommit, "the commit stays in the metadata write-back reads").toBe("sha789");
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
// ── Default-branch resolution ─────────────────────────────────────
|
|
@@ -120,7 +123,10 @@ describe("provisionGit", () => {
|
|
|
120
123
|
|
|
121
124
|
const result = await provisionGit(makeOptions({ backend }));
|
|
122
125
|
expect(result.sourceType).toBe("git_repo");
|
|
123
|
-
|
|
126
|
+
const fresh = await provisionGit(
|
|
127
|
+
makeOptions({ backend: mockWorkspaceBackend({ execute: routingExecute() }) }),
|
|
128
|
+
);
|
|
129
|
+
expect(result.workspaceDescription, "a reused clone is described exactly as a fresh one").toBe(fresh.workspaceDescription);
|
|
124
130
|
|
|
125
131
|
expect(calls(backend).some((c) => c.includes("git init"))).toBe(false);
|
|
126
132
|
expect(calls(backend).some((c) => c.includes("git fetch"))).toBe(false);
|
|
@@ -395,7 +401,7 @@ describe("provisionGit", () => {
|
|
|
395
401
|
}));
|
|
396
402
|
|
|
397
403
|
expect(result.gitMetadata!.gitCredentialsConfigured).toBe(true);
|
|
398
|
-
expect(
|
|
404
|
+
expect(calls(backend).some((c) => c.includes("git init")), "the reuse path, not a fresh clone").toBe(false);
|
|
399
405
|
});
|
|
400
406
|
|
|
401
407
|
it("handles credential setup failure gracefully", async () => {
|