@stigmer/runner 3.2.1 → 3.2.2
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/call-transform.js +12 -2
- package/dist/activities/call-transform.js.map +1 -1
- package/dist/activities/call-validate.js +20 -2
- package/dist/activities/call-validate.js.map +1 -1
- package/dist/activities/discover-mcp-server.js +6 -20
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +12 -0
- package/dist/activities/execute-cursor/index.js +53 -12
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +19 -0
- package/dist/activities/execute-cursor/prompt-builder.js +21 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +19 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +12 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +4 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +2 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/context-bridge.d.ts +30 -0
- package/dist/shared/context-bridge.js +45 -0
- package/dist/shared/context-bridge.js.map +1 -0
- package/dist/shared/sender-identity.d.ts +50 -0
- package/dist/shared/sender-identity.js +69 -0
- package/dist/shared/sender-identity.js.map +1 -0
- package/dist/shared/with-timeout.d.ts +17 -0
- package/dist/shared/with-timeout.js +34 -0
- package/dist/shared/with-timeout.js.map +1 -0
- package/dist/workflow-engine/do-executor.d.ts +7 -0
- package/dist/workflow-engine/do-executor.js +59 -1
- package/dist/workflow-engine/do-executor.js.map +1 -1
- package/dist/workflow-engine/tasks/call-function.js +68 -1
- package/dist/workflow-engine/tasks/call-function.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +26 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +5 -1
- package/dist/workflows/engine-core.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/config.test.ts +8 -0
- package/src/activities/__tests__/call-validate.test.ts +137 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +60 -0
- package/src/activities/call-transform.ts +20 -2
- package/src/activities/call-validate.ts +27 -2
- package/src/activities/discover-mcp-server.ts +7 -28
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +65 -0
- package/src/activities/execute-cursor/index.ts +71 -15
- package/src/activities/execute-cursor/prompt-builder.ts +44 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +58 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +35 -0
- package/src/activities/execute-deep-agent/setup.ts +4 -0
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +23 -0
- package/src/runner-manager.ts +6 -1
- package/src/runner.ts +6 -1
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/context-bridge.test.ts +51 -0
- package/src/shared/__tests__/sender-identity.test.ts +92 -0
- package/src/shared/__tests__/with-timeout.test.ts +45 -0
- package/src/shared/context-bridge.ts +51 -0
- package/src/shared/sender-identity.ts +85 -0
- package/src/shared/with-timeout.ts +39 -0
- package/src/workflow-engine/__tests__/do-executor.test.ts +155 -0
- package/src/workflow-engine/__tests__/tasks/call-function.test.ts +94 -0
- package/src/workflow-engine/do-executor.ts +65 -1
- package/src/workflow-engine/tasks/call-function.ts +84 -3
- package/src/workflow-engine/types.ts +27 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +1 -0
- package/src/workflows/engine-core.ts +5 -1
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { ApplicationFailure } from "@temporalio/activity";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
evaluateExpression,
|
|
19
|
+
isStrictExpr,
|
|
20
|
+
sanitizeExpr,
|
|
21
|
+
} from "../workflow-engine/expression.js";
|
|
18
22
|
|
|
19
23
|
export interface TransformConfig {
|
|
20
24
|
readonly engine: string;
|
|
@@ -38,6 +42,13 @@ export async function transformAction(
|
|
|
38
42
|
"TRANSFORM_MISSING_EXPRESSION",
|
|
39
43
|
);
|
|
40
44
|
}
|
|
45
|
+
if (typeof config.expression !== "string") {
|
|
46
|
+
throw ApplicationFailure.nonRetryable(
|
|
47
|
+
`transform: 'expression' must be a jq string, got ${typeof config.expression}. ` +
|
|
48
|
+
"Fix the expression in the workflow's transform task_config.",
|
|
49
|
+
"TRANSFORM_INVALID_EXPRESSION",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
41
52
|
|
|
42
53
|
const engine = normalizeEngine(config.engine || "JQ");
|
|
43
54
|
|
|
@@ -50,5 +61,12 @@ export async function transformAction(
|
|
|
50
61
|
|
|
51
62
|
const data = config.input !== undefined ? config.input : taskInput;
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
// The expression arrives unresolved (deferred code — see the
|
|
65
|
+
// call-function builder). Accept both the strict `${ ... }` wrapper
|
|
66
|
+
// and the bare jq form the converter emits.
|
|
67
|
+
const expression = isStrictExpr(config.expression)
|
|
68
|
+
? sanitizeExpr(config.expression)
|
|
69
|
+
: config.expression;
|
|
70
|
+
|
|
71
|
+
return evaluateExpression(expression, data, {});
|
|
54
72
|
}
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { ApplicationFailure } from "@temporalio/activity";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
evaluateExpression,
|
|
23
|
+
isStrictExpr,
|
|
24
|
+
sanitizeExpr,
|
|
25
|
+
} from "../workflow-engine/expression.js";
|
|
22
26
|
|
|
23
27
|
export interface ValidateConfig {
|
|
24
28
|
readonly input: unknown;
|
|
@@ -160,7 +164,28 @@ async function validateRules(
|
|
|
160
164
|
const errors: ValidationError[] = [];
|
|
161
165
|
|
|
162
166
|
for (const rule of rules) {
|
|
163
|
-
|
|
167
|
+
// Config guard: the expression must be a jq string. Anything else is
|
|
168
|
+
// a workflow-definition defect (or an upstream resolution bug) — name
|
|
169
|
+
// the rule and what to fix instead of crashing in the jq engine.
|
|
170
|
+
if (typeof rule.expression !== "string" || rule.expression.length === 0) {
|
|
171
|
+
errors.push({
|
|
172
|
+
rule: rule.name,
|
|
173
|
+
message:
|
|
174
|
+
`Rule '${rule.name}' has an invalid 'expression': expected a jq ` +
|
|
175
|
+
`predicate string, got ${typeof rule.expression}. Fix the rule in ` +
|
|
176
|
+
`the workflow's validate task_config.`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Rule expressions arrive unresolved (deferred code — see the
|
|
182
|
+
// call-function builder). Accept both the strict `${ ... }` wrapper
|
|
183
|
+
// and a bare jq predicate.
|
|
184
|
+
const expr = isStrictExpr(rule.expression)
|
|
185
|
+
? sanitizeExpr(rule.expression)
|
|
186
|
+
: rule.expression;
|
|
187
|
+
|
|
188
|
+
const result = await evaluateExpression(expr, data, {});
|
|
164
189
|
if (!result) {
|
|
165
190
|
errors.push({
|
|
166
191
|
rule: rule.name,
|
|
@@ -25,6 +25,7 @@ import { StigmerClient } from "../client/stigmer-client.js";
|
|
|
25
25
|
import { mcpServerToResolved } from "../shared/mcp-resolver.js";
|
|
26
26
|
import { toMcpClientConfig } from "../shared/mcp-manager.js";
|
|
27
27
|
import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
|
|
28
|
+
import { withTimeout } from "../shared/with-timeout.js";
|
|
28
29
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
29
30
|
import type { Config } from "../config.js";
|
|
30
31
|
|
|
@@ -326,7 +327,12 @@ async function connectAndDiscover(
|
|
|
326
327
|
const resourceTemplates: DiscoveredResourceTemplateResult[] = [];
|
|
327
328
|
|
|
328
329
|
try {
|
|
329
|
-
|
|
330
|
+
const timeoutMessage =
|
|
331
|
+
`MCP server '${slug}' did not respond within ` +
|
|
332
|
+
`${Math.round(SESSION_INIT_TIMEOUT_MS / 1000)}s. If this server requires compilation or ` +
|
|
333
|
+
`package installation on first run (e.g. go run, npx), the cold ` +
|
|
334
|
+
`start may have exceeded the discovery timeout.`;
|
|
335
|
+
await withTimeout(SESSION_INIT_TIMEOUT_MS, timeoutMessage, async () => {
|
|
330
336
|
await client.initializeConnections();
|
|
331
337
|
|
|
332
338
|
const mcpClient = await client.getClient(slug);
|
|
@@ -411,33 +417,6 @@ async function classifyHttpOAuthFailure(
|
|
|
411
417
|
return detectOAuthChallenge(connection.url, connection.headers, slug);
|
|
412
418
|
}
|
|
413
419
|
|
|
414
|
-
async function withTimeout<T>(
|
|
415
|
-
ms: number,
|
|
416
|
-
serverSlug: string,
|
|
417
|
-
fn: () => Promise<T>,
|
|
418
|
-
): Promise<T> {
|
|
419
|
-
return new Promise<T>((resolve, reject) => {
|
|
420
|
-
const timer = setTimeout(() => {
|
|
421
|
-
reject(new Error(
|
|
422
|
-
`MCP server '${serverSlug}' did not respond within ` +
|
|
423
|
-
`${Math.round(ms / 1000)}s. If this server requires compilation or ` +
|
|
424
|
-
`package installation on first run (e.g. go run, npx), the cold ` +
|
|
425
|
-
`start may have exceeded the discovery timeout.`,
|
|
426
|
-
));
|
|
427
|
-
}, ms);
|
|
428
|
-
|
|
429
|
-
fn()
|
|
430
|
-
.then((result) => {
|
|
431
|
-
clearTimeout(timer);
|
|
432
|
-
resolve(result);
|
|
433
|
-
})
|
|
434
|
-
.catch((err) => {
|
|
435
|
-
clearTimeout(timer);
|
|
436
|
-
reject(err);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
420
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
442
421
|
// Temporal Activity Factory
|
|
443
422
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -87,6 +87,71 @@ describe("buildPrompt", () => {
|
|
|
87
87
|
expect(prompt).toContain(USER_MESSAGE);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("carries the rollover context bridge on the first execution (DD-013)", () => {
|
|
91
|
+
const prompt = buildPrompt(
|
|
92
|
+
input({
|
|
93
|
+
resolution: resolution("local", "created_first_execution"),
|
|
94
|
+
contextBridge: "Subject: Orders\nUser: where is my order?\nAssistant: Shipped.",
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(prompt).toContain("<previous_conversation_context>");
|
|
98
|
+
expect(prompt).toContain("User: where is my order?");
|
|
99
|
+
// The bridge is CONTEXT; the approval protocol keeps its pinned
|
|
100
|
+
// last-before-task slot so instructions outweigh it.
|
|
101
|
+
expect(prompt.indexOf("<previous_conversation_context>"))
|
|
102
|
+
.toBeLessThan(prompt.indexOf("<tool_approval_protocol>"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("never bridges a successfully resumed agent — its native context IS the conversation", () => {
|
|
106
|
+
const prompt = buildPrompt(
|
|
107
|
+
input({
|
|
108
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
109
|
+
contextBridge: "Subject: Orders\nUser: hi\nAssistant: hello",
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
expect(prompt).toBe(USER_MESSAGE);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("omits the bridge section when the session carries none", () => {
|
|
116
|
+
const prompt = buildPrompt(
|
|
117
|
+
input({ resolution: resolution("local", "created_first_execution") }),
|
|
118
|
+
);
|
|
119
|
+
expect(prompt).not.toContain("<previous_conversation_context>");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("carries the channel sender identity on the first execution", () => {
|
|
123
|
+
const prompt = buildPrompt(
|
|
124
|
+
input({
|
|
125
|
+
resolution: resolution("local", "created_first_execution"),
|
|
126
|
+
senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
expect(prompt).toContain("<conversation_sender>");
|
|
130
|
+
expect(prompt).toContain("WhatsApp phone number");
|
|
131
|
+
expect(prompt).toContain("15550001111");
|
|
132
|
+
// Identity is CONTEXT like the bridge; the approval protocol keeps its
|
|
133
|
+
// pinned last-before-task slot.
|
|
134
|
+
expect(prompt.indexOf("<conversation_sender>"))
|
|
135
|
+
.toBeLessThan(prompt.indexOf("<tool_approval_protocol>"));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("never re-sends the identity to a successfully resumed agent — its native context carries it", () => {
|
|
139
|
+
const prompt = buildPrompt(
|
|
140
|
+
input({
|
|
141
|
+
resolution: resolution("local", "resumed_successfully"),
|
|
142
|
+
senderIdentity: { value: "U0USER", kind: "slack_user_id" },
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
expect(prompt).toBe(USER_MESSAGE);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("omits the sender section when the session carries no identity (console sessions)", () => {
|
|
149
|
+
const prompt = buildPrompt(
|
|
150
|
+
input({ resolution: resolution("local", "created_first_execution") }),
|
|
151
|
+
);
|
|
152
|
+
expect(prompt).not.toContain("<conversation_sender>");
|
|
153
|
+
});
|
|
154
|
+
|
|
90
155
|
it("uses the reinvocation prompt for a HITL reinvocation (human-meaningful, no opaque ids)", () => {
|
|
91
156
|
const approvalDecisions = new Map<string, ApprovalAction>([
|
|
92
157
|
["tool-call-1", ApprovalAction.APPROVE],
|
|
@@ -45,6 +45,8 @@ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_p
|
|
|
45
45
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
46
46
|
import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
|
|
47
47
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
48
|
+
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
49
|
+
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
48
50
|
import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
49
51
|
import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
|
|
50
52
|
import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
@@ -115,6 +117,7 @@ import type { ClassifiedError } from "./error-classifier.js";
|
|
|
115
117
|
import { createAgent, createCloudAgent } from "./session-lifecycle.js";
|
|
116
118
|
import { setMaxListeners } from "node:events";
|
|
117
119
|
import { startHeartbeat } from "../../shared/heartbeat.js";
|
|
120
|
+
import { withTimeout } from "../../shared/with-timeout.js";
|
|
118
121
|
import { getShutdownSignalForQueue } from "../../runner-manager.js";
|
|
119
122
|
|
|
120
123
|
/**
|
|
@@ -257,6 +260,25 @@ async function executeCursorInner(
|
|
|
257
260
|
// can be classified with the same context as the run.wait() error path.
|
|
258
261
|
let errorContext = { model: "default", mode: "local", agentId: "" };
|
|
259
262
|
|
|
263
|
+
// Periodic heartbeat for the ENTIRE activity, started before any phase runs.
|
|
264
|
+
// Setup phases make network calls (blueprint resolution, workspace clone, MCP
|
|
265
|
+
// backfill, Agent.create) that can stall; the scattered manual heartbeat()
|
|
266
|
+
// pulses between them leave every individual call uncovered. The production
|
|
267
|
+
// stale-proxy incident hung inside Agent.create with zero heartbeats and
|
|
268
|
+
// surfaced as an opaque 5-minute Temporal timeout. The label names the
|
|
269
|
+
// current phase so a stall is attributed in Temporal heartbeat details, and
|
|
270
|
+
// cancellation stays observable throughout. Safe ONLY because every SDK call
|
|
271
|
+
// below is itself bounded (agentResolveTimeoutMs, stall watchdog) — an
|
|
272
|
+
// unbounded hang under a live heartbeat would keep a dead activity alive
|
|
273
|
+
// forever.
|
|
274
|
+
let heartbeatPhase = "setup";
|
|
275
|
+
const taskQueue = Context.current().info.taskQueue;
|
|
276
|
+
const shutdownSignal = getShutdownSignalForQueue(taskQueue);
|
|
277
|
+
periodicHeartbeat = startHeartbeat(30_000, () => ({
|
|
278
|
+
phase: heartbeatPhase,
|
|
279
|
+
execution: executionId,
|
|
280
|
+
}), { shutdownSignal });
|
|
281
|
+
|
|
260
282
|
try {
|
|
261
283
|
// Phase 1: Hydrate execution from DB
|
|
262
284
|
await reportSetupProgress(client, executionId, "Fetching execution");
|
|
@@ -270,6 +292,7 @@ async function executeCursorInner(
|
|
|
270
292
|
const blueprint = await resolveBlueprint(client, session, config.workspaceRootDir);
|
|
271
293
|
|
|
272
294
|
// Phase 2b: Resolve execution environment (MCP server credentials)
|
|
295
|
+
heartbeatPhase = "resolving_environment";
|
|
273
296
|
await reportSetupProgress(client, executionId, "Resolving environment");
|
|
274
297
|
const { envVars, secretKeys } = await resolveExecutionEnv(client, executionId);
|
|
275
298
|
heartbeat();
|
|
@@ -280,6 +303,7 @@ async function executeCursorInner(
|
|
|
280
303
|
// disabled the runner must provision the workspace itself, mirroring the
|
|
281
304
|
// native harness. Git provisioning is idempotent across multi-turn and
|
|
282
305
|
// HITL reinvocations.
|
|
306
|
+
heartbeatPhase = "provisioning_workspace";
|
|
283
307
|
await reportSetupProgress(client, executionId, "Provisioning workspace");
|
|
284
308
|
const workspaceProvision = await provisionCursorWorkspace(
|
|
285
309
|
config, session, envVars, sessionId ?? "",
|
|
@@ -554,6 +578,7 @@ async function executeCursorInner(
|
|
|
554
578
|
);
|
|
555
579
|
|
|
556
580
|
// Phase 4a: Connect backfill for undiscovered MCP servers
|
|
581
|
+
heartbeatPhase = "resolving_mcp_servers";
|
|
557
582
|
const sessionOrg = session.metadata?.org ?? "";
|
|
558
583
|
mcpResolution = await backfillMcpServersIfNeeded(
|
|
559
584
|
client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
|
|
@@ -815,10 +840,22 @@ async function executeCursorInner(
|
|
|
815
840
|
agents: cursorSubAgents,
|
|
816
841
|
};
|
|
817
842
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
843
|
+
// Agent.create/Agent.resume have no timeout of their own — a degraded
|
|
844
|
+
// transport (dead proxy connection, stale HTTP/2 session) hangs them
|
|
845
|
+
// forever, which the periodic heartbeat would happily keep alive. The
|
|
846
|
+
// bound converts that hang into an immediate, named transport failure.
|
|
847
|
+
// The message carries "timed out" so the error classifier's network
|
|
848
|
+
// patterns mark it retryable.
|
|
849
|
+
heartbeatPhase = "resolving_agent";
|
|
850
|
+
const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
|
|
851
|
+
let resolution: AgentResolution = await withTimeout(
|
|
852
|
+
config.agentResolveTimeoutMs,
|
|
853
|
+
() =>
|
|
854
|
+
`Cursor agent ${threadId ? "resume" : "create"} timed out after ${resolveTimeoutSeconds}s ` +
|
|
855
|
+
`(${config.proxyEndpoint ? `via proxy ${config.proxyEndpoint}` : "direct Cursor API connection"}). ` +
|
|
856
|
+
`The transport connection is likely dead. Retry the message; if this persists, ` +
|
|
857
|
+
`check proxy and network health.`,
|
|
858
|
+
() => resolveAgent(threadId, createOptions, agentMode),
|
|
822
859
|
);
|
|
823
860
|
|
|
824
861
|
console.log(
|
|
@@ -878,6 +915,8 @@ async function executeCursorInner(
|
|
|
878
915
|
appliedToolCallIds,
|
|
879
916
|
interactionMode,
|
|
880
917
|
buildFromPlan,
|
|
918
|
+
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
919
|
+
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
881
920
|
});
|
|
882
921
|
|
|
883
922
|
// Phase 10a: Inject structured output instruction for Cursor harness
|
|
@@ -962,16 +1001,10 @@ async function executeCursorInner(
|
|
|
962
1001
|
maxCostUsd,
|
|
963
1002
|
};
|
|
964
1003
|
|
|
965
|
-
//
|
|
966
|
-
// (
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
const taskQueue = Context.current().info.taskQueue;
|
|
970
|
-
const shutdownSignal = getShutdownSignalForQueue(taskQueue);
|
|
971
|
-
periodicHeartbeat = startHeartbeat(30_000, () => ({
|
|
972
|
-
phase: "cursor_streaming",
|
|
973
|
-
execution: executionId,
|
|
974
|
-
}), { shutdownSignal });
|
|
1004
|
+
// The activity-wide periodic heartbeat (started at entry) keeps Temporal
|
|
1005
|
+
// informed during silent SDK operations (long tool calls, MCP requests,
|
|
1006
|
+
// model thinking); relabel it for the streaming phase.
|
|
1007
|
+
heartbeatPhase = "cursor_streaming";
|
|
975
1008
|
|
|
976
1009
|
// The Cursor SDK registers abort listeners on the cancellation signal for
|
|
977
1010
|
// each concurrent tool call (fetch, MCP, shell). With 10+ parallel tools,
|
|
@@ -1460,6 +1493,8 @@ async function executeCursorInner(
|
|
|
1460
1493
|
attachmentPaths,
|
|
1461
1494
|
pendingApprovals: adjudicatedApprovals,
|
|
1462
1495
|
interactionMode,
|
|
1496
|
+
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1497
|
+
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1463
1498
|
});
|
|
1464
1499
|
|
|
1465
1500
|
console.log(
|
|
@@ -1866,6 +1901,12 @@ async function executeCursorInner(
|
|
|
1866
1901
|
|
|
1867
1902
|
return slimStatus(status);
|
|
1868
1903
|
} finally {
|
|
1904
|
+
// Stop the activity-wide periodic heartbeat on EVERY exit path
|
|
1905
|
+
// (idempotent). The epilogue and catch stop it at the pause/shutdown
|
|
1906
|
+
// disambiguation points; this covers early returns (e.g. the
|
|
1907
|
+
// pure-reconcile resume) so no orphaned timer survives the activity.
|
|
1908
|
+
periodicHeartbeat?.stop();
|
|
1909
|
+
|
|
1869
1910
|
// End the OTel turn span + record metrics with the final token snapshot on
|
|
1870
1911
|
// EVERY exit path (idempotent). Placed here so the span covers any recovery
|
|
1871
1912
|
// retry (whose tokens accrue after the primary stream) and never leaks on an
|
|
@@ -2019,6 +2060,18 @@ export interface BuildPromptInput {
|
|
|
2019
2060
|
* .build_from_plan): both prompt paths carry the implement-plan directive.
|
|
2020
2061
|
*/
|
|
2021
2062
|
buildFromPlan?: boolean;
|
|
2063
|
+
/**
|
|
2064
|
+
* Rollover context bridge from `SessionSpec.metadata` (cloud DD-013).
|
|
2065
|
+
* Only the enhanced-prompt path consumes it — a resumed agent's native
|
|
2066
|
+
* context IS the previous conversation, so it needs no bridge.
|
|
2067
|
+
*/
|
|
2068
|
+
contextBridge?: string;
|
|
2069
|
+
/**
|
|
2070
|
+
* Channel sender identity from `SessionSpec.metadata`. Like the bridge,
|
|
2071
|
+
* only the enhanced-prompt path consumes it — a resumed agent's native
|
|
2072
|
+
* context already carries it from the session's first turn.
|
|
2073
|
+
*/
|
|
2074
|
+
senderIdentity?: import("../../shared/sender-identity.js").SenderIdentity;
|
|
2022
2075
|
}
|
|
2023
2076
|
|
|
2024
2077
|
/**
|
|
@@ -2083,7 +2136,8 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2083
2136
|
}
|
|
2084
2137
|
|
|
2085
2138
|
// First execution, or a fresh agent created after a resume failure: there is
|
|
2086
|
-
// no prior conversation to inherit, so start a new turn with full context
|
|
2139
|
+
// no prior conversation to inherit, so start a new turn with full context —
|
|
2140
|
+
// including the rollover bridge, when the session carries one.
|
|
2087
2141
|
return buildEnhancedPrompt({
|
|
2088
2142
|
instructions,
|
|
2089
2143
|
userMessage,
|
|
@@ -2094,6 +2148,8 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2094
2148
|
attachmentPaths,
|
|
2095
2149
|
interactionMode,
|
|
2096
2150
|
buildFromPlan,
|
|
2151
|
+
contextBridge: input.contextBridge,
|
|
2152
|
+
senderIdentity: input.senderIdentity,
|
|
2097
2153
|
});
|
|
2098
2154
|
}
|
|
2099
2155
|
|
|
@@ -20,6 +20,11 @@ import { resolve } from "node:path";
|
|
|
20
20
|
import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
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
|
+
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
24
|
+
import {
|
|
25
|
+
formatSenderIdentityText,
|
|
26
|
+
type SenderIdentity,
|
|
27
|
+
} from "../../shared/sender-identity.js";
|
|
23
28
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
24
29
|
import {
|
|
25
30
|
buildImplementPlanDirective,
|
|
@@ -60,6 +65,22 @@ export interface EnhancedPromptOptions {
|
|
|
60
65
|
* The user message itself is just a short label ("Build from plan").
|
|
61
66
|
*/
|
|
62
67
|
buildFromPlan?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Rollover context bridge (cloud DD-013): a digest of the previous
|
|
70
|
+
* session's conversation, read from `SessionSpec.metadata`. Lands in the
|
|
71
|
+
* first message, so it persists in the cursor agent's own conversation
|
|
72
|
+
* store for the session's lifetime — buildEnhancedPrompt firing only on
|
|
73
|
+
* first/fresh executions is exactly the right delivery.
|
|
74
|
+
*/
|
|
75
|
+
contextBridge?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Channel sender identity (attribution, not authorization): the
|
|
78
|
+
* provider-verified identifier of the person on the channel, read from
|
|
79
|
+
* `SessionSpec.metadata`. Like the bridge, it lands in the first message
|
|
80
|
+
* and persists in the cursor agent's own conversation store — identity
|
|
81
|
+
* is constant per conversation (channel sessions are keyed per-sender).
|
|
82
|
+
*/
|
|
83
|
+
senderIdentity?: SenderIdentity;
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
/**
|
|
@@ -119,6 +140,21 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
119
140
|
sections.push(formatResponseRules());
|
|
120
141
|
}
|
|
121
142
|
|
|
143
|
+
// The sender identity precedes the bridge: it is standing context about
|
|
144
|
+
// WHO the conversation is with, which the carried conversation may refer
|
|
145
|
+
// back to.
|
|
146
|
+
if (options.senderIdentity) {
|
|
147
|
+
sections.push(formatSenderIdentitySection(options.senderIdentity));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The rollover context bridge sits directly before the protocol + task so
|
|
151
|
+
// the carried conversation is the freshest CONTEXT the model reads —
|
|
152
|
+
// while the approval protocol keeps its pinned last-before-task slot (it
|
|
153
|
+
// is an INSTRUCTION and must outweigh everything, including this bridge).
|
|
154
|
+
if (options.contextBridge) {
|
|
155
|
+
sections.push(formatContextBridgeSection(options.contextBridge));
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
// Always last before the task: the platform's tool-approval protocol. Placed
|
|
123
159
|
// here for recency so it outweighs any "ask the user first" guidance Cursor
|
|
124
160
|
// surfaces from a connected MCP server (see formatToolApprovalProtocol).
|
|
@@ -252,6 +288,14 @@ export function formatInstructions(instructions: string): string {
|
|
|
252
288
|
return `<agent_instructions>\n${instructions}\n</agent_instructions>`;
|
|
253
289
|
}
|
|
254
290
|
|
|
291
|
+
export function formatContextBridgeSection(bridge: string): string {
|
|
292
|
+
return `<previous_conversation_context>\n${formatContextBridgeText(bridge)}\n</previous_conversation_context>`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function formatSenderIdentitySection(identity: SenderIdentity): string {
|
|
296
|
+
return `<conversation_sender>\n${formatSenderIdentityText(identity)}\n</conversation_sender>`;
|
|
297
|
+
}
|
|
298
|
+
|
|
255
299
|
export function formatSkillsSection(skills: SkillMetadata[]): string {
|
|
256
300
|
const entries = skills.map(
|
|
257
301
|
(s) => `- **${s.name}**: ${s.description}\n Path: \`${s.path}\``,
|
|
@@ -208,6 +208,64 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
208
208
|
expect(prompt).not.toContain("## Workspace");
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
describe("rollover context bridge (DD-013)", () => {
|
|
212
|
+
const base = {
|
|
213
|
+
instructions: "Test",
|
|
214
|
+
provisionResults: [],
|
|
215
|
+
containerRoot: "",
|
|
216
|
+
skillsPromptSection: "",
|
|
217
|
+
workspaceFileRefs: [],
|
|
218
|
+
workspaceRoot: "",
|
|
219
|
+
injectedFiles: [],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it("appends the bridge as standing session context (every-turn injection)", () => {
|
|
223
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
224
|
+
...base,
|
|
225
|
+
contextBridge: "Subject: Orders\nUser: where is my order?\nAssistant: Shipped.",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(prompt).toContain("## Previous conversation context");
|
|
229
|
+
expect(prompt).toContain("User: where is my order?");
|
|
230
|
+
expect(prompt).toContain("Do not repeat it back");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("omits the section when the session carries no bridge", () => {
|
|
234
|
+
const prompt = buildEnhancedSystemPrompt(base);
|
|
235
|
+
|
|
236
|
+
expect(prompt).not.toContain("## Previous conversation context");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("channel sender identity", () => {
|
|
241
|
+
const base = {
|
|
242
|
+
instructions: "Test",
|
|
243
|
+
provisionResults: [],
|
|
244
|
+
containerRoot: "",
|
|
245
|
+
skillsPromptSection: "",
|
|
246
|
+
workspaceFileRefs: [],
|
|
247
|
+
workspaceRoot: "",
|
|
248
|
+
injectedFiles: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
it("appends the sender as standing session context (every-turn injection)", () => {
|
|
252
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
253
|
+
...base,
|
|
254
|
+
senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(prompt).toContain("## Conversation sender");
|
|
258
|
+
expect(prompt).toContain("WhatsApp phone number");
|
|
259
|
+
expect(prompt).toContain("15550001111");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("omits the section when the session carries no identity (console sessions)", () => {
|
|
263
|
+
const prompt = buildEnhancedSystemPrompt(base);
|
|
264
|
+
|
|
265
|
+
expect(prompt).not.toContain("## Conversation sender");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
211
269
|
describe("plan mode", () => {
|
|
212
270
|
const base = {
|
|
213
271
|
instructions: "Test",
|
|
@@ -10,6 +10,11 @@ import { relative } from "node:path";
|
|
|
10
10
|
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
11
|
import type { ProvisionResult, GitMetadata } from "../../shared/workspace/types.js";
|
|
12
12
|
import { SourceType } from "../../shared/workspace/types.js";
|
|
13
|
+
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
14
|
+
import {
|
|
15
|
+
formatSenderIdentityText,
|
|
16
|
+
type SenderIdentity,
|
|
17
|
+
} from "../../shared/sender-identity.js";
|
|
13
18
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
14
19
|
import {
|
|
15
20
|
buildImplementPlanDirective,
|
|
@@ -106,6 +111,24 @@ export interface PromptBuilderInput {
|
|
|
106
111
|
* message itself is just a short label ("Build from plan").
|
|
107
112
|
*/
|
|
108
113
|
buildFromPlan?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Rollover context bridge (cloud DD-013): a digest of the previous
|
|
116
|
+
* session's conversation, read from `SessionSpec.metadata`. Injected on
|
|
117
|
+
* EVERY turn by design: the native system prompt is rebuilt per
|
|
118
|
+
* invocation (never checkpointed with the message history), so a
|
|
119
|
+
* first-turn-only injection would vanish from turn 2 onward. The bridge
|
|
120
|
+
* is standing session context, like skills.
|
|
121
|
+
*/
|
|
122
|
+
contextBridge?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Channel sender identity (attribution, not authorization): the
|
|
125
|
+
* provider-verified identifier of the person on the channel, read from
|
|
126
|
+
* `SessionSpec.metadata`. Injected on EVERY turn like the bridge — the
|
|
127
|
+
* native system prompt is rebuilt per invocation, and the sender is
|
|
128
|
+
* constant for the session's lifetime (channel sessions are keyed
|
|
129
|
+
* per-sender).
|
|
130
|
+
*/
|
|
131
|
+
senderIdentity?: SenderIdentity;
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
export interface InjectedFile {
|
|
@@ -147,6 +170,18 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
147
170
|
prompt += buildInjectedFilesSection(input.injectedFiles);
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
if (input.senderIdentity) {
|
|
174
|
+
prompt +=
|
|
175
|
+
"\n\n## Conversation sender\n\n" +
|
|
176
|
+
formatSenderIdentityText(input.senderIdentity);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (input.contextBridge) {
|
|
180
|
+
prompt +=
|
|
181
|
+
"\n\n## Previous conversation context\n\n" +
|
|
182
|
+
formatContextBridgeText(input.contextBridge);
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
prompt += RESPONSE_RULES;
|
|
151
186
|
prompt += SUB_AGENT_RULES;
|
|
152
187
|
|
|
@@ -23,6 +23,8 @@ import type { DynamicStructuredTool } from "@langchain/core/tools";
|
|
|
23
23
|
import type { Config } from "../../config.js";
|
|
24
24
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
25
25
|
import { createCheckpointer } from "../../shared/checkpointer/factory.js";
|
|
26
|
+
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
27
|
+
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
26
28
|
import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
|
|
27
29
|
import { resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
28
30
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
@@ -386,6 +388,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
386
388
|
injectedFiles,
|
|
387
389
|
interactionMode: execution.spec!.executionConfig?.interactionMode,
|
|
388
390
|
buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
|
|
391
|
+
contextBridge: readContextBridge(session.spec!.metadata),
|
|
392
|
+
senderIdentity: readSenderIdentity(session.spec!.metadata),
|
|
389
393
|
});
|
|
390
394
|
|
|
391
395
|
// Step 9: Construct the LLM model. Resolution to the provider API id
|
|
@@ -221,6 +221,7 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
|
|
|
221
221
|
case: "taskStarted",
|
|
222
222
|
value: create(TaskStartedPayloadSchema, {
|
|
223
223
|
taskKind: TASK_KIND_MAP[desc.taskKind] ?? 0,
|
|
224
|
+
inputSummary: toJsonObject(desc.inputSummary),
|
|
224
225
|
attemptNumber: desc.attemptNumber,
|
|
225
226
|
}),
|
|
226
227
|
};
|
|
@@ -233,6 +234,7 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
|
|
|
233
234
|
value: create(TaskCompletedPayloadSchema, {
|
|
234
235
|
taskKind: TASK_KIND_MAP[desc.taskKind] ?? 0,
|
|
235
236
|
durationMs: BigInt(desc.durationMs),
|
|
237
|
+
outputSummary: toJsonObject(desc.outputSummary),
|
|
236
238
|
costMicros: BigInt(desc.costMicros),
|
|
237
239
|
tokensUsed: BigInt(desc.tokensUsed),
|
|
238
240
|
}),
|