@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
|
@@ -29,10 +29,17 @@
|
|
|
29
29
|
* the execution with an actionable error.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { mkdir, copyFile, writeFile } from "node:fs/promises";
|
|
32
|
+
import { mkdir, copyFile, readFile, stat, writeFile } from "node:fs/promises";
|
|
33
33
|
import { join, basename } from "node:path";
|
|
34
34
|
import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
35
35
|
import type { ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
36
|
+
import {
|
|
37
|
+
isVisionCandidate,
|
|
38
|
+
type VisionBudget,
|
|
39
|
+
type VisionDegradedReason,
|
|
40
|
+
type VisionImage,
|
|
41
|
+
type VisionOutcome,
|
|
42
|
+
} from "../../shared/attachment-vision.js";
|
|
36
43
|
import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
37
44
|
import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
|
|
38
45
|
|
|
@@ -42,6 +49,15 @@ export interface ResolvedAttachment {
|
|
|
42
49
|
filename: string;
|
|
43
50
|
/** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
|
|
44
51
|
relativePath: string;
|
|
52
|
+
/** Present when the attachment was accepted into the turn's vision payload. */
|
|
53
|
+
vision?: VisionImage;
|
|
54
|
+
/**
|
|
55
|
+
* Present when the attachment was plausibly an image but could not ride
|
|
56
|
+
* inline (see {@link VisionDegradedReason}) — disclosed in the prompt so the
|
|
57
|
+
* agent never silently ignores a photo the user believes it can see.
|
|
58
|
+
* Attachments that were never image-shaped carry neither field.
|
|
59
|
+
*/
|
|
60
|
+
visionDegraded?: VisionDegradedReason;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
export interface AttachmentResolverOptions {
|
|
@@ -54,6 +70,12 @@ export interface AttachmentResolverOptions {
|
|
|
54
70
|
* then fails with an actionable error rather than a silent skip.
|
|
55
71
|
*/
|
|
56
72
|
storage: ArtifactStorage | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The turn's vision selector (attachment-vision.ts owns all policy).
|
|
75
|
+
* `undefined` disables inline image delivery; file materialization is
|
|
76
|
+
* identical either way — vision is strictly additive.
|
|
77
|
+
*/
|
|
78
|
+
visionBudget?: VisionBudget;
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
export class AttachmentResolutionError extends Error {
|
|
@@ -109,9 +131,10 @@ async function resolveAttachment(
|
|
|
109
131
|
): Promise<ResolvedAttachment> {
|
|
110
132
|
// Local-mode fast path: the file is already on this machine's disk.
|
|
111
133
|
if (options.mode === "local" && attachment.localPath) {
|
|
112
|
-
const filename = attachment.filename ||
|
|
134
|
+
const filename = safeInputName(attachment.filename || attachment.localPath);
|
|
135
|
+
let vision: VisionOutcome | undefined;
|
|
113
136
|
try {
|
|
114
|
-
await
|
|
137
|
+
vision = await materializeLocalFile(attachment, filename, inputsDir, options.visionBudget);
|
|
115
138
|
} catch (err) {
|
|
116
139
|
throw new AttachmentResolutionError(
|
|
117
140
|
attachment.filename,
|
|
@@ -122,6 +145,7 @@ async function resolveAttachment(
|
|
|
122
145
|
return {
|
|
123
146
|
filename,
|
|
124
147
|
relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
|
|
148
|
+
...visionOutcomeFields(vision),
|
|
125
149
|
};
|
|
126
150
|
}
|
|
127
151
|
|
|
@@ -140,7 +164,7 @@ async function resolveAttachment(
|
|
|
140
164
|
);
|
|
141
165
|
}
|
|
142
166
|
|
|
143
|
-
const filename = attachment.filename ||
|
|
167
|
+
const filename = safeInputName(attachment.filename || attachment.storageKey);
|
|
144
168
|
let content: Buffer;
|
|
145
169
|
try {
|
|
146
170
|
content = await options.storage.download(attachment.storageKey);
|
|
@@ -153,8 +177,70 @@ async function resolveAttachment(
|
|
|
153
177
|
}
|
|
154
178
|
await writeFile(join(inputsDir, filename), content);
|
|
155
179
|
|
|
180
|
+
// The bytes are already in hand for the file write — offer them to the
|
|
181
|
+
// vision budget before they go out of scope (the sniff decides eligibility;
|
|
182
|
+
// no pre-filter needed on this branch).
|
|
183
|
+
const vision = options.visionBudget?.offer(filename, attachment.contentType, content);
|
|
184
|
+
|
|
156
185
|
return {
|
|
157
186
|
filename,
|
|
158
187
|
relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
|
|
188
|
+
...visionOutcomeFields(vision),
|
|
159
189
|
};
|
|
160
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Materialize a local-path attachment, reading the bytes only when they are
|
|
194
|
+
* plausibly a vision candidate within the per-image cap — a 25 MB PDF (or an
|
|
195
|
+
* oversized image, detected by stat) keeps the plain `copyFile` and never
|
|
196
|
+
* enters memory. Returns the vision outcome, or `undefined` when vision is
|
|
197
|
+
* disabled or the file is not a candidate.
|
|
198
|
+
*/
|
|
199
|
+
async function materializeLocalFile(
|
|
200
|
+
attachment: Attachment,
|
|
201
|
+
filename: string,
|
|
202
|
+
inputsDir: string,
|
|
203
|
+
visionBudget: VisionBudget | undefined,
|
|
204
|
+
): Promise<VisionOutcome | undefined> {
|
|
205
|
+
const dest = join(inputsDir, filename);
|
|
206
|
+
if (!visionBudget || !isVisionCandidate(attachment.contentType, filename)) {
|
|
207
|
+
await copyFile(attachment.localPath, dest);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const info = await stat(attachment.localPath);
|
|
211
|
+
if (visionBudget.exceedsImageCap(info.size)) {
|
|
212
|
+
await copyFile(attachment.localPath, dest);
|
|
213
|
+
return visionBudget.offerOversized();
|
|
214
|
+
}
|
|
215
|
+
const content = await readFile(attachment.localPath);
|
|
216
|
+
await writeFile(dest, content);
|
|
217
|
+
return visionBudget.offer(filename, attachment.contentType, content);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function visionOutcomeFields(
|
|
221
|
+
outcome: VisionOutcome | undefined,
|
|
222
|
+
): Pick<ResolvedAttachment, "vision" | "visionDegraded"> {
|
|
223
|
+
if (outcome === undefined || outcome.kind === "skipped") return {};
|
|
224
|
+
return outcome.kind === "accepted"
|
|
225
|
+
? { vision: outcome.image }
|
|
226
|
+
: { visionDegraded: outcome.reason };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Reduce a caller-influenced name to a single, safe path component for writing
|
|
231
|
+
* under the inputs dir. The name (an attachment's original filename, or a
|
|
232
|
+
* storage key's tail) is untrusted — a value like `../../evil.md` would steer
|
|
233
|
+
* the write outside `.stigmer/inputs/`. Taking the basename strips any path
|
|
234
|
+
* structure; the residual `.`/`..`/empty cases (which basename does not strip)
|
|
235
|
+
* are rejected loudly so the write target is always a real file inside inputs.
|
|
236
|
+
*/
|
|
237
|
+
function safeInputName(raw: string): string {
|
|
238
|
+
const name = basename(raw);
|
|
239
|
+
if (name === "" || name === "." || name === "..") {
|
|
240
|
+
throw new AttachmentResolutionError(
|
|
241
|
+
raw,
|
|
242
|
+
`'${raw}' does not yield a usable filename for materialization`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return name;
|
|
246
|
+
}
|
|
@@ -35,7 +35,7 @@ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agent
|
|
|
35
35
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
36
36
|
import type { AgentExecution, AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
37
37
|
import { ExecutionControlSignal, ExecutionPhase, FileChangeSetStatus, InteractionMode, MessageType, ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
38
|
-
import type { Run, ConversationTurn } from "@cursor/sdk";
|
|
38
|
+
import type { Run, ConversationTurn, SDKUserMessage } from "@cursor/sdk";
|
|
39
39
|
|
|
40
40
|
import type { Config } from "../../config.js";
|
|
41
41
|
import { StigmerClient } from "../../client/stigmer-client.js";
|
|
@@ -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,
|
|
@@ -57,6 +58,12 @@ import { readSessionContext } from "../../shared/session-context.js";
|
|
|
57
58
|
import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
58
59
|
import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
|
|
59
60
|
import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
61
|
+
import {
|
|
62
|
+
CURSOR_VISION_PROFILE,
|
|
63
|
+
VisionBudget,
|
|
64
|
+
toCursorImages,
|
|
65
|
+
type NotViewableEntry,
|
|
66
|
+
} from "../../shared/attachment-vision.js";
|
|
60
67
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
61
68
|
import { DeltaEnricher } from "./delta-enricher.js";
|
|
62
69
|
import { TodoTracker } from "./todo-tracker.js";
|
|
@@ -69,6 +76,10 @@ import {
|
|
|
69
76
|
discoverChannelMessaging,
|
|
70
77
|
synthesizeChannelAttachment,
|
|
71
78
|
} from "../../shared/channel-attachment.js";
|
|
79
|
+
import {
|
|
80
|
+
readChannelConversationId,
|
|
81
|
+
synthesizeConversationAttachment,
|
|
82
|
+
} from "../../shared/conversation-attachment.js";
|
|
72
83
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
73
84
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
74
85
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
@@ -79,7 +90,7 @@ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
|
79
90
|
import { resolveSkills } from "./skill-resolver.js";
|
|
80
91
|
import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
|
|
81
92
|
import { resolveAttachments } from "./attachment-resolver.js";
|
|
82
|
-
import { buildEnhancedPrompt, buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
93
|
+
import { buildEnhancedPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInputFiles, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
83
94
|
import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
|
|
84
95
|
import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
|
|
85
96
|
import {
|
|
@@ -685,6 +696,31 @@ async function executeCursorInner(
|
|
|
685
696
|
};
|
|
686
697
|
}
|
|
687
698
|
}
|
|
699
|
+
|
|
700
|
+
// Phase 4a4: Synthesize the conversation participation attachment
|
|
701
|
+
// (channel-conversations DD-008 D-c) — the third sibling. The
|
|
702
|
+
// channel-id session label IS the attachment decision (stamped
|
|
703
|
+
// server-side on every channel session; a free local read, unlike
|
|
704
|
+
// the channels discovery RPC above). HTTP-only: synthesize answers
|
|
705
|
+
// undefined with no bridge endpoint by design (see
|
|
706
|
+
// shared/conversation-attachment.ts).
|
|
707
|
+
const conversationAttachment = synthesizeConversationAttachment(
|
|
708
|
+
readChannelConversationId(session.metadata?.labels),
|
|
709
|
+
{
|
|
710
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
711
|
+
credential: attachmentCredential,
|
|
712
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
if (conversationAttachment) {
|
|
716
|
+
const resolvedServers = injectSynthesizedAttachment(
|
|
717
|
+
mcpResolution.resolvedServers, conversationAttachment, "conversation participation",
|
|
718
|
+
);
|
|
719
|
+
mcpResolution = {
|
|
720
|
+
resolvedServers,
|
|
721
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
688
724
|
const mcpConfig = mcpResolution.cursorConfig;
|
|
689
725
|
|
|
690
726
|
// Phase 4b: Merge approval policies from all layers.
|
|
@@ -731,14 +767,38 @@ async function executeCursorInner(
|
|
|
731
767
|
|
|
732
768
|
// Phase 5b: Resolve attachments (fail-hard — explicit user inputs; see
|
|
733
769
|
// attachment-resolver.ts). Downloads by storage key through the same
|
|
734
|
-
// artifactStorage resolved for status offload above.
|
|
770
|
+
// artifactStorage resolved for status offload above. The vision budget
|
|
771
|
+
// rides along so image attachments are selected for inline delivery while
|
|
772
|
+
// their bytes are already in hand (attachment-vision.ts owns all policy).
|
|
773
|
+
const visionBudget = new VisionBudget(CURSOR_VISION_PROFILE);
|
|
735
774
|
const attachmentResults = await resolveAttachments(spec.attachments, {
|
|
736
775
|
sessionId,
|
|
737
776
|
primaryWorkspaceDir,
|
|
738
777
|
mode: config.mode,
|
|
739
778
|
storage: artifactStorage,
|
|
779
|
+
visionBudget,
|
|
740
780
|
});
|
|
741
781
|
const attachmentPaths = attachmentResults.map((a) => a.relativePath);
|
|
782
|
+
// Vision facts, derived once from the single resolution result: the
|
|
783
|
+
// images the model will see inline (in attachment order) and the ones
|
|
784
|
+
// that degraded to path-only, disclosed in the prompt.
|
|
785
|
+
const visionImages = attachmentResults.flatMap((a) => (a.vision ? [a.vision] : []));
|
|
786
|
+
const visionNotViewable: NotViewableEntry[] = attachmentResults.flatMap((a) =>
|
|
787
|
+
a.visionDegraded ? [{ path: a.relativePath, reason: a.visionDegraded }] : [],
|
|
788
|
+
);
|
|
789
|
+
const visionPromptInfo = visionImages.length > 0 || visionNotViewable.length > 0
|
|
790
|
+
? {
|
|
791
|
+
inlineFilenames: visionImages.map((v) => v.filename),
|
|
792
|
+
notViewable: visionNotViewable,
|
|
793
|
+
}
|
|
794
|
+
: undefined;
|
|
795
|
+
if (visionPromptInfo) {
|
|
796
|
+
console.log(
|
|
797
|
+
`[attachment-vision] execution=${executionId} inline=${visionImages.length} ` +
|
|
798
|
+
`(${visionImages.reduce((n, v) => n + v.byteSize, 0)} bytes) ` +
|
|
799
|
+
`degraded=${JSON.stringify(visionNotViewable.map((d) => `${d.path}:${d.reason}`))}`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
742
802
|
setupTiming.mark("resolve_attachments");
|
|
743
803
|
|
|
744
804
|
// Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
|
|
@@ -1064,6 +1124,7 @@ async function executeCursorInner(
|
|
|
1064
1124
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1065
1125
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
1066
1126
|
attachmentPaths,
|
|
1127
|
+
vision: visionPromptInfo,
|
|
1067
1128
|
pendingApprovals: adjudicatedApprovals,
|
|
1068
1129
|
appliedToolCallIds,
|
|
1069
1130
|
interactionMode,
|
|
@@ -1071,6 +1132,7 @@ async function executeCursorInner(
|
|
|
1071
1132
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1072
1133
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1073
1134
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1135
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1074
1136
|
});
|
|
1075
1137
|
|
|
1076
1138
|
// Phase 10a: Inject structured output instruction for Cursor harness
|
|
@@ -1080,6 +1142,20 @@ async function executeCursorInner(
|
|
|
1080
1142
|
effectivePrompt += `\n\n---\nCRITICAL OUTPUT REQUIREMENT:\nYour final response MUST be a single valid JSON object (no markdown, no commentary, no code fences) that matches this schema:\n${schemaStr}\n\nRespond with ONLY the JSON object. Nothing else.`;
|
|
1081
1143
|
}
|
|
1082
1144
|
|
|
1145
|
+
// Phase 10a1: The turn's vision payload. The invariant is "images
|
|
1146
|
+
// accompany the user's turn message — where the message goes, they go":
|
|
1147
|
+
// every send that delivers this turn's message carries them (the primary
|
|
1148
|
+
// send and both fresh-agent recovery retries, whose empty conversations
|
|
1149
|
+
// genuinely need the re-send), while a HITL re-invocation — whose prompt
|
|
1150
|
+
// carries no user message and whose resumed agent already holds the
|
|
1151
|
+
// images in its native conversation — carries none. Computed ONCE here so
|
|
1152
|
+
// all send sites agree by construction.
|
|
1153
|
+
const turnImages = isHitlReinvocation(approvalDecisions)
|
|
1154
|
+
? []
|
|
1155
|
+
: toCursorImages(visionImages);
|
|
1156
|
+
const toSendMessage = (sendPrompt: string): string | SDKUserMessage =>
|
|
1157
|
+
turnImages.length > 0 ? { text: sendPrompt, images: turnImages } : sendPrompt;
|
|
1158
|
+
|
|
1083
1159
|
// Phase 10a2: Log Stigmer preamble size for context trimming diagnostics
|
|
1084
1160
|
const promptChars = effectivePrompt.length;
|
|
1085
1161
|
const promptEstimatedTokens = Math.ceil(promptChars / 4);
|
|
@@ -1194,7 +1270,7 @@ async function executeCursorInner(
|
|
|
1194
1270
|
// The stall watchdog is armed inside consumeCursorTurnStream (it needs the
|
|
1195
1271
|
// run to cancel), stored on turnState.stallWatchdog so this shared onDelta can
|
|
1196
1272
|
// reset it and the activity's finally can stop it as a backstop.
|
|
1197
|
-
const run = await resolution.agent.send(effectivePrompt, {
|
|
1273
|
+
const run = await resolution.agent.send(toSendMessage(effectivePrompt), {
|
|
1198
1274
|
onDelta: (event) => {
|
|
1199
1275
|
if (!turnFirstEventEmitted) {
|
|
1200
1276
|
turnFirstEventEmitted = true;
|
|
@@ -1565,7 +1641,10 @@ async function executeCursorInner(
|
|
|
1565
1641
|
// poisoned-handle path leaked the fresh agent — it closed the stale one.)
|
|
1566
1642
|
resolution = { ...resolution, agent: freshAgent, agentId: freshAgent.agentId, isNew: true };
|
|
1567
1643
|
turnState.streamErrorMessage = undefined;
|
|
1568
|
-
|
|
1644
|
+
// The retry carries the turn's images too (same toSendMessage): the
|
|
1645
|
+
// fresh agent's conversation is empty, so skipping them here would
|
|
1646
|
+
// silently lose the user's photo on a recovered turn.
|
|
1647
|
+
const retryRun = await freshAgent.send(toSendMessage(retryPrompt), {
|
|
1569
1648
|
onDelta: makeCursorTurnOnDelta(onDeltaDeps),
|
|
1570
1649
|
});
|
|
1571
1650
|
await consumeCursorTurnStream(retryRun, streamDeps);
|
|
@@ -1704,11 +1783,18 @@ async function executeCursorInner(
|
|
|
1704
1783
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1705
1784
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
1706
1785
|
attachmentPaths,
|
|
1786
|
+
vision: visionPromptInfo,
|
|
1707
1787
|
pendingApprovals: adjudicatedApprovals,
|
|
1708
1788
|
interactionMode,
|
|
1789
|
+
// buildFromPlan was silently dropped here until T03 Sitting 3 —
|
|
1790
|
+
// a build turn that hit handle recovery lost its directive. The
|
|
1791
|
+
// fresh prompt must carry every per-turn directive the original
|
|
1792
|
+
// did.
|
|
1793
|
+
buildFromPlan,
|
|
1709
1794
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1710
1795
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1711
1796
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1797
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1712
1798
|
});
|
|
1713
1799
|
|
|
1714
1800
|
console.log(
|
|
@@ -2273,6 +2359,12 @@ export interface BuildPromptInput {
|
|
|
2273
2359
|
workspaceDirs: string[];
|
|
2274
2360
|
workspaceFileRefs: string[];
|
|
2275
2361
|
attachmentPaths: string[];
|
|
2362
|
+
/**
|
|
2363
|
+
* Vision facts for the input-files section (T04): which attachments the
|
|
2364
|
+
* model sees inline and which degraded to path-only. PER-TURN like the
|
|
2365
|
+
* catchup — it rides both the enhanced prompt and a resumed turn's prefix.
|
|
2366
|
+
*/
|
|
2367
|
+
vision?: import("./prompt-builder.js").VisionPromptInfo;
|
|
2276
2368
|
pendingApprovals: import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb").PendingApproval[];
|
|
2277
2369
|
/**
|
|
2278
2370
|
* Approved whole-file writes the runner already applied itself (exact-apply).
|
|
@@ -2305,6 +2397,18 @@ export interface BuildPromptInput {
|
|
|
2305
2397
|
* turn.
|
|
2306
2398
|
*/
|
|
2307
2399
|
sessionContext?: string;
|
|
2400
|
+
/**
|
|
2401
|
+
* Conversation catchup from the execution spec's `conversation_catchup`
|
|
2402
|
+
* (cloud DD-006): what happened on the channel conversation that the
|
|
2403
|
+
* agent has not seen. PER-TURN, so unlike the three standing values
|
|
2404
|
+
* above it rides BOTH prompt paths — the enhanced prompt and a resumed
|
|
2405
|
+
* turn's prefix (the `interaction_mode` shape). Handback lands
|
|
2406
|
+
* mid-session on a resumed agent: the resumed path is the one that
|
|
2407
|
+
* matters. Once delivered, the digest persists in the agent's own
|
|
2408
|
+
* conversation store; the next turn's field is composed fresh and is
|
|
2409
|
+
* usually blank.
|
|
2410
|
+
*/
|
|
2411
|
+
conversationCatchup?: string;
|
|
2308
2412
|
}
|
|
2309
2413
|
|
|
2310
2414
|
/**
|
|
@@ -2323,6 +2427,20 @@ export interface BuildPromptInput {
|
|
|
2323
2427
|
* 3. first execution / fresh -> buildEnhancedPrompt (full instructions +
|
|
2324
2428
|
* agent after resume failure skills; no prior conversation to inherit)
|
|
2325
2429
|
*/
|
|
2430
|
+
/**
|
|
2431
|
+
* Whether this activity invocation is a HITL re-invocation — the turn resumes
|
|
2432
|
+
* an agent purely to convey approval decisions, carrying NO user message.
|
|
2433
|
+
* The single discriminator for everything that must ride with the user's
|
|
2434
|
+
* message and nothing else: the reinvocation prompt shape (below) and the
|
|
2435
|
+
* vision payload (images accompany the message; a resumed agent already holds
|
|
2436
|
+
* them in its native conversation).
|
|
2437
|
+
*/
|
|
2438
|
+
export function isHitlReinvocation(
|
|
2439
|
+
approvalDecisions: Map<string, ApprovalAction> | undefined,
|
|
2440
|
+
): approvalDecisions is Map<string, ApprovalAction> {
|
|
2441
|
+
return approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2326
2444
|
export function buildPrompt(input: BuildPromptInput): string {
|
|
2327
2445
|
const {
|
|
2328
2446
|
resolution,
|
|
@@ -2336,14 +2454,13 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2336
2454
|
attachmentPaths,
|
|
2337
2455
|
interactionMode,
|
|
2338
2456
|
buildFromPlan,
|
|
2457
|
+
conversationCatchup,
|
|
2339
2458
|
} = input;
|
|
2340
2459
|
|
|
2341
|
-
const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
2342
|
-
|
|
2343
2460
|
// HITL reinvocation: the agent is resumed, so its native context carries the
|
|
2344
2461
|
// prior conversation; the reinvocation prompt conveys the approval decisions
|
|
2345
2462
|
// (and which approved writes the runner already exact-applied).
|
|
2346
|
-
if (isHitlReinvocation) {
|
|
2463
|
+
if (isHitlReinvocation(approvalDecisions)) {
|
|
2347
2464
|
return buildReinvocationPrompt(
|
|
2348
2465
|
input.pendingApprovals,
|
|
2349
2466
|
approvalDecisions,
|
|
@@ -2353,15 +2470,28 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2353
2470
|
|
|
2354
2471
|
// A successfully resumed agent carries its own conversation context via the
|
|
2355
2472
|
// SDK's native store — send the raw user message with no preamble. The
|
|
2356
|
-
// exceptions are the per-EXECUTION
|
|
2473
|
+
// exceptions are the per-EXECUTION values, which never inherit from the
|
|
2357
2474
|
// session's first turn: the interaction-mode prefix (a follow-up can switch
|
|
2358
2475
|
// Agent→Plan mid-session, and for Cursor the prompt is the only plan-mode
|
|
2359
|
-
// enforcement)
|
|
2360
|
-
//
|
|
2476
|
+
// enforcement), the implement-plan directive (the build turn is usually a
|
|
2477
|
+
// follow-up on a resumed agent), THIS turn's attachments (spec.attachments
|
|
2478
|
+
// is per-execution — a file sent on a follow-up turn materializes for this
|
|
2479
|
+
// turn and would otherwise never be announced at all), and the conversation
|
|
2480
|
+
// catchup (handback ALWAYS lands mid-session on a resumed agent — this
|
|
2481
|
+
// prefix is the property the metadata lane structurally cannot deliver,
|
|
2482
|
+
// cloud DD-006). Catchup last: it is context, and context sits closest to
|
|
2483
|
+
// the task (the enhanced prompt's own ordering doctrine); the input files
|
|
2484
|
+
// precede it because they are this turn's payload, not background.
|
|
2361
2485
|
if (resolution.reason === "resumed_successfully") {
|
|
2362
2486
|
const prefixes = [
|
|
2363
2487
|
formatInteractionModePrefix(interactionMode),
|
|
2364
2488
|
formatImplementPlanSection(buildFromPlan, attachmentPaths),
|
|
2489
|
+
attachmentPaths.length > 0
|
|
2490
|
+
? formatInputFiles(attachmentPaths, input.vision)
|
|
2491
|
+
: undefined,
|
|
2492
|
+
conversationCatchup !== undefined
|
|
2493
|
+
? formatConversationCatchupSection(conversationCatchup)
|
|
2494
|
+
: undefined,
|
|
2365
2495
|
].filter((p): p is string => p !== undefined);
|
|
2366
2496
|
return prefixes.length > 0
|
|
2367
2497
|
? [...prefixes, userMessage].join("\n\n")
|
|
@@ -2381,11 +2511,13 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2381
2511
|
workspaceDirs,
|
|
2382
2512
|
workspaceFileRefs,
|
|
2383
2513
|
attachmentPaths,
|
|
2514
|
+
vision: input.vision,
|
|
2384
2515
|
interactionMode,
|
|
2385
2516
|
buildFromPlan,
|
|
2386
2517
|
contextBridge: input.contextBridge,
|
|
2387
2518
|
senderIdentity: input.senderIdentity,
|
|
2388
2519
|
sessionContext: input.sessionContext,
|
|
2520
|
+
conversationCatchup,
|
|
2389
2521
|
});
|
|
2390
2522
|
}
|
|
2391
2523
|
|
|
@@ -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,
|
|
@@ -31,6 +32,10 @@ import {
|
|
|
31
32
|
type SenderIdentity,
|
|
32
33
|
} from "../../shared/sender-identity.js";
|
|
33
34
|
import { formatSessionContextText } from "../../shared/session-context.js";
|
|
35
|
+
import {
|
|
36
|
+
visionDisclosureLines,
|
|
37
|
+
type NotViewableEntry,
|
|
38
|
+
} from "../../shared/attachment-vision.js";
|
|
34
39
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
35
40
|
import {
|
|
36
41
|
buildImplementPlanDirective,
|
|
@@ -55,6 +60,17 @@ export interface SkillMetadata {
|
|
|
55
60
|
path: string;
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Vision facts about this turn's attachments, rendered inside the
|
|
65
|
+
* input-files section: which images the model can see inline (in send
|
|
66
|
+
* order) and which degraded to the file-pointer story. Derived per turn
|
|
67
|
+
* from the resolved attachments — never carried across turns.
|
|
68
|
+
*/
|
|
69
|
+
export interface VisionPromptInfo {
|
|
70
|
+
inlineFilenames: string[];
|
|
71
|
+
notViewable: NotViewableEntry[];
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
export interface EnhancedPromptOptions {
|
|
59
75
|
instructions: string;
|
|
60
76
|
userMessage: string;
|
|
@@ -75,6 +91,8 @@ export interface EnhancedPromptOptions {
|
|
|
75
91
|
workspaceDirs: string[];
|
|
76
92
|
workspaceFileRefs: string[];
|
|
77
93
|
attachmentPaths: string[];
|
|
94
|
+
/** Inline/degraded image facts for the input-files section (T04 vision). */
|
|
95
|
+
vision?: VisionPromptInfo;
|
|
78
96
|
interactionMode?: InteractionMode;
|
|
79
97
|
/**
|
|
80
98
|
* The execution is a Build-from-plan turn (spec.execution_config
|
|
@@ -107,6 +125,16 @@ export interface EnhancedPromptOptions {
|
|
|
107
125
|
* store — the context is constant for the session's lifetime.
|
|
108
126
|
*/
|
|
109
127
|
sessionContext?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Conversation catchup (cloud DD-006): what happened on the channel
|
|
130
|
+
* conversation that the agent has not seen, read from the execution
|
|
131
|
+
* spec's `conversation_catchup`. PER-TURN, unlike the three standing
|
|
132
|
+
* siblings above: it rides BOTH prompt paths — this enhanced prompt and
|
|
133
|
+
* a resumed turn's prefix (the `interaction_mode` shape) — because
|
|
134
|
+
* handback lands mid-session on a resumed agent, the exact case the
|
|
135
|
+
* metadata lane cannot reach.
|
|
136
|
+
*/
|
|
137
|
+
conversationCatchup?: string;
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
/**
|
|
@@ -168,7 +196,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
if (options.attachmentPaths.length > 0) {
|
|
171
|
-
sections.push(formatInputFiles(options.attachmentPaths));
|
|
199
|
+
sections.push(formatInputFiles(options.attachmentPaths, options.vision));
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
if (options.workspaceFileRefs.length > 0) {
|
|
@@ -200,6 +228,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
200
228
|
sections.push(formatContextBridgeSection(options.contextBridge));
|
|
201
229
|
}
|
|
202
230
|
|
|
231
|
+
// Catchup after the bridge (DD-007 D-d: bridge first, catchup second) —
|
|
232
|
+
// the bridge carries the pre-takeover conversation, the catchup the human
|
|
233
|
+
// episode, strictly newer by construction; recency puts it closer to the
|
|
234
|
+
// task.
|
|
235
|
+
if (options.conversationCatchup) {
|
|
236
|
+
sections.push(formatConversationCatchupSection(options.conversationCatchup));
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
// Always last before the task: the platform's tool-approval protocol. Placed
|
|
204
240
|
// here for recency so it outweighs any "ask the user first" guidance Cursor
|
|
205
241
|
// surfaces from a connected MCP server (see formatToolApprovalProtocol).
|
|
@@ -345,6 +381,10 @@ export function formatSessionContextSection(context: string): string {
|
|
|
345
381
|
return `<session_context>\n${formatSessionContextText(context)}\n</session_context>`;
|
|
346
382
|
}
|
|
347
383
|
|
|
384
|
+
export function formatConversationCatchupSection(digest: string): string {
|
|
385
|
+
return `<conversation_catchup>\n${formatConversationCatchupText(digest)}\n</conversation_catchup>`;
|
|
386
|
+
}
|
|
387
|
+
|
|
348
388
|
export function formatSkillsSection(skills: SkillMetadata[]): string {
|
|
349
389
|
const entries = skills.map(
|
|
350
390
|
(s) => `- **${s.name}**: ${s.description}\n Path: \`${s.path}\``,
|
|
@@ -439,12 +479,20 @@ export function formatWorkspaceContext(dirs: string[]): string {
|
|
|
439
479
|
].join("\n");
|
|
440
480
|
}
|
|
441
481
|
|
|
442
|
-
export function formatInputFiles(paths: string[]): string {
|
|
482
|
+
export function formatInputFiles(paths: string[], vision?: VisionPromptInfo): string {
|
|
443
483
|
const entries = paths.map((p) => `- \`${p}\``);
|
|
484
|
+
// The vision lines (shared wording, attachment-vision.ts) tell the model
|
|
485
|
+
// which of these files it can already SEE inline versus which degraded to
|
|
486
|
+
// path-only — without them an agent silently ignores a photo the user
|
|
487
|
+
// believes it can see.
|
|
488
|
+
const disclosure = vision
|
|
489
|
+
? visionDisclosureLines(vision.inlineFilenames, vision.notViewable)
|
|
490
|
+
: [];
|
|
444
491
|
return [
|
|
445
492
|
"<input_files>",
|
|
446
493
|
"The following files have been provided as inputs. Read them when relevant to the task:",
|
|
447
494
|
...entries,
|
|
495
|
+
...disclosure,
|
|
448
496
|
"</input_files>",
|
|
449
497
|
].join("\n");
|
|
450
498
|
}
|