@stigmer/runner 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent.js +85 -10
  3. package/dist/activities/call-agent.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.d.ts +12 -0
  5. package/dist/activities/execute-cursor/index.js +80 -10
  6. package/dist/activities/execute-cursor/index.js.map +1 -1
  7. package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
  8. package/dist/activities/execute-cursor/model-pricing.js +19 -0
  9. package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
  10. package/dist/activities/execute-cursor/prompt-builder.d.ts +11 -0
  11. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  12. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  13. package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
  14. package/dist/activities/execute-cursor/service-tier.js +187 -0
  15. package/dist/activities/execute-cursor/service-tier.js.map +1 -0
  16. package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
  17. package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
  18. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  19. package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
  20. package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
  21. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  22. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +28 -0
  23. package/dist/activities/execute-deep-agent/mcp-gate.js +22 -0
  24. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -0
  25. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +11 -0
  26. package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
  27. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/setup.js +30 -4
  29. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  30. package/dist/client/stigmer-client.d.ts +6 -1
  31. package/dist/client/stigmer-client.js +5 -2
  32. package/dist/client/stigmer-client.js.map +1 -1
  33. package/dist/main.js +18 -0
  34. package/dist/main.js.map +1 -1
  35. package/dist/runner.js +48 -0
  36. package/dist/runner.js.map +1 -1
  37. package/dist/sandbox-token-renewal.d.ts +65 -0
  38. package/dist/sandbox-token-renewal.js +169 -0
  39. package/dist/sandbox-token-renewal.js.map +1 -0
  40. package/dist/shared/artifact-storage.d.ts +17 -3
  41. package/dist/shared/artifact-storage.js +22 -4
  42. package/dist/shared/artifact-storage.js.map +1 -1
  43. package/dist/shared/channel-attachment.d.ts +3 -1
  44. package/dist/shared/channel-attachment.js +3 -1
  45. package/dist/shared/channel-attachment.js.map +1 -1
  46. package/dist/shared/conversation-attachment.d.ts +81 -0
  47. package/dist/shared/conversation-attachment.js +102 -0
  48. package/dist/shared/conversation-attachment.js.map +1 -0
  49. package/dist/shared/conversation-catchup.d.ts +33 -0
  50. package/dist/shared/conversation-catchup.js +53 -0
  51. package/dist/shared/conversation-catchup.js.map +1 -0
  52. package/dist/workflow-engine/loader.js +99 -2
  53. package/dist/workflow-engine/loader.js.map +1 -1
  54. package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
  55. package/dist/workflow-engine/tasks/call-agent.js +0 -2
  56. package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
  57. package/dist/workflow-engine/types.d.ts +39 -7
  58. package/dist/workflow-engine/types.js.map +1 -1
  59. package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
  60. package/dist/workflows/call-agent-orchestrator.js +8 -2
  61. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
  64. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
  65. package/src/activities/__tests__/call-agent.test.ts +219 -4
  66. package/src/activities/call-agent.ts +94 -10
  67. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
  68. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
  69. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
  70. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
  71. package/src/activities/execute-cursor/index.ts +111 -11
  72. package/src/activities/execute-cursor/model-pricing.ts +23 -0
  73. package/src/activities/execute-cursor/prompt-builder.ts +23 -0
  74. package/src/activities/execute-cursor/service-tier.ts +244 -0
  75. package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
  76. package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
  77. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +42 -0
  78. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +39 -1
  79. package/src/activities/execute-deep-agent/mcp-gate.ts +37 -0
  80. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -2
  81. package/src/activities/execute-deep-agent/setup.ts +40 -4
  82. package/src/client/stigmer-client.ts +11 -4
  83. package/src/main.ts +20 -0
  84. package/src/runner.ts +62 -0
  85. package/src/sandbox-token-renewal.ts +212 -0
  86. package/src/shared/__tests__/channel-attachment.test.ts +3 -3
  87. package/src/shared/__tests__/conversation-attachment.test.ts +138 -0
  88. package/src/shared/__tests__/conversation-catchup.test.ts +70 -0
  89. package/src/shared/__tests__/synthesized-attachment.test.ts +120 -0
  90. package/src/shared/artifact-storage.ts +32 -7
  91. package/src/shared/channel-attachment.ts +3 -1
  92. package/src/shared/conversation-attachment.ts +115 -0
  93. package/src/shared/conversation-catchup.ts +60 -0
  94. package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
  95. package/src/workflow-engine/__tests__/loader.test.ts +192 -7
  96. package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
  97. package/src/workflow-engine/loader.ts +113 -2
  98. package/src/workflow-engine/tasks/call-agent.ts +0 -2
  99. package/src/workflow-engine/types.ts +40 -7
  100. package/src/workflows/call-agent-orchestrator.ts +8 -2
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
3
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
4
 
