@stigmer/runner 3.12.3 → 3.12.5

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 (227) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-http.js +12 -0
  3. package/dist/activities/call-http.js.map +1 -1
  4. package/dist/activities/call-llm.d.ts +18 -0
  5. package/dist/activities/call-llm.js +56 -2
  6. package/dist/activities/call-llm.js.map +1 -1
  7. package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
  8. package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
  9. package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
  10. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  11. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  12. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  13. package/dist/activities/execute-cursor/index.d.ts +10 -1
  14. package/dist/activities/execute-cursor/index.js +108 -57
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  17. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  18. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  19. package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
  20. package/dist/activities/execute-cursor/service-tier.js +5 -21
  21. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  23. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  24. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  25. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  27. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  28. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  30. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  31. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/index.js +57 -5
  33. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  34. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  35. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  37. package/dist/activities/execute-deep-agent/setup.js +31 -16
  38. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
  40. package/dist/activities/execute-deep-agent/shell-env.js +10 -7
  41. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  43. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  45. package/dist/config.js +10 -5
  46. package/dist/config.js.map +1 -1
  47. package/dist/encryption/config.js +7 -2
  48. package/dist/encryption/config.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/main.js +12 -6
  52. package/dist/main.js.map +1 -1
  53. package/dist/middleware/path-normalization.d.ts +49 -49
  54. package/dist/middleware/path-normalization.js +73 -84
  55. package/dist/middleware/path-normalization.js.map +1 -1
  56. package/dist/middleware/types.d.ts +3 -3
  57. package/dist/payload-codecs.js +2 -1
  58. package/dist/payload-codecs.js.map +1 -1
  59. package/dist/runner-manager.d.ts +1 -1
  60. package/dist/runner-manager.js +42 -22
  61. package/dist/runner-manager.js.map +1 -1
  62. package/dist/runner.js +30 -6
  63. package/dist/runner.js.map +1 -1
  64. package/dist/shared/args-preview.d.ts +8 -0
  65. package/dist/shared/args-preview.js +14 -3
  66. package/dist/shared/args-preview.js.map +1 -1
  67. package/dist/shared/artifact-storage.d.ts +11 -1
  68. package/dist/shared/artifact-storage.js +10 -1
  69. package/dist/shared/artifact-storage.js.map +1 -1
  70. package/dist/shared/declared-preferences.d.ts +47 -0
  71. package/dist/shared/declared-preferences.js +64 -0
  72. package/dist/shared/declared-preferences.js.map +1 -0
  73. package/dist/shared/fingerprint-secret.d.ts +3 -2
  74. package/dist/shared/fingerprint-secret.js +5 -3
  75. package/dist/shared/fingerprint-secret.js.map +1 -1
  76. package/dist/shared/llm-backend.js +8 -1
  77. package/dist/shared/llm-backend.js.map +1 -1
  78. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  79. package/dist/shared/mcp-enabled-tools.js +2 -3
  80. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  81. package/dist/shared/mcp-resolver.d.ts +20 -15
  82. package/dist/shared/mcp-resolver.js +11 -12
  83. package/dist/shared/mcp-resolver.js.map +1 -1
  84. package/dist/shared/model-client.d.ts +15 -0
  85. package/dist/shared/model-client.js +57 -13
  86. package/dist/shared/model-client.js.map +1 -1
  87. package/dist/shared/placeholder-resolver.d.ts +9 -2
  88. package/dist/shared/placeholder-resolver.js +9 -2
  89. package/dist/shared/placeholder-resolver.js.map +1 -1
  90. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  91. package/dist/shared/plan-mode-permissions.js +27 -55
  92. package/dist/shared/plan-mode-permissions.js.map +1 -1
  93. package/dist/shared/registry-endpoint.d.ts +5 -0
  94. package/dist/shared/registry-endpoint.js +7 -1
  95. package/dist/shared/registry-endpoint.js.map +1 -1
  96. package/dist/shared/runner-credential-keys.d.ts +26 -1
  97. package/dist/shared/runner-credential-keys.js +34 -1
  98. package/dist/shared/runner-credential-keys.js.map +1 -1
  99. package/dist/shared/runner-credential-store.d.ts +77 -0
  100. package/dist/shared/runner-credential-store.js +111 -0
  101. package/dist/shared/runner-credential-store.js.map +1 -0
  102. package/dist/shared/service-tier.d.ts +55 -0
  103. package/dist/shared/service-tier.js +67 -0
  104. package/dist/shared/service-tier.js.map +1 -0
  105. package/dist/shared/skill-mount.d.ts +89 -0
  106. package/dist/shared/skill-mount.js +142 -0
  107. package/dist/shared/skill-mount.js.map +1 -0
  108. package/dist/shared/skill-writer.d.ts +28 -26
  109. package/dist/shared/skill-writer.js +79 -102
  110. package/dist/shared/skill-writer.js.map +1 -1
  111. package/dist/shared/worker-shutdown.d.ts +67 -0
  112. package/dist/shared/worker-shutdown.js +79 -0
  113. package/dist/shared/worker-shutdown.js.map +1 -0
  114. package/dist/shared/workspace/types.d.ts +3 -2
  115. package/dist/shared/zip-extract.d.ts +10 -3
  116. package/dist/shared/zip-extract.js +10 -3
  117. package/dist/shared/zip-extract.js.map +1 -1
  118. package/dist/workflow-engine/loader.js +38 -1
  119. package/dist/workflow-engine/loader.js.map +1 -1
  120. package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
  121. package/dist/workflow-engine/tasks/call-function.js +49 -5
  122. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  123. package/dist/workflow-engine/tasks/human-input.js +23 -2
  124. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  125. package/dist/workflow-engine/types.d.ts +6 -0
  126. package/dist/workflow-engine/types.js.map +1 -1
  127. package/dist/workflows/engine-core.js +36 -8
  128. package/dist/workflows/engine-core.js.map +1 -1
  129. package/package.json +15 -11
  130. package/src/activities/__tests__/call-http.test.ts +36 -0
  131. package/src/activities/__tests__/call-llm.test.ts +77 -0
  132. package/src/activities/call-http.ts +17 -0
  133. package/src/activities/call-llm.ts +78 -2
  134. package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
  135. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  136. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  137. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
  138. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  139. package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
  140. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  141. package/src/activities/execute-cursor/index.ts +129 -55
  142. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  143. package/src/activities/execute-cursor/service-tier.ts +5 -29
  144. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  145. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  146. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  147. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  148. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  149. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  150. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  151. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  152. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  153. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  154. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  155. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  156. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  157. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  158. package/src/activities/execute-deep-agent/index.ts +57 -5
  159. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  160. package/src/activities/execute-deep-agent/setup.ts +36 -19
  161. package/src/activities/execute-deep-agent/shell-env.ts +10 -7
  162. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  163. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  164. package/src/config.ts +10 -5
  165. package/src/encryption/config.ts +8 -2
  166. package/src/index.ts +1 -1
  167. package/src/main.ts +16 -6
  168. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  169. package/src/middleware/path-normalization.ts +78 -90
  170. package/src/middleware/types.ts +3 -3
  171. package/src/payload-codecs.ts +2 -1
  172. package/src/runner-manager.ts +55 -22
  173. package/src/runner.ts +39 -6
  174. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  175. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  176. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  177. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  178. package/src/shared/__tests__/model-client.test.ts +99 -0
  179. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  180. package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
  181. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  182. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  183. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  184. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  185. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  186. package/src/shared/__tests__/zip-extract.test.ts +46 -11
  187. package/src/shared/args-preview.ts +17 -3
  188. package/src/shared/artifact-storage.ts +20 -1
  189. package/src/shared/declared-preferences.ts +84 -0
  190. package/src/shared/fingerprint-secret.ts +5 -3
  191. package/src/shared/llm-backend.ts +7 -1
  192. package/src/shared/mcp-enabled-tools.ts +2 -3
  193. package/src/shared/mcp-resolver.ts +20 -20
  194. package/src/shared/model-client.ts +76 -13
  195. package/src/shared/placeholder-resolver.ts +9 -2
  196. package/src/shared/plan-mode-permissions.ts +27 -58
  197. package/src/shared/registry-endpoint.ts +9 -1
  198. package/src/shared/runner-credential-keys.ts +36 -1
  199. package/src/shared/runner-credential-store.ts +115 -0
  200. package/src/shared/service-tier.ts +78 -0
  201. package/src/shared/skill-mount.ts +179 -0
  202. package/src/shared/skill-writer.ts +96 -130
  203. package/src/shared/worker-shutdown.ts +99 -0
  204. package/src/shared/workspace/types.ts +3 -2
  205. package/src/shared/zip-extract.ts +14 -7
  206. package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
  207. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  208. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
  209. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  210. package/src/workflow-engine/loader.ts +46 -1
  211. package/src/workflow-engine/tasks/call-function.ts +74 -13
  212. package/src/workflow-engine/tasks/human-input.ts +33 -5
  213. package/src/workflow-engine/types.ts +6 -0
  214. package/src/workflows/engine-core.ts +39 -8
  215. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  216. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  217. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  218. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  219. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  220. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  221. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  222. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  223. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  224. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  225. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  226. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  227. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -1528,6 +1528,57 @@ do:
1528
1528
  }
1529
1529
  });
1530
1530
 
1531
+ // The server converter persists on_timeout as the proto enum NAME
1532
+ // (HumanInputTimeoutPolicy.String()), so every applied workflow carries
1533
+ // that form in its validated YAML. stigmer/stigmer#779: the loader used
1534
+ // to cast it unvalidated, and the orchestrator's switch silently treated
1535
+ // the unrecognized string as fail.
1536
+ const humanInputYamlWithOnTimeout = (onTimeout: string) => `
1537
+ document:
1538
+ dsl: '1.0.0'
1539
+ name: test
1540
+ do:
1541
+ - timedApproval:
1542
+ call: human_input
1543
+ with:
1544
+ prompt: "Approve within time limit"
1545
+ timeout: 3600
1546
+ on_timeout: ${onTimeout}
1547
+ `;
1548
+
1549
+ it("call: human_input normalizes proto enum-name on_timeout values to the internal policy words", () => {
1550
+ const cases = [
1551
+ ["HUMAN_INPUT_TIMEOUT_FAIL", "fail"],
1552
+ ["HUMAN_INPUT_TIMEOUT_APPROVE", "approve"],
1553
+ ["HUMAN_INPUT_TIMEOUT_DENY", "deny"],
1554
+ ] as const;
1555
+ for (const [wireForm, internalForm] of cases) {
1556
+ const model = loadWorkflowFromYaml(humanInputYamlWithOnTimeout(wireForm));
1557
+ const task = model.do[0].task;
1558
+ expect(task.kind).toBe("human_input");
1559
+ if (task.kind === "human_input") {
1560
+ expect(task.humanInput.onTimeout).toBe(internalForm);
1561
+ }
1562
+ }
1563
+ });
1564
+
1565
+ it("call: human_input rejects the not-implemented escalate policy at load time", () => {
1566
+ for (const form of ["HUMAN_INPUT_TIMEOUT_ESCALATE", "escalate"]) {
1567
+ expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout(form)))
1568
+ .toThrow(/timedApproval.*escalate.*not implemented/);
1569
+ }
1570
+ });
1571
+
1572
+ it("call: human_input rejects unknown on_timeout values instead of silently failing at timeout", () => {
1573
+ expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("sometimes")))
1574
+ .toThrow(/timedApproval.*unknown on_timeout value 'sometimes'.*fail, approve, deny/);
1575
+ });
1576
+
1577
+ it("call: human_input rejects non-string on_timeout values", () => {
1578
+ expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("42")))
1579
+ .toThrow(/timedApproval.*on_timeout/);
1580
+ });
1581
+
1531
1582
  it("call: human_input with payload and ui_hint", () => {
1532
1583
  const yaml = `
1533
1584
  document:
@@ -208,6 +208,111 @@ describe("CallFunctionTaskBuilder", () => {
208
208
  // The config resolver must never pre-evaluate them — the pre-fix
209
209
  // behavior substituted the evaluated boolean back into the config and
210
210
  // crashed the validate activity with `expr.includes is not a function`.
211
+ describe("llm on_invalid policy orchestration (#686)", () => {
212
+ const schemaTaskDef = (extra: Record<string, unknown>): CallFunctionTaskDef => ({
213
+ kind: "call:function",
214
+ call: "llm",
215
+ with: {
216
+ model: "gpt-4o-mini",
217
+ prompt: "Classify",
218
+ response_schema: { type: "object", properties: { answer: { type: "number" } } },
219
+ ...extra,
220
+ },
221
+ });
222
+
223
+ const invalidResult = {
224
+ result: undefined, model: "gpt-4o-mini", provider: "openai",
225
+ input_tokens: 0, output_tokens: 0, parse_error: "answer: Required",
226
+ };
227
+ const validResult = {
228
+ result: { answer: 42 }, model: "gpt-4o-mini", provider: "openai",
229
+ input_tokens: 10, output_tokens: 5,
230
+ };
231
+
232
+ it("ON_INVALID_RETRY re-prompts with the validation errors and succeeds", async () => {
233
+ mockCallFunction
234
+ .mockResolvedValueOnce(invalidResult)
235
+ .mockResolvedValueOnce(validResult);
236
+
237
+ const builder = new CallFunctionTaskBuilder(
238
+ "classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 2 }),
239
+ );
240
+ const result = await builder.build()(null, createState(), makeCtx());
241
+
242
+ expect(result).toMatchObject({ structured: { answer: 42 } });
243
+ expect(mockCallFunction).toHaveBeenCalledTimes(2);
244
+ const retryConfig = mockCallFunction.mock.calls[1][1] as Record<string, unknown>;
245
+ expect(retryConfig.prompt).toContain("Classify");
246
+ expect(retryConfig.prompt).toContain("answer: Required");
247
+ expect(retryConfig.prompt).toContain("RETRY");
248
+ });
249
+
250
+ it("ON_INVALID_RETRY exhausts max_retries then fails without a fallback_task", async () => {
251
+ mockCallFunction.mockResolvedValue(invalidResult);
252
+
253
+ const builder = new CallFunctionTaskBuilder(
254
+ "classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 2 }),
255
+ );
256
+
257
+ await expect(builder.build()(null, createState(), makeCtx()))
258
+ .rejects.toThrow(/validation failed after 3 attempt\(s\).*answer: Required/);
259
+ // first attempt + max_retries retries
260
+ expect(mockCallFunction).toHaveBeenCalledTimes(3);
261
+ });
262
+
263
+ it("ON_INVALID_RETRY defaults max_retries to 1 (proto contract)", async () => {
264
+ mockCallFunction.mockResolvedValue(invalidResult);
265
+
266
+ const builder = new CallFunctionTaskBuilder(
267
+ "classify", schemaTaskDef({ on_invalid: "ON_INVALID_RETRY" }),
268
+ );
269
+
270
+ await expect(builder.build()(null, createState(), makeCtx())).rejects.toThrow();
271
+ expect(mockCallFunction).toHaveBeenCalledTimes(2);
272
+ });
273
+
274
+ it("exhausted retries branch to fallback_task when set", async () => {
275
+ mockCallFunction.mockResolvedValue(invalidResult);
276
+
277
+ const builder = new CallFunctionTaskBuilder(
278
+ "classify",
279
+ schemaTaskDef({ on_invalid: "ON_INVALID_RETRY", max_retries: 1, fallback_task: "human_review" }),
280
+ );
281
+ const result = await builder.build()(null, createState(), makeCtx());
282
+
283
+ expect(result).toEqual({
284
+ __flow_directive__: "human_review",
285
+ validation_errors: ["answer: Required"],
286
+ });
287
+ expect(mockCallFunction).toHaveBeenCalledTimes(2);
288
+ });
289
+
290
+ it("ON_INVALID_FALLBACK branches immediately without retrying", async () => {
291
+ mockCallFunction.mockResolvedValue(invalidResult);
292
+
293
+ const builder = new CallFunctionTaskBuilder(
294
+ "classify",
295
+ schemaTaskDef({ on_invalid: "ON_INVALID_FALLBACK", fallback_task: "human_review" }),
296
+ );
297
+ const result = await builder.build()(null, createState(), makeCtx());
298
+
299
+ expect(result).toMatchObject({ __flow_directive__: "human_review" });
300
+ expect(mockCallFunction).toHaveBeenCalledTimes(1);
301
+ });
302
+
303
+ it("a parse_error without a soft policy passes through as normal output (activity owns the failure)", async () => {
304
+ // Default policy: the activity throws LLM_SCHEMA_VALIDATION itself and
305
+ // never returns parse_error — the engine must not add a second layer.
306
+ mockCallFunction.mockResolvedValue(validResult);
307
+
308
+ const builder = new CallFunctionTaskBuilder("classify", schemaTaskDef({}));
309
+ const result = await builder.build()(null, createState(), makeCtx());
310
+
311
+ expect(result).toMatchObject({ structured: { answer: 42 } });
312
+ expect(mockCallFunction).toHaveBeenCalledTimes(1);
313
+ });
314
+ });
315
+
211
316
  describe("deferred expression fields", () => {
212
317
  it("passes validate rules[].expression through unresolved while input and message interpolate", async () => {
213
318
  mockCallFunction.mockResolvedValue({ valid: true, errors: [] });
@@ -246,6 +246,123 @@ describe("executeHumanInputTask", () => {
246
246
  });
247
247
  });
248
248
 
249
+ // HumanInputTaskConfig.outcomes contract (human_input.proto): with custom
250
+ // outcomes, timeout auto-approve resolves to the FIRST outcome and timeout
251
+ // auto-deny to the LAST — downstream `then` routing and outcome switches
252
+ // must see declared outcome names, never the internal approve/deny words a
253
+ // reviewer was never offered. stigmer/stigmer#779 made this path reachable
254
+ // for the first time.
255
+ describe("timeout outcome mapping with custom outcomes", () => {
256
+ const outcomes = [
257
+ { name: "proceed", label: "Proceed", then: "deployStep" },
258
+ { name: "needs_revision", label: "Needs revision", then: "gatherMore" },
259
+ { name: "reject", label: "Reject" },
260
+ ];
261
+
262
+ it("maps timeout auto-approve to the FIRST outcome and routes its then", async () => {
263
+ const awaitFn: AwaitHumanInputFn = async () => ({
264
+ outcome: "approve",
265
+ auto_resolved: true,
266
+ reason: "timeout",
267
+ });
268
+
269
+ const taskDef: HumanInputTaskDef = {
270
+ kind: "human_input",
271
+ humanInput: { prompt: "Review", timeout: 5, onTimeout: "approve", outcomes },
272
+ };
273
+
274
+ const state = createState();
275
+ const result = await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn));
276
+
277
+ expect(result).toEqual({
278
+ outcome: "proceed",
279
+ auto_resolved: true,
280
+ reason: "timeout",
281
+ __flow_directive__: "deployStep",
282
+ });
283
+ expect(state.data.gate).toEqual({
284
+ outcome: "proceed",
285
+ auto_resolved: true,
286
+ reason: "timeout",
287
+ });
288
+ });
289
+
290
+ it("maps timeout auto-deny to the LAST outcome", async () => {
291
+ const awaitFn: AwaitHumanInputFn = async () => ({
292
+ outcome: "deny",
293
+ auto_resolved: true,
294
+ reason: "timeout",
295
+ });
296
+
297
+ const taskDef: HumanInputTaskDef = {
298
+ kind: "human_input",
299
+ humanInput: { prompt: "Review", timeout: 5, onTimeout: "deny", outcomes },
300
+ };
301
+
302
+ const result = await executeHumanInputTask(taskDef, "gate", createState(), makeCtx(awaitFn));
303
+
304
+ // "reject" is the last outcome and declares no `then` — no directive.
305
+ expect(result).toEqual({ outcome: "reject", auto_resolved: true, reason: "timeout" });
306
+ });
307
+
308
+ it("reports the mapped outcome on the approval_resolved event", async () => {
309
+ const emitted: WorkflowEventDescriptor[][] = [];
310
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
311
+ const awaitFn: AwaitHumanInputFn = async () => ({
312
+ outcome: "approve",
313
+ auto_resolved: true,
314
+ reason: "timeout",
315
+ });
316
+
317
+ const taskDef: HumanInputTaskDef = {
318
+ kind: "human_input",
319
+ humanInput: { prompt: "Review", timeout: 5, onTimeout: "approve", outcomes },
320
+ };
321
+
322
+ await executeHumanInputTask(taskDef, "gate", createState(), makeCtx(awaitFn, emitFn));
323
+
324
+ const resolved = emitted.flat().find((e) => e.type === "approval_resolved");
325
+ expect(resolved).toMatchObject({ outcome: "proceed", autoResolved: true });
326
+ });
327
+
328
+ it("leaves reviewer-selected outcomes untouched", async () => {
329
+ const awaitFn: AwaitHumanInputFn = async () => ({
330
+ outcome: "needs_revision",
331
+ reviewer: "alice",
332
+ });
333
+
334
+ const taskDef: HumanInputTaskDef = {
335
+ kind: "human_input",
336
+ humanInput: { prompt: "Review", timeout: 5, onTimeout: "approve", outcomes },
337
+ };
338
+
339
+ const result = await executeHumanInputTask(taskDef, "gate", createState(), makeCtx(awaitFn));
340
+
341
+ expect(result).toEqual({
342
+ outcome: "needs_revision",
343
+ reviewer: "alice",
344
+ __flow_directive__: "gatherMore",
345
+ });
346
+ });
347
+
348
+ it("keeps plain approve/deny for binary gates without custom outcomes", async () => {
349
+ const awaitFn: AwaitHumanInputFn = async () => ({
350
+ outcome: "approve",
351
+ auto_resolved: true,
352
+ reason: "timeout",
353
+ });
354
+
355
+ const taskDef: HumanInputTaskDef = {
356
+ kind: "human_input",
357
+ humanInput: { prompt: "Review", timeout: 5, onTimeout: "approve" },
358
+ };
359
+
360
+ const result = await executeHumanInputTask(taskDef, "gate", createState(), makeCtx(awaitFn));
361
+
362
+ expect(result).toEqual({ outcome: "approve", auto_resolved: true, reason: "timeout" });
363
+ });
364
+ });
365
+
249
366
  describe("event emission", () => {
250
367
  it("emits approval_requested before blocking with correct fields", async () => {
251
368
  const emitted: WorkflowEventDescriptor[][] = [];
@@ -608,6 +608,51 @@ function parseServiceTier(raw: unknown): string | undefined {
608
608
  return canonical;
609
609
  }
610
610
 
611
+ /**
612
+ * Maps on_timeout values to the runner's internal policy words, mirroring
613
+ * SERVICE_TIER_SHORTHANDS. The persisted CNCF YAML carries the proto enum
614
+ * NAMES (the server converter emits `HumanInputTimeoutPolicy.String()`),
615
+ * while hand-written fixtures use the lowercase words — both are accepted.
616
+ * Unknown values are authoring errors: a timeout policy must never silently
617
+ * fall back to fail (stigmer/stigmer#779 — the unvalidated cast let every
618
+ * enum-name policy reach the orchestrator unrecognized, so gates configured
619
+ * to auto-approve/deny failed at their first real timeout instead).
620
+ */
621
+ const ON_TIMEOUT_VOCABULARY: Record<string, "fail" | "approve" | "deny"> = {
622
+ fail: "fail",
623
+ approve: "approve",
624
+ deny: "deny",
625
+ HUMAN_INPUT_TIMEOUT_FAIL: "fail",
626
+ HUMAN_INPUT_TIMEOUT_APPROVE: "approve",
627
+ HUMAN_INPUT_TIMEOUT_DENY: "deny",
628
+ };
629
+
630
+ function parseOnTimeout(
631
+ taskName: string,
632
+ raw: unknown,
633
+ ): "fail" | "approve" | "deny" | undefined {
634
+ if (raw === undefined || raw === null) return undefined;
635
+ if (typeof raw !== "string") {
636
+ throw new Error(`human_input task '${taskName}': 'on_timeout' must be a string`);
637
+ }
638
+ // The proto declares HUMAN_INPUT_TIMEOUT_ESCALATE but no runtime exists
639
+ // for it yet; refuse at load rather than misbehave at the gate's timeout.
640
+ if (raw === "HUMAN_INPUT_TIMEOUT_ESCALATE" || raw === "escalate") {
641
+ throw new Error(
642
+ `human_input task '${taskName}': on_timeout policy 'escalate' is not implemented — ` +
643
+ `use fail, approve, or deny (custom outcomes with 'then' cover reviewer-driven branching)`,
644
+ );
645
+ }
646
+ const policy = ON_TIMEOUT_VOCABULARY[raw];
647
+ if (!policy) {
648
+ throw new Error(
649
+ `human_input task '${taskName}': unknown on_timeout value '${raw}' ` +
650
+ `(expected: fail, approve, deny, or a HUMAN_INPUT_TIMEOUT_* enum name)`,
651
+ );
652
+ }
653
+ return policy;
654
+ }
655
+
611
656
  function parseHumanInputConfig(taskName: string, raw: unknown): import("./types.js").HumanInputConfig {
612
657
  if (!raw || typeof raw !== "object") {
613
658
  throw new Error(`human_input task '${taskName}' requires a 'with' configuration block`);
@@ -630,7 +675,7 @@ function parseHumanInputConfig(taskName: string, raw: unknown): import("./types.
630
675
  ? obj.approvers.filter((a: unknown) => typeof a === "string") as string[]
631
676
  : undefined,
632
677
  timeout: typeof obj.timeout === "number" ? obj.timeout : undefined,
633
- onTimeout: (obj.on_timeout as "fail" | "approve" | "deny") ?? undefined,
678
+ onTimeout: parseOnTimeout(taskName, obj.on_timeout),
634
679
  // Any JSON shape is a valid payload (expression string, object, array),
635
680
  // so only null/undefined mean "no payload" here.
636
681
  payload: obj.payload ?? undefined,
@@ -162,26 +162,87 @@ export class CallFunctionTaskBuilder implements TaskBuilder {
162
162
  const executionId =
163
163
  (state.env.__stigmer_execution_id as string | undefined) || undefined;
164
164
 
165
- const result = await ctx.callFunction(
166
- callType,
167
- resolved,
168
- state.env,
169
- {
170
- workflowExecutionId: executionId,
171
- },
172
- );
165
+ const fnMeta = { workflowExecutionId: executionId };
173
166
 
174
167
  if (callType === "llm") {
175
- return normalizeLlmOutput(
176
- result as LlmActivityResult,
177
- !!resolved.response_schema,
178
- );
168
+ return this.executeLlmCall(resolved, state.env, fnMeta, ctx);
179
169
  }
180
170
 
181
- return result;
171
+ return ctx.callFunction(callType, resolved, state.env, fnMeta);
182
172
  };
183
173
  }
184
174
 
175
+ /**
176
+ * Executes a `call: llm` with schema-validation policy orchestration —
177
+ * the llm twin of call-agent's output-contract loop (#686).
178
+ *
179
+ * With ON_INVALID_RETRY / ON_INVALID_FALLBACK the activity reports a
180
+ * validation miss as a `parse_error` result instead of throwing; each
181
+ * retry is its own activity invocation (visible in Temporal history)
182
+ * re-prompting with the validation errors so the model can self-correct.
183
+ * Exhausted retries (or immediate FALLBACK) branch to `fallback_task`
184
+ * via the engine's flow directive; without one, the task fails — the
185
+ * proto contract. ON_INVALID_FAIL (default) keeps the activity's
186
+ * throwing path: one attempt, LLM_SCHEMA_VALIDATION on miss.
187
+ */
188
+ private async executeLlmCall(
189
+ resolved: Record<string, unknown>,
190
+ env: Record<string, unknown>,
191
+ fnMeta: { workflowExecutionId?: string },
192
+ ctx: Parameters<TaskExecutorFn>[2],
193
+ ): Promise<unknown> {
194
+ const hasSchema = !!resolved.response_schema;
195
+ const onInvalid = (resolved.on_invalid as string | undefined) ?? "ON_INVALID_FAIL";
196
+ const softHandling =
197
+ hasSchema && (onInvalid === "ON_INVALID_RETRY" || onInvalid === "ON_INVALID_FALLBACK");
198
+
199
+ // Proto: max_retries "Default: 1", meaningful only for ON_INVALID_RETRY.
200
+ const maxRetries =
201
+ onInvalid === "ON_INVALID_RETRY" ? (resolved.max_retries as number | undefined) ?? 1 : 0;
202
+
203
+ let attempts = 0;
204
+ let lastParseError = "";
205
+
206
+ do {
207
+ const attemptConfig =
208
+ attempts === 0
209
+ ? resolved
210
+ : {
211
+ ...resolved,
212
+ prompt:
213
+ `${resolved.prompt}\n\n[RETRY — Your previous response did not match ` +
214
+ `the required output schema. Validation errors: ${lastParseError}. ` +
215
+ `Please ensure your response strictly matches the JSON schema provided.]`,
216
+ };
217
+
218
+ const result = (await ctx.callFunction(
219
+ "llm",
220
+ attemptConfig,
221
+ env,
222
+ fnMeta,
223
+ )) as LlmActivityResult;
224
+
225
+ if (!softHandling || !result.parse_error) {
226
+ return normalizeLlmOutput(result, hasSchema);
227
+ }
228
+
229
+ lastParseError = result.parse_error;
230
+ attempts++;
231
+ } while (onInvalid === "ON_INVALID_RETRY" && attempts <= maxRetries);
232
+
233
+ if (typeof resolved.fallback_task === "string" && resolved.fallback_task !== "") {
234
+ return {
235
+ __flow_directive__: resolved.fallback_task,
236
+ validation_errors: [lastParseError],
237
+ };
238
+ }
239
+
240
+ throw new Error(
241
+ `LLM output validation failed after ${attempts} attempt(s) ` +
242
+ `for task '${this.taskName}': ${lastParseError}`,
243
+ );
244
+ }
245
+
185
246
  async shouldRun(): Promise<boolean> {
186
247
  return true;
187
248
  }
@@ -96,11 +96,14 @@ export async function executeHumanInputTask(
96
96
  ]);
97
97
  }
98
98
 
99
- const result: HumanInputResult = await ctx.awaitHumanInput({
100
- signalName,
101
- timeoutSeconds,
102
- onTimeout,
103
- });
99
+ const result: HumanInputResult = applyTimeoutOutcomeContract(
100
+ await ctx.awaitHumanInput({
101
+ signalName,
102
+ timeoutSeconds,
103
+ onTimeout,
104
+ }),
105
+ config.outcomes,
106
+ );
104
107
 
105
108
  if (ctx.emitEvents) {
106
109
  await ctx.emitEvents([{
@@ -132,6 +135,31 @@ function validateConfig(config: HumanInputConfig, taskName: string): void {
132
135
  }
133
136
  }
134
137
 
138
+ /**
139
+ * Applies the proto contract for timeout auto-resolution with custom
140
+ * outcomes (HumanInputTaskConfig.outcomes doc): auto-approve resolves to
141
+ * the FIRST declared outcome and auto-deny to the LAST, so `then` routing
142
+ * and downstream outcome switches see declared outcome names — never the
143
+ * orchestrator's internal approve/deny words, which a reviewer of a
144
+ * custom-outcome gate was never offered. Binary gates (no custom outcomes)
145
+ * keep the plain approve/deny result.
146
+ */
147
+ function applyTimeoutOutcomeContract(
148
+ result: HumanInputResult,
149
+ outcomes: HumanInputConfig["outcomes"],
150
+ ): HumanInputResult {
151
+ if (!result.auto_resolved || result.reason !== "timeout" || !outcomes?.length) {
152
+ return result;
153
+ }
154
+ if (result.outcome === "approve") {
155
+ return { ...result, outcome: outcomes[0].name };
156
+ }
157
+ if (result.outcome === "deny") {
158
+ return { ...result, outcome: outcomes[outcomes.length - 1].name };
159
+ }
160
+ return result;
161
+ }
162
+
135
163
  interface ResolvedReviewPayload {
136
164
  /** Resolved payload for inline delivery on the approval_requested event. */
137
165
  readonly payload?: unknown;
@@ -336,6 +336,12 @@ export interface HttpCallConfig {
336
336
  readonly query?: Record<string, string>;
337
337
  readonly output?: string;
338
338
  readonly redirect?: string;
339
+ /**
340
+ * Request timeout in seconds (proto: HttpCallTaskConfig.timeout_seconds,
341
+ * 1-300). Aborts the request and fails the task with HTTP_CALL_TIMEOUT;
342
+ * the engine also widens the activity's startToClose to fit it (#686).
343
+ */
344
+ readonly timeout_seconds?: number;
339
345
  }
340
346
 
341
347
  export type EndpointDef = string | { readonly uri: string; readonly authentication?: Record<string, unknown> };
@@ -67,17 +67,44 @@ const evalProxy = proxyLocalActivities<EvalActivities>({
67
67
  startToCloseTimeout: "10s",
68
68
  });
69
69
 
70
+ const CALL_PROXY_RETRY = {
71
+ maximumAttempts: 5,
72
+ initialInterval: "1s",
73
+ backoffCoefficient: 2,
74
+ maximumInterval: "1m",
75
+ } as const;
76
+
70
77
  const callProxy = proxyActivities<HttpActivities & GrpcActivities & FunctionActivities>({
71
78
  startToCloseTimeout: "5m",
72
79
  heartbeatTimeout: "30s",
73
- retry: {
74
- maximumAttempts: 5,
75
- initialInterval: "1s",
76
- backoffCoefficient: 2,
77
- maximumInterval: "1m",
78
- },
80
+ retry: CALL_PROXY_RETRY,
79
81
  });
80
82
 
83
+ /**
84
+ * Task-config timeouts (llm_call.timeout ≤600s, http_call.timeout_seconds
85
+ * ≤300s) can exceed or crowd the default 5m startToClose, which would kill
86
+ * the activity before its own well-typed LLM_TIMEOUT / HTTP_CALL_TIMEOUT
87
+ * failure fires. When a task declares a budget, proxy its call with
88
+ * startToClose = budget + 30s so the in-activity bound always wins (#686).
89
+ *
90
+ * proxyActivities in workflow code is a deterministic proxy construction
91
+ * (no Temporal commands); the memo just avoids rebuilding per call.
92
+ */
93
+ const timeoutAwareProxies = new Map<number, typeof callProxy>();
94
+ function callProxyFor(timeoutSeconds: number | undefined): typeof callProxy {
95
+ if (!timeoutSeconds || !Number.isFinite(timeoutSeconds)) return callProxy;
96
+ let proxy = timeoutAwareProxies.get(timeoutSeconds);
97
+ if (!proxy) {
98
+ proxy = proxyActivities<HttpActivities & GrpcActivities & FunctionActivities>({
99
+ startToCloseTimeout: `${timeoutSeconds + 30}s`,
100
+ heartbeatTimeout: "30s",
101
+ retry: CALL_PROXY_RETRY,
102
+ });
103
+ timeoutAwareProxies.set(timeoutSeconds, proxy);
104
+ }
105
+ return proxy;
106
+ }
107
+
81
108
  const runProxy = proxyActivities<RunActivities>({
82
109
  startToCloseTimeout: "5m",
83
110
  heartbeatTimeout: "30s",
@@ -240,7 +267,7 @@ export async function runWorkflowEngine(
240
267
  runWorkflow: (config: RunWorkflowExecutionConfig) => orchestrateRunWorkflow(config),
241
268
  awaitHumanInput: (config: HumanInputExecutionConfig) => orchestrateHumanInput(config),
242
269
  callHttp: (config: HttpCallConfig, runtimeEnv: Record<string, unknown>) =>
243
- callProxy.CallHttp(config, runtimeEnv),
270
+ callProxyFor(config.timeout_seconds).CallHttp(config, runtimeEnv),
244
271
  callGrpc: (config: GrpcCallConfig, runtimeEnv: Record<string, unknown>) =>
245
272
  callProxy.CallGrpc(config, runtimeEnv),
246
273
  callFunction: (
@@ -249,7 +276,11 @@ export async function runWorkflowEngine(
249
276
  runtimeEnv: Record<string, unknown>,
250
277
  fnMeta: CallFunctionMetadata,
251
278
  ) =>
252
- callProxy.CallFunction(
279
+ callProxyFor(
280
+ call === "llm" && typeof config.timeout === "number"
281
+ ? config.timeout
282
+ : undefined,
283
+ ).CallFunction(
253
284
  call,
254
285
  config,
255
286
  runtimeEnv,
@@ -1,19 +0,0 @@
1
- /**
2
- * Connect backfill for MCP servers without discovered capabilities.
3
- *
4
- * Thin wrapper around the shared connect-backfill module that preserves
5
- * the cursor-specific McpResolutionResult interface (which includes
6
- * cursorConfig). The core backfill logic lives in shared/connect-backfill.ts.
7
- *
8
- * Non-fatal: if connect fails for any server, the original servers are
9
- * kept and execution continues with empty policies (fail-closed).
10
- */
11
- import type { McpResolutionResult } from "./mcp-resolver.js";
12
- import type { StigmerClient } from "../../client/stigmer-client.js";
13
- import type { McpTransportPosture } from "../../shared/mcp-transport-guard.js";
14
- import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
15
- /**
16
- * Run connect backfill and return an updated cursor-specific
17
- * McpResolutionResult with rebuilt cursorConfig.
18
- */
19
- export declare function backfillMcpServersIfNeeded(client: StigmerClient, currentResult: McpResolutionResult, usages: McpServerUsage[], envVars: Record<string, string>, org: string, transportPosture: McpTransportPosture, onHeartbeat?: () => void, secretKeys?: ReadonlySet<string>): Promise<McpResolutionResult>;
@@ -1,27 +0,0 @@
1
- /**
2
- * Connect backfill for MCP servers without discovered capabilities.
3
- *
4
- * Thin wrapper around the shared connect-backfill module that preserves
5
- * the cursor-specific McpResolutionResult interface (which includes
6
- * cursorConfig). The core backfill logic lives in shared/connect-backfill.ts.
7
- *
8
- * Non-fatal: if connect fails for any server, the original servers are
9
- * kept and execution continues with empty policies (fail-closed).
10
- */
11
- import { toCursorMcpConfig } from "./mcp-resolver.js";
12
- import { backfillMcpServersIfNeeded as sharedBackfill } from "../../shared/connect-backfill.js";
13
- /**
14
- * Run connect backfill and return an updated cursor-specific
15
- * McpResolutionResult with rebuilt cursorConfig.
16
- */
17
- export async function backfillMcpServersIfNeeded(client, currentResult, usages, envVars, org, transportPosture, onHeartbeat, secretKeys) {
18
- const updatedServers = await sharedBackfill(client, currentResult.resolvedServers, usages, envVars, org, transportPosture, onHeartbeat, secretKeys);
19
- if (updatedServers === currentResult.resolvedServers) {
20
- return currentResult;
21
- }
22
- return {
23
- cursorConfig: toCursorMcpConfig(updatedServers),
24
- resolvedServers: updatedServers,
25
- };
26
- }
27
- //# sourceMappingURL=connect-backfill.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"connect-backfill.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/connect-backfill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAItD,OAAO,EAAE,0BAA0B,IAAI,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEhG;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,MAAqB,EACrB,aAAkC,EAClC,MAAwB,EACxB,OAA+B,EAC/B,GAAW,EACX,gBAAqC,EACrC,WAAwB,EACxB,UAAgC;IAEhC,MAAM,cAAc,GAAG,MAAM,cAAc,CACzC,MAAM,EACN,aAAa,CAAC,eAAe,EAC7B,MAAM,EACN,OAAO,EACP,GAAG,EACH,gBAAgB,EAChB,WAAW,EACX,UAAU,CACX,CAAC;IAEF,IAAI,cAAc,KAAK,aAAa,CAAC,eAAe,EAAE,CAAC;QACrD,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,OAAO;QACL,YAAY,EAAE,iBAAiB,CAAC,cAAc,CAAC;QAC/C,eAAe,EAAE,cAAc;KAChC,CAAC;AACJ,CAAC"}