@stigmer/runner 3.7.0 → 3.8.0
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/activities/execute-cursor/index.d.ts +12 -0
- package/dist/activities/execute-cursor/index.js +41 -5
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +28 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js +22 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -0
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +11 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +30 -4
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/shared/channel-attachment.d.ts +3 -1
- package/dist/shared/channel-attachment.js +3 -1
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/conversation-attachment.d.ts +81 -0
- package/dist/shared/conversation-attachment.js +102 -0
- package/dist/shared/conversation-attachment.js.map +1 -0
- package/dist/shared/conversation-catchup.d.ts +33 -0
- package/dist/shared/conversation-catchup.js +53 -0
- package/dist/shared/conversation-catchup.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
- package/src/activities/execute-cursor/index.ts +62 -4
- package/src/activities/execute-cursor/prompt-builder.ts +23 -0
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +42 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +39 -1
- package/src/activities/execute-deep-agent/mcp-gate.ts +37 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -2
- package/src/activities/execute-deep-agent/setup.ts +40 -4
- package/src/shared/__tests__/channel-attachment.test.ts +3 -3
- package/src/shared/__tests__/conversation-attachment.test.ts +138 -0
- package/src/shared/__tests__/conversation-catchup.test.ts +70 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +120 -0
- package/src/shared/channel-attachment.ts +3 -1
- package/src/shared/conversation-attachment.ts +115 -0
- package/src/shared/conversation-catchup.ts +60 -0
|
@@ -519,3 +519,82 @@ describe("formatImplementPlanSection", () => {
|
|
|
519
519
|
expect(prompt).toBe(USER_MESSAGE);
|
|
520
520
|
});
|
|
521
521
|
});
|
|
522
|
+
|
|
523
|
+
describe("conversation catchup (cloud DD-006, T03 Sitting 3)", () => {
|
|
524
|
+
const DIGEST =
|
|
525
|
+
"Customer: where is my order?\nTeammate: I've refunded you in full.";
|
|
526
|
+
|
|
527
|
+
it("prefixes the catchup on a RESUMED turn — handback lands mid-session, the case the metadata lane cannot reach", () => {
|
|
528
|
+
const prompt = buildPrompt(
|
|
529
|
+
input({
|
|
530
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
531
|
+
conversationCatchup: DIGEST,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
expect(prompt.startsWith("<conversation_catchup>")).toBe(true);
|
|
536
|
+
expect(prompt).toContain(DIGEST);
|
|
537
|
+
expect(prompt.endsWith(USER_MESSAGE)).toBe(true);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("orders a resumed turn's prefixes directives-first, catchup last — context sits closest to the task", () => {
|
|
541
|
+
const prompt = buildPrompt(
|
|
542
|
+
input({
|
|
543
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
544
|
+
interactionMode: InteractionMode.PLAN,
|
|
545
|
+
conversationCatchup: DIGEST,
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect(prompt.indexOf("<interaction_mode>"))
|
|
550
|
+
.toBeLessThan(prompt.indexOf("<conversation_catchup>"));
|
|
551
|
+
expect(prompt.indexOf("<conversation_catchup>"))
|
|
552
|
+
.toBeLessThan(prompt.indexOf(USER_MESSAGE));
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("carries the catchup on the first execution too, AFTER the bridge (DD-007 D-d: bridge first, catchup second)", () => {
|
|
556
|
+
const prompt = buildPrompt(
|
|
557
|
+
input({
|
|
558
|
+
resolution: resolution("local", "created_first_execution"),
|
|
559
|
+
contextBridge: "User: hi\nAssistant: hello",
|
|
560
|
+
conversationCatchup: DIGEST,
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
expect(prompt).toContain("<conversation_catchup>");
|
|
565
|
+
expect(prompt.indexOf("<previous_conversation_context>"))
|
|
566
|
+
.toBeLessThan(prompt.indexOf("<conversation_catchup>"));
|
|
567
|
+
// Still CONTEXT: the approval protocol keeps its pinned
|
|
568
|
+
// last-before-task slot.
|
|
569
|
+
expect(prompt.indexOf("<conversation_catchup>"))
|
|
570
|
+
.toBeLessThan(prompt.indexOf("<tool_approval_protocol>"));
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("never reaches a HITL reinvocation — the same turn's original prompt already carried it", () => {
|
|
574
|
+
const decisions = new Map([["call-1", ApprovalAction.APPROVE]]);
|
|
575
|
+
const prompt = buildPrompt(
|
|
576
|
+
input({
|
|
577
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
578
|
+
approvalDecisions: decisions,
|
|
579
|
+
pendingApprovals: [
|
|
580
|
+
create(PendingApprovalSchema, { toolCallId: "call-1", message: "Write file: a.txt" }),
|
|
581
|
+
],
|
|
582
|
+
conversationCatchup: DIGEST,
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(prompt).not.toContain("<conversation_catchup>");
|
|
587
|
+
expect(prompt).not.toContain(DIGEST);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("a resumed turn without a catchup stays the bare user message — most turns carry none", () => {
|
|
591
|
+
const prompt = buildPrompt(
|
|
592
|
+
input({
|
|
593
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
594
|
+
conversationCatchup: undefined,
|
|
595
|
+
}),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
expect(prompt).toBe(USER_MESSAGE);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
@@ -48,6 +48,7 @@ import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantTo
|
|
|
48
48
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
49
49
|
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
50
50
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
51
|
+
import { readConversationCatchup } from "../../shared/conversation-catchup.js";
|
|
51
52
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
52
53
|
import {
|
|
53
54
|
injectCallerIdentityEnv,
|
|
@@ -69,6 +70,10 @@ import {
|
|
|
69
70
|
discoverChannelMessaging,
|
|
70
71
|
synthesizeChannelAttachment,
|
|
71
72
|
} from "../../shared/channel-attachment.js";
|
|
73
|
+
import {
|
|
74
|
+
readChannelConversationId,
|
|
75
|
+
synthesizeConversationAttachment,
|
|
76
|
+
} from "../../shared/conversation-attachment.js";
|
|
72
77
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
73
78
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
74
79
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
@@ -79,7 +84,7 @@ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
|
79
84
|
import { resolveSkills } from "./skill-resolver.js";
|
|
80
85
|
import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
|
|
81
86
|
import { resolveAttachments } from "./attachment-resolver.js";
|
|
82
|
-
import { buildEnhancedPrompt, buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
87
|
+
import { buildEnhancedPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
83
88
|
import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
|
|
84
89
|
import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
|
|
85
90
|
import {
|
|
@@ -685,6 +690,31 @@ async function executeCursorInner(
|
|
|
685
690
|
};
|
|
686
691
|
}
|
|
687
692
|
}
|
|
693
|
+
|
|
694
|
+
// Phase 4a4: Synthesize the conversation participation attachment
|
|
695
|
+
// (channel-conversations DD-008 D-c) — the third sibling. The
|
|
696
|
+
// channel-id session label IS the attachment decision (stamped
|
|
697
|
+
// server-side on every channel session; a free local read, unlike
|
|
698
|
+
// the channels discovery RPC above). HTTP-only: synthesize answers
|
|
699
|
+
// undefined with no bridge endpoint by design (see
|
|
700
|
+
// shared/conversation-attachment.ts).
|
|
701
|
+
const conversationAttachment = synthesizeConversationAttachment(
|
|
702
|
+
readChannelConversationId(session.metadata?.labels),
|
|
703
|
+
{
|
|
704
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
705
|
+
credential: attachmentCredential,
|
|
706
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
if (conversationAttachment) {
|
|
710
|
+
const resolvedServers = injectSynthesizedAttachment(
|
|
711
|
+
mcpResolution.resolvedServers, conversationAttachment, "conversation participation",
|
|
712
|
+
);
|
|
713
|
+
mcpResolution = {
|
|
714
|
+
resolvedServers,
|
|
715
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
688
718
|
const mcpConfig = mcpResolution.cursorConfig;
|
|
689
719
|
|
|
690
720
|
// Phase 4b: Merge approval policies from all layers.
|
|
@@ -1071,6 +1101,7 @@ async function executeCursorInner(
|
|
|
1071
1101
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1072
1102
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1073
1103
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1104
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1074
1105
|
});
|
|
1075
1106
|
|
|
1076
1107
|
// Phase 10a: Inject structured output instruction for Cursor harness
|
|
@@ -1706,9 +1737,15 @@ async function executeCursorInner(
|
|
|
1706
1737
|
attachmentPaths,
|
|
1707
1738
|
pendingApprovals: adjudicatedApprovals,
|
|
1708
1739
|
interactionMode,
|
|
1740
|
+
// buildFromPlan was silently dropped here until T03 Sitting 3 —
|
|
1741
|
+
// a build turn that hit handle recovery lost its directive. The
|
|
1742
|
+
// fresh prompt must carry every per-turn directive the original
|
|
1743
|
+
// did.
|
|
1744
|
+
buildFromPlan,
|
|
1709
1745
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1710
1746
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1711
1747
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1748
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1712
1749
|
});
|
|
1713
1750
|
|
|
1714
1751
|
console.log(
|
|
@@ -2305,6 +2342,18 @@ export interface BuildPromptInput {
|
|
|
2305
2342
|
* turn.
|
|
2306
2343
|
*/
|
|
2307
2344
|
sessionContext?: string;
|
|
2345
|
+
/**
|
|
2346
|
+
* Conversation catchup from the execution spec's `conversation_catchup`
|
|
2347
|
+
* (cloud DD-006): what happened on the channel conversation that the
|
|
2348
|
+
* agent has not seen. PER-TURN, so unlike the three standing values
|
|
2349
|
+
* above it rides BOTH prompt paths — the enhanced prompt and a resumed
|
|
2350
|
+
* turn's prefix (the `interaction_mode` shape). Handback lands
|
|
2351
|
+
* mid-session on a resumed agent: the resumed path is the one that
|
|
2352
|
+
* matters. Once delivered, the digest persists in the agent's own
|
|
2353
|
+
* conversation store; the next turn's field is composed fresh and is
|
|
2354
|
+
* usually blank.
|
|
2355
|
+
*/
|
|
2356
|
+
conversationCatchup?: string;
|
|
2308
2357
|
}
|
|
2309
2358
|
|
|
2310
2359
|
/**
|
|
@@ -2336,6 +2385,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2336
2385
|
attachmentPaths,
|
|
2337
2386
|
interactionMode,
|
|
2338
2387
|
buildFromPlan,
|
|
2388
|
+
conversationCatchup,
|
|
2339
2389
|
} = input;
|
|
2340
2390
|
|
|
2341
2391
|
const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
@@ -2353,15 +2403,22 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2353
2403
|
|
|
2354
2404
|
// A successfully resumed agent carries its own conversation context via the
|
|
2355
2405
|
// SDK's native store — send the raw user message with no preamble. The
|
|
2356
|
-
// exceptions are the per-EXECUTION
|
|
2406
|
+
// exceptions are the per-EXECUTION values, which never inherit from the
|
|
2357
2407
|
// session's first turn: the interaction-mode prefix (a follow-up can switch
|
|
2358
2408
|
// Agent→Plan mid-session, and for Cursor the prompt is the only plan-mode
|
|
2359
|
-
// enforcement)
|
|
2360
|
-
//
|
|
2409
|
+
// enforcement), the implement-plan directive (the build turn is usually a
|
|
2410
|
+
// follow-up on a resumed agent), and the conversation catchup (handback
|
|
2411
|
+
// ALWAYS lands mid-session on a resumed agent — this prefix is the property
|
|
2412
|
+
// the metadata lane structurally cannot deliver, cloud DD-006). Catchup
|
|
2413
|
+
// last: it is context, and context sits closest to the task (the enhanced
|
|
2414
|
+
// prompt's own ordering doctrine).
|
|
2361
2415
|
if (resolution.reason === "resumed_successfully") {
|
|
2362
2416
|
const prefixes = [
|
|
2363
2417
|
formatInteractionModePrefix(interactionMode),
|
|
2364
2418
|
formatImplementPlanSection(buildFromPlan, attachmentPaths),
|
|
2419
|
+
conversationCatchup !== undefined
|
|
2420
|
+
? formatConversationCatchupSection(conversationCatchup)
|
|
2421
|
+
: undefined,
|
|
2365
2422
|
].filter((p): p is string => p !== undefined);
|
|
2366
2423
|
return prefixes.length > 0
|
|
2367
2424
|
? [...prefixes, userMessage].join("\n\n")
|
|
@@ -2386,6 +2443,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2386
2443
|
contextBridge: input.contextBridge,
|
|
2387
2444
|
senderIdentity: input.senderIdentity,
|
|
2388
2445
|
sessionContext: input.sessionContext,
|
|
2446
|
+
conversationCatchup,
|
|
2389
2447
|
});
|
|
2390
2448
|
}
|
|
2391
2449
|
|
|
@@ -21,6 +21,7 @@ import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agenti
|
|
|
21
21
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
22
22
|
import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
23
23
|
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
24
|
+
import { formatConversationCatchupText } from "../../shared/conversation-catchup.js";
|
|
24
25
|
import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
|
|
25
26
|
import {
|
|
26
27
|
formatChannelTemplatesSection,
|
|
@@ -107,6 +108,16 @@ export interface EnhancedPromptOptions {
|
|
|
107
108
|
* store — the context is constant for the session's lifetime.
|
|
108
109
|
*/
|
|
109
110
|
sessionContext?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Conversation catchup (cloud DD-006): what happened on the channel
|
|
113
|
+
* conversation that the agent has not seen, read from the execution
|
|
114
|
+
* spec's `conversation_catchup`. PER-TURN, unlike the three standing
|
|
115
|
+
* siblings above: it rides BOTH prompt paths — this enhanced prompt and
|
|
116
|
+
* a resumed turn's prefix (the `interaction_mode` shape) — because
|
|
117
|
+
* handback lands mid-session on a resumed agent, the exact case the
|
|
118
|
+
* metadata lane cannot reach.
|
|
119
|
+
*/
|
|
120
|
+
conversationCatchup?: string;
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
/**
|
|
@@ -200,6 +211,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
200
211
|
sections.push(formatContextBridgeSection(options.contextBridge));
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
// Catchup after the bridge (DD-007 D-d: bridge first, catchup second) —
|
|
215
|
+
// the bridge carries the pre-takeover conversation, the catchup the human
|
|
216
|
+
// episode, strictly newer by construction; recency puts it closer to the
|
|
217
|
+
// task.
|
|
218
|
+
if (options.conversationCatchup) {
|
|
219
|
+
sections.push(formatConversationCatchupSection(options.conversationCatchup));
|
|
220
|
+
}
|
|
221
|
+
|
|
203
222
|
// Always last before the task: the platform's tool-approval protocol. Placed
|
|
204
223
|
// here for recency so it outweighs any "ask the user first" guidance Cursor
|
|
205
224
|
// surfaces from a connected MCP server (see formatToolApprovalProtocol).
|
|
@@ -345,6 +364,10 @@ export function formatSessionContextSection(context: string): string {
|
|
|
345
364
|
return `<session_context>\n${formatSessionContextText(context)}\n</session_context>`;
|
|
346
365
|
}
|
|
347
366
|
|
|
367
|
+
export function formatConversationCatchupSection(digest: string): string {
|
|
368
|
+
return `<conversation_catchup>\n${formatConversationCatchupText(digest)}\n</conversation_catchup>`;
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
export function formatSkillsSection(skills: SkillMetadata[]): string {
|
|
349
372
|
const entries = skills.map(
|
|
350
373
|
(s) => `- **${s.name}**: ${s.description}\n Path: \`${s.path}\``,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deep-agent MCP gate, arm by arm. A missing arm here means a tool
|
|
3
|
+
* source silently dropped for exactly the agents whose only source it
|
|
4
|
+
* is — the failure DD-006 D7 documented for channel messaging and the
|
|
5
|
+
* reason the predicate lives in its own testable module instead of
|
|
6
|
+
* inline in setup.ts (whose import graph forbids a setup.test.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { shouldConnectMcp } from "../mcp-gate.js";
|
|
12
|
+
|
|
13
|
+
const nothing = {
|
|
14
|
+
mcpServerUsageCount: 0,
|
|
15
|
+
datastoreUsageCount: 0,
|
|
16
|
+
channelMessagingCount: 0,
|
|
17
|
+
conversationChannelId: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("shouldConnectMcp", () => {
|
|
21
|
+
it("skips the MCP block when no tool source exists", () => {
|
|
22
|
+
expect(shouldConnectMcp(nothing)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("enters on declared MCP server usages alone", () => {
|
|
26
|
+
expect(shouldConnectMcp({ ...nothing, mcpServerUsageCount: 1 })).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("enters on datastore usages alone (the records attachment)", () => {
|
|
30
|
+
expect(shouldConnectMcp({ ...nothing, datastoreUsageCount: 1 })).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("enters on a serving proactive channel alone (the channels attachment)", () => {
|
|
34
|
+
expect(shouldConnectMcp({ ...nothing, channelMessagingCount: 1 })).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("enters on a channel conversation alone (the conversation attachment)", () => {
|
|
38
|
+
// The reply-only pilot shape: no declared servers, no datastores,
|
|
39
|
+
// no proactive channel — the escalation tool is the ONLY source.
|
|
40
|
+
expect(shouldConnectMcp({ ...nothing, conversationChannelId: "agch_1" })).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
3
|
-
import { buildEnhancedSystemPrompt } from "../prompt-builder.js";
|
|
3
|
+
import { buildEnhancedSystemPrompt, composeUserMessage } from "../prompt-builder.js";
|
|
4
4
|
import { PLAN_MODE_DIRECTIVE } from "../../../shared/plan-mode-prompt.js";
|
|
5
5
|
import { SourceType } from "../../../shared/workspace/types.js";
|
|
6
6
|
import type { ProvisionResult } from "../../../shared/workspace/types.js";
|
|
@@ -408,3 +408,41 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
408
408
|
});
|
|
409
409
|
});
|
|
410
410
|
});
|
|
411
|
+
|
|
412
|
+
describe("composeUserMessage (conversation catchup, cloud DD-006 / A27)", () => {
|
|
413
|
+
const MESSAGE = "where is my order?";
|
|
414
|
+
const DIGEST =
|
|
415
|
+
"Customer: I want a refund\nTeammate: I've refunded you in full.";
|
|
416
|
+
|
|
417
|
+
it("prepends the framed catchup to the turn's user message — history durability rides the checkpointer, not the rebuilt system prompt", () => {
|
|
418
|
+
const composed = composeUserMessage(MESSAGE, DIGEST);
|
|
419
|
+
|
|
420
|
+
expect(composed.endsWith(MESSAGE)).toBe(true);
|
|
421
|
+
expect(composed).toContain(DIGEST);
|
|
422
|
+
expect(composed).toContain("you have not seen");
|
|
423
|
+
expect(composed.indexOf(DIGEST)).toBeLessThan(composed.indexOf(MESSAGE));
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("separates the catchup from the customer's message with a horizontal rule", () => {
|
|
427
|
+
expect(composeUserMessage(MESSAGE, DIGEST)).toContain("\n\n---\n\n");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("leaves the message untouched when there is no catchup — most turns carry none", () => {
|
|
431
|
+
expect(composeUserMessage(MESSAGE, undefined)).toBe(MESSAGE);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("never renders in the system prompt — the rebuilt-per-invocation lane would forget the digest one turn later", () => {
|
|
435
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
436
|
+
instructions: "Test",
|
|
437
|
+
provisionResults: [],
|
|
438
|
+
containerRoot: "",
|
|
439
|
+
skillsPromptSection: "",
|
|
440
|
+
workspaceFileRefs: [],
|
|
441
|
+
workspaceRoot: "/workspace",
|
|
442
|
+
injectedFiles: [],
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(prompt).not.toContain("Conversation catchup");
|
|
446
|
+
expect(prompt).not.toContain("you have not seen");
|
|
447
|
+
});
|
|
448
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deep-agent harness's MCP gate: whether this execution enters MCP
|
|
3
|
+
* resolution at all (resolve, backfill, synthesized-attachment
|
|
4
|
+
* injection, connect). Unlike the Cursor harness — which resolves MCP
|
|
5
|
+
* unconditionally — deep-agent skips the whole block when no tool
|
|
6
|
+
* source exists, so EVERY tool source must appear here or its tools are
|
|
7
|
+
* silently dropped for exactly the agents whose only source it is
|
|
8
|
+
* (proactive-messaging DD-006 D7 learned this for channel messaging).
|
|
9
|
+
*
|
|
10
|
+
* Extracted from setup.ts as a pure function because setup.ts is
|
|
11
|
+
* untestable at file load (its import graph is why no setup.test.ts
|
|
12
|
+
* exists); the gate is the one piece whose regression is silent, so it
|
|
13
|
+
* gets its own module and an arm-by-arm test.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One flag per tool source. Adding a source? It gates here or it is dropped. */
|
|
17
|
+
export interface McpToolSources {
|
|
18
|
+
/** Declared MCP server usages (agent spec + session spec). */
|
|
19
|
+
readonly mcpServerUsageCount: number;
|
|
20
|
+
/** Declared datastore usages (the records attachment, T05). */
|
|
21
|
+
readonly datastoreUsageCount: number;
|
|
22
|
+
/** Serving proactive-messaging channels (the channels attachment, DD-006). */
|
|
23
|
+
readonly channelMessagingCount: number;
|
|
24
|
+
/** The serving channel id when this session IS a live channel
|
|
25
|
+
* conversation (the conversation attachment, DD-008). */
|
|
26
|
+
readonly conversationChannelId: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when any tool source demands MCP resolution and connect. */
|
|
30
|
+
export function shouldConnectMcp(sources: McpToolSources): boolean {
|
|
31
|
+
return (
|
|
32
|
+
sources.mcpServerUsageCount > 0 ||
|
|
33
|
+
sources.datastoreUsageCount > 0 ||
|
|
34
|
+
sources.channelMessagingCount > 0 ||
|
|
35
|
+
sources.conversationChannelId !== undefined
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -11,6 +11,7 @@ import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecuti
|
|
|
11
11
|
import type { ProvisionResult, GitMetadata } from "../../shared/workspace/types.js";
|
|
12
12
|
import { SourceType } from "../../shared/workspace/types.js";
|
|
13
13
|
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
14
|
+
import { formatConversationCatchupText } from "../../shared/conversation-catchup.js";
|
|
14
15
|
import {
|
|
15
16
|
formatSenderIdentityText,
|
|
16
17
|
type SenderIdentity,
|
|
@@ -241,9 +242,28 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
241
242
|
return prompt;
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Compose the turn's USER MESSAGE for the graph invocation: the framed
|
|
247
|
+
* conversation catchup (cloud DD-006), when present, prepended to the
|
|
248
|
+
* customer's message. In the user message and never the system prompt (A27):
|
|
249
|
+
* the system prompt is rebuilt per invocation and would forget the digest one
|
|
250
|
+
* turn later, while a message enters the checkpointer with the turn and
|
|
251
|
+
* persists in history — the same durability the cursor harness gets from its
|
|
252
|
+
* prompt prefix. The caller's `spec.message` is never mutated; the prepend
|
|
253
|
+
* exists only in the graph input.
|
|
254
|
+
*/
|
|
255
|
+
export function composeUserMessage(
|
|
256
|
+
message: string,
|
|
257
|
+
conversationCatchup: string | undefined,
|
|
258
|
+
): string {
|
|
259
|
+
return conversationCatchup
|
|
260
|
+
? `${formatConversationCatchupText(conversationCatchup)}\n\n---\n\n${message}`
|
|
261
|
+
: message;
|
|
262
|
+
}
|
|
263
|
+
|
|
244
264
|
function buildWorkspacePromptSection(
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
provisionResults: ProvisionResult[],
|
|
266
|
+
containerRoot: string,
|
|
247
267
|
): string {
|
|
248
268
|
if (provisionResults.length === 0) return "";
|
|
249
269
|
|
|
@@ -25,6 +25,7 @@ import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
|
25
25
|
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
26
26
|
import { createCheckpointer } from "../../shared/checkpointer/factory.js";
|
|
27
27
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
28
|
+
import { readConversationCatchup } from "../../shared/conversation-catchup.js";
|
|
28
29
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
29
30
|
import {
|
|
30
31
|
injectCallerIdentityEnv,
|
|
@@ -44,7 +45,12 @@ import {
|
|
|
44
45
|
formatChannelTemplatesSection,
|
|
45
46
|
synthesizeChannelAttachment,
|
|
46
47
|
} from "../../shared/channel-attachment.js";
|
|
48
|
+
import {
|
|
49
|
+
readChannelConversationId,
|
|
50
|
+
synthesizeConversationAttachment,
|
|
51
|
+
} from "../../shared/conversation-attachment.js";
|
|
47
52
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
53
|
+
import { shouldConnectMcp } from "./mcp-gate.js";
|
|
48
54
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
49
55
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
50
56
|
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
@@ -59,7 +65,7 @@ import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root
|
|
|
59
65
|
import { buildWorkspaceFileTree } from "../../shared/workspace/file-tree.js";
|
|
60
66
|
import { reportSetupProgress } from "../../shared/status.js";
|
|
61
67
|
import { resolveEnvironment, type EnvironmentResult } from "./environment.js";
|
|
62
|
-
import { buildEnhancedSystemPrompt } from "./prompt-builder.js";
|
|
68
|
+
import { buildEnhancedSystemPrompt, composeUserMessage } from "./prompt-builder.js";
|
|
63
69
|
import { buildMiddlewareStack } from "../../middleware/index.js";
|
|
64
70
|
import type { GracefulStopMiddleware } from "../../middleware/index.js";
|
|
65
71
|
import { createThinkTool, createWebFetchTool, resolveGuardPosture } from "../../tools/index.js";
|
|
@@ -361,8 +367,18 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
361
367
|
// answer — no tool, no section, execution unharmed.
|
|
362
368
|
const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
|
|
363
369
|
|
|
370
|
+
// The conversation-attachment decision (DD-008 D-c): the channel-id
|
|
371
|
+
// session label, stamped server-side on every channel session — a
|
|
372
|
+
// free, synchronous read, so no hoisted discovery needed.
|
|
373
|
+
const conversationChannelId = readChannelConversationId(session.metadata?.labels);
|
|
374
|
+
|
|
364
375
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
365
|
-
if (
|
|
376
|
+
if (shouldConnectMcp({
|
|
377
|
+
mcpServerUsageCount: mcpServerUsages.length,
|
|
378
|
+
datastoreUsageCount: datastoreUsages.length,
|
|
379
|
+
channelMessagingCount: channelMessaging.length,
|
|
380
|
+
conversationChannelId,
|
|
381
|
+
})) {
|
|
366
382
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
367
383
|
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
368
384
|
// The MCP-bound env map (and ONLY it) carries the reserved
|
|
@@ -424,6 +440,21 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
424
440
|
);
|
|
425
441
|
}
|
|
426
442
|
}
|
|
443
|
+
|
|
444
|
+
// The conversation participation attachment (DD-008 D-c) — the
|
|
445
|
+
// third sibling, same after-backfill rule. HTTP-only: synthesize
|
|
446
|
+
// answers undefined with no bridge endpoint by design (see
|
|
447
|
+
// shared/conversation-attachment.ts).
|
|
448
|
+
const conversationAttachment = synthesizeConversationAttachment(conversationChannelId, {
|
|
449
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
450
|
+
credential: attachmentCredential,
|
|
451
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
452
|
+
});
|
|
453
|
+
if (conversationAttachment) {
|
|
454
|
+
backfilledServers = injectSynthesizedAttachment(
|
|
455
|
+
backfilledServers, conversationAttachment, "conversation participation",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
427
458
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
428
459
|
timing.mark("backfill_mcp");
|
|
429
460
|
|
|
@@ -743,8 +774,13 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
743
774
|
...(isPlanMode ? { permissions: planModePermissions } : {}),
|
|
744
775
|
} as Parameters<typeof createDeepAgent>[0]);
|
|
745
776
|
|
|
746
|
-
// Step 11: Prepare invocation input and config
|
|
747
|
-
|
|
777
|
+
// Step 11: Prepare invocation input and config. The conversation catchup
|
|
778
|
+
// (cloud DD-006) rides the USER MESSAGE, not the system prompt — see
|
|
779
|
+
// composeUserMessage for the durability rationale (A27).
|
|
780
|
+
let userMessage = composeUserMessage(
|
|
781
|
+
execution.spec!.message,
|
|
782
|
+
readConversationCatchup(execution.spec!.conversationCatchup),
|
|
783
|
+
);
|
|
748
784
|
if (outputSchema) {
|
|
749
785
|
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.`;
|
|
750
786
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* discovery with the never-throw failure posture, both connection
|
|
4
4
|
* shapes, the structural approval-freedom the datastore attachment
|
|
5
5
|
* pinned before it, and the prompt section's filter/order/cap rules
|
|
6
|
-
* (DD-006 D6). The cross-repo
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* (DD-006 D6). The route is the cross-repo string, guarded here and in
|
|
7
|
+
* the mcp-server integration test (the TOOL_CALL_LIMIT precedent); the
|
|
8
|
+
* slug and roster are runner-internal and guarded here alone.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { describe, expect, it, vi } from "vitest";
|