4
5
  let mockGetAgentByReference: ReturnType<typeof vi.fn>;
5
6
  let mockCreateSession: ReturnType<typeof vi.fn>;
@@ -93,11 +94,11 @@ describe("callAgentAction", () => {
93
94
  expect(ref.slug).toBe("my-agent");
94
95
  });
95
96
 
96
- it("uses config.org over __stigmer_org_id when both are present", async () => {
97
+ it("org/slug agent reference overrides the workflow org for lookup only", async () => {
97
98
  await expect(
98
99
  callAgentAction(
99
- { agent: "my-agent", message: "Hello", org: "explicit-org" },
100
- { __stigmer_org_id: "fallback-org" },
100
+ { agent: "explicit-org/my-agent", message: "Hello" },
101
+ { __stigmer_org_id: "workflow-org" },
101
102
  "wfl_parent789",
102
103
  ),
103
104
  ).rejects.toThrow("CompleteAsyncError");
@@ -106,6 +107,11 @@ describe("callAgentAction", () => {
106
107
  expect(ref.org).toBe("explicit-org");
107
108
  expect(ref.slug).toBe("my-agent");
108
109
  expect(ref.kind).toBe(ApiResourceKind.agent);
110
+
111
+ // The execution itself is still created in the workflow's org — the
112
+ // cross-org reference changes agent lookup, never the billing org.
113
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
114
+ expect(execution.metadata?.org).toBe("workflow-org");
109
115
  });
110
116
  });
111
117
 
@@ -120,7 +126,7 @@ describe("callAgentAction", () => {
120
126
  ).rejects.toThrow("call:agent requires an organization context");
121
127
  });
122
128
 
123
- it("falls back to __stigmer_org_id from runtime env", async () => {
129
+ it("uses __stigmer_org_id from runtime env", async () => {
124
130
  await expect(
125
131
  callAgentAction(
126
132
  { agent: "my-agent", message: "Hello" },
@@ -260,4 +266,213 @@ describe("callAgentAction", () => {
260
266
  expect(execution.spec.activityTaskQueue).toBe("");
261
267
  });
262
268
  });
269
+
270
+ describe("run_config → ExecutionConfig mapping (#358)", () => {
271
+ it("maps model_name, max_cost_usd, and max_tool_rounds onto ExecutionConfig", async () => {
272
+ await expect(
273
+ callAgentAction(
274
+ {
275
+ agent: "my-agent",
276
+ message: "Hello",
277
+ run_config: {
278
+ model_name: "claude-sonnet-4-6",
279
+ max_cost_usd: 0.75,
280
+ max_tool_rounds: 15,
281
+ },
282
+ },
283
+ { __stigmer_org_id: "test-org" },
284
+ "wfl_parent",
285
+ ),
286
+ ).rejects.toThrow("CompleteAsyncError");
287
+
288
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
289
+ expect(execution.spec.executionConfig).toBeDefined();
290
+ expect(execution.spec.executionConfig.modelName).toBe("claude-sonnet-4-6");
291
+ expect(execution.spec.executionConfig.maxCostUsd).toBe(0.75);
292
+ expect(execution.spec.executionConfig.maxToolRounds).toBe(15);
293
+ });
294
+
295
+ it("maps a canonical service_tier onto ExecutionConfig.serviceTier (#357)", async () => {
296
+ await expect(
297
+ callAgentAction(
298
+ {
299
+ agent: "my-agent",
300
+ message: "Hello",
301
+ run_config: { service_tier: "SERVICE_TIER_FAST" },
302
+ },
303
+ { __stigmer_org_id: "test-org" },
304
+ "wfl_parent",
305
+ ),
306
+ ).rejects.toThrow("CompleteAsyncError");
307
+
308
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
309
+ expect(execution.spec.executionConfig).toBeDefined();
310
+ expect(execution.spec.executionConfig.serviceTier).toBe(ServiceTier.FAST);
311
+ });
312
+
313
+ it("fails loudly on a service_tier value with no proto mapping", async () => {
314
+ // The loader canonicalizes tiers; an unmapped value reaching the
315
+ // activity means loader/activity drift. A pricing directive must
316
+ // never be silently dropped.
317
+ await expect(
318
+ callAgentAction(
319
+ {
320
+ agent: "my-agent",
321
+ message: "Hello",
322
+ run_config: { service_tier: "SERVICE_TIER_TURBO" },
323
+ },
324
+ { __stigmer_org_id: "test-org" },
325
+ "wfl_parent",
326
+ ),
327
+ ).rejects.toThrow(
328
+ "call:agent run_config.service_tier 'SERVICE_TIER_TURBO' has no proto mapping",
329
+ );
330
+ });
331
+
332
+ it("omits ExecutionConfig entirely when run_config and output are absent", async () => {
333
+ await expect(
334
+ callAgentAction(
335
+ { agent: "my-agent", message: "Hello" },
336
+ { __stigmer_org_id: "test-org" },
337
+ "wfl_parent",
338
+ ),
339
+ ).rejects.toThrow("CompleteAsyncError");
340
+
341
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
342
+ expect(execution.spec.executionConfig).toBeUndefined();
343
+ });
344
+
345
+ it("treats zero bounds as no override", async () => {
346
+ await expect(
347
+ callAgentAction(
348
+ {
349
+ agent: "my-agent",
350
+ message: "Hello",
351
+ run_config: { model_name: "claude-sonnet-4-6", max_cost_usd: 0, max_tool_rounds: 0 },
352
+ },
353
+ { __stigmer_org_id: "test-org" },
354
+ "wfl_parent",
355
+ ),
356
+ ).rejects.toThrow("CompleteAsyncError");
357
+
358
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
359
+ expect(execution.spec.executionConfig.modelName).toBe("claude-sonnet-4-6");
360
+ // Proto zero values: unset numerics read back as 0, but nothing was
361
+ // deliberately written — the guards must not have set them from a
362
+ // zero "no override" input.
363
+ expect(execution.spec.executionConfig.maxCostUsd).toBe(0);
364
+ expect(execution.spec.executionConfig.maxToolRounds).toBe(0);
365
+ });
366
+ });
367
+
368
+ describe("workspace entries and workflow provenance (#358 Phase 2)", () => {
369
+ it("maps git workspace entries onto the created session's spec", async () => {
370
+ await expect(
371
+ callAgentAction(
372
+ {
373
+ agent: "my-agent",
374
+ message: "Hello",
375
+ workspace_entries: [
376
+ { name: "app", source: { git_repo: { url: "https://github.com/acme/app", branch: "main" } } },
377
+ { source: { git_repo: { url: "https://github.com/acme/lib" } } },
378
+ ],
379
+ },
380
+ { __stigmer_org_id: "test-org" },
381
+ "wfl_parent",
382
+ ),
383
+ ).rejects.toThrow("CompleteAsyncError");
384
+
385
+ const session = mockCreateSession.mock.calls[0][0];
386
+ const entries = session.spec.workspaceEntries;
387
+ expect(entries).toHaveLength(2);
388
+ expect(entries[0].name).toBe("app");
389
+ expect(entries[0].source.source.case).toBe("gitRepo");
390
+ expect(entries[0].source.source.value.url).toBe("https://github.com/acme/app");
391
+ expect(entries[0].source.source.value.branch).toBe("main");
392
+ expect(entries[1].source.source.value.url).toBe("https://github.com/acme/lib");
393
+ });
394
+
395
+ it("creates sessions without workspace entries when none are configured", async () => {
396
+ await expect(
397
+ callAgentAction(
398
+ { agent: "my-agent", message: "Hello" },
399
+ { __stigmer_org_id: "test-org" },
400
+ "wfl_parent",
401
+ ),
402
+ ).rejects.toThrow("CompleteAsyncError");
403
+
404
+ const session = mockCreateSession.mock.calls[0][0];
405
+ expect(session.spec.workspaceEntries).toHaveLength(0);
406
+ });
407
+
408
+ it("stamps workflow provenance labels the server keys environment resolution on", async () => {
409
+ await expect(
410
+ callAgentAction(
411
+ {
412
+ agent: "my-agent",
413
+ message: "Hello",
414
+ __wfExecId: "wex_prov1",
415
+ __taskName: "triage",
416
+ } as any,
417
+ { __stigmer_org_id: "test-org" },
418
+ "wfl_parent",
419
+ ),
420
+ ).rejects.toThrow("CompleteAsyncError");
421
+
422
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
423
+ expect(execution.metadata.labels["stigmer.ai/workflow-execution-id"]).toBe("wex_prov1");
424
+ expect(execution.metadata.labels["stigmer.ai/workflow-task"]).toBe("triage");
425
+ });
426
+
427
+ it("stamps no provenance labels without workflow context", async () => {
428
+ await expect(
429
+ callAgentAction(
430
+ { agent: "my-agent", message: "Hello" },
431
+ { __stigmer_org_id: "test-org" },
432
+ "wfl_parent",
433
+ ),
434
+ ).rejects.toThrow("CompleteAsyncError");
435
+
436
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
437
+ expect(Object.keys(execution.metadata.labels ?? {})).toHaveLength(0);
438
+ });
439
+ });
440
+
441
+ describe("env secret marking", () => {
442
+ it("preserves the agent-declared secret flag when a task env override supplies the value", async () => {
443
+ // The agent declares API_TOKEN as secret. A task-level env override
444
+ // provides the value — the secret marking must survive (#358: the
445
+ // override used to hardcode isSecret:false, a redaction downgrade).
446
+ mockGetAgentByReference.mockResolvedValue({
447
+ metadata: { id: "agt_test123" },
448
+ status: { defaultInstanceId: "ain_default456" },
449
+ spec: {
450
+ env: {
451
+ API_TOKEN: { isSecret: true },
452
+ REGION: { isSecret: false },
453
+ },
454
+ },
455
+ });
456
+
457
+ await expect(
458
+ callAgentAction(
459
+ {
460
+ agent: "my-agent",
461
+ message: "Hello",
462
+ env: { API_TOKEN: "resolved-secret-value", REGION: "us-east-1", EXTRA: "plain" },
463
+ },
464
+ { __stigmer_org_id: "test-org" },
465
+ "wfl_parent",
466
+ ),
467
+ ).rejects.toThrow("CompleteAsyncError");
468
+
469
+ const execution = mockCreateAgentExecution.mock.calls[0][0];
470
+ const runtimeEnv = execution.spec.runtimeEnv;
471
+ expect(runtimeEnv.API_TOKEN.value).toBe("resolved-secret-value");
472
+ expect(runtimeEnv.API_TOKEN.isSecret).toBe(true);
473
+ expect(runtimeEnv.REGION.isSecret).toBe(false);
474
+ // Keys the agent never declared stay non-secret.
475
+ expect(runtimeEnv.EXTRA.isSecret).toBe(false);
476
+ });
477
+ });
263
478
  });
@@ -31,6 +31,13 @@ import { AgentExecutionSpecSchema, ExecutionConfigSchema } from "@stigmer/protos
31
31
  import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
32
32
  import { SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
33
33
  import { Harness, ExecutionTarget } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
34
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
35
+ import {
36
+ WorkspaceEntrySchema,
37
+ WorkspaceSourceSchema,
38
+ GitRepoSourceSchema,
39
+ type WorkspaceEntry,
40
+ } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
34
41
  import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
35
42
  import { ExecutionValueSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
36
43
  import type { ExecutionValue } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
@@ -62,14 +69,15 @@ export async function callAgentAction(
62
69
  );
63
70
  }
64
71
 
65
- const orgId = resolved.org
66
- ?? (runtimeEnv["__stigmer_org_id"] as string | undefined)
67
- ?? "";
72
+ // The execution is always created in the workflow's org — the workflow
73
+ // owner pays for the run. A cross-org agent reference ("org/slug") only
74
+ // changes where the agent blueprint is looked up, never the billing org.
75
+ const orgId = (runtimeEnv["__stigmer_org_id"] as string | undefined) ?? "";
68
76
 
69
77
  if (!orgId) {
70
78
  throw new Error(
71
79
  "call:agent requires an organization context. " +
72
- "Set 'org' in the task config or ensure '__stigmer_org_id' is in the workflow environment.",
80
+ "Ensure '__stigmer_org_id' is in the workflow environment.",
73
81
  );
74
82
  }
75
83
 
@@ -142,6 +150,7 @@ export async function callAgentAction(
142
150
  harness,
143
151
  executionTarget,
144
152
  subject: "Auto-created session",
153
+ workspaceEntries: buildWorkspaceEntries(resolved.workspace_entries),
145
154
  }),
146
155
  }),
147
156
  );
