@stigmer/runner 3.1.10 → 3.1.11
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/promote-task-output.d.ts +6 -1
- package/dist/activities/promote-task-output.js +10 -5
- package/dist/activities/promote-task-output.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +10 -1
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/workflow-engine/loader.js +4 -0
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/resolve.js +9 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflow-engine/task-status-accumulator.d.ts +8 -1
- package/dist/workflow-engine/task-status-accumulator.js +2 -1
- package/dist/workflow-engine/task-status-accumulator.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +7 -0
- package/dist/workflow-engine/tasks/human-input.js +70 -4
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +19 -1
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +1 -1
- package/dist/workflows/engine-core.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/promote-task-output.ts +10 -4
- package/src/activities/workflow-event-activities.ts +11 -2
- package/src/workflow-engine/__tests__/loader.test.ts +93 -0
- package/src/workflow-engine/__tests__/resolve.test.ts +50 -0
- package/src/workflow-engine/__tests__/task-status-accumulator.test.ts +35 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +248 -2
- package/src/workflow-engine/loader.ts +4 -0
- package/src/workflow-engine/resolve.ts +9 -1
- package/src/workflow-engine/task-status-accumulator.ts +9 -1
- package/src/workflow-engine/tasks/human-input.ts +111 -15
- package/src/workflow-engine/types.ts +18 -0
- 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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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();
|