@stigmer/runner 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
  3. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  4. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  5. package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
  6. package/dist/activities/execute-cursor/approval-state.js +13 -1
  7. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  8. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +9 -1
  9. package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
  10. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  11. package/dist/activities/execute-cursor/hook-script.js +38 -0
  12. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  13. package/dist/activities/execute-cursor/index.d.ts +2 -0
  14. package/dist/activities/execute-cursor/index.js +56 -13
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
  17. package/dist/activities/execute-cursor/message-translator.js +78 -2
  18. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  19. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
  20. package/dist/activities/execute-cursor/prompt-builder.js +4 -0
  21. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
  23. package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
  24. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  25. package/dist/activities/execute-cursor/turn-boundary.js +15 -2
  26. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
  28. package/dist/activities/execute-deep-agent/hitl.js +48 -1
  29. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/index.js +10 -1
  31. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
  33. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  34. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
  36. package/dist/activities/execute-deep-agent/setup.js +36 -3
  37. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
  39. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  40. package/dist/activities/execute-deep-agent/status-builder.js +6 -0
  41. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
  43. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  44. package/dist/activities/workflow-event-activities.js +9 -0
  45. package/dist/activities/workflow-event-activities.js.map +1 -1
  46. package/dist/config.d.ts +8 -0
  47. package/dist/config.js +2 -0
  48. package/dist/config.js.map +1 -1
  49. package/dist/middleware/approval-gate.d.ts +20 -0
  50. package/dist/middleware/approval-gate.js +16 -1
  51. package/dist/middleware/approval-gate.js.map +1 -1
  52. package/dist/runner-manager.js +3 -0
  53. package/dist/runner-manager.js.map +1 -1
  54. package/dist/runner.js +3 -0
  55. package/dist/runner.js.map +1 -1
  56. package/dist/shared/approval-policy.d.ts +24 -1
  57. package/dist/shared/approval-policy.js +35 -1
  58. package/dist/shared/approval-policy.js.map +1 -1
  59. package/dist/shared/datastore-attachment.d.ts +79 -0
  60. package/dist/shared/datastore-attachment.js +129 -0
  61. package/dist/shared/datastore-attachment.js.map +1 -0
  62. package/dist/shared/with-timeout.d.ts +10 -0
  63. package/dist/shared/with-timeout.js +14 -1
  64. package/dist/shared/with-timeout.js.map +1 -1
  65. package/dist/workflow-engine/tasks/human-input.js +1 -0
  66. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  67. package/dist/workflow-engine/types.d.ts +22 -0
  68. package/dist/workflow-engine/types.js.map +1 -1
  69. package/dist/workflows/human-input-orchestrator.d.ts +2 -1
  70. package/dist/workflows/human-input-orchestrator.js +2 -1
  71. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  74. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  75. package/src/activities/__tests__/workflow-event-activities.test.ts +29 -0
  76. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
  77. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +25 -0
  78. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
  79. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
  80. package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
  81. package/src/activities/execute-cursor/approval-state.ts +32 -1
  82. package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
  83. package/src/activities/execute-cursor/hook-script.ts +40 -0
  84. package/src/activities/execute-cursor/index.ts +64 -15
  85. package/src/activities/execute-cursor/message-translator.ts +82 -1
  86. package/src/activities/execute-cursor/prompt-builder.ts +12 -1
  87. package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
  88. package/src/activities/execute-cursor/turn-boundary.ts +23 -0
  89. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  90. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  91. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  92. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
  93. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  94. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  95. package/src/activities/execute-deep-agent/hitl.ts +51 -0
  96. package/src/activities/execute-deep-agent/index.ts +11 -1
  97. package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
  98. package/src/activities/execute-deep-agent/setup.ts +57 -2
  99. package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
  100. package/src/activities/execute-deep-agent/status-builder.ts +7 -0
  101. package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
  102. package/src/activities/workflow-event-activities.ts +9 -0
  103. package/src/config.ts +11 -0
  104. package/src/middleware/__tests__/approval-gate.test.ts +95 -0
  105. package/src/middleware/approval-gate.ts +37 -0
  106. package/src/runner-manager.ts +3 -0
  107. package/src/runner.ts +3 -0
  108. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  109. package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
  110. package/src/shared/__tests__/with-timeout.test.ts +16 -1
  111. package/src/shared/approval-policy.ts +41 -2
  112. package/src/shared/datastore-attachment.ts +166 -0
  113. package/src/shared/with-timeout.ts +15 -1
  114. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
  115. package/src/workflow-engine/tasks/human-input.ts +1 -0
  116. package/src/workflow-engine/types.ts +23 -0
  117. package/src/workflows/human-input-orchestrator.ts +2 -1
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { withTimeout } from "../with-timeout.js";
2
+ import { withTimeout, TimeoutError } from "../with-timeout.js";
3
3
 
4
4
  describe("withTimeout", () => {
5
5
  it("resolves with the function's result when it completes in time", async () => {
@@ -14,6 +14,21 @@ describe("withTimeout", () => {
14
14
  ).rejects.toThrow("Cursor agent create timed out after 20ms");
15
15
  });
16
16
 
17
+ it("rejects with a TimeoutError so callers can react to expiry by type", async () => {
18
+ const hang = () => new Promise<never>(() => {});
19
+ await expect(withTimeout(20, "expired", hang)).rejects.toBeInstanceOf(TimeoutError);
20
+ });
21
+
22
+ it("does not wrap the function's own rejection in a TimeoutError", async () => {
23
+ const boom = new Error("underlying failure");
24
+ let caught: unknown;
25
+ await withTimeout(1_000, "should not fire", () => Promise.reject(boom)).catch((err) => {
26
+ caught = err;
27
+ });
28
+ expect(caught).toBe(boom);
29
+ expect(caught).not.toBeInstanceOf(TimeoutError);
30
+ });
31
+
17
32
  it("evaluates a lazy message only on expiry", async () => {
18
33
  let evaluated = 0;
19
34
  const message = () => {
@@ -18,7 +18,7 @@
18
18
  import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
19
19
  import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
20
20
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
21
- import { ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
+ import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
22
22
  import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
23
23
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
24
24
 
@@ -147,6 +147,42 @@ export function deriveActiveLeases(execution: AgentExecution): ActiveLeases {
147
147
  };
148
148
  }
149
149
 
150
+ /**
151
+ * Whether this execution runs in UNATTENDED approval mode
152
+ * (ExecutionConfig.approval_mode): the creating surface — a messaging
153
+ * channel, a guest share — has no approver, so a gated tool is resolved as
154
+ * an automatic skip (the model is told to adapt) instead of pausing the
155
+ * execution for a decision that can never arrive.
156
+ *
157
+ * The mode changes HOW a gate resolves, never WHAT is gated: the four-level
158
+ * policy merge below is identical in both modes. Both harnesses read the
159
+ * mode through this one helper (the native gate skips instead of
160
+ * interrupting; the Cursor hook records a non-pausing "unattended" denial),
161
+ * so the surfaces can never diverge on what "unattended" means.
162
+ */
163
+ export function isUnattendedApprovalMode(execution: AgentExecution): boolean {
164
+ return execution.spec?.executionConfig?.approvalMode === ApprovalMode.UNATTENDED;
165
+ }
166
+
167
+ /**
168
+ * The tool-result text for an unattended auto-skip — ONE definition for both
169
+ * harnesses (the native gate returns it as the skip ToolMessage; the Cursor
170
+ * turn boundary backfills it onto stamped SKIPPED rows), so the transcript
171
+ * reads identically wherever the skip happened. Deliberately instructs
172
+ * plain-language adaptation with NO tool/approval vocabulary reaching the end
173
+ * user (the channel/guest anti-leak posture).
174
+ */
175
+ export function unattendedSkipMessage(toolName: string): string {
176
+ return (
177
+ `Tool '${toolName}' requires an approval that is not available in ` +
178
+ `this conversation, so it was skipped automatically. Do not retry ` +
179
+ `it or attempt a workaround. Adapt your plan, and explain to the ` +
180
+ `user in plain language what you could not do and what they can ` +
181
+ `do instead — never mention tools, approvals, or platform ` +
182
+ `mechanics.`
183
+ );
184
+ }
185
+
150
186
  /**
151
187
  * Provenance of a gate decision: which policy layer (or decision point) is
152
188
  * responsible for the final requires-approval verdict.
@@ -170,7 +206,8 @@ export type PolicySource =
170
206
  | "approval_lease" // Layer 4: a run-lifetime scoped lease cleared this action
171
207
  | "builtin_category" // Non-MCP built-in gated by the shared tool taxonomy
172
208
  | "file_capture" // Capture mode: a git-tracked built-in file edit flows, reviewed post-hoc via the file_review ledger (not gated; audit-only on the shadow receipt)
173
- | "annotation_destructive_tighten"; // Layer 1 sub-case: connect-time destructiveHint tightener force-gated this MCP tool
209
+ | "annotation_destructive_tighten" // Layer 1 sub-case: connect-time destructiveHint tightener force-gated this MCP tool
210
+ | "unattended_skip"; // Layer 4 resolution: unattended approval mode auto-skipped this gated call (no approver on the creating surface)
174
211
 
175
212
  /**
176
213
  * Monotonic identifier of the policy-engine logic that produced a decision,
@@ -205,6 +242,8 @@ export function toProtoPolicySource(source: PolicySource | undefined): ApprovalP
205
242
  return ApprovalPolicySource.BUILTIN_CATEGORY;
206
243
  case "annotation_destructive_tighten":
207
244
  return ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN;
245
+ case "unattended_skip":
246
+ return ApprovalPolicySource.UNATTENDED_SKIP;
208
247
  case "file_capture":
209
248
  // Capture-mode flow is never persisted on a gated tool call (the file tool
210
249
  // is not gated — it has no WAITING_APPROVAL row); it exists only on the
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The runner-synthesized datastore records attachment (T05, DD-001 SD-2).
3
+ *
4
+ * When an agent declares `datastore_usages`, the runner synthesizes ONE
5
+ * MCP attachment serving the five record tools (DD-005) — the agent
6
+ * builder writes a usage line and gets tools; no McpServer resource, no
7
+ * Environment, no credential in any manifest. Harness-agnostic like
8
+ * mcp-resolver.ts: both ExecuteCursor and ExecuteDeepAgent inject the
9
+ * synthesized entry through their existing ResolvedMcpServer paths.
10
+ *
11
+ * Two connection shapes, one roster (the bridge's records-only roster,
12
+ * T05 R1):
13
+ * - Bridge endpoint configured (cloud): Streamable HTTP against the
14
+ * bridge's /records route, with the execution's own session-scoped
15
+ * credential as the Bearer token — the server's reach chain (DD-006
16
+ * Path 1) authorizes from that token; the bridge stays a
17
+ * non-validating passthrough.
18
+ * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
19
+ * stdio child with STIGMER_MCP_ROSTER=records against the local
20
+ * backend (zero new distribution — the CLI already embeds the
21
+ * bridge). No credential: the local backend is unauthenticated and
22
+ * OSS callers resolve as the local principal (T05 R2).
23
+ *
24
+ * Approval-free by construction (DD-001 SD-3): empty toolApprovals +
25
+ * pinnedToolApprovals mean mergeApprovalPolicies emits no entries for
26
+ * this server — zero classifier involvement. `discoveredCapabilitiesEmpty`
27
+ * is false and the attachment has no McpServerUsage, so the connect
28
+ * backfill (whose destructiveHint tightener would force-gate
29
+ * delete_record — silently skipped on channels under UNATTENDED mode)
30
+ * is structurally unable to touch it. Callers must still inject AFTER
31
+ * resolve + backfill; both harness call sites do.
32
+ */
33
+
34
+ import type { DatastoreUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
35
+ import type { ResolvedMcpServer } from "./mcp-resolver.js";
36
+
37
+ /**
38
+ * The synthesized attachment's slug. Reserved: a user McpServer with
39
+ * this slug is shadowed by the synthesized attachment (the record tools
40
+ * must exist whenever `datastore_usages` says so), with a warning.
41
+ */
42
+ export const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
43
+
44
+ /** The bridge route serving the records-only roster (mcp-server T05 R1). */
45
+ export const RECORDS_ROUTE = "/records";
46
+
47
+ export interface DatastoreAttachmentOptions {
48
+ /**
49
+ * The bridge's HTTP endpoint (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
50
+ * https://mcp.stigmer.ai). Null selects the OSS stdio shape.
51
+ */
52
+ bridgeEndpoint: string | null;
53
+ /**
54
+ * The execution's session-scoped credential (the sandbox token a
55
+ * cloud runner holds, or the desktop runner's exchanged scoped
56
+ * token). Null attaches no Authorization header (OSS/local).
57
+ */
58
+ credential: string | null;
59
+ /**
60
+ * The stigmer backend endpoint the stdio child dials
61
+ * (config.stigmerBackendEndpoint). Only used for the OSS shape.
62
+ */
63
+ backendEndpoint: string;
64
+ }
65
+
66
+ /**
67
+ * Synthesize the records attachment for an agent's datastore usages.
68
+ * Returns undefined when the agent uses no datastores — the attachment
69
+ * exists exactly when the usage edge does.
70
+ */
71
+ export function synthesizeDatastoreAttachment(
72
+ datastoreUsages: DatastoreUsage[],
73
+ options: DatastoreAttachmentOptions,
74
+ ): ResolvedMcpServer | undefined {
75
+ if (datastoreUsages.length === 0) {
76
+ return undefined;
77
+ }
78
+
79
+ // Approval-free by construction + backfill-proof: see file header.
80
+ const base = {
81
+ slug: DATASTORE_ATTACHMENT_SLUG,
82
+ toolApprovals: [],
83
+ pinnedToolApprovals: [],
84
+ discoveredCapabilitiesEmpty: false,
85
+ };
86
+
87
+ if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
88
+ return {
89
+ ...base,
90
+ connectionType: "http",
91
+ url: options.bridgeEndpoint.replace(/\/+$/, "") + RECORDS_ROUTE,
92
+ headers: options.credential !== null && options.credential !== ""
93
+ ? { Authorization: `Bearer ${options.credential}` }
94
+ : undefined,
95
+ };
96
+ }
97
+
98
+ return {
99
+ ...base,
100
+ connectionType: "stdio",
101
+ command: "stigmer",
102
+ args: ["mcp-server"],
103
+ env: {
104
+ STIGMER_MCP_ROSTER: "records",
105
+ STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
106
+ },
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Inject the synthesized attachment into a resolved server list —
112
+ * AFTER resolve + backfill (see file header). A user server shadowing
113
+ * the reserved slug is replaced, loudly.
114
+ */
115
+ export function injectDatastoreAttachment(
116
+ resolvedServers: ResolvedMcpServer[],
117
+ attachment: ResolvedMcpServer,
118
+ ): ResolvedMcpServer[] {
119
+ const shadowed = resolvedServers.some((s) => s.slug === attachment.slug);
120
+ if (shadowed) {
121
+ console.warn(
122
+ `MCP server slug "${attachment.slug}" is reserved for the datastore ` +
123
+ "records attachment; the user-defined server is replaced.",
124
+ );
125
+ }
126
+ return [
127
+ ...resolvedServers.filter((s) => s.slug !== attachment.slug),
128
+ attachment,
129
+ ];
130
+ }
131
+
132
+ /**
133
+ * The `<available_datastores>` prompt section (DD-005 SD-5, the
134
+ * skills-section precedent): names the attached datastores and points
135
+ * the model at describe_datastore first. Shared by both harnesses so
136
+ * the section text cannot drift (the sender-identity precedent).
137
+ */
138
+ export function formatDatastoresSection(datastoreUsages: DatastoreUsage[]): string {
139
+ const slugs = datastoreUsages
140
+ .map((u) => u.datastoreRef?.slug)
141
+ .filter((s): s is string => !!s);
142
+ const entries = slugs.map((slug) => `- ${slug}`);
143
+ return [
144
+ "<available_datastores>",
145
+ "You have access to the following datastores through the record tools",
146
+ "(describe_datastore, find_records, insert_record, update_record, delete_record).",
147
+ "Before your first operation against a datastore, call describe_datastore to",
148
+ "learn its collections, field encodings, and which operations you are allowed",
149
+ "to perform.",
150
+ "",
151
+ ...entries,
152
+ "</available_datastores>",
153
+ ].join("\n");
154
+ }
155
+
156
+ /**
157
+ * The gRPC dial target (host:port) for a backend endpoint URL — the
158
+ * shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
159
+ */
160
+ function grpcTarget(endpoint: string): string {
161
+ try {
162
+ return new URL(endpoint).host;
163
+ } catch {
164
+ return endpoint;
165
+ }
166
+ }
@@ -15,6 +15,20 @@
15
15
  * `AbortSignal.timeout` and pass the signal to the callee instead.
16
16
  */
17
17
 
18
+ /**
19
+ * Thrown when the bound expires before `fn` settles. A distinct class so
20
+ * callers can react to expiry specifically (`err instanceof TimeoutError`)
21
+ * instead of sniffing message text — e.g. the agent-resolution transport
22
+ * recovery retries on expiry but must let deterministic failures (auth,
23
+ * validation) propagate untouched.
24
+ */
25
+ export class TimeoutError extends Error {
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "TimeoutError";
29
+ }
30
+ }
31
+
18
32
  export async function withTimeout<T>(
19
33
  ms: number,
20
34
  timeoutMessage: string | (() => string),
@@ -23,7 +37,7 @@ export async function withTimeout<T>(
23
37
  return new Promise<T>((resolve, reject) => {
24
38
  const timer = setTimeout(() => {
25
39
  const message = typeof timeoutMessage === "function" ? timeoutMessage() : timeoutMessage;
26
- reject(new Error(message));
40
+ reject(new TimeoutError(message));
27
41
  }, ms);
28
42
 
29
43
  fn()
@@ -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
 
@@ -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,
@@ -563,13 +563,33 @@ export interface HumanInputExecutionConfig {
563
563
 
564
564
  export interface HumanInputResult {
565
565
  readonly outcome: string;
566
+ /** Canonical reviewer identity (identity-account ID). Empty/absent when unattributed. */
566
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;
567
575
  readonly responded_at?: string;
568
576
  readonly form_data?: Record<string, unknown>;
569
577
  readonly auto_resolved?: boolean;
570
578
  readonly reason?: string;
571
579
  }
572
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
+
573
593
  export interface HumanInputConfig {
574
594
  readonly prompt: string;
575
595
  readonly outcomes?: HumanInputOutcome[];
@@ -843,7 +863,10 @@ export interface ApprovalRequestedEvent extends EventBase {
843
863
  export interface ApprovalResolvedEvent extends EventBase {
844
864
  readonly type: "approval_resolved";
845
865
  readonly outcome: string;
866
+ /** Canonical reviewer identity. Empty when unattributed. */
846
867
  readonly resolvedBy: string;
868
+ /** Display snapshot of the reviewer from the signal payload, if stamped. */
869
+ readonly resolvedByActor?: HumanInputReviewerActor;
847
870
  readonly comment: string;
848
871
  readonly waitDurationMs: number;
849
872
  readonly autoResolved: boolean;
@@ -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
  */