@@ -170,10 +179,17 @@ export async function callAgentAction(
170
179
  );
171
180
  }
172
181
 
173
- // Task-config-level env takes precedence over auto-forwarded values
182
+ // Task-config-level env takes precedence over auto-forwarded values.
183
+ // The agent's declared secret marking survives the override: a key the
184
+ // agent declares secret stays secret no matter which channel supplied
185
+ // the value, so an explicit task-level `env:` entry cannot downgrade
186
+ // redaction (issue #358 — the override used to hardcode isSecret:false).
174
187
  if (resolved.env) {
175
188
  for (const [key, value] of Object.entries(resolved.env)) {
176
- executionRuntimeEnv[key] = { value: String(value), isSecret: false };
189
+ executionRuntimeEnv[key] = {
190
+ value: String(value),
191
+ isSecret: agentEnvDecls[key]?.isSecret ?? false,
192
+ };
177
193
  }
178
194
  }
179
195
 
@@ -196,13 +212,41 @@ export async function callAgentAction(
196
212
  const parentQueue = runtimeEnv["__stigmer_activity_task_queue"] as string | undefined;
197
213
  const activityTaskQueue = parentQueue?.startsWith("wfexec:") ? parentQueue : "";
198
214
 
199
- const hasModel = !!resolved.config?.model;
215
+ // Honest RunConfig → ExecutionConfig mapping (issue #358): every field
216
+ // the author may set is forwarded to a field the runner enforces.
217
+ // model_name replaces the agent's default outright; max_cost_usd feeds
218
+ // the harness-generic cost guards (cost-cap middleware / cursor
219
+ // cost-guard); max_tool_rounds feeds resolveRecursionLimit (native
220
+ // harness only); service_tier feeds the cursor harness's explicit
221
+ // variant selection (issue #357). Zero/unset means "no override" and is
222
+ // omitted.
223
+ const runConfig = resolved.run_config;
224
+ const hasModel = !!runConfig?.model_name;
225
+ const hasCostCap = (runConfig?.max_cost_usd ?? 0) > 0;
226
+ const hasToolRounds = (runConfig?.max_tool_rounds ?? 0) > 0;
227
+ // Loader guarantees a canonical enum name; an unknown one here means the
228
+ // loader and this mapping drifted — fail the task, never silently drop a
229
+ // pricing directive.
230
+ const SERVICE_TIER_BY_NAME: Record<string, ServiceTier> = {
231
+ SERVICE_TIER_STANDARD: ServiceTier.STANDARD,
232
+ SERVICE_TIER_FAST: ServiceTier.FAST,
233
+ };
234
+ const serviceTier = runConfig?.service_tier
235
+ ? SERVICE_TIER_BY_NAME[runConfig.service_tier]
236
+ : undefined;
237
+ if (runConfig?.service_tier && serviceTier === undefined) {
238
+ throw new Error(
239
+ `call:agent run_config.service_tier '${runConfig.service_tier}' has no proto mapping`,
240
+ );
241
+ }
242
+ const hasServiceTier = serviceTier !== undefined;
200
243
  const hasOutputSchema = !!resolved.output?.schema;
201
244
 
202
245
  console.log(
203
246
  `[CallAgent] schema propagation diagnostic: ` +
204
247
  `hasOutputSchema=${hasOutputSchema}, ` +
205
- `hasModel=${hasModel}, ` +
248
+ `hasModel=${hasModel}, hasCostCap=${hasCostCap}, hasToolRounds=${hasToolRounds}, ` +
249
+ `hasServiceTier=${hasServiceTier}, ` +
206
250
  `configKeys=[${Object.keys(resolved).join(",")}], ` +
207
251
  `hasOutput=${resolved.output !== undefined}, ` +
208
252
  `outputKeys=${resolved.output ? JSON.stringify(Object.keys(resolved.output)) : "N/A"}, ` +
@@ -220,15 +264,29 @@ export async function callAgentAction(
220
264
  runtimeEnv: runtimeEnvProto,
221
265
  });
222
266
 
223
- if (hasModel || hasOutputSchema) {
267
+ if (hasModel || hasCostCap || hasToolRounds || hasServiceTier || hasOutputSchema) {
224
268
  const execConfig = create(ExecutionConfigSchema, {});
225
- if (hasModel) execConfig.modelName = resolved.config!.model!;
269
+ if (hasModel) execConfig.modelName = runConfig!.model_name!;
270
+ if (hasCostCap) execConfig.maxCostUsd = runConfig!.max_cost_usd!;
271
+ if (hasToolRounds) execConfig.maxToolRounds = runConfig!.max_tool_rounds!;
272
+ if (hasServiceTier) execConfig.serviceTier = serviceTier!;
226
273
  if (hasOutputSchema) {
227
274
  execConfig.structuredOutputSchema = resolved.output!.schema as JsonObject;
228
275
  }
229
276
  executionSpec.executionConfig = execConfig;
230
277
  }
231
278
 
279
+ // Workflow provenance labels: the server's CreateExecutionContextStep
280
+ // keys the agent_call environment_refs resolution on these (the
281
+ // schedule-label lineage). The cloud edition additionally gates the
282
+ // branch on the trusted runner caller identity, so the labels are only
283
+ // load-bearing inside that trust boundary.
284
+ const labels: Record<string, string> = {};
285
+ if (wfExecId && taskName) {
286
+ labels["stigmer.ai/workflow-execution-id"] = wfExecId;
287
+ labels["stigmer.ai/workflow-task"] = taskName;
288
+ }
289
+
232
290
  await client.createAgentExecution(
233
291
  create(AgentExecutionSchema, {
234
292
  apiVersion: "agentic.stigmer.ai/v1",
@@ -236,6 +294,7 @@ export async function callAgentAction(
236
294
  metadata: create(ApiResourceMetadataSchema, {
237
295
  name: executionName,
238
296
  org: orgId,
297
+ labels,
239
298
  }),
240
299
  spec: executionSpec,
241
300
  }),
@@ -244,6 +303,31 @@ export async function callAgentAction(
244
303
  throw new CompleteAsyncError();
245
304
  }
246
305
 
306
+ // buildWorkspaceEntries maps the task's git-only workspace entries onto the
307
+ // shared session WorkspaceEntry proto. Provisioning credentials are NOT the
308
+ // runner's concern here: the provisioner resolves GITHUB_TOKEN from the
309
+ // merged environment (DD-018 D-4), which the task's environment_refs feed
310
+ // via server-side resolution.
311
+ function buildWorkspaceEntries(
312
+ entries: AgentCallConfig["workspace_entries"],
313
+ ): WorkspaceEntry[] {
314
+ if (!entries || entries.length === 0) return [];
315
+ return entries.map((entry) =>
316
+ create(WorkspaceEntrySchema, {
317
+ name: entry.name ?? "",
318
+ source: create(WorkspaceSourceSchema, {
319
+ source: {
320
+ case: "gitRepo",
321
+ value: create(GitRepoSourceSchema, {
322
+ url: entry.source.git_repo.url,
323
+ branch: entry.source.git_repo.branch ?? "",
324
+ }),
325
+ },
326
+ }),
327
+ }),
328
+ );
329
+ }
330
+
247
331
  function parseAgentReference(
248
332
  agentStr: string,
249
333
  defaultOrg: string,
@@ -519,3 +519,82 @@ describe("formatImplementPlanSection", () => {
519
519
  expect(prompt).toBe(USER_MESSAGE);
520
520
  });
521
521
  });
522
+
523
+ describe("conversation catchup (cloud DD-006, T03 Sitting 3)", () => {
524
+ const DIGEST =
525
+ "Customer: where is my order?\nTeammate: I've refunded you in full.";
526
+
527
+ it("prefixes the catchup on a RESUMED turn — handback lands mid-session, the case the metadata lane cannot reach", () => {
528
+ const prompt = buildPrompt(
529
+ input({
530
+ resolution: resolution("local", "resumed_successfully"),
531
+ conversationCatchup: DIGEST,
532
+ }),
533
+ );
534
+
535
+ expect(prompt.startsWith("<conversation_catchup>")).toBe(true);
536
+ expect(prompt).toContain(DIGEST);
537
+ expect(prompt.endsWith(USER_MESSAGE)).toBe(true);
538
+ });
539
+
540
+ it("orders a resumed turn's prefixes directives-first, catchup last — context sits closest to the task", () => {
541
+ const prompt = buildPrompt(
542
+ input({
543
+ resolution: resolution("local", "resumed_successfully"),
544
+ interactionMode: InteractionMode.PLAN,
545
+ conversationCatchup: DIGEST,
546
+ }),
547
+ );
548
+
549
+ expect(prompt.indexOf("<interaction_mode>"))
550
+ .toBeLessThan(prompt.indexOf("<conversation_catchup>"));
551
+ expect(prompt.indexOf("<conversation_catchup>"))
552
+ .toBeLessThan(prompt.indexOf(USER_MESSAGE));
553
+ });
554
+
555
+ it("carries the catchup on the first execution too, AFTER the bridge (DD-007 D-d: bridge first, catchup second)", () => {
556
+ const prompt = buildPrompt(
557
+ input({
558
+ resolution: resolution("local", "created_first_execution"),
559
+ contextBridge: "User: hi\nAssistant: hello",
560
+ conversationCatchup: DIGEST,
561
+ }),
562
+ );
563
+
564
+ expect(prompt).toContain("<conversation_catchup>");
565
+ expect(prompt.indexOf("<previous_conversation_context>"))
566
+ .toBeLessThan(prompt.indexOf("<conversation_catchup>"));
567
+ // Still CONTEXT: the approval protocol keeps its pinned
568
+ // last-before-task slot.
569
+ expect(prompt.indexOf("<conversation_catchup>"))
570
+ .toBeLessThan(prompt.indexOf("<tool_approval_protocol>"));
571
+ });
572
+
573
+ it("never reaches a HITL reinvocation — the same turn's original prompt already carried it", () => {
574
+ const decisions = new Map([["call-1", ApprovalAction.APPROVE]]);
575
+ const prompt = buildPrompt(
576
+ input({
577
+ resolution: resolution("local", "resumed_successfully"),
578
+ approvalDecisions: decisions,
579
+ pendingApprovals: [
580
+ create(PendingApprovalSchema, { toolCallId: "call-1", message: "Write file: a.txt" }),
581
+ ],
582
+ conversationCatchup: DIGEST,
583
+ }),
584
+ );
585
+
586
+ expect(prompt).not.toContain("<conversation_catchup>");
587
+ expect(prompt).not.toContain(DIGEST);
588
+ });
589
+
590
+ it("a resumed turn without a catchup stays the bare user message — most turns carry none", () => {
591
+ const prompt = buildPrompt(
592
+ input({
593
+ resolution: resolution("local", "resumed_successfully"),
594
+ conversationCatchup: undefined,
595
+ }),
596
+ );
597
+
598
+ expect(prompt).toBe(USER_MESSAGE);
599
+ });
600
+ });
@@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeAll } from "vitest";
7
7
  */
8
8
  describe("getCursorModelPricing — speed variant resolution", () => {
9
9
  let getCursorModelPricing: typeof import("../model-pricing.js").getCursorModelPricing;
10
+ let getCursorModelPricingForVariant: typeof import("../model-pricing.js").getCursorModelPricingForVariant;
10
11
  let computeTurnCost: typeof import("../model-pricing.js").computeTurnCost;
11
12
 
12
13
  beforeAll(async () => {
@@ -58,6 +59,7 @@ describe("getCursorModelPricing — speed variant resolution", () => {
58
59
  const mod = await import("../model-pricing.js");
59
60
  await mod.ensureLoaded();
60
61
  getCursorModelPricing = mod.getCursorModelPricing;
62
+ getCursorModelPricingForVariant = mod.getCursorModelPricingForVariant;
61
63
  computeTurnCost = mod.computeTurnCost;
62
64
  });
63
65
 
@@ -89,4 +91,22 @@ describe("getCursorModelPricing — speed variant resolution", () => {
89
91
  const cost = computeTurnCost(p, 266_945, 7_069, 0, 222_432);
90
92
  expect(cost).toBeCloseTo(0.28406, 5);
91
93
  });
94
+
95
+ it("getCursorModelPricingForVariant('fast') prices a base id at fast rates (#357)", () => {
96
+ // The explicit-tier path: the caller KNOWS the variant (it requested
97
+ // it) — no wire-id suffix inference involved.
98
+ const p = getCursorModelPricingForVariant("composer-2.5", "fast");
99
+ expect(p.inputPricePerMillion).toBe(3.0);
100
+ expect(p.outputPricePerMillion).toBe(15.0);
101
+ });
102
+
103
+ it("getCursorModelPricingForVariant(null) keeps base rates", () => {
104
+ const p = getCursorModelPricingForVariant("composer-2.5", null);
105
+ expect(p.inputPricePerMillion).toBe(0.5);
106
+ });
107
+
108
+ it("getCursorModelPricingForVariant('fast') falls back to base rates when unpriced", () => {
109
+ const p = getCursorModelPricingForVariant("claude-opus-4-6", "fast");
110
+ expect(p.inputPricePerMillion).toBe(5.0);
111
+ });
92
112
  });