@osolmaz/pi-workflows 0.12.0 → 0.12.1
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/README.md +4 -3
- package/dist/builtins/autoimplement.workflow.d.ts +485 -126
- package/dist/builtins/autoimplement.workflow.js +17 -105
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +4 -4
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +2 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +5 -3
- package/dist/builtins/sanity-check.workflow.js +105 -21
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +22 -28
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/index.js +62 -33
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +14 -13
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -2
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +10 -0
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +5 -0
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +33 -45
- package/docs/HUMAN_DECISIONS.md +25 -35
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +5 -11
- package/docs/WORKFLOW_COMPOSITION.md +8 -7
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +202 -94
- package/docs/run-bundles.md +6 -6
- package/docs/workflows.md +26 -6
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/schemas/human-decision-accepted-v1.schema.json +15 -3
- package/schemas/human-decision-continuation-v1.schema.json +10 -1
- package/schemas/human-decision-delivery-v1.schema.json +8 -0
- package/schemas/human-decision-receipt-v1.schema.json +8 -0
- package/schemas/human-decision-request-v1.schema.json +24 -4
- package/skills/autoimplement/SKILL.md +27 -0
- package/skills/monitor/SKILL.md +31 -3
- package/skills/pi-workflows/SKILL.md +2 -1
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/catalog.ts +4 -4
- package/src/builtins/index.ts +11 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +186 -41
- package/src/extension/decision-channels.ts +29 -59
- package/src/extension/index.ts +79 -41
- package/src/extension/session-events.ts +2 -2
- package/src/extension/widget.ts +24 -5
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +17 -15
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +5 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +17 -0
- package/src/workflows/store.ts +5 -0
- package/src/workflows/types.ts +39 -56
- package/dist/builtins/sanity-check-session.d.ts +0 -17
- package/dist/builtins/sanity-check-session.js +0 -168
- package/dist/builtins/sanity-check-session.js.map +0 -1
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
- package/src/builtins/sanity-check-session.ts +0 -205
package/src/extension/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { compositionMetadata } from "../workflows/composition.js";
|
|
|
18
18
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
19
19
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
20
20
|
import {
|
|
21
|
+
BuiltinWorkflowRevisionChangedError,
|
|
21
22
|
ClaimLostError,
|
|
22
23
|
errorMessage,
|
|
23
24
|
isClaimLostError,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
} from "../workflows/store.js";
|
|
42
43
|
import type {
|
|
43
44
|
AcceptedHumanDecision,
|
|
45
|
+
ResolvedHumanDecision,
|
|
44
46
|
AgentStepContract,
|
|
45
47
|
HumanDecisionRequest,
|
|
46
48
|
HumanDecisionSubmission,
|
|
@@ -48,6 +50,7 @@ import type {
|
|
|
48
50
|
WorkflowDefinitionSnapshot,
|
|
49
51
|
WorkflowRunResult,
|
|
50
52
|
WorkflowRunState,
|
|
53
|
+
WorkflowSource,
|
|
51
54
|
WorkflowUpdateRecord,
|
|
52
55
|
} from "../workflows/types.js";
|
|
53
56
|
import {
|
|
@@ -194,19 +197,14 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
|
194
197
|
value !== null && typeof value === "object"
|
|
195
198
|
? (value as { schema?: unknown }).schema
|
|
196
199
|
: undefined;
|
|
197
|
-
if (
|
|
198
|
-
schema !== "pi-workflows.human-decision-request.v1" &&
|
|
199
|
-
schema !== "pi-workflows.human-decision-request.v2"
|
|
200
|
-
) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
200
|
+
if (schema !== "pi-workflows.human-decision-request.v1") return null;
|
|
203
201
|
return value as HumanDecisionRequest;
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
type PreparedLaunchOptions = {
|
|
207
205
|
presentation?: boolean;
|
|
208
206
|
parentRunId?: string;
|
|
209
|
-
humanDecision?:
|
|
207
|
+
humanDecision?: ResolvedHumanDecision;
|
|
210
208
|
};
|
|
211
209
|
|
|
212
210
|
type StartRunOptions = {
|
|
@@ -218,8 +216,8 @@ type StartRunOptions = {
|
|
|
218
216
|
signal?: AbortSignal;
|
|
219
217
|
/** Continue a checkpointed run: input becomes the legacy answer payload. */
|
|
220
218
|
parentRunId?: string;
|
|
221
|
-
/**
|
|
222
|
-
humanDecision?:
|
|
219
|
+
/** Durable human or timeout response for a protected decision continuation. */
|
|
220
|
+
humanDecision?: ResolvedHumanDecision;
|
|
223
221
|
/** Resume a parked run at its stopped node instead of starting fresh. */
|
|
224
222
|
resume?: boolean;
|
|
225
223
|
/** An existing queue claim token, when the caller already claimed the run. */
|
|
@@ -237,6 +235,29 @@ function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unkn
|
|
|
237
235
|
};
|
|
238
236
|
}
|
|
239
237
|
|
|
238
|
+
function continuationSourceChangedError(
|
|
239
|
+
runId: string,
|
|
240
|
+
previous: WorkflowSource,
|
|
241
|
+
current: WorkflowSource,
|
|
242
|
+
): Error {
|
|
243
|
+
if (
|
|
244
|
+
previous.kind === "builtin" &&
|
|
245
|
+
current.kind === "builtin" &&
|
|
246
|
+
previous.id === current.id &&
|
|
247
|
+
previous.revision !== current.revision
|
|
248
|
+
) {
|
|
249
|
+
return new BuiltinWorkflowRevisionChangedError({
|
|
250
|
+
runId,
|
|
251
|
+
workflowId: current.id,
|
|
252
|
+
previousRevision: previous.revision,
|
|
253
|
+
currentRevision: current.revision,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return new Error(
|
|
257
|
+
`Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
240
261
|
function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
|
|
241
262
|
return {
|
|
242
263
|
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
@@ -1210,8 +1231,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1210
1231
|
parent.state.workflowSource !== undefined &&
|
|
1211
1232
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
1212
1233
|
) {
|
|
1213
|
-
throw
|
|
1214
|
-
|
|
1234
|
+
throw continuationSourceChangedError(
|
|
1235
|
+
options.parentRunId,
|
|
1236
|
+
parent.state.workflowSource,
|
|
1237
|
+
workflowSource,
|
|
1215
1238
|
);
|
|
1216
1239
|
}
|
|
1217
1240
|
}
|
|
@@ -2061,7 +2084,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2061
2084
|
};
|
|
2062
2085
|
const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
|
|
2063
2086
|
const acceptance = await store.accept(request, submission);
|
|
2064
|
-
if (acceptance.status === "conflict") {
|
|
2087
|
+
if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
|
|
2065
2088
|
throw new Error("That human decision was already answered differently.");
|
|
2066
2089
|
}
|
|
2067
2090
|
accepted = acceptance.decision;
|
|
@@ -2093,6 +2116,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2093
2116
|
schema: "pi-workflows.human-decision-continuation.v1",
|
|
2094
2117
|
decisionId: request.decisionId,
|
|
2095
2118
|
requestDigest: request.requestDigest,
|
|
2119
|
+
provenance: accepted.provenance,
|
|
2096
2120
|
parentRunId: waiting.parentRunId,
|
|
2097
2121
|
runId: continuationRunId,
|
|
2098
2122
|
createdAt: accepted.acceptedAt,
|
|
@@ -2218,60 +2242,72 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2218
2242
|
queueRecord !== undefined &&
|
|
2219
2243
|
(queueRecord.originSessionId === null ||
|
|
2220
2244
|
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
2221
|
-
|
|
2222
|
-
if (
|
|
2245
|
+
let resolved = await store.readResolved(request.decisionId);
|
|
2246
|
+
if (resolved === null) {
|
|
2223
2247
|
let cancellation = await store.readCancellation(request.decisionId);
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2248
|
+
const expired =
|
|
2249
|
+
request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
|
|
2250
|
+
if (cancellation === null && expired) {
|
|
2251
|
+
if (request.defaultResponse === undefined) {
|
|
2252
|
+
await store.cancel(request, "expired");
|
|
2253
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
2254
|
+
} else {
|
|
2255
|
+
try {
|
|
2256
|
+
resolved = (await store.resolveTimeout(request)).decision;
|
|
2257
|
+
} catch {
|
|
2258
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
2259
|
+
resolved = await store.readResolved(request.decisionId);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2231
2262
|
}
|
|
2232
2263
|
if (cancellation !== null) {
|
|
2233
2264
|
await settleHumanDecisionChannels(cancellation);
|
|
2234
2265
|
continue;
|
|
2235
2266
|
}
|
|
2236
|
-
if (
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
lastWaitingRunId
|
|
2244
|
-
|
|
2267
|
+
if (resolved === null) {
|
|
2268
|
+
if (!deliverPending || !ownedBySession) continue;
|
|
2269
|
+
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
2270
|
+
for (const channelId of channels) {
|
|
2271
|
+
const channel = telegramDecisionChannels.get(channelId);
|
|
2272
|
+
if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
|
|
2273
|
+
}
|
|
2274
|
+
if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
|
|
2275
|
+
lastWaitingRunId = request.runId;
|
|
2276
|
+
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
2277
|
+
}
|
|
2278
|
+
continue;
|
|
2245
2279
|
}
|
|
2246
|
-
continue;
|
|
2247
2280
|
}
|
|
2248
2281
|
|
|
2249
2282
|
if (!ownedBySession) continue;
|
|
2283
|
+
const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
|
|
2284
|
+
if (currentParent === null || currentParent.state.status !== "waiting") continue;
|
|
2250
2285
|
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
2251
2286
|
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
2252
2287
|
schema: "pi-workflows.human-decision-continuation.v1" as const,
|
|
2253
2288
|
decisionId: request.decisionId,
|
|
2254
2289
|
requestDigest: request.requestDigest,
|
|
2290
|
+
provenance: resolved.provenance,
|
|
2255
2291
|
parentRunId: request.runId,
|
|
2256
2292
|
runId,
|
|
2257
|
-
createdAt:
|
|
2293
|
+
createdAt: resolved.acceptedAt,
|
|
2258
2294
|
};
|
|
2259
2295
|
await store.recordContinuation(request.decisionId, continuation);
|
|
2260
2296
|
const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
|
|
2261
2297
|
if (existing === null && activeRun === null) {
|
|
2262
|
-
if (
|
|
2298
|
+
if (currentParent.state.workflowSource === undefined) continue;
|
|
2263
2299
|
const workflowRef =
|
|
2264
|
-
|
|
2265
|
-
? `builtin:${
|
|
2266
|
-
:
|
|
2267
|
-
await startRun(ctx, workflowRef,
|
|
2300
|
+
currentParent.state.workflowSource.kind === "builtin"
|
|
2301
|
+
? `builtin:${currentParent.state.workflowSource.id}`
|
|
2302
|
+
: currentParent.state.workflowSource.path;
|
|
2303
|
+
await startRun(ctx, workflowRef, currentParent.state.input, {
|
|
2268
2304
|
parentRunId: request.runId,
|
|
2269
|
-
humanDecision:
|
|
2305
|
+
humanDecision: resolved,
|
|
2270
2306
|
runId: continuation.runId,
|
|
2271
2307
|
quiet: true,
|
|
2272
2308
|
});
|
|
2273
2309
|
}
|
|
2274
|
-
await settleHumanDecisionChannels(
|
|
2310
|
+
await settleHumanDecisionChannels(resolved);
|
|
2275
2311
|
if (activeRun !== null) break;
|
|
2276
2312
|
}
|
|
2277
2313
|
};
|
|
@@ -2352,8 +2388,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2352
2388
|
parent.state.workflowSource !== undefined &&
|
|
2353
2389
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
2354
2390
|
) {
|
|
2355
|
-
throw
|
|
2356
|
-
|
|
2391
|
+
throw continuationSourceChangedError(
|
|
2392
|
+
options.parentRunId,
|
|
2393
|
+
parent.state.workflowSource,
|
|
2394
|
+
workflowSource,
|
|
2357
2395
|
);
|
|
2358
2396
|
}
|
|
2359
2397
|
}
|
|
@@ -9,7 +9,7 @@ export type PublicAssistantMessageEvent =
|
|
|
9
9
|
| { type: "toolcall_start"; contentIndex: number; partial: unknown }
|
|
10
10
|
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
11
11
|
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
|
|
12
|
-
| { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
|
|
12
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
|
|
13
13
|
| { type: "error"; reason: "aborted" | "error"; error: unknown };
|
|
14
14
|
|
|
15
15
|
export type TurnStartEventLike = { turnIndex: number };
|
|
@@ -44,7 +44,7 @@ export type NormalizedAssistantEvent =
|
|
|
44
44
|
| { type: "toolcall_start"; contentIndex: number }
|
|
45
45
|
| { type: "toolcall_delta"; contentIndex: number; delta: string }
|
|
46
46
|
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
|
|
47
|
-
| { type: "done"; reason: "stop" | "length" | "toolUse" }
|
|
47
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
|
|
48
48
|
| { type: "error"; reason: "aborted" | "error" };
|
|
49
49
|
|
|
50
50
|
/** Remove cumulative snapshots while preserving every semantic stream event. */
|
package/src/extension/widget.ts
CHANGED
|
@@ -162,9 +162,11 @@ function progressLines(
|
|
|
162
162
|
: progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
|
|
163
163
|
const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
|
|
164
164
|
const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
|
|
165
|
-
const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const lines = prioritizeWidgetProgress(prioritizeProgressEstimates(estimates)).map((estimate) => {
|
|
166
|
+
const indentation =
|
|
167
|
+
estimate.key.startsWith("agents/") && estimate.key.split("/").length > 2 ? " " : "";
|
|
168
|
+
return `${indentation}${formatProgressLine(estimate, now)}`;
|
|
169
|
+
});
|
|
168
170
|
const schedule = (state.updates ?? []).find(
|
|
169
171
|
(record) => record.type === "monitor.schedule" && record.key === "next-check",
|
|
170
172
|
);
|
|
@@ -180,6 +182,24 @@ function progressLines(
|
|
|
180
182
|
return lines;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function prioritizeWidgetProgress(estimates: ProgressEstimate[]): ProgressEstimate[] {
|
|
186
|
+
const weight = (estimate: ProgressEstimate) => {
|
|
187
|
+
if (!estimate.key.startsWith("agents/")) return 0;
|
|
188
|
+
const child = estimate.key.split("/").length > 2;
|
|
189
|
+
if (!child) return -4;
|
|
190
|
+
if (estimate.data.status === "failed" || estimate.data.status === "blocked") return -3;
|
|
191
|
+
if (
|
|
192
|
+
estimate.data.status === "running" ||
|
|
193
|
+
estimate.data.status === "pending" ||
|
|
194
|
+
estimate.data.status === "waiting"
|
|
195
|
+
) {
|
|
196
|
+
return -2;
|
|
197
|
+
}
|
|
198
|
+
return 2;
|
|
199
|
+
};
|
|
200
|
+
return [...estimates].sort((left, right) => weight(left) - weight(right));
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
|
|
184
204
|
const output = state.outputs.estimate;
|
|
185
205
|
if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
|
|
@@ -309,8 +329,7 @@ function nodeRuntimeSegments(
|
|
|
309
329
|
})
|
|
310
330
|
: undefined;
|
|
311
331
|
const requestAudience =
|
|
312
|
-
|
|
313
|
-
request?.schema === "pi-workflows.human-decision-request.v2") &&
|
|
332
|
+
request?.schema === "pi-workflows.human-decision-request.v1" &&
|
|
314
333
|
typeof request.audience === "string"
|
|
315
334
|
? request.audience
|
|
316
335
|
: human.audience;
|
|
@@ -313,8 +313,7 @@ function renderCellText(
|
|
|
313
313
|
? (state.finalOutput as { schema?: unknown }).schema
|
|
314
314
|
: undefined;
|
|
315
315
|
const waitingRequest =
|
|
316
|
-
waitingSchema === "pi-workflows.human-decision-request.v1"
|
|
317
|
-
waitingSchema === "pi-workflows.human-decision-request.v2"
|
|
316
|
+
waitingSchema === "pi-workflows.human-decision-request.v1"
|
|
318
317
|
? (state.finalOutput as {
|
|
319
318
|
audience?: unknown;
|
|
320
319
|
presentationDigest?: unknown;
|
package/src/viewer/render.ts
CHANGED
|
@@ -147,13 +147,13 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
|
|
|
147
147
|
suffix = ansi.yellow(
|
|
148
148
|
human === undefined
|
|
149
149
|
? " waiting"
|
|
150
|
-
: ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request
|
|
150
|
+
: ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request === null ? "" : ` · ${sanitizeText(request.presentation.summary)}`} · ${Object.values(
|
|
151
151
|
human.choices,
|
|
152
152
|
)
|
|
153
153
|
.map((choice) => sanitizeText(choice.label))
|
|
154
154
|
.join(
|
|
155
155
|
" / ",
|
|
156
|
-
)}${request
|
|
156
|
+
)}${request === null ? "" : ` · ${request.presentationDigest.slice(7, 19)}`}`,
|
|
157
157
|
);
|
|
158
158
|
} else if (result) {
|
|
159
159
|
glyph = result.outcome === "ok" ? ansi.green("✓") : ansi.red("✗");
|
|
@@ -210,8 +210,7 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
|
|
|
210
210
|
function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
211
211
|
if (value === null || typeof value !== "object") return null;
|
|
212
212
|
const schema = (value as { schema?: unknown }).schema;
|
|
213
|
-
return schema === "pi-workflows.human-decision-request.v1"
|
|
214
|
-
schema === "pi-workflows.human-decision-request.v2"
|
|
213
|
+
return schema === "pi-workflows.human-decision-request.v1"
|
|
215
214
|
? (value as HumanDecisionRequest)
|
|
216
215
|
: null;
|
|
217
216
|
}
|
|
@@ -271,12 +270,14 @@ export function renderRunDetailLines(
|
|
|
271
270
|
lines.push("");
|
|
272
271
|
lines.push(ansi.bold("progress"));
|
|
273
272
|
for (const track of progress) {
|
|
274
|
-
|
|
273
|
+
const depth = track.key.startsWith("agents/") ? track.key.split("/").length - 2 : 0;
|
|
274
|
+
const indentation = " ".repeat(Math.max(0, depth));
|
|
275
|
+
lines.push(fitWidth(`${indentation}${formatProgressLine(track.estimate, now)}`, size.width));
|
|
275
276
|
const latest = track.samples.at(-1);
|
|
276
277
|
lines.push(
|
|
277
278
|
fitWidth(
|
|
278
279
|
ansi.dim(
|
|
279
|
-
|
|
280
|
+
`${indentation} ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
|
|
280
281
|
),
|
|
281
282
|
size.width,
|
|
282
283
|
),
|
package/src/workflows/catalog.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isWorkflowDefinition } from "./definition.js";
|
|
2
|
-
import {
|
|
2
|
+
import { BuiltinWorkflowRevisionChangedError } from "./errors.js";
|
|
3
3
|
import type { WorkflowDefinition, WorkflowSource } from "./types.js";
|
|
4
4
|
|
|
5
5
|
const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -102,7 +102,12 @@ export class BuiltinWorkflowCatalog {
|
|
|
102
102
|
throw new Error(`Unknown built-in workflow: ${source.id}`);
|
|
103
103
|
}
|
|
104
104
|
if (entry.revision !== source.revision) {
|
|
105
|
-
throw new
|
|
105
|
+
throw new BuiltinWorkflowRevisionChangedError({
|
|
106
|
+
runId,
|
|
107
|
+
workflowId: entry.id,
|
|
108
|
+
previousRevision: source.revision,
|
|
109
|
+
currentRevision: entry.revision,
|
|
110
|
+
});
|
|
106
111
|
}
|
|
107
112
|
return entry.definition;
|
|
108
113
|
}
|
|
@@ -480,6 +480,7 @@ function wrapNode(
|
|
|
480
480
|
case "checkpoint": {
|
|
481
481
|
const human = node.humanDecision;
|
|
482
482
|
const audience = human?.audience;
|
|
483
|
+
const onTimeout = human?.onTimeout;
|
|
483
484
|
return {
|
|
484
485
|
...common,
|
|
485
486
|
nodeType: "checkpoint",
|
|
@@ -496,6 +497,14 @@ function wrapNode(
|
|
|
496
497
|
: audience!,
|
|
497
498
|
choices: human.choices,
|
|
498
499
|
request: (context: WorkflowNodeContext) => human.request(project(context)),
|
|
500
|
+
...(onTimeout !== undefined
|
|
501
|
+
? {
|
|
502
|
+
onTimeout:
|
|
503
|
+
typeof onTimeout === "function"
|
|
504
|
+
? (context: WorkflowNodeContext) => onTimeout(project(context))
|
|
505
|
+
: onTimeout,
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
499
508
|
},
|
|
500
509
|
}
|
|
501
510
|
: {}),
|
|
@@ -56,7 +56,18 @@ export function decisionPresentationDigest(presentation: DecisionPresentation):
|
|
|
56
56
|
export function validateHumanDecisionRequestIntegrity(
|
|
57
57
|
request: HumanDecisionRequest,
|
|
58
58
|
): HumanDecisionRequest {
|
|
59
|
-
if (
|
|
59
|
+
if (
|
|
60
|
+
request.schema !== "pi-workflows.human-decision-request.v1" ||
|
|
61
|
+
!Object.hasOwn(request, "subject") ||
|
|
62
|
+
!Object.hasOwn(request, "presentation") ||
|
|
63
|
+
!Object.hasOwn(request, "subjectDigest") ||
|
|
64
|
+
!Object.hasOwn(request, "presentationDigest") ||
|
|
65
|
+
!Object.hasOwn(request, "revision")
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
60
71
|
const presentation = normalizeDecisionPresentation(request.presentation);
|
|
61
72
|
const subjectDigest = digestCanonical(request.subject);
|
|
62
73
|
const presentationDigest = digestCanonical(presentation);
|
|
@@ -70,6 +81,7 @@ export function validateHumanDecisionRequestIntegrity(
|
|
|
70
81
|
title: request.title,
|
|
71
82
|
choices: request.choices,
|
|
72
83
|
...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
|
|
84
|
+
...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
|
|
73
85
|
subject: request.subject,
|
|
74
86
|
presentation,
|
|
75
87
|
revision: request.revision,
|
|
@@ -90,10 +102,7 @@ export function humanDecisionChannelRequest(
|
|
|
90
102
|
request: HumanDecisionRequest,
|
|
91
103
|
): HumanDecisionChannelRequest {
|
|
92
104
|
validateHumanDecisionRequestIntegrity(request);
|
|
93
|
-
const presentation =
|
|
94
|
-
request.schema === "pi-workflows.human-decision-request.v2"
|
|
95
|
-
? normalizeDecisionPresentation(request.presentation)
|
|
96
|
-
: legacyDecisionPresentation(request.body);
|
|
105
|
+
const presentation = normalizeDecisionPresentation(request.presentation);
|
|
97
106
|
const presentationDigest = decisionPresentationDigest(presentation);
|
|
98
107
|
return {
|
|
99
108
|
schema: "pi-workflows.human-decision-channel-request.v1",
|
|
@@ -108,25 +117,26 @@ export function humanDecisionChannelRequest(
|
|
|
108
117
|
title: request.title,
|
|
109
118
|
presentation,
|
|
110
119
|
presentationDigest,
|
|
111
|
-
revision: request.
|
|
120
|
+
revision: request.revision,
|
|
112
121
|
choices: request.choices,
|
|
113
122
|
createdAt: request.createdAt,
|
|
114
123
|
...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
|
|
124
|
+
...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
|
|
115
125
|
};
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
export function
|
|
128
|
+
export function valueDecisionPresentation(value: unknown): DecisionPresentation {
|
|
119
129
|
const blocks: DecisionPresentationBlock[] = [];
|
|
120
|
-
if (typeof
|
|
121
|
-
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
appendValueText(blocks, value, "Decision details");
|
|
122
132
|
} else {
|
|
123
|
-
|
|
133
|
+
appendValue(blocks, value, undefined);
|
|
124
134
|
}
|
|
125
135
|
const summary =
|
|
126
|
-
typeof
|
|
136
|
+
typeof value === "string"
|
|
127
137
|
? "Review the decision message below."
|
|
128
138
|
: "Review the decision details below.";
|
|
129
|
-
return normalizeDecisionPresentation(
|
|
139
|
+
return normalizeDecisionPresentation(boundValuePresentation(summary, blocks, value));
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
export function decisionDocumentSegments(
|
|
@@ -150,6 +160,13 @@ export function decisionDocumentSegments(
|
|
|
150
160
|
segments.push({ kind: block.kind, text: block.text });
|
|
151
161
|
}
|
|
152
162
|
}
|
|
163
|
+
if (request.expiresAt !== undefined && request.defaultResponse !== undefined) {
|
|
164
|
+
const selected = request.choices[request.defaultResponse.choice];
|
|
165
|
+
segments.push({
|
|
166
|
+
kind: "paragraph",
|
|
167
|
+
text: `If no answer is accepted by ${request.expiresAt}, this decision continues automatically with: ${selected?.label ?? request.defaultResponse.choice}.`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
153
170
|
const choices = Object.values(request.choices).map((choice) =>
|
|
154
171
|
choice.input === undefined
|
|
155
172
|
? `• ${choice.label}`
|
|
@@ -251,10 +268,10 @@ function normalizeString(value: unknown, label: string): string {
|
|
|
251
268
|
return text;
|
|
252
269
|
}
|
|
253
270
|
|
|
254
|
-
function
|
|
271
|
+
function boundValuePresentation(
|
|
255
272
|
summary: string,
|
|
256
273
|
blocks: DecisionPresentationBlock[],
|
|
257
|
-
|
|
274
|
+
value: unknown,
|
|
258
275
|
): DecisionPresentation {
|
|
259
276
|
const candidate: DecisionPresentation = {
|
|
260
277
|
schema: "pi-workflows.decision-presentation.v1",
|
|
@@ -263,13 +280,13 @@ function boundLegacyPresentation(
|
|
|
263
280
|
};
|
|
264
281
|
if (
|
|
265
282
|
blocks.length <= MAX_PRESENTATION_BLOCKS &&
|
|
266
|
-
blocks.every(
|
|
283
|
+
blocks.every(valueBlockFitsSchema) &&
|
|
267
284
|
presentationCodeUnits(candidate) <= MAX_PRESENTATION_CODE_UNITS
|
|
268
285
|
) {
|
|
269
286
|
return candidate;
|
|
270
287
|
}
|
|
271
|
-
const originalCodeUnits = canonicalJson(
|
|
272
|
-
const omission = `Some
|
|
288
|
+
const originalCodeUnits = canonicalJson(value).length;
|
|
289
|
+
const omission = `Some decision content is not shown because it exceeds the readable presentation limit. Inspect the canonical source value before answering. Value digest: ${digestCanonical(value)}. Original value: ${originalCodeUnits} UTF-16 code units. Generated blocks: ${blocks.length}.`;
|
|
273
290
|
const budget = MAX_PRESENTATION_CODE_UNITS - summary.length - omission.length;
|
|
274
291
|
const kept: DecisionPresentationBlock[] = [];
|
|
275
292
|
let used = 0;
|
|
@@ -278,7 +295,7 @@ function boundLegacyPresentation(
|
|
|
278
295
|
const units = presentationBlockCodeUnits(block);
|
|
279
296
|
if (
|
|
280
297
|
kept.length >= MAX_PRESENTATION_BLOCKS ||
|
|
281
|
-
!
|
|
298
|
+
!valueBlockFitsSchema(block) ||
|
|
282
299
|
used + units > budget
|
|
283
300
|
) {
|
|
284
301
|
omitted = true;
|
|
@@ -298,7 +315,7 @@ function boundLegacyPresentation(
|
|
|
298
315
|
};
|
|
299
316
|
}
|
|
300
317
|
|
|
301
|
-
function
|
|
318
|
+
function valueBlockFitsSchema(block: DecisionPresentationBlock): boolean {
|
|
302
319
|
if (block.kind === "paragraph" || block.kind === "preformatted") {
|
|
303
320
|
return block.text.length <= MAX_PRESENTATION_STRING_CODE_UNITS;
|
|
304
321
|
}
|
|
@@ -332,47 +349,43 @@ function presentationCodeUnits(presentation: DecisionPresentation): number {
|
|
|
332
349
|
return total;
|
|
333
350
|
}
|
|
334
351
|
|
|
335
|
-
function
|
|
352
|
+
function appendValue(
|
|
336
353
|
blocks: DecisionPresentationBlock[],
|
|
337
354
|
value: unknown,
|
|
338
355
|
label: string | undefined,
|
|
339
356
|
): void {
|
|
340
357
|
if (isScalar(value)) {
|
|
341
|
-
const scalar =
|
|
358
|
+
const scalar = valueScalar(value);
|
|
342
359
|
if (label === undefined || scalar.length > MAX_PRESENTATION_STRING_CODE_UNITS) {
|
|
343
|
-
|
|
344
|
-
blocks,
|
|
345
|
-
scalar,
|
|
346
|
-
label === undefined ? "Decision details" : legacyLabel(label),
|
|
347
|
-
);
|
|
360
|
+
appendValueText(blocks, scalar, label === undefined ? "Decision details" : valueLabel(label));
|
|
348
361
|
} else {
|
|
349
|
-
blocks.push({ kind: "fields", items: [{ label:
|
|
362
|
+
blocks.push({ kind: "fields", items: [{ label: valueLabel(label), value: scalar }] });
|
|
350
363
|
}
|
|
351
364
|
return;
|
|
352
365
|
}
|
|
353
366
|
if (Array.isArray(value)) {
|
|
354
|
-
if (label !== undefined) blocks.push({ kind: "section", title:
|
|
367
|
+
if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
|
|
355
368
|
if (value.length === 0) {
|
|
356
369
|
blocks.push({ kind: "paragraph", text: "None" });
|
|
357
370
|
return;
|
|
358
371
|
}
|
|
359
372
|
if (
|
|
360
373
|
value.every(isScalar) &&
|
|
361
|
-
value.every((item) =>
|
|
374
|
+
value.every((item) => valueScalar(item).length <= MAX_PRESENTATION_STRING_CODE_UNITS)
|
|
362
375
|
) {
|
|
363
|
-
const items = value.map(
|
|
376
|
+
const items = value.map(valueScalar);
|
|
364
377
|
for (let index = 0; index < items.length; index += MAX_PRESENTATION_ITEMS) {
|
|
365
378
|
blocks.push({ kind: "bullets", items: items.slice(index, index + MAX_PRESENTATION_ITEMS) });
|
|
366
379
|
}
|
|
367
380
|
return;
|
|
368
381
|
}
|
|
369
|
-
value.forEach((item, index) =>
|
|
382
|
+
value.forEach((item, index) => appendValue(blocks, item, `Item ${index + 1}`));
|
|
370
383
|
return;
|
|
371
384
|
}
|
|
372
385
|
const entries = Object.entries(value as Record<string, unknown>).sort(([left], [right]) =>
|
|
373
386
|
compareKeys(left, right),
|
|
374
387
|
);
|
|
375
|
-
if (label !== undefined) blocks.push({ kind: "section", title:
|
|
388
|
+
if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
|
|
376
389
|
if (entries.length === 0) {
|
|
377
390
|
blocks.push({ kind: "paragraph", text: "None" });
|
|
378
391
|
return;
|
|
@@ -380,34 +393,34 @@ function appendLegacyValue(
|
|
|
380
393
|
const fields = entries
|
|
381
394
|
.filter(
|
|
382
395
|
([, child]) =>
|
|
383
|
-
isScalar(child) &&
|
|
396
|
+
isScalar(child) && valueScalar(child).length <= MAX_PRESENTATION_STRING_CODE_UNITS,
|
|
384
397
|
)
|
|
385
|
-
.map(([key, child]) => ({ label:
|
|
398
|
+
.map(([key, child]) => ({ label: valueLabel(key), value: valueScalar(child) }));
|
|
386
399
|
for (let index = 0; index < fields.length; index += MAX_PRESENTATION_ITEMS) {
|
|
387
400
|
blocks.push({ kind: "fields", items: fields.slice(index, index + MAX_PRESENTATION_ITEMS) });
|
|
388
401
|
}
|
|
389
402
|
for (const [key, child] of entries.filter(
|
|
390
403
|
([, child]) =>
|
|
391
|
-
!isScalar(child) ||
|
|
404
|
+
!isScalar(child) || valueScalar(child).length > MAX_PRESENTATION_STRING_CODE_UNITS,
|
|
392
405
|
)) {
|
|
393
|
-
|
|
406
|
+
appendValue(blocks, child, key);
|
|
394
407
|
}
|
|
395
408
|
}
|
|
396
409
|
|
|
397
|
-
function
|
|
410
|
+
function appendValueText(
|
|
398
411
|
blocks: DecisionPresentationBlock[],
|
|
399
412
|
value: string,
|
|
400
413
|
section: string,
|
|
401
414
|
): void {
|
|
402
|
-
const text =
|
|
415
|
+
const text = sanitizeValueText(value);
|
|
403
416
|
blocks.push({ kind: "section", title: section });
|
|
404
417
|
for (const part of splitByCodeUnits(text, MAX_PRESENTATION_STRING_CODE_UNITS)) {
|
|
405
418
|
blocks.push({ kind: "paragraph", text: part.trim().length === 0 ? "None" : part });
|
|
406
419
|
}
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
function
|
|
410
|
-
const sanitized =
|
|
422
|
+
function valueLabel(value: string): string {
|
|
423
|
+
const sanitized = sanitizeValueText(value)
|
|
411
424
|
.replaceAll(/[_-]+/gu, " ")
|
|
412
425
|
.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2")
|
|
413
426
|
.replaceAll(/\s+/gu, " ")
|
|
@@ -416,13 +429,13 @@ function legacyLabel(value: string): string {
|
|
|
416
429
|
return `${sanitized[0]!.toUpperCase()}${sanitized.slice(1)}`;
|
|
417
430
|
}
|
|
418
431
|
|
|
419
|
-
function
|
|
432
|
+
function valueScalar(value: unknown): string {
|
|
420
433
|
if (value === null) return "None";
|
|
421
434
|
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
422
|
-
return
|
|
435
|
+
return sanitizeValueText(String(value));
|
|
423
436
|
}
|
|
424
437
|
|
|
425
|
-
function
|
|
438
|
+
function sanitizeValueText(value: string): string {
|
|
426
439
|
return value
|
|
427
440
|
.replaceAll("\r\n", "\n")
|
|
428
441
|
.replaceAll("\r", "\n")
|