@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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-transform.js +12 -2
- package/dist/activities/call-transform.js.map +1 -1
- package/dist/activities/call-validate.js +20 -2
- package/dist/activities/call-validate.js.map +1 -1
- package/dist/activities/discover-mcp-server.js +6 -20
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
- package/dist/activities/execute-cursor/approval-state.js +13 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +38 -0
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +12 -0
- package/dist/activities/execute-cursor/index.js +72 -15
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
- package/dist/activities/execute-cursor/message-translator.js +78 -2
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +19 -0
- package/dist/activities/execute-cursor/prompt-builder.js +21 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
- package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.js +15 -2
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
- package/dist/activities/execute-deep-agent/hitl.js +48 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +10 -1
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +19 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +12 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
- package/dist/activities/execute-deep-agent/setup.js +16 -1
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +11 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +20 -0
- package/dist/middleware/approval-gate.js +16 -1
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +2 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +24 -1
- package/dist/shared/approval-policy.js +35 -1
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/context-bridge.d.ts +30 -0
- package/dist/shared/context-bridge.js +45 -0
- package/dist/shared/context-bridge.js.map +1 -0
- package/dist/shared/sender-identity.d.ts +50 -0
- package/dist/shared/sender-identity.js +69 -0
- package/dist/shared/sender-identity.js.map +1 -0
- package/dist/shared/with-timeout.d.ts +27 -0
- package/dist/shared/with-timeout.js +47 -0
- package/dist/shared/with-timeout.js.map +1 -0
- package/dist/workflow-engine/do-executor.d.ts +7 -0
- package/dist/workflow-engine/do-executor.js +59 -1
- package/dist/workflow-engine/do-executor.js.map +1 -1
- package/dist/workflow-engine/tasks/call-function.js +68 -1
- package/dist/workflow-engine/tasks/call-function.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +1 -0
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +48 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +5 -1
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +2 -1
- package/dist/workflows/human-input-orchestrator.js +2 -1
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/config.test.ts +8 -0
- package/src/activities/__tests__/call-validate.test.ts +137 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +89 -0
- package/src/activities/call-transform.ts +20 -2
- package/src/activities/call-validate.ts +27 -2
- package/src/activities/discover-mcp-server.ts +7 -28
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +65 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
- package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
- package/src/activities/execute-cursor/approval-state.ts +32 -1
- package/src/activities/execute-cursor/hook-script.ts +40 -0
- package/src/activities/execute-cursor/index.ts +86 -17
- package/src/activities/execute-cursor/message-translator.ts +82 -1
- package/src/activities/execute-cursor/prompt-builder.ts +44 -0
- package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
- package/src/activities/execute-cursor/turn-boundary.ts +23 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +58 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/hitl.ts +51 -0
- package/src/activities/execute-deep-agent/index.ts +11 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +35 -0
- package/src/activities/execute-deep-agent/setup.ts +31 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
- package/src/activities/execute-deep-agent/status-builder.ts +7 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
- package/src/activities/workflow-event-activities.ts +11 -0
- package/src/config.ts +23 -0
- package/src/middleware/__tests__/approval-gate.test.ts +95 -0
- package/src/middleware/approval-gate.ts +37 -0
- package/src/runner-manager.ts +6 -1
- package/src/runner.ts +6 -1
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/context-bridge.test.ts +51 -0
- package/src/shared/__tests__/sender-identity.test.ts +92 -0
- package/src/shared/__tests__/with-timeout.test.ts +60 -0
- package/src/shared/approval-policy.ts +41 -2
- package/src/shared/context-bridge.ts +51 -0
- package/src/shared/sender-identity.ts +85 -0
- package/src/shared/with-timeout.ts +53 -0
- package/src/workflow-engine/__tests__/do-executor.test.ts +155 -0
- package/src/workflow-engine/__tests__/tasks/call-function.test.ts +94 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
- package/src/workflow-engine/do-executor.ts +65 -1
- package/src/workflow-engine/tasks/call-function.ts +84 -3
- package/src/workflow-engine/tasks/human-input.ts +1 -0
- package/src/workflow-engine/types.ts +50 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +1 -0
- package/src/workflows/engine-core.ts +5 -1
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
*/
|