@stigmer/runner 3.1.10 → 3.1.12

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 (33) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/promote-task-output.d.ts +6 -1
  3. package/dist/activities/promote-task-output.js +10 -5
  4. package/dist/activities/promote-task-output.js.map +1 -1
  5. package/dist/activities/workflow-event-activities.js +10 -1
  6. package/dist/activities/workflow-event-activities.js.map +1 -1
  7. package/dist/workflow-engine/loader.js +4 -0
  8. package/dist/workflow-engine/loader.js.map +1 -1
  9. package/dist/workflow-engine/resolve.js +9 -2
  10. package/dist/workflow-engine/resolve.js.map +1 -1
  11. package/dist/workflow-engine/task-status-accumulator.d.ts +8 -1
  12. package/dist/workflow-engine/task-status-accumulator.js +2 -1
  13. package/dist/workflow-engine/task-status-accumulator.js.map +1 -1
  14. package/dist/workflow-engine/tasks/human-input.d.ts +7 -0
  15. package/dist/workflow-engine/tasks/human-input.js +70 -4
  16. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  17. package/dist/workflow-engine/types.d.ts +19 -1
  18. package/dist/workflow-engine/types.js.map +1 -1
  19. package/dist/workflows/engine-core.js +1 -1
  20. package/dist/workflows/engine-core.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/activities/promote-task-output.ts +10 -4
  23. package/src/activities/workflow-event-activities.ts +11 -2
  24. package/src/workflow-engine/__tests__/loader.test.ts +93 -0
  25. package/src/workflow-engine/__tests__/resolve.test.ts +50 -0
  26. package/src/workflow-engine/__tests__/task-status-accumulator.test.ts +35 -0
  27. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +248 -2
  28. package/src/workflow-engine/loader.ts +4 -0
  29. package/src/workflow-engine/resolve.ts +9 -1
  30. package/src/workflow-engine/task-status-accumulator.ts +9 -1
  31. package/src/workflow-engine/tasks/human-input.ts +111 -15
  32. package/src/workflow-engine/types.ts +18 -0
  33. package/src/workflows/engine-core.ts +2 -2
@@ -25,6 +25,13 @@ export interface TaskStatusEntry {
25
25
  readonly costMicros?: number;
26
26
  readonly inputTokens?: number;
27
27
  readonly outputTokens?: number;
28
+ /**
29
+ * Review-surface hint from the human_input task's ui_hint config.
30
+ * Set on the waiting_approval transition and retained through later
31
+ * transitions (spread semantics), so the persisted status keeps the
32
+ * record of what kind of review the gate presented.
33
+ */
34
+ readonly uiHint?: string;
28
35
  }
29
36
 
30
37
  /**
@@ -177,7 +184,7 @@ export class TaskStatusAccumulator {
177
184
  });
178
185
  }
179
186
 
180
- taskWaitingApproval(name: string): void {
187
+ taskWaitingApproval(name: string, uiHint?: string): void {
181
188
  const existing = this.entries.get(name);
182
189
  this.entries.set(name, {
183
190
  ...existing,
@@ -186,6 +193,7 @@ export class TaskStatusAccumulator {
186
193
  taskKind: existing?.taskKind ?? "",
187
194
  status: "waiting_approval",
188
195
  startedAt: existing?.startedAt,
196
+ uiHint: uiHint ?? existing?.uiHint,
189
197
  });
190
198
  }
191
199
 
@@ -17,6 +17,8 @@
17
17
  * call: human_input
18
18
  * with:
19
19
  * prompt: "Please review this deployment"
20
+ * payload: ${ $context.deployment_plan }
21
+ * ui_hint: deployment-plan
20
22
  * timeout: 300
21
23
  * on_timeout: approve
22
24
  * outcomes:
@@ -24,16 +26,22 @@
24
26
  * - name: deny
25
27
  * - name: revise
26
28
  * then: gatherMore
29
+ *
30
+ * The payload (the material under review) is expression-resolved at gate
31
+ * activation and attached to the approval_requested event — inline when
32
+ * small, as an artifact reference when at/above the promotion threshold —
33
+ * so the approval record captures exactly what the reviewer saw.
27
34
  */
28
35
 
29
36
  import type {
37
+ ArtifactCreatedEvent,
30
38
  HumanInputTaskDef,
31
39
  HumanInputConfig,
32
40
  WorkflowState,
33
41
  TaskExecutionContext,
34
42
  HumanInputResult,
35
43
  } from "../types.js";
36
- import { resolveEmbeddedExpressions } from "../resolve.js";
44
+ import { resolveConfigExpressions, resolveEmbeddedExpressions } from "../resolve.js";
37
45
  import { isStrictExpr } from "../expression-utils.js";
38
46
 
39
47
  const SIGNAL_PREFIX = "human_input_";
@@ -60,25 +68,32 @@ export async function executeHumanInputTask(
60
68
  const resolvedPrompt = await resolvePromptExpressions(
61
69
  config.prompt, state, ctx,
62
70
  );
71
+ const review = await resolveReviewPayload(config, taskName, state, ctx);
63
72
 
64
73
  const approvalRequestedAt = Date.now();
65
74
 
66
- ctx.taskStatusAccumulator?.taskWaitingApproval(taskName);
75
+ ctx.taskStatusAccumulator?.taskWaitingApproval(taskName, config.uiHint);
67
76
 
68
77
  if (ctx.emitEvents) {
69
- await ctx.emitEvents([{
70
- type: "approval_requested",
71
- taskName,
72
- occurredAt: new Date().toISOString(),
73
- prompt: resolvedPrompt,
74
- approvers: config.approvers ?? [],
75
- timeoutSeconds,
76
- outcomes: (config.outcomes ?? []).map((o) => ({
77
- name: o.name,
78
- label: o.label ?? "",
79
- })),
80
- formSchema: config.formSchema,
81
- }]);
78
+ await ctx.emitEvents([
79
+ {
80
+ type: "approval_requested",
81
+ taskName,
82
+ occurredAt: new Date().toISOString(),
83
+ prompt: resolvedPrompt,
84
+ approvers: config.approvers ?? [],
85
+ timeoutSeconds,
86
+ outcomes: (config.outcomes ?? []).map((o) => ({
87
+ name: o.name,
88
+ label: o.label ?? "",
89
+ })),
90
+ formSchema: config.formSchema,
91
+ payload: review.payload,
92
+ uiHint: config.uiHint,
93
+ payloadArtifactId: review.payloadArtifactId,
94
+ },
95
+ ...review.artifactEvents,
96
+ ]);
82
97
  }
83
98
 
84
99
  const result: HumanInputResult = await ctx.awaitHumanInput({
@@ -116,6 +131,87 @@ function validateConfig(config: HumanInputConfig, taskName: string): void {
116
131
  }
117
132
  }
118
133
 
134
+ interface ResolvedReviewPayload {
135
+ /** Resolved payload for inline delivery on the approval_requested event. */
136
+ readonly payload?: unknown;
137
+ /** Artifact holding the payload when it exceeded the promotion threshold. */
138
+ readonly payloadArtifactId?: string;
139
+ /** artifact_created events to emit alongside approval_requested. */
140
+ readonly artifactEvents: readonly ArtifactCreatedEvent[];
141
+ }
142
+
143
+ const NO_REVIEW_PAYLOAD: ResolvedReviewPayload = { artifactEvents: [] };
144
+
145
+ /**
146
+ * Resolves the review payload's `${ ... }` expressions and decides how it
147
+ * rides the approval_requested event: inline when small, or as an artifact
148
+ * reference when at/above the promotion threshold (256KB).
149
+ *
150
+ * Resolution goes through `resolveConfigExpressions` for its two-phase
151
+ * injection safety: review material resolved from `$context` (article
152
+ * drafts, webhook data) may contain literal `${ ... }` text that must
153
+ * never be re-evaluated as an expression.
154
+ *
155
+ * A resolution failure fails the task — a gate whose review material
156
+ * failed to materialize must not present an empty review. Promotion
157
+ * failure falls back to inline delivery (best-effort, matching the
158
+ * do-executor's task-output promotion policy).
159
+ */
160
+ async function resolveReviewPayload(
161
+ config: HumanInputConfig,
162
+ taskName: string,
163
+ state: WorkflowState,
164
+ ctx: TaskExecutionContext,
165
+ ): Promise<ResolvedReviewPayload> {
166
+ if (config.payload === undefined || config.payload === null) {
167
+ return NO_REVIEW_PAYLOAD;
168
+ }
169
+
170
+ let resolved: unknown;
171
+ try {
172
+ const result = await resolveConfigExpressions(
173
+ { payload: config.payload }, null, state, ctx.evaluateExpressions,
174
+ );
175
+ resolved = result.payload;
176
+ } catch (err) {
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ throw new Error(
179
+ `human_input task '${taskName}': failed to resolve 'payload' expressions: ${message}`,
180
+ );
181
+ }
182
+
183
+ // jq resolves missing paths to null rather than erroring. A configured
184
+ // payload that materializes as nothing is an authoring error (typically a
185
+ // wrong $context path) — fail loudly instead of presenting an empty review.
186
+ if (resolved === undefined || resolved === null) {
187
+ throw new Error(
188
+ `human_input task '${taskName}': 'payload' resolved to null — ` +
189
+ `check that the expression references existing workflow data`,
190
+ );
191
+ }
192
+
193
+ if (ctx.promoteTaskOutput) {
194
+ try {
195
+ const promotion = await ctx.promoteTaskOutput(
196
+ resolved,
197
+ state.env?.__stigmer_execution_id as string ?? "",
198
+ taskName,
199
+ `${taskName} — review-payload.json`,
200
+ );
201
+ if (promotion.artifactIds.length > 0) {
202
+ return {
203
+ payloadArtifactId: promotion.artifactIds[0],
204
+ artifactEvents: promotion.artifactCreatedEvents,
205
+ };
206
+ }
207
+ } catch {
208
+ // Promotion is best-effort; fall through to inline delivery.
209
+ }
210
+ }
211
+
212
+ return { payload: resolved, artifactEvents: [] };
213
+ }
214
+
119
215
  /**
120
216
  * Resolves `${ ... }` expressions in the prompt string against the
121
217
  * current workflow state. Handles both strict (whole-value) and embedded
@@ -565,6 +565,14 @@ export interface HumanInputConfig {
565
565
  readonly approvers?: readonly string[];
566
566
  readonly timeout?: number;
567
567
  readonly onTimeout?: "fail" | "approve" | "deny";
568
+ /**
569
+ * Material for the reviewer to examine — a whole-value expression
570
+ * string, or an object/array with embedded expressions. Resolved at
571
+ * gate activation and attached to the approval_requested event.
572
+ */
573
+ readonly payload?: unknown;
574
+ /** Renderer discriminator forwarded verbatim to the approval UI. */
575
+ readonly uiHint?: string;
568
576
  }
569
577
 
570
578
  export interface HumanInputOutcome {
@@ -795,6 +803,14 @@ export interface ApprovalRequestedEvent extends EventBase {
795
803
  readonly timeoutSeconds: number;
796
804
  readonly outcomes: ReadonlyArray<{ name: string; label: string }>;
797
805
  readonly formSchema?: Record<string, unknown>;
806
+ /**
807
+ * Resolved review payload the gate presents. Mutually exclusive with
808
+ * payloadArtifactId — payloads at/above the promotion threshold live
809
+ * in the artifact store and only the reference rides the event.
810
+ */
811
+ readonly payload?: unknown;
812
+ readonly uiHint?: string;
813
+ readonly payloadArtifactId?: string;
798
814
  }
799
815
 
800
816
  export interface ApprovalResolvedEvent extends EventBase {
@@ -885,6 +901,8 @@ export type PromoteTaskOutputFn = (
885
901
  taskOutput: unknown,
886
902
  workflowExecutionId: string,
887
903
  taskName: string,
904
+ /** Artifact display name override; defaults to "<taskName> — output.json". */
905
+ displayName?: string,
888
906
  ) => Promise<PromoteTaskOutputResult>;
889
907
 
890
908
  // ─────────────────────────────────────────────────────────────────────
@@ -243,8 +243,8 @@ export async function runWorkflowEngine(
243
243
  taskName: agentMeta.taskName,
244
244
  workflowExecutionId: agentMeta.workflowExecutionId || executionId,
245
245
  }),
246
- promoteTaskOutput: (taskOutput: unknown, wexId: string, taskName: string) =>
247
- promoteProxy.PromoteTaskOutput(taskOutput, wexId || executionId, taskName),
246
+ promoteTaskOutput: (taskOutput: unknown, wexId: string, taskName: string, displayName?: string) =>
247
+ promoteProxy.PromoteTaskOutput(taskOutput, wexId || executionId, taskName, displayName),
248
248
  };
249
249
 
250
250
  const state = createState();