@stigmer/runner 3.7.0 → 3.9.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/README.md +2 -2
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +16 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +56 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +27 -0
- package/dist/activities/execute-cursor/index.js +101 -10
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +25 -1
- package/dist/activities/execute-cursor/prompt-builder.js +22 -2
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +17 -0
- package/dist/activities/execute-deep-agent/attachment-injector.js +34 -4
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +15 -7
- package/dist/activities/execute-deep-agent/hitl.js +6 -15
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +4 -1
- package/dist/activities/execute-deep-agent/index.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 +28 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +29 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +71 -6
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/runner-manager.js +14 -0
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +14 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +10 -0
- package/dist/shared/artifact-storage.js +49 -8
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-vision.d.ts +203 -0
- package/dist/shared/attachment-vision.js +264 -0
- package/dist/shared/attachment-vision.js.map +1 -0
- 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 +3 -3
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +179 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +183 -2
- package/src/activities/execute-cursor/attachment-resolver.ts +90 -4
- package/src/activities/execute-cursor/index.ts +143 -11
- package/src/activities/execute-cursor/prompt-builder.ts +50 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +185 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +13 -13
- 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/__tests__/vision-input.test.ts +152 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +65 -4
- package/src/activities/execute-deep-agent/hitl.ts +14 -19
- package/src/activities/execute-deep-agent/index.ts +4 -5
- package/src/activities/execute-deep-agent/mcp-gate.ts +37 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +59 -4
- package/src/activities/execute-deep-agent/setup.ts +90 -6
- package/src/runner-manager.ts +19 -0
- package/src/runner.ts +19 -0
- package/src/shared/__tests__/artifact-storage.test.ts +76 -1
- package/src/shared/__tests__/attachment-vision.test.ts +323 -0
- 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/artifact-storage.ts +55 -8
- package/src/shared/attachment-vision.ts +373 -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
|
@@ -37,10 +37,15 @@ const ACTION_MAP: ReadonlyMap<ApprovalAction, string> = new Map([
|
|
|
37
37
|
[ApprovalAction.REJECT, "reject"],
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Either a genuine approval resume carrying the `Command(resume)` payload, or
|
|
42
|
+
* not one — in which case the caller uses `setup.langgraphInput` (the single
|
|
43
|
+
* construction site of the turn's user message; this module deliberately does
|
|
44
|
+
* NOT build a second copy that could drift from it).
|
|
45
|
+
*/
|
|
46
|
+
export type ResumeResult =
|
|
47
|
+
| { readonly isResumeFromApproval: true; readonly graphInput: Command }
|
|
48
|
+
| { readonly isResumeFromApproval: false };
|
|
44
49
|
|
|
45
50
|
export interface GraphStateSnapshot {
|
|
46
51
|
readonly values: Record<string, unknown>;
|
|
@@ -71,8 +76,8 @@ export interface InterruptValue {
|
|
|
71
76
|
* Resolve the resume input for a reinvocation after approval.
|
|
72
77
|
*
|
|
73
78
|
* Returns a `Command(resume=...)` if there are pending interrupts with
|
|
74
|
-
* matching approval decisions
|
|
75
|
-
*
|
|
79
|
+
* matching approval decisions; otherwise reports "not a resume" and the
|
|
80
|
+
* caller falls back to `setup.langgraphInput`.
|
|
76
81
|
*
|
|
77
82
|
* The graph checkpoint snapshot is read once by the caller and passed in: the
|
|
78
83
|
* same snapshot also decides whether status must be seeded from the persisted
|
|
@@ -82,22 +87,15 @@ export interface InterruptValue {
|
|
|
82
87
|
export function resolveResumeInput(
|
|
83
88
|
execution: AgentExecution,
|
|
84
89
|
graphState: GraphStateSnapshot,
|
|
85
|
-
userMessage: string,
|
|
86
90
|
): ResumeResult {
|
|
87
91
|
const pendingInterrupts = extractPendingInterrupts(graphState);
|
|
88
92
|
if (pendingInterrupts.length === 0) {
|
|
89
|
-
return {
|
|
90
|
-
graphInput: { messages: [{ role: "user", content: userMessage }] },
|
|
91
|
-
isResumeFromApproval: false,
|
|
92
|
-
};
|
|
93
|
+
return { isResumeFromApproval: false };
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const decisions = extractApprovalDecisions(execution);
|
|
96
97
|
if (decisions.size === 0) {
|
|
97
|
-
return {
|
|
98
|
-
graphInput: { messages: [{ role: "user", content: userMessage }] },
|
|
99
|
-
isResumeFromApproval: false,
|
|
100
|
-
};
|
|
98
|
+
return { isResumeFromApproval: false };
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
const resumeDict: Record<string, { action: string; comment?: string }> = {};
|
|
@@ -117,10 +115,7 @@ export function resolveResumeInput(
|
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
if (Object.keys(resumeDict).length === 0) {
|
|
120
|
-
return {
|
|
121
|
-
graphInput: { messages: [{ role: "user", content: userMessage }] },
|
|
122
|
-
isResumeFromApproval: false,
|
|
123
|
-
};
|
|
118
|
+
return { isResumeFromApproval: false };
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
console.log(
|
|
@@ -161,12 +161,11 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
161
161
|
unattended: setup.unattended,
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
const resume = resolveResumeInput(
|
|
165
|
-
setup.execution,
|
|
166
|
-
graphState,
|
|
167
|
-
setup.execution.spec!.message,
|
|
168
|
-
);
|
|
164
|
+
const resume = resolveResumeInput(setup.execution, graphState);
|
|
169
165
|
|
|
166
|
+
// Not an approval resume -> setup.langgraphInput, the single
|
|
167
|
+
// construction site of the turn's user message (string or multimodal
|
|
168
|
+
// content blocks when the turn carries inline images).
|
|
170
169
|
const effectiveInput = resume.isResumeFromApproval
|
|
171
170
|
? resume.graphInput
|
|
172
171
|
: setup.langgraphInput;
|
|
@@ -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,11 +11,16 @@ 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,
|
|
17
18
|
} from "../../shared/sender-identity.js";
|
|
18
19
|
import { formatSessionContextText } from "../../shared/session-context.js";
|
|
20
|
+
import {
|
|
21
|
+
visionDisclosureLines,
|
|
22
|
+
type NotViewableEntry,
|
|
23
|
+
} from "../../shared/attachment-vision.js";
|
|
19
24
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
20
25
|
import {
|
|
21
26
|
buildImplementPlanDirective,
|
|
@@ -107,6 +112,12 @@ export interface PromptBuilderInput {
|
|
|
107
112
|
workspaceFileRefs: string[];
|
|
108
113
|
workspaceRoot: string;
|
|
109
114
|
injectedFiles: InjectedFile[];
|
|
115
|
+
/**
|
|
116
|
+
* Vision facts about this turn's attachments (T04): which images the model
|
|
117
|
+
* sees inline in the user message and which degraded to path-only.
|
|
118
|
+
* Rendered inside the Input Files section.
|
|
119
|
+
*/
|
|
120
|
+
vision?: VisionPromptInfo;
|
|
110
121
|
/**
|
|
111
122
|
* The execution's interaction mode. PLAN appends the shared plan-mode
|
|
112
123
|
* directive so the model knows the turn's deliverable is a plan document.
|
|
@@ -157,6 +168,17 @@ export interface InjectedFile {
|
|
|
157
168
|
size?: number | null;
|
|
158
169
|
}
|
|
159
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Which images ride the user message inline (in send order) and which
|
|
173
|
+
* degraded to the file-pointer story — the shared vision wording
|
|
174
|
+
* (attachment-vision.ts) keeps this prompt and the Cursor harness's
|
|
175
|
+
* input-files section telling the agent the same thing.
|
|
176
|
+
*/
|
|
177
|
+
export interface VisionPromptInfo {
|
|
178
|
+
inlineFilenames: string[];
|
|
179
|
+
notViewable: NotViewableEntry[];
|
|
180
|
+
}
|
|
181
|
+
|
|
160
182
|
/**
|
|
161
183
|
* Assemble the full system prompt from base instructions and contextual
|
|
162
184
|
* sections. Pure function with no I/O.
|
|
@@ -195,7 +217,7 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
if (input.injectedFiles.length > 0) {
|
|
198
|
-
prompt += buildInjectedFilesSection(input.injectedFiles);
|
|
220
|
+
prompt += buildInjectedFilesSection(input.injectedFiles, input.vision);
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
if (input.senderIdentity) {
|
|
@@ -241,9 +263,28 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
241
263
|
return prompt;
|
|
242
264
|
}
|
|
243
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Compose the turn's USER MESSAGE for the graph invocation: the framed
|
|
268
|
+
* conversation catchup (cloud DD-006), when present, prepended to the
|
|
269
|
+
* customer's message. In the user message and never the system prompt (A27):
|
|
270
|
+
* the system prompt is rebuilt per invocation and would forget the digest one
|
|
271
|
+
* turn later, while a message enters the checkpointer with the turn and
|
|
272
|
+
* persists in history — the same durability the cursor harness gets from its
|
|
273
|
+
* prompt prefix. The caller's `spec.message` is never mutated; the prepend
|
|
274
|
+
* exists only in the graph input.
|
|
275
|
+
*/
|
|
276
|
+
export function composeUserMessage(
|
|
277
|
+
message: string,
|
|
278
|
+
conversationCatchup: string | undefined,
|
|
279
|
+
): string {
|
|
280
|
+
return conversationCatchup
|
|
281
|
+
? `${formatConversationCatchupText(conversationCatchup)}\n\n---\n\n${message}`
|
|
282
|
+
: message;
|
|
283
|
+
}
|
|
284
|
+
|
|
244
285
|
function buildWorkspacePromptSection(
|
|
245
|
-
|
|
246
|
-
|
|
286
|
+
provisionResults: ProvisionResult[],
|
|
287
|
+
containerRoot: string,
|
|
247
288
|
): string {
|
|
248
289
|
if (provisionResults.length === 0) return "";
|
|
249
290
|
|
|
@@ -354,7 +395,10 @@ function buildReferencedFilesSection(
|
|
|
354
395
|
return section;
|
|
355
396
|
}
|
|
356
397
|
|
|
357
|
-
function buildInjectedFilesSection(
|
|
398
|
+
function buildInjectedFilesSection(
|
|
399
|
+
files: InjectedFile[],
|
|
400
|
+
vision?: VisionPromptInfo,
|
|
401
|
+
): string {
|
|
358
402
|
let section = "\n\n## Input Files\n\n";
|
|
359
403
|
section +=
|
|
360
404
|
"The following files have been provided as read-only reference " +
|
|
@@ -370,5 +414,16 @@ function buildInjectedFilesSection(files: InjectedFile[]): string {
|
|
|
370
414
|
section += `- \`${f.path}\`${sizeInfo}\n`;
|
|
371
415
|
}
|
|
372
416
|
|
|
417
|
+
// The vision lines (shared wording, attachment-vision.ts) tell the model
|
|
418
|
+
// which of these files it can already SEE inline in the user message versus
|
|
419
|
+
// which degraded to path-only — without them an agent silently ignores a
|
|
420
|
+
// photo the user believes it can see.
|
|
421
|
+
if (vision) {
|
|
422
|
+
const lines = visionDisclosureLines(vision.inlineFilenames, vision.notViewable);
|
|
423
|
+
if (lines.length > 0) {
|
|
424
|
+
section += "\n" + lines.join("\n") + "\n";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
373
428
|
return section;
|
|
374
429
|
}
|
|
@@ -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";
|
|
@@ -93,6 +99,12 @@ import {
|
|
|
93
99
|
} from "../../shared/skill-writer.js";
|
|
94
100
|
import { filterSkills, SKILL_COUNT_THRESHOLD } from "../../shared/skill-relevance.js";
|
|
95
101
|
import { injectAttachments } from "./attachment-injector.js";
|
|
102
|
+
import {
|
|
103
|
+
DEEP_AGENT_VISION_PROFILE,
|
|
104
|
+
VisionBudget,
|
|
105
|
+
toLangChainImageBlocks,
|
|
106
|
+
type NotViewableEntry,
|
|
107
|
+
} from "../../shared/attachment-vision.js";
|
|
96
108
|
import { transformAndCompileSubagents } from "./subagent-transformer.js";
|
|
97
109
|
import {
|
|
98
110
|
resolveRecursionLimit,
|
|
@@ -361,8 +373,18 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
361
373
|
// answer — no tool, no section, execution unharmed.
|
|
362
374
|
const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
|
|
363
375
|
|
|
376
|
+
// The conversation-attachment decision (DD-008 D-c): the channel-id
|
|
377
|
+
// session label, stamped server-side on every channel session — a
|
|
378
|
+
// free, synchronous read, so no hoisted discovery needed.
|
|
379
|
+
const conversationChannelId = readChannelConversationId(session.metadata?.labels);
|
|
380
|
+
|
|
364
381
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
365
|
-
if (
|
|
382
|
+
if (shouldConnectMcp({
|
|
383
|
+
mcpServerUsageCount: mcpServerUsages.length,
|
|
384
|
+
datastoreUsageCount: datastoreUsages.length,
|
|
385
|
+
channelMessagingCount: channelMessaging.length,
|
|
386
|
+
conversationChannelId,
|
|
387
|
+
})) {
|
|
366
388
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
367
389
|
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
368
390
|
// The MCP-bound env map (and ONLY it) carries the reserved
|
|
@@ -424,6 +446,21 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
424
446
|
);
|
|
425
447
|
}
|
|
426
448
|
}
|
|
449
|
+
|
|
450
|
+
// The conversation participation attachment (DD-008 D-c) — the
|
|
451
|
+
// third sibling, same after-backfill rule. HTTP-only: synthesize
|
|
452
|
+
// answers undefined with no bridge endpoint by design (see
|
|
453
|
+
// shared/conversation-attachment.ts).
|
|
454
|
+
const conversationAttachment = synthesizeConversationAttachment(conversationChannelId, {
|
|
455
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
456
|
+
credential: attachmentCredential,
|
|
457
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
458
|
+
});
|
|
459
|
+
if (conversationAttachment) {
|
|
460
|
+
backfilledServers = injectSynthesizedAttachment(
|
|
461
|
+
backfilledServers, conversationAttachment, "conversation participation",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
427
464
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
428
465
|
timing.mark("backfill_mcp");
|
|
429
466
|
|
|
@@ -477,14 +514,39 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
477
514
|
timing.mark("resolve_skills");
|
|
478
515
|
}
|
|
479
516
|
|
|
480
|
-
// Step 7c: Inject attachments
|
|
517
|
+
// Step 7c: Inject attachments. The vision budget rides along so image
|
|
518
|
+
// attachments are selected for inline delivery while their bytes are
|
|
519
|
+
// already in hand (attachment-vision.ts owns all policy).
|
|
481
520
|
const attachments = execution.spec!.attachments || [];
|
|
521
|
+
const visionBudget = new VisionBudget(DEEP_AGENT_VISION_PROFILE);
|
|
482
522
|
const injectedFiles = await injectAttachments({
|
|
483
523
|
backend: workspaceBackend,
|
|
484
524
|
attachments,
|
|
485
525
|
storage: artifactStorage,
|
|
486
526
|
isLocalMode: config.mode === "local",
|
|
527
|
+
visionBudget,
|
|
487
528
|
});
|
|
529
|
+
// Vision facts, derived once from the single injection result: the images
|
|
530
|
+
// the model will see inline (in attachment order) and the ones that
|
|
531
|
+
// degraded to path-only, disclosed in the system prompt's Input Files
|
|
532
|
+
// section.
|
|
533
|
+
const visionImages = injectedFiles.flatMap((f) => (f.vision ? [f.vision] : []));
|
|
534
|
+
const visionNotViewable: NotViewableEntry[] = injectedFiles.flatMap((f) =>
|
|
535
|
+
f.visionDegraded ? [{ path: f.path, reason: f.visionDegraded }] : [],
|
|
536
|
+
);
|
|
537
|
+
const visionPromptInfo = visionImages.length > 0 || visionNotViewable.length > 0
|
|
538
|
+
? {
|
|
539
|
+
inlineFilenames: visionImages.map((v) => v.filename),
|
|
540
|
+
notViewable: visionNotViewable,
|
|
541
|
+
}
|
|
542
|
+
: undefined;
|
|
543
|
+
if (visionPromptInfo) {
|
|
544
|
+
console.log(
|
|
545
|
+
`[attachment-vision] execution=${executionId} inline=${visionImages.length} ` +
|
|
546
|
+
`(${visionImages.reduce((n, v) => n + v.byteSize, 0)} bytes) ` +
|
|
547
|
+
`degraded=${JSON.stringify(visionNotViewable.map((d) => `${d.path}:${d.reason}`))}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
488
550
|
timing.mark("inject_attachments");
|
|
489
551
|
|
|
490
552
|
// Step 8: Build enhanced system prompt
|
|
@@ -504,6 +566,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
504
566
|
workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
|
|
505
567
|
workspaceRoot: workspaceBackend.rootDir,
|
|
506
568
|
injectedFiles,
|
|
569
|
+
vision: visionPromptInfo,
|
|
507
570
|
interactionMode: execution.spec!.executionConfig?.interactionMode,
|
|
508
571
|
buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
|
|
509
572
|
contextBridge: readContextBridge(session.spec!.metadata),
|
|
@@ -743,13 +806,34 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
743
806
|
...(isPlanMode ? { permissions: planModePermissions } : {}),
|
|
744
807
|
} as Parameters<typeof createDeepAgent>[0]);
|
|
745
808
|
|
|
746
|
-
// Step 11: Prepare invocation input and config
|
|
747
|
-
|
|
809
|
+
// Step 11: Prepare invocation input and config. The conversation catchup
|
|
810
|
+
// (cloud DD-006) rides the USER MESSAGE, not the system prompt — see
|
|
811
|
+
// composeUserMessage for the durability rationale (A27).
|
|
812
|
+
let userMessage = composeUserMessage(
|
|
813
|
+
execution.spec!.message,
|
|
814
|
+
readConversationCatchup(execution.spec!.conversationCatchup),
|
|
815
|
+
);
|
|
748
816
|
if (outputSchema) {
|
|
749
817
|
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
818
|
}
|
|
819
|
+
// With inline images the user message becomes a multimodal content-block
|
|
820
|
+
// array — image blocks (with filename labels) FIRST, the composed text
|
|
821
|
+
// last, per Anthropic's images-before-text guidance. Without images the
|
|
822
|
+
// content stays a plain string, byte-identical to the pre-vision shape.
|
|
823
|
+
// The LangGraph messages reducer coerces either form into a HumanMessage
|
|
824
|
+
// untouched (verified against the installed @langchain/langgraph).
|
|
751
825
|
const langgraphInput = {
|
|
752
|
-
messages: [
|
|
826
|
+
messages: [
|
|
827
|
+
{
|
|
828
|
+
role: "user",
|
|
829
|
+
content: visionImages.length > 0
|
|
830
|
+
? [
|
|
831
|
+
...toLangChainImageBlocks(visionImages),
|
|
832
|
+
{ type: "text", text: userMessage },
|
|
833
|
+
]
|
|
834
|
+
: userMessage,
|
|
835
|
+
},
|
|
836
|
+
],
|
|
753
837
|
};
|
|
754
838
|
|
|
755
839
|
const langgraphConfig: Record<string, unknown> = {
|
package/src/runner-manager.ts
CHANGED
|
@@ -332,6 +332,25 @@ export async function createStigmerRunnerManager(
|
|
|
332
332
|
|
|
333
333
|
const activities = await createAllActivities(config);
|
|
334
334
|
markBoot("activities_imported");
|
|
335
|
+
// Surface the resolved artifact store at boot (see runner.ts for rationale):
|
|
336
|
+
// a path/type misconfiguration is the #285 failure class, and it should be
|
|
337
|
+
// visible here rather than only when a read fails mid-execution.
|
|
338
|
+
try {
|
|
339
|
+
const { loadArtifactStorageConfig } = await import(
|
|
340
|
+
"./shared/artifact-storage.js"
|
|
341
|
+
);
|
|
342
|
+
const artifactCfg = loadArtifactStorageConfig(config);
|
|
343
|
+
console.log(
|
|
344
|
+
`[runner-manager] Artifact store: type=${artifactCfg.type}` +
|
|
345
|
+
(artifactCfg.type === "local"
|
|
346
|
+
? ` | root=${artifactCfg.localPath}`
|
|
347
|
+
: ` | proxy=${artifactCfg.proxyEndpoint ?? "(unset)"}`),
|
|
348
|
+
);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.warn(
|
|
351
|
+
`[runner-manager] Artifact store: could not resolve config for boot log: ${err}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
335
354
|
const payloadCodec = await createPayloadCodec(config);
|
|
336
355
|
|
|
337
356
|
const connection = await NativeConnection.connect({
|
package/src/runner.ts
CHANGED
|
@@ -282,6 +282,25 @@ export async function createStigmerRunner(
|
|
|
282
282
|
`Mode: ${config.mode} | ` +
|
|
283
283
|
`Max concurrency: ${config.maxConcurrentActivities}`,
|
|
284
284
|
);
|
|
285
|
+
// Surface the resolved artifact store at boot so a path/type misconfiguration
|
|
286
|
+
// (the #285 class of failure) is visible immediately instead of only when an
|
|
287
|
+
// attachment or offload read fails minutes into an execution.
|
|
288
|
+
try {
|
|
289
|
+
const { loadArtifactStorageConfig } = await import(
|
|
290
|
+
"./shared/artifact-storage.js"
|
|
291
|
+
);
|
|
292
|
+
const artifactCfg = loadArtifactStorageConfig(config);
|
|
293
|
+
console.log(
|
|
294
|
+
`[runner] Artifact store: type=${artifactCfg.type}` +
|
|
295
|
+
(artifactCfg.type === "local"
|
|
296
|
+
? ` | root=${artifactCfg.localPath}`
|
|
297
|
+
: ` | proxy=${artifactCfg.proxyEndpoint ?? "(unset)"}`),
|
|
298
|
+
);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.warn(
|
|
301
|
+
`[runner] Artifact store: could not resolve config for boot log: ${err}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
285
304
|
|
|
286
305
|
const payloadCodec = await createPayloadCodec(config);
|
|
287
306
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { mkdtemp, rm, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
4
|
+
import { tmpdir, homedir } from "node:os";
|
|
5
5
|
import {
|
|
6
6
|
LocalArtifactStorage,
|
|
7
7
|
ProxyArtifactStorage,
|
|
@@ -101,6 +101,63 @@ describe("LocalArtifactStorage", () => {
|
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// ── LocalArtifactStorage path containment ────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe("LocalArtifactStorage path containment", () => {
|
|
107
|
+
let tempDir: string;
|
|
108
|
+
let storage: LocalArtifactStorage;
|
|
109
|
+
|
|
110
|
+
// Keys that clean to a location outside the storage root. `join(base, key)`
|
|
111
|
+
// silently resolves `..` segments, so without a containment check these would
|
|
112
|
+
// read or write outside the store — the runner-side mirror of the Go finding.
|
|
113
|
+
const escapingKeys = [
|
|
114
|
+
"../escape.txt",
|
|
115
|
+
"../../escape.txt",
|
|
116
|
+
"attachments/x/../../../../escape.txt",
|
|
117
|
+
"a/b/../../../escape.txt",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
beforeEach(async () => {
|
|
121
|
+
tempDir = await mkdtemp(join(tmpdir(), "artifact-contain-"));
|
|
122
|
+
storage = new LocalArtifactStorage(tempDir, "http://localhost:7235");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(async () => {
|
|
126
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("refuses to upload a key that escapes the storage root", async () => {
|
|
130
|
+
for (const key of escapingKeys) {
|
|
131
|
+
await expect(storage.upload(key, Buffer.from("owned"))).rejects.toThrow(
|
|
132
|
+
/outside the artifact storage root/,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("refuses to download a key that escapes the storage root", async () => {
|
|
138
|
+
for (const key of escapingKeys) {
|
|
139
|
+
await expect(storage.download(key)).rejects.toThrow(
|
|
140
|
+
/outside the artifact storage root/,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("refuses exists() for a key that escapes the storage root", async () => {
|
|
146
|
+
for (const key of escapingKeys) {
|
|
147
|
+
await expect(storage.exists(key)).rejects.toThrow(
|
|
148
|
+
/outside the artifact storage root/,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("still allows keys with `..` segments that stay inside the root", async () => {
|
|
154
|
+
// Containment rejects escapes, not the mere presence of a `..` segment.
|
|
155
|
+
const key = "attachments/x/../y/plan.md";
|
|
156
|
+
await storage.upload(key, Buffer.from("ok"));
|
|
157
|
+
expect((await storage.download(key)).toString()).toBe("ok");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
104
161
|
// ── ProxyArtifactStorage ─────────────────────────────────────────────
|
|
105
162
|
|
|
106
163
|
describe("ProxyArtifactStorage", () => {
|
|
@@ -480,6 +537,24 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
480
537
|
});
|
|
481
538
|
expect(cfg.type).toBe("proxy");
|
|
482
539
|
});
|
|
540
|
+
|
|
541
|
+
// #285: the local default must be the SAME directory the stigmer-server
|
|
542
|
+
// writes to (~/.stigmer/data/artifacts), not the container-era
|
|
543
|
+
// /var/stigmer/artifacts. Asserting the resolved path — not just the type —
|
|
544
|
+
// is the guard that would have caught the original drift.
|
|
545
|
+
it("defaults localPath to the shared ~/.stigmer/data/artifacts root and serveUrl to :7235", () => {
|
|
546
|
+
const cfg = loadArtifactStorageConfig(baseConfig);
|
|
547
|
+
expect(cfg.localPath).toBe(join(homedir(), ".stigmer", "data", "artifacts"));
|
|
548
|
+
expect(cfg.localServeUrl).toBe("http://localhost:7235");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("respects explicit LOCAL_ARTIFACT_PATH and LOCAL_ARTIFACT_SERVE_URL", () => {
|
|
552
|
+
process.env.LOCAL_ARTIFACT_PATH = "/custom/artifacts";
|
|
553
|
+
process.env.LOCAL_ARTIFACT_SERVE_URL = "http://localhost:9999";
|
|
554
|
+
const cfg = loadArtifactStorageConfig(baseConfig);
|
|
555
|
+
expect(cfg.localPath).toBe("/custom/artifacts");
|
|
556
|
+
expect(cfg.localServeUrl).toBe("http://localhost:9999");
|
|
557
|
+
});
|
|
483
558
|
});
|
|
484
559
|
|
|
485
560
|
// ── resolveUsableArtifactStorage (DD-26 follow-up #1) ─────────────────
|