@stigmer/runner 3.2.1 → 3.2.3

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 (145) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-transform.js +12 -2
  3. package/dist/activities/call-transform.js.map +1 -1
  4. package/dist/activities/call-validate.js +20 -2
  5. package/dist/activities/call-validate.js.map +1 -1
  6. package/dist/activities/discover-mcp-server.js +6 -20
  7. package/dist/activities/discover-mcp-server.js.map +1 -1
  8. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
  9. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  10. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  11. package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
  12. package/dist/activities/execute-cursor/approval-state.js +13 -1
  13. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  14. package/dist/activities/execute-cursor/hook-script.js +38 -0
  15. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  16. package/dist/activities/execute-cursor/index.d.ts +12 -0
  17. package/dist/activities/execute-cursor/index.js +72 -15
  18. package/dist/activities/execute-cursor/index.js.map +1 -1
  19. package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
  20. package/dist/activities/execute-cursor/message-translator.js +78 -2
  21. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/prompt-builder.d.ts +19 -0
  23. package/dist/activities/execute-cursor/prompt-builder.js +21 -0
  24. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  25. package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
  26. package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
  27. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  28. package/dist/activities/execute-cursor/turn-boundary.js +15 -2
  29. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
  31. package/dist/activities/execute-deep-agent/hitl.js +48 -1
  32. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/index.js +10 -1
  34. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +19 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js +12 -0
  37. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
  39. package/dist/activities/execute-deep-agent/setup.js +16 -1
  40. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
  42. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  43. package/dist/activities/execute-deep-agent/status-builder.js +6 -0
  44. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  45. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
  46. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  47. package/dist/activities/workflow-event-activities.js +11 -0
  48. package/dist/activities/workflow-event-activities.js.map +1 -1
  49. package/dist/config.d.ts +17 -0
  50. package/dist/config.js +14 -0
  51. package/dist/config.js.map +1 -1
  52. package/dist/middleware/approval-gate.d.ts +20 -0
  53. package/dist/middleware/approval-gate.js +16 -1
  54. package/dist/middleware/approval-gate.js.map +1 -1
  55. package/dist/runner-manager.d.ts +2 -0
  56. package/dist/runner-manager.js +2 -1
  57. package/dist/runner-manager.js.map +1 -1
  58. package/dist/runner.d.ts +2 -0
  59. package/dist/runner.js +2 -1
  60. package/dist/runner.js.map +1 -1
  61. package/dist/shared/approval-policy.d.ts +24 -1
  62. package/dist/shared/approval-policy.js +35 -1
  63. package/dist/shared/approval-policy.js.map +1 -1
  64. package/dist/shared/context-bridge.d.ts +30 -0
  65. package/dist/shared/context-bridge.js +45 -0
  66. package/dist/shared/context-bridge.js.map +1 -0
  67. package/dist/shared/sender-identity.d.ts +50 -0
  68. package/dist/shared/sender-identity.js +69 -0
  69. package/dist/shared/sender-identity.js.map +1 -0
  70. package/dist/shared/with-timeout.d.ts +27 -0
  71. package/dist/shared/with-timeout.js +47 -0
  72. package/dist/shared/with-timeout.js.map +1 -0
  73. package/dist/workflow-engine/do-executor.d.ts +7 -0
  74. package/dist/workflow-engine/do-executor.js +59 -1
  75. package/dist/workflow-engine/do-executor.js.map +1 -1
  76. package/dist/workflow-engine/tasks/call-function.js +68 -1
  77. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  78. package/dist/workflow-engine/tasks/human-input.js +1 -0
  79. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  80. package/dist/workflow-engine/types.d.ts +48 -0
  81. package/dist/workflow-engine/types.js.map +1 -1
  82. package/dist/workflows/engine-core.js +5 -1
  83. package/dist/workflows/engine-core.js.map +1 -1
  84. package/dist/workflows/human-input-orchestrator.d.ts +2 -1
  85. package/dist/workflows/human-input-orchestrator.js +2 -1
  86. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  87. package/package.json +2 -2
  88. package/src/__tests__/config.test.ts +8 -0
  89. package/src/activities/__tests__/call-validate.test.ts +137 -0
  90. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  91. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  92. package/src/activities/__tests__/workflow-event-activities.test.ts +89 -0
  93. package/src/activities/call-transform.ts +20 -2
  94. package/src/activities/call-validate.ts +27 -2
  95. package/src/activities/discover-mcp-server.ts +7 -28
  96. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
  97. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +65 -0
  98. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
  99. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
  100. package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
  101. package/src/activities/execute-cursor/approval-state.ts +32 -1
  102. package/src/activities/execute-cursor/hook-script.ts +40 -0
  103. package/src/activities/execute-cursor/index.ts +86 -17
  104. package/src/activities/execute-cursor/message-translator.ts +82 -1
  105. package/src/activities/execute-cursor/prompt-builder.ts +44 -0
  106. package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
  107. package/src/activities/execute-cursor/turn-boundary.ts +23 -0
  108. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  109. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  110. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  111. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
  112. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  113. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +58 -0
  114. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  115. package/src/activities/execute-deep-agent/hitl.ts +51 -0
  116. package/src/activities/execute-deep-agent/index.ts +11 -1
  117. package/src/activities/execute-deep-agent/prompt-builder.ts +35 -0
  118. package/src/activities/execute-deep-agent/setup.ts +31 -0
  119. package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
  120. package/src/activities/execute-deep-agent/status-builder.ts +7 -0
  121. package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
  122. package/src/activities/workflow-event-activities.ts +11 -0
  123. package/src/config.ts +23 -0
  124. package/src/middleware/__tests__/approval-gate.test.ts +95 -0
  125. package/src/middleware/approval-gate.ts +37 -0
  126. package/src/runner-manager.ts +6 -1
  127. package/src/runner.ts +6 -1
  128. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  129. package/src/shared/__tests__/context-bridge.test.ts +51 -0
  130. package/src/shared/__tests__/sender-identity.test.ts +92 -0
  131. package/src/shared/__tests__/with-timeout.test.ts +60 -0
  132. package/src/shared/approval-policy.ts +41 -2
  133. package/src/shared/context-bridge.ts +51 -0
  134. package/src/shared/sender-identity.ts +85 -0
  135. package/src/shared/with-timeout.ts +53 -0
  136. package/src/workflow-engine/__tests__/do-executor.test.ts +155 -0
  137. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +94 -0
  138. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
  139. package/src/workflow-engine/do-executor.ts +65 -1
  140. package/src/workflow-engine/tasks/call-function.ts +84 -3
  141. package/src/workflow-engine/tasks/human-input.ts +1 -0
  142. package/src/workflow-engine/types.ts +50 -0
  143. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +1 -0
  144. package/src/workflows/engine-core.ts +5 -1
  145. package/src/workflows/human-input-orchestrator.ts +2 -1
@@ -202,4 +202,98 @@ describe("CallFunctionTaskBuilder", () => {
202
202
  const calledMeta = mockCallFunction.mock.calls[0][3];
203
203
  expect(calledMeta.workflowExecutionId).toBeUndefined();
204
204
  });
205
+
206
+ // Deferred-code fields (upstream #7 regression): rule/transform
207
+ // expressions are jq CODE the activity evaluates against its own data.
208
+ // The config resolver must never pre-evaluate them — the pre-fix
209
+ // behavior substituted the evaluated boolean back into the config and
210
+ // crashed the validate activity with `expr.includes is not a function`.
211
+ describe("deferred expression fields", () => {
212
+ it("passes validate rules[].expression through unresolved while input and message interpolate", async () => {
213
+ mockCallFunction.mockResolvedValue({ valid: true, errors: [] });
214
+
215
+ const taskDef: CallFunctionTaskDef = {
216
+ kind: "call:function",
217
+ call: "validate",
218
+ with: {
219
+ input: "${ $context.total_order }",
220
+ rules: [
221
+ {
222
+ name: "total_is_positive",
223
+ expression: "${ .total > 0 }",
224
+ message: "Total ${ $context.total_order.currency } must be positive",
225
+ },
226
+ ],
227
+ on_fail: "VALIDATION_FAIL_RAISE",
228
+ },
229
+ };
230
+
231
+ const builder = new CallFunctionTaskBuilder("check_order", taskDef);
232
+ const executor = builder.build();
233
+ const state = createState();
234
+ state.context = { total_order: { total: 150, currency: "USD" } };
235
+
236
+ await executor(null, state, makeCtx());
237
+
238
+ const calledConfig = mockCallFunction.mock.calls[0][1];
239
+ // Data fields resolve...
240
+ expect(calledConfig.input).toEqual({ total: 150, currency: "USD" });
241
+ expect(calledConfig.rules[0].message).toBe("Total USD must be positive");
242
+ // ...the rule predicate does NOT — it stays deferred code.
243
+ expect(calledConfig.rules[0].expression).toBe("${ .total > 0 }");
244
+ });
245
+
246
+ it("passes a ${ }-wrapped transform expression through unresolved", async () => {
247
+ mockCallFunction.mockResolvedValue({ total: 150 });
248
+
249
+ const taskDef: CallFunctionTaskDef = {
250
+ kind: "call:function",
251
+ call: "transform",
252
+ with: {
253
+ engine: "TRANSFORM_ENGINE_JQ",
254
+ expression: "${ { total: .qty } }",
255
+ input: "${ $context.order }",
256
+ },
257
+ };
258
+
259
+ const builder = new CallFunctionTaskBuilder("total_order", taskDef);
260
+ const executor = builder.build();
261
+ const state = createState();
262
+ state.context = { order: { qty: 3 } };
263
+
264
+ await executor(null, state, makeCtx());
265
+
266
+ const calledConfig = mockCallFunction.mock.calls[0][1];
267
+ expect(calledConfig.input).toEqual({ qty: 3 });
268
+ expect(calledConfig.expression).toBe("${ { total: .qty } }");
269
+ });
270
+
271
+ it("does not mutate the original task definition across invocations", async () => {
272
+ mockCallFunction.mockResolvedValue({ valid: true, errors: [] });
273
+
274
+ const withConfig = {
275
+ input: "${ $context.order }",
276
+ rules: [{ name: "r", expression: "${ .total > 0 }" }],
277
+ };
278
+ const taskDef: CallFunctionTaskDef = {
279
+ kind: "call:function",
280
+ call: "validate",
281
+ with: withConfig,
282
+ };
283
+
284
+ const builder = new CallFunctionTaskBuilder("check", taskDef);
285
+ const executor = builder.build();
286
+ const state = createState();
287
+ state.context = { order: { total: 1 } };
288
+
289
+ await executor(null, state, makeCtx());
290
+ await executor(null, state, makeCtx());
291
+
292
+ // Retry-safety: the shared definition object stays pristine.
293
+ expect(withConfig.input).toBe("${ $context.order }");
294
+ expect(withConfig.rules[0].expression).toBe("${ .total > 0 }");
295
+ const secondCall = mockCallFunction.mock.calls[1][1];
296
+ expect(secondCall.rules[0].expression).toBe("${ .total > 0 }");
297
+ });
298
+ });
205
299
  });
@@ -317,10 +317,50 @@ describe("executeHumanInputTask", () => {
317
317
  expect(resolved.taskName).toBe("gate");
318
318
  expect(resolved.outcome).toBe("reject");
319
319
  expect(resolved.resolvedBy).toBe("carol");
320
+ expect(resolved.resolvedByActor).toBeUndefined();
320
321
  expect(resolved.autoResolved).toBe(false);
321
322
  expect(resolved.waitDurationMs).toBeGreaterThanOrEqual(0);
322
323
  });
323
324
 
325
+ it("carries the reviewer_actor display snapshot into approval_resolved and the task output", async () => {
326
+ const emitted: WorkflowEventDescriptor[][] = [];
327
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
328
+ const reviewerActor = {
329
+ id: "ida_01abc",
330
+ display_name: "Ada Lovelace",
331
+ email: "ada@example.com",
332
+ avatar: "https://example.com/ada.png",
333
+ };
334
+ const awaitFn: AwaitHumanInputFn = async () => ({
335
+ outcome: "approve",
336
+ reviewer: "ida_01abc",
337
+ reviewer_actor: reviewerActor,
338
+ responded_at: "2026-05-20T10:00:00Z",
339
+ });
340
+
341
+ const taskDef: HumanInputTaskDef = {
342
+ kind: "human_input",
343
+ humanInput: { prompt: "Confirm?" },
344
+ };
345
+
346
+ const state = createState();
347
+ await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn, emitFn));
348
+
349
+ const resolved = emitted[1][0];
350
+ if (resolved.type !== "approval_resolved") throw new Error("unexpected");
351
+ expect(resolved.resolvedBy).toBe("ida_01abc");
352
+ expect(resolved.resolvedByActor).toEqual(reviewerActor);
353
+
354
+ // The whole signal payload IS the task output — the actor lands there
355
+ // for downstream tasks (e.g. notification templates) without copying.
356
+ expect(state.data.gate).toEqual({
357
+ outcome: "approve",
358
+ reviewer: "ida_01abc",
359
+ reviewer_actor: reviewerActor,
360
+ responded_at: "2026-05-20T10:00:00Z",
361
+ });
362
+ });
363
+
324
364
  it("does not error when emitEvents is undefined (backward compat)", async () => {
325
365
  const awaitFn: AwaitHumanInputFn = async () => ({ outcome: "approve" });
326
366
 
@@ -42,6 +42,39 @@ import { truncatePayload } from "./task-status-accumulator.js";
42
42
  import { extractRootErrorMessage, extractStructuredError } from "./error-utils.js";
43
43
  import type { RecoveryContext } from "./recovery.js";
44
44
 
45
+ /**
46
+ * Deterministic-version gate id for the task_started emission reorder
47
+ * (input resolution moved ahead of the event so it can carry
48
+ * `input_summary`). Pre-change histories replay the old order — emit
49
+ * first, resolve second — via `TaskExecutionContext.isPatched`.
50
+ */
51
+ export const TASK_STARTED_INPUT_SUMMARY_PATCH = "task-started-input-summary";
52
+
53
+ /**
54
+ * Truncation budget for event I/O summaries. Deliberately far below the
55
+ * 64KB snapshot cap: events are the live-stream preview source and the
56
+ * proto documents them as "truncated to prevent event bloat" — the full
57
+ * I/O lives on `status.tasks[]`.
58
+ */
59
+ const EVENT_SUMMARY_MAX_BYTES = 8_192;
60
+
61
+ /**
62
+ * Projects a task I/O value into the shape an event summary Struct can
63
+ * carry: truncated to the event budget, objects only (mirroring the
64
+ * snapshot path's `toJsonObject` semantics — proto Struct fields cannot
65
+ * carry scalars or arrays, and `status.tasks[].input/output` share the
66
+ * same limitation). Oversize values come back as `truncatePayload`'s
67
+ * `{_truncated, _preview}` marker object, which IS carried.
68
+ */
69
+ function toEventSummary(value: unknown): Record<string, unknown> | undefined {
70
+ if (value === null || value === undefined) return undefined;
71
+ const truncated = truncatePayload(value, EVENT_SUMMARY_MAX_BYTES);
72
+ if (truncated === null || typeof truncated !== "object" || Array.isArray(truncated)) {
73
+ return undefined;
74
+ }
75
+ return truncated as Record<string, unknown>;
76
+ }
77
+
45
78
  /**
46
79
  * Returns the task kind string used for event emission. For call:function
47
80
  * tasks, appends the specific function name (e.g., "call:function:llm")
@@ -156,21 +189,42 @@ export async function executeDoTasks(
156
189
  const kind = eventTaskKind(entry);
157
190
  effectiveCtx.taskStatusAccumulator?.taskStarted(entry.key, kind);
158
191
  const attemptNumber = effectiveCtx.taskStatusAccumulator?.getAttempt(entry.key) ?? 1;
159
- if (effectiveCtx.emitEvents) {
192
+
193
+ const emitTaskStarted = async (
194
+ inputSummary?: Record<string, unknown>,
195
+ ): Promise<void> => {
196
+ if (!effectiveCtx.emitEvents) return;
160
197
  await effectiveCtx.emitEvents([{
161
198
  type: "task_started",
162
199
  taskName: entry.key,
163
200
  occurredAt: new Date().toISOString(),
164
201
  taskKind: kind,
165
202
  attemptNumber,
203
+ inputSummary,
166
204
  }]);
205
+ };
206
+
207
+ // Emission-order gate: carrying `input_summary` on task_started
208
+ // requires resolving the input BEFORE emitting, which reorders two
209
+ // local-activity commands — a non-deterministic change for in-flight
210
+ // histories (a gated or waiting task can be parked across a deploy).
211
+ // Pre-change histories replay the old emit-then-resolve order.
212
+ const emitStartedAfterResolve =
213
+ effectiveCtx.isPatched?.(TASK_STARTED_INPUT_SUMMARY_PATCH) ?? true;
214
+ if (!emitStartedAfterResolve) {
215
+ await emitTaskStarted();
167
216
  }
217
+ let startedEmitted = !emitStartedAfterResolve;
168
218
 
169
219
  const taskStartMs = Date.now();
170
220
  let taskOutput: unknown;
171
221
  try {
172
222
  const effectiveInput = await resolveTaskInput(entry, input, state, effectiveCtx);
173
223
  effectiveCtx.taskStatusAccumulator?.taskStartedWithInput(entry.key, kind, truncatePayload(effectiveInput));
224
+ if (emitStartedAfterResolve) {
225
+ await emitTaskStarted(toEventSummary(effectiveInput));
226
+ startedEmitted = true;
227
+ }
174
228
  taskOutput = await runSingleTask(entry, effectiveInput, state, doc, effectiveCtx);
175
229
  } catch (taskErr) {
176
230
  const taskDurationMs = Date.now() - taskStartMs;
@@ -190,6 +244,12 @@ export async function executeDoTasks(
190
244
  });
191
245
  }
192
246
  if (effectiveCtx.emitEvents) {
247
+ // Input resolution failed before the reordered task_started could
248
+ // fire — emit it now so consumers always see the started→failed pair.
249
+ if (!startedEmitted) {
250
+ await emitTaskStarted();
251
+ startedEmitted = true;
252
+ }
193
253
  await effectiveCtx.emitEvents([{
194
254
  type: "task_failed",
195
255
  taskName: entry.key,
@@ -239,6 +299,10 @@ export async function executeDoTasks(
239
299
  durationMs: taskDurationMs,
240
300
  costMicros: costInfo.costMicros,
241
301
  tokensUsed: costInfo.inputTokens + costInfo.outputTokens,
302
+ // The live-stream preview source for task cards: post-promotion
303
+ // output (the artifact-promoted form is what downstream sees),
304
+ // truncated to the event budget. Full output rides the snapshot.
305
+ outputSummary: toEventSummary(promotedOutput),
242
306
  }];
243
307
  for (const evt of promotionEvents) {
244
308
  events.push(evt as unknown as (typeof events)[number]);
@@ -17,6 +17,80 @@ import type {
17
17
  } from "../types.js";
18
18
  import { resolveConfigExpressions } from "../resolve.js";
19
19
 
20
+ /**
21
+ * Detaches deferred-code fields from a call-function config before
22
+ * expression resolution and returns a restorer that puts the originals
23
+ * back afterwards.
24
+ *
25
+ * Some config fields carry jq CODE that the activity evaluates later
26
+ * against its own input — not data to interpolate at config time:
27
+ *
28
+ * - `transform.expression` — evaluated by `transformAction` against the
29
+ * transform's `input`.
30
+ * - `validate.rules[].expression` — evaluated by `validateAction` per
31
+ * rule against the validated data.
32
+ *
33
+ * Without the detach, a `${ }`-wrapped predicate is pre-evaluated here
34
+ * against the WRONG input and its non-string RESULT is substituted back
35
+ * into the config; the activity then tries to evaluate that result as jq
36
+ * (the `expr.includes is not a function` crash class). All other fields
37
+ * — including `rules[].message` and the llm/eval prompt fields — are
38
+ * data and continue to interpolate normally. The original config object
39
+ * is never mutated (shared across retries).
40
+ */
41
+ function detachDeferredCode(
42
+ callType: string,
43
+ config: Record<string, unknown>,
44
+ ): {
45
+ readonly config: Record<string, unknown>;
46
+ readonly restore: (resolved: Record<string, unknown>) => void;
47
+ } {
48
+ if (callType === "transform" && "expression" in config) {
49
+ const { expression, ...rest } = config;
50
+ return {
51
+ config: rest,
52
+ restore: (resolved) => {
53
+ resolved.expression = expression;
54
+ },
55
+ };
56
+ }
57
+
58
+ if (callType === "validate" && Array.isArray(config.rules)) {
59
+ const originalRules = config.rules as readonly unknown[];
60
+ const strippedRules = originalRules.map((rule) => {
61
+ if (isPlainObject(rule) && "expression" in rule) {
62
+ const { expression: _deferred, ...rest } = rule;
63
+ return rest;
64
+ }
65
+ return rule;
66
+ });
67
+ return {
68
+ config: { ...config, rules: strippedRules },
69
+ restore: (resolved) => {
70
+ const resolvedRules = resolved.rules;
71
+ if (!Array.isArray(resolvedRules)) return;
72
+ for (let i = 0; i < resolvedRules.length; i++) {
73
+ const original = originalRules[i];
74
+ const target = resolvedRules[i];
75
+ if (
76
+ isPlainObject(original) &&
77
+ isPlainObject(target) &&
78
+ "expression" in original
79
+ ) {
80
+ target.expression = original.expression;
81
+ }
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ return { config, restore: () => {} };
88
+ }
89
+
90
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
91
+ return value !== null && typeof value === "object" && !Array.isArray(value);
92
+ }
93
+
20
94
  interface LlmActivityResult {
21
95
  readonly result: unknown;
22
96
  readonly model: string;
@@ -64,15 +138,22 @@ export class CallFunctionTaskBuilder implements TaskBuilder {
64
138
  build(): TaskExecutorFn {
65
139
  return async (input, state, ctx) => {
66
140
  const withConfig = this.taskDef.with ?? {};
141
+ const callType = this.taskDef.call;
67
142
 
68
- const resolved = await resolveConfigExpressions(
143
+ // Deferred-code fields (transform/validate expressions) must reach
144
+ // the activity as unresolved jq strings — see detachDeferredCode.
145
+ const deferred = detachDeferredCode(
146
+ callType,
69
147
  withConfig as Record<string, unknown>,
148
+ );
149
+
150
+ const resolved = await resolveConfigExpressions(
151
+ deferred.config,
70
152
  input,
71
153
  state,
72
154
  ctx.evaluateExpressions,
73
155
  );
74
-
75
- const callType = this.taskDef.call;
156
+ deferred.restore(resolved);
76
157
  if ((callType === "transform" || callType === "validate") &&
77
158
  !("input" in resolved) || resolved.input === undefined) {
78
159
  resolved.input = input;
@@ -109,6 +109,7 @@ export async function executeHumanInputTask(
109
109
  occurredAt: new Date().toISOString(),
110
110
  outcome: result.outcome,
111
111
  resolvedBy: result.reviewer ?? "",
112
+ resolvedByActor: result.reviewer_actor,
112
113
  comment: "",
113
114
  waitDurationMs: Date.now() - approvalRequestedAt,
114
115
  autoResolved: result.auto_resolved ?? false,
@@ -449,6 +449,18 @@ export interface TaskExecutionContext {
449
449
  */
450
450
  readonly checkPause?: () => Promise<void>;
451
451
 
452
+ /**
453
+ * Deterministic version gate for engine behavior changes that alter
454
+ * the workflow's command sequence — Temporal `patched()` semantics:
455
+ * `true` for new executions (recording a marker) and for replays whose
456
+ * history carries the marker; `false` when replaying pre-change
457
+ * histories, which must re-execute the old command order. Wired to
458
+ * Temporal's `patched()` in the workflow function; the kernel stays
459
+ * Temporal-agnostic and treats an absent gate as "change active"
460
+ * (tests and the minimal context never replay old histories).
461
+ */
462
+ readonly isPatched?: (changeId: string) => boolean;
463
+
452
464
  /**
453
465
  * Accumulates per-task status entries throughout the workflow run.
454
466
  * Passed to the emit activity on every event batch so the server
@@ -551,13 +563,33 @@ export interface HumanInputExecutionConfig {
551
563
 
552
564
  export interface HumanInputResult {
553
565
  readonly outcome: string;
566
+ /** Canonical reviewer identity (identity-account ID). Empty/absent when unattributed. */
554
567
  readonly reviewer?: string;
568
+ /**
569
+ * Display snapshot of the reviewer, stamped server-side by the control
570
+ * plane at decision time. Rides the signal opaquely into the task output
571
+ * (for downstream tasks, e.g. notifications) and the approval_resolved
572
+ * event (for UIs). Absent when no attribution exists — never synthesized.
573
+ */
574
+ readonly reviewer_actor?: HumanInputReviewerActor;
555
575
  readonly responded_at?: string;
556
576
  readonly form_data?: Record<string, unknown>;
557
577
  readonly auto_resolved?: boolean;
558
578
  readonly reason?: string;
559
579
  }
560
580
 
581
+ /** Write-time snapshot of the reviewer's display identity. */
582
+ export interface HumanInputReviewerActor {
583
+ /** Canonical identity (matches HumanInputResult.reviewer). */
584
+ readonly id: string;
585
+ /** Human-readable name; empty when unknown. */
586
+ readonly display_name?: string;
587
+ /** Email address; empty when unknown. */
588
+ readonly email?: string;
589
+ /** Avatar URL; empty when unknown. */
590
+ readonly avatar?: string;
591
+ }
592
+
561
593
  export interface HumanInputConfig {
562
594
  readonly prompt: string;
563
595
  readonly outcomes?: HumanInputOutcome[];
@@ -764,6 +796,14 @@ export interface TaskStartedEvent extends EventBase {
764
796
  readonly type: "task_started";
765
797
  readonly taskKind: string;
766
798
  readonly attemptNumber: number;
799
+ /**
800
+ * Truncated resolved task input (proto `TaskStartedPayload.input_summary`).
801
+ * The live-stream preview source for task cards — the FULL input lives on
802
+ * the status snapshot. Only present when the input is a plain object
803
+ * (proto Struct constraint, mirroring `status.tasks[].input`) and when
804
+ * emission happens after input resolution (the patched()-gated path).
805
+ */
806
+ readonly inputSummary?: Record<string, unknown>;
767
807
  }
768
808
 
769
809
  export interface TaskCompletedEvent extends EventBase {
@@ -772,6 +812,13 @@ export interface TaskCompletedEvent extends EventBase {
772
812
  readonly durationMs: number;
773
813
  readonly costMicros: number;
774
814
  readonly tokensUsed: number;
815
+ /**
816
+ * Truncated task output (proto `TaskCompletedPayload.output_summary`).
817
+ * The live-stream preview source for task cards — the FULL output lives
818
+ * on the status snapshot. Only present when the output is a plain object
819
+ * (proto Struct constraint, mirroring `status.tasks[].output`).
820
+ */
821
+ readonly outputSummary?: Record<string, unknown>;
775
822
  }
776
823
 
777
824
  export interface TaskFailedEvent extends EventBase {
@@ -816,7 +863,10 @@ export interface ApprovalRequestedEvent extends EventBase {
816
863
  export interface ApprovalResolvedEvent extends EventBase {
817
864
  readonly type: "approval_resolved";
818
865
  readonly outcome: string;
866
+ /** Canonical reviewer identity. Empty when unattributed. */
819
867
  readonly resolvedBy: string;
868
+ /** Display snapshot of the reviewer from the signal payload, if stamped. */
869
+ readonly resolvedByActor?: HumanInputReviewerActor;
820
870
  readonly comment: string;
821
871
  readonly waitDurationMs: number;
822
872
  readonly autoResolved: boolean;
@@ -33,6 +33,7 @@ vi.mock("@temporalio/workflow", () => ({
33
33
  setHandler: vi.fn(),
34
34
  condition: vi.fn(),
35
35
  sleep: vi.fn(),
36
+ patched: vi.fn(() => true),
36
37
  workflowInfo: vi.fn(() => ({ workflowId: "test-wf-id" })),
37
38
  ApplicationFailure: class ApplicationFailure extends Error {
38
39
  constructor(msg: string) { super(msg); }
@@ -13,7 +13,7 @@
13
13
  * type-only imports, and pure JS/TS logic.
14
14
  */
15
15
 
16
- import { proxyLocalActivities, proxyActivities, log, workflowInfo, sleep } from "@temporalio/workflow";
16
+ import { proxyLocalActivities, proxyActivities, log, patched, workflowInfo, sleep } from "@temporalio/workflow";
17
17
  import { ApplicationFailure, CancelledFailure, ActivityFailure } from "@temporalio/workflow";
18
18
  import { recordExecutionStartMetric, recordExecutionEndMetric } from "./metrics-sink.js";
19
19
 
@@ -200,6 +200,10 @@ export async function runWorkflowEngine(
200
200
  checkPause: options?.checkPause,
201
201
  emitEvents,
202
202
  taskStatusAccumulator,
203
+ // Deterministic version gate for engine command-order changes (the
204
+ // kernel never imports Temporal APIs — it receives patched() as an
205
+ // opaque callback, like every other capability on this context).
206
+ isPatched: (changeId: string) => patched(changeId),
203
207
  sleep: async (durationMs: number) => {
204
208
  try {
205
209
  await sleep(durationMs);
@@ -8,7 +8,8 @@
8
8
  * - "approve": returns auto-approved output
9
9
  * - "deny": returns auto-denied output
10
10
  *
11
- * Signal payload shape: { outcome, form_data?, reviewer, responded_at }
11
+ * Signal payload shape:
12
+ * { outcome, form_data?, reviewer, reviewer_actor?, responded_at }
12
13
  *
13
14
  * TEMPORAL SANDBOX: This file runs inside the deterministic workflow isolate.
14
15
  */