@osolmaz/pi-workflows 0.12.0 → 0.13.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.
- package/README.md +56 -31
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +797 -142
- package/dist/builtins/autoimplement.workflow.js +17 -105
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -2
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -23
- package/dist/builtins/monitor.workflow.js +508 -329
- 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/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.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 +449 -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 +50 -6
- package/dist/builtins/sanity-check.workflow.js +150 -28
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +4 -2
- package/dist/extension/decision-channels.js +333 -146
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +209 -127
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +29 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +15 -10
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +9 -23
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +20 -2
- 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/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +7 -7
- package/dist/workflows/engine.js +196 -115
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +16 -3
- package/dist/workflows/errors.js +18 -3
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +45 -8
- package/dist/workflows/human-decision.js +641 -274
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +5 -6
- package/dist/workflows/index.js +4 -5
- 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 +27 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +907 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +88 -126
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +36 -47
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +179 -237
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -26
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +127 -53
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/plugins/herdr/viewer.mjs +1 -11
- 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/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -135
- package/skills/pi-workflows/SKILL.md +9 -8
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/index.ts +18 -1
- package/src/builtins/monitor.workflow.ts +673 -382
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plain-summary.workflow.ts +185 -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 +248 -48
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +456 -203
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +251 -151
- package/src/extension/recorder.ts +13 -22
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +32 -5
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +12 -6
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +19 -30
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/composition.ts +21 -3
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +228 -130
- package/src/workflows/errors.ts +27 -3
- package/src/workflows/human-decision.ts +921 -349
- package/src/workflows/index.ts +20 -30
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +36 -1
- package/src/workflows/store.ts +1300 -1163
- package/src/workflows/types.ts +100 -137
- 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/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- 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/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -76,19 +76,12 @@ function failureMessage(error: unknown): string {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Records
|
|
80
|
-
* bundle. The hot event path only stamps and queues records; filesystem work
|
|
81
|
-
* runs on a separate ordered append chain.
|
|
79
|
+
* Records settled Pi entries and normalized temporal events in SQLite.
|
|
82
80
|
*/
|
|
83
81
|
export class SessionRecorder {
|
|
84
82
|
private readonly store: WorkflowRunStore;
|
|
85
|
-
private readonly runDir: string;
|
|
86
83
|
private readonly runId: string;
|
|
87
|
-
/**
|
|
88
|
-
* Capture segment for this recorder. Undefined writes the legacy flat
|
|
89
|
-
* stream; a second recorder attaching to the same bundle (resume or
|
|
90
|
-
* handoff) writes a segment instead so the first capture stays intact.
|
|
91
|
-
*/
|
|
84
|
+
/** Capture segment for this recorder after an ownership handoff. */
|
|
92
85
|
private segmentId: string | undefined;
|
|
93
86
|
private cursor: string | null = null;
|
|
94
87
|
private readonly recorded: string[] = [];
|
|
@@ -122,9 +115,8 @@ export class SessionRecorder {
|
|
|
122
115
|
private flushPromise: Promise<void> | null = null;
|
|
123
116
|
private captureFailure: WorkflowSessionCaptureFailure | null = null;
|
|
124
117
|
|
|
125
|
-
constructor(store: WorkflowRunStore,
|
|
118
|
+
constructor(store: WorkflowRunStore, runId: string) {
|
|
126
119
|
this.store = store;
|
|
127
|
-
this.runDir = runDir;
|
|
128
120
|
this.runId = runId;
|
|
129
121
|
}
|
|
130
122
|
|
|
@@ -135,13 +127,12 @@ export class SessionRecorder {
|
|
|
135
127
|
this.bound = true;
|
|
136
128
|
this.cursor = ctx.sessionManager.getLeafId();
|
|
137
129
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
if (await this.store.hasSessionBinding(this.runDir)) {
|
|
130
|
+
// An existing binding belongs to an earlier capture segment.
|
|
131
|
+
if (await this.store.hasSessionBinding(this.runId)) {
|
|
141
132
|
this.segmentId = randomUUID();
|
|
142
133
|
}
|
|
143
134
|
await this.store.writeSessionBinding(
|
|
144
|
-
this.
|
|
135
|
+
this.runId,
|
|
145
136
|
{
|
|
146
137
|
schema: SESSION_BINDING_SCHEMA,
|
|
147
138
|
runId: this.runId,
|
|
@@ -153,7 +144,7 @@ export class SessionRecorder {
|
|
|
153
144
|
this.segmentId,
|
|
154
145
|
);
|
|
155
146
|
await this.store.writeSessionCapture(
|
|
156
|
-
this.
|
|
147
|
+
this.runId,
|
|
157
148
|
{
|
|
158
149
|
schema: SESSION_CAPTURE_SCHEMA,
|
|
159
150
|
eventSchema: SESSION_EVENT_SCHEMA,
|
|
@@ -344,7 +335,7 @@ export class SessionRecorder {
|
|
|
344
335
|
}
|
|
345
336
|
const appended: RecordedEntry[] = [];
|
|
346
337
|
for (const entry of branch.slice(startIndex)) {
|
|
347
|
-
await this.store.appendSessionEntry(this.
|
|
338
|
+
await this.store.appendSessionEntry(this.runId, entry, this.segmentId);
|
|
348
339
|
const recorded = { id: entry.id, entry, claimed: false };
|
|
349
340
|
appended.push(recorded);
|
|
350
341
|
this.unclaimedEntries.push(recorded);
|
|
@@ -426,9 +417,9 @@ export class SessionRecorder {
|
|
|
426
417
|
try {
|
|
427
418
|
await this.flushAllEvents();
|
|
428
419
|
await this.entryChain;
|
|
429
|
-
const counts = await this.store.sessionCounts(this.
|
|
420
|
+
const counts = await this.store.sessionCounts(this.runId, this.segmentId);
|
|
430
421
|
await this.store.writeSessionCapture(
|
|
431
|
-
this.
|
|
422
|
+
this.runId,
|
|
432
423
|
{
|
|
433
424
|
schema: SESSION_CAPTURE_SCHEMA,
|
|
434
425
|
eventSchema: SESSION_EVENT_SCHEMA,
|
|
@@ -443,9 +434,9 @@ export class SessionRecorder {
|
|
|
443
434
|
// the workflow's terminal persistence hook.
|
|
444
435
|
this.failCapture("capture_finalize_failed", failureMessage(error));
|
|
445
436
|
try {
|
|
446
|
-
const counts = await this.store.sessionCounts(this.
|
|
437
|
+
const counts = await this.store.sessionCounts(this.runId, this.segmentId);
|
|
447
438
|
await this.store.writeSessionCapture(
|
|
448
|
-
this.
|
|
439
|
+
this.runId,
|
|
449
440
|
{
|
|
450
441
|
schema: SESSION_CAPTURE_SCHEMA,
|
|
451
442
|
eventSchema: SESSION_EVENT_SCHEMA,
|
|
@@ -602,7 +593,7 @@ export class SessionRecorder {
|
|
|
602
593
|
}
|
|
603
594
|
this.flushPromise = this.store
|
|
604
595
|
.appendSessionEventBatch(
|
|
605
|
-
this.
|
|
596
|
+
this.runId,
|
|
606
597
|
batch.map((queued) => queued.record),
|
|
607
598
|
this.segmentId,
|
|
608
599
|
)
|
|
@@ -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. */
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Box, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
3
4
|
import { sanitizeText } from "../workflows/text.js";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
5
|
+
import type {
|
|
6
|
+
AgentStepContract,
|
|
7
|
+
AgentStepPresentation,
|
|
8
|
+
AgentStepSubmission,
|
|
9
|
+
} from "../workflows/types.js";
|
|
10
|
+
import { visibleAssistantText, type PromptDeliveryKind } from "./executor.js";
|
|
6
11
|
|
|
7
12
|
export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE = "pi-workflows-agent-step";
|
|
8
13
|
export const WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA = "pi-workflows.agent-step-message.v1";
|
|
@@ -69,6 +74,10 @@ export function buildWorkflowAgentStepView(
|
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
const assistant = contract?.completion === "assistant";
|
|
78
|
+
const expectedOutput = assistant
|
|
79
|
+
? `normal assistant response${contract?.maxOutputChars === undefined ? "" : ` (maximum ${contract.maxOutputChars} characters)`}`
|
|
80
|
+
: (contract?.expectedOutput ?? "a JSON object with your result");
|
|
72
81
|
const metadata = [
|
|
73
82
|
`Workflow: ${workflowName}`,
|
|
74
83
|
...(runTitle !== undefined ? [`Run title: ${runTitle}`] : []),
|
|
@@ -76,7 +85,8 @@ export function buildWorkflowAgentStepView(
|
|
|
76
85
|
`Node id: ${nodeId}`,
|
|
77
86
|
`Attempt id: ${cleanSingleLine(contract?.attemptId ?? "unknown")}`,
|
|
78
87
|
`Delivery: ${kind}`,
|
|
79
|
-
`
|
|
88
|
+
`Completion: ${assistant ? "assistant response" : "workflow submission"}`,
|
|
89
|
+
`Expected output: ${cleanSingleLine(expectedOutput)}`,
|
|
80
90
|
];
|
|
81
91
|
const prompt = contentText(message.content);
|
|
82
92
|
return {
|
|
@@ -86,6 +96,93 @@ export function buildWorkflowAgentStepView(
|
|
|
86
96
|
};
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Adopt an already visible response for the same assistant-output attempt.
|
|
101
|
+
* This is used only when an interrupted run resumes in its origin session.
|
|
102
|
+
*/
|
|
103
|
+
export function recoverAssistantStep(
|
|
104
|
+
entries: readonly unknown[],
|
|
105
|
+
contract: AgentStepContract,
|
|
106
|
+
): AgentStepSubmission | undefined {
|
|
107
|
+
if (contract.completion !== "assistant") return undefined;
|
|
108
|
+
for (let promptIndex = entries.length - 1; promptIndex >= 0; promptIndex -= 1) {
|
|
109
|
+
const prompt = sessionEntry(entries[promptIndex]);
|
|
110
|
+
if (
|
|
111
|
+
prompt?.type !== "custom_message" ||
|
|
112
|
+
prompt.customType !== WORKFLOW_AGENT_STEP_MESSAGE_TYPE
|
|
113
|
+
) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const details = parseDetails(prompt.details);
|
|
117
|
+
if (details === undefined || !sameAttempt(details.contract, contract)) continue;
|
|
118
|
+
|
|
119
|
+
let response: Record<string, unknown> | undefined;
|
|
120
|
+
for (let responseIndex = promptIndex + 1; responseIndex < entries.length; responseIndex += 1) {
|
|
121
|
+
const candidate = sessionEntry(entries[responseIndex]);
|
|
122
|
+
if (candidate === undefined) continue;
|
|
123
|
+
if (
|
|
124
|
+
candidate.type === "custom_message" &&
|
|
125
|
+
candidate.customType === WORKFLOW_AGENT_STEP_MESSAGE_TYPE
|
|
126
|
+
) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (candidate.type !== "message") continue;
|
|
130
|
+
const message = sessionEntry(candidate.message);
|
|
131
|
+
if (message?.role === "user") break;
|
|
132
|
+
if (isAssistantWithVisibleText(message)) response = candidate;
|
|
133
|
+
}
|
|
134
|
+
if (response === undefined) return undefined;
|
|
135
|
+
const output = visibleAssistantText(response.message, contract.maxOutputChars);
|
|
136
|
+
const entryId = typeof response.id === "string" ? response.id : undefined;
|
|
137
|
+
const promptId = typeof prompt.id === "string" ? prompt.id : undefined;
|
|
138
|
+
return {
|
|
139
|
+
output,
|
|
140
|
+
assistantMessage: {
|
|
141
|
+
sha256: createHash("sha256").update(output).digest("hex"),
|
|
142
|
+
...(entryId !== undefined ? { entryId } : {}),
|
|
143
|
+
...(contract.maxOutputChars !== undefined ? { maxChars: contract.maxOutputChars } : {}),
|
|
144
|
+
recovered: true,
|
|
145
|
+
},
|
|
146
|
+
...(promptId !== undefined && entryId !== undefined
|
|
147
|
+
? { conversation: { firstEntryId: promptId, lastEntryId: entryId } }
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sameAttempt(left: AgentStepContract, right: AgentStepContract): boolean {
|
|
155
|
+
return (
|
|
156
|
+
left.runId === right.runId &&
|
|
157
|
+
left.workflowName === right.workflowName &&
|
|
158
|
+
left.nodeId === right.nodeId &&
|
|
159
|
+
left.attemptId === right.attemptId &&
|
|
160
|
+
left.completion === "assistant" &&
|
|
161
|
+
left.maxOutputChars === right.maxOutputChars
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sessionEntry(value: unknown): Record<string, unknown> | undefined {
|
|
166
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
167
|
+
? (value as Record<string, unknown>)
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isAssistantWithVisibleText(message: unknown): boolean {
|
|
172
|
+
if (message === null || typeof message !== "object") return false;
|
|
173
|
+
const candidate = message as { role?: unknown; content?: unknown; stopReason?: unknown };
|
|
174
|
+
if (candidate.role !== "assistant" || !Array.isArray(candidate.content)) return false;
|
|
175
|
+
if (candidate.stopReason !== "stop" && candidate.stopReason !== "length") return false;
|
|
176
|
+
return candidate.content.some(
|
|
177
|
+
(part) =>
|
|
178
|
+
part !== null &&
|
|
179
|
+
typeof part === "object" &&
|
|
180
|
+
(part as { type?: unknown }).type === "text" &&
|
|
181
|
+
typeof (part as { text?: unknown }).text === "string" &&
|
|
182
|
+
(part as { text: string }).text.trim().length > 0,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
89
186
|
function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefined {
|
|
90
187
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
91
188
|
const candidate = value as Partial<WorkflowAgentStepMessageDetails>;
|
|
@@ -100,7 +197,12 @@ function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefin
|
|
|
100
197
|
typeof contract.runId !== "string" ||
|
|
101
198
|
typeof contract.workflowName !== "string" ||
|
|
102
199
|
typeof contract.nodeId !== "string" ||
|
|
103
|
-
typeof contract.attemptId !== "string"
|
|
200
|
+
typeof contract.attemptId !== "string" ||
|
|
201
|
+
(contract.completion !== "submit" && contract.completion !== "assistant") ||
|
|
202
|
+
(contract.maxOutputChars !== undefined &&
|
|
203
|
+
(typeof contract.maxOutputChars !== "number" ||
|
|
204
|
+
!Number.isInteger(contract.maxOutputChars) ||
|
|
205
|
+
contract.maxOutputChars <= 0))
|
|
104
206
|
) {
|
|
105
207
|
return undefined;
|
|
106
208
|
}
|
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;
|
|
@@ -278,6 +298,14 @@ function nodeRuntimeSegments(
|
|
|
278
298
|
now: Date,
|
|
279
299
|
): string[] {
|
|
280
300
|
const segments: string[] = [];
|
|
301
|
+
const node = snapshot.nodes[nodeId];
|
|
302
|
+
if (
|
|
303
|
+
node?.nodeType === "agent" &&
|
|
304
|
+
typeof node.expectedOutput === "object" &&
|
|
305
|
+
node.expectedOutput.kind === "assistant-message"
|
|
306
|
+
) {
|
|
307
|
+
segments.push("assistant response");
|
|
308
|
+
}
|
|
281
309
|
const completedAttempts = state.steps.filter((step) => step.nodeId === nodeId).length;
|
|
282
310
|
const attempts = completedAttempts + (state.currentNode === nodeId ? 1 : 0);
|
|
283
311
|
if (attempts > 1) {
|
|
@@ -309,8 +337,7 @@ function nodeRuntimeSegments(
|
|
|
309
337
|
})
|
|
310
338
|
: undefined;
|
|
311
339
|
const requestAudience =
|
|
312
|
-
|
|
313
|
-
request?.schema === "pi-workflows.human-decision-request.v2") &&
|
|
340
|
+
request?.schema === "pi-workflows.human-decision-request.v1" &&
|
|
314
341
|
typeof request.audience === "string"
|
|
315
342
|
? request.audience
|
|
316
343
|
: human.audience;
|
package/src/host/rpc-executor.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type RpcStepExecutorOptions = {
|
|
|
43
43
|
env?: Record<string, string>;
|
|
44
44
|
/** Extra pi arguments, for example a model override. */
|
|
45
45
|
piArgs?: string[];
|
|
46
|
+
/** Park assistant-message nodes for a visible origin session. */
|
|
47
|
+
assistantMessageMode?: "park";
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
/**
|
|
@@ -53,6 +55,7 @@ export type RpcStepExecutorOptions = {
|
|
|
53
55
|
* the model sees the same tool contract as an in-session run.
|
|
54
56
|
*/
|
|
55
57
|
export class RpcStepExecutor implements AgentStepExecutor {
|
|
58
|
+
readonly assistantMessageMode: "park" | "unsupported";
|
|
56
59
|
private readonly options: RpcStepExecutorOptions;
|
|
57
60
|
private child: ChildProcess | null = null;
|
|
58
61
|
private childExited: { code: number | null; signal: string | null } | null = null;
|
|
@@ -63,6 +66,7 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
63
66
|
|
|
64
67
|
constructor(options: RpcStepExecutorOptions) {
|
|
65
68
|
this.options = options;
|
|
69
|
+
this.assistantMessageMode = options.assistantMessageMode ?? "unsupported";
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
async runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission> {
|
package/src/host/runner.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
5
5
|
import {
|
|
6
6
|
ControllerManager,
|
|
7
7
|
loadDiscoveredControllers,
|
|
8
|
-
projectControllerStorePath,
|
|
9
8
|
SqliteControllerStore,
|
|
10
9
|
WorkflowEngineScheduler,
|
|
11
10
|
type WorkflowRunQueueRecord,
|
|
12
11
|
} from "../controllers/index.js";
|
|
13
12
|
import type { JsonObject } from "../controllers/types.js";
|
|
13
|
+
import { workflowStatePath } from "../state/database.js";
|
|
14
14
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
15
15
|
import {
|
|
16
16
|
ClaimLostError,
|
|
@@ -19,9 +19,8 @@ import {
|
|
|
19
19
|
WorkflowSourceChangedError,
|
|
20
20
|
} from "../workflows/errors.js";
|
|
21
21
|
import { resolveWorkflowRef, resolveWorkflowSource } from "../workflows/loader.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import type { WorkflowDefinition } from "../workflows/types.js";
|
|
22
|
+
import { WorkflowRunStore } from "../workflows/store.js";
|
|
23
|
+
import type { ResolvedHumanDecision, WorkflowDefinition } from "../workflows/types.js";
|
|
25
24
|
import { HostProcessRegistry } from "./processes.js";
|
|
26
25
|
import { RpcStepExecutor } from "./rpc-executor.js";
|
|
27
26
|
|
|
@@ -32,10 +31,8 @@ const RUN_CLAIM_RENEW_MS = 10_000;
|
|
|
32
31
|
export type WorkflowHostOptions = {
|
|
33
32
|
cwd: string;
|
|
34
33
|
runnerId?: string;
|
|
35
|
-
/** Explicit
|
|
36
|
-
|
|
37
|
-
/** Explicit run-bundle root; defaults to the shared runs directory. */
|
|
38
|
-
runsDir?: string;
|
|
34
|
+
/** Explicit database path; defaults to the canonical workflow state database. */
|
|
35
|
+
databasePath?: string;
|
|
39
36
|
registry?: HostProcessRegistry;
|
|
40
37
|
piArgs?: string[];
|
|
41
38
|
/** Extra environment for headless children (for example a test provider). */
|
|
@@ -49,7 +46,7 @@ export type WorkflowHostOptions = {
|
|
|
49
46
|
* The always-on runner: claims parked workflow runs and reconciles durable
|
|
50
47
|
* controllers without a Pi session. Conversation nodes execute in headless
|
|
51
48
|
* `pi --mode rpc` children. Everything the host does is recoverable: claims
|
|
52
|
-
* expire,
|
|
49
|
+
* expire, SQLite leases fence stale writers, and child processes are reaped by
|
|
53
50
|
* the next host.
|
|
54
51
|
*/
|
|
55
52
|
export class WorkflowHost {
|
|
@@ -61,7 +58,6 @@ export class WorkflowHost {
|
|
|
61
58
|
private manager: ControllerManager | null = null;
|
|
62
59
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
63
60
|
private readonly activeRuns = new Map<string, Promise<void>>();
|
|
64
|
-
private readonly migrationBlockedRuns = new Set<string>();
|
|
65
61
|
private readonly schedulerExecutors = new Map<WorkflowEngine, RpcStepExecutor>();
|
|
66
62
|
/** Runs whose resume refused (edited source); skipped until a host restart. */
|
|
67
63
|
private readonly skippedRuns = new Set<string>();
|
|
@@ -72,14 +68,15 @@ export class WorkflowHost {
|
|
|
72
68
|
constructor(options: WorkflowHostOptions) {
|
|
73
69
|
this.options = options;
|
|
74
70
|
this.runnerId = options.runnerId ?? `host-${randomUUID().slice(0, 8)}`;
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const databasePath = options.databasePath ?? workflowStatePath();
|
|
72
|
+
this.store = new SqliteControllerStore(databasePath, { projectPath: options.cwd });
|
|
73
|
+
this.stateDir = path.join(
|
|
74
|
+
path.dirname(this.store.filePath),
|
|
75
|
+
"hosts",
|
|
76
|
+
hostProjectScope(options.cwd),
|
|
77
77
|
);
|
|
78
|
-
this.stateDir = path.dirname(this.store.filePath);
|
|
79
78
|
this.registry = options.registry ?? new HostProcessRegistry(this.stateDir);
|
|
80
|
-
this.childRunStore = new WorkflowRunStore(
|
|
81
|
-
options.runsDir ?? process.env.PI_WORKFLOWS_RUNS_DIR ?? undefined,
|
|
82
|
-
);
|
|
79
|
+
this.childRunStore = new WorkflowRunStore(databasePath);
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
private log(message: string): void {
|
|
@@ -97,16 +94,6 @@ export class WorkflowHost {
|
|
|
97
94
|
this.log(`reaped ${reaped.length} orphaned headless session(s): ${reaped.join(", ")}`);
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
const migration = await migrateLegacyWorkflowSources({
|
|
101
|
-
catalog: builtinWorkflowCatalog,
|
|
102
|
-
store: this.childRunStore,
|
|
103
|
-
queue: this.store,
|
|
104
|
-
});
|
|
105
|
-
for (const blocked of migration.blocked) {
|
|
106
|
-
this.migrationBlockedRuns.add(blocked.runId);
|
|
107
|
-
this.log(`run ${blocked.runId} parked: ${blocked.reason}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
97
|
const definitions = await loadDiscoveredControllers({ cwd: this.options.cwd });
|
|
111
98
|
if (definitions.length > 0) {
|
|
112
99
|
const scheduler = new WorkflowEngineScheduler({
|
|
@@ -192,7 +179,7 @@ export class WorkflowHost {
|
|
|
192
179
|
runnerId: this.runnerId,
|
|
193
180
|
claimToken: randomUUID(),
|
|
194
181
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
195
|
-
excludeRunIds: [...this.skippedRuns
|
|
182
|
+
excludeRunIds: [...this.skippedRuns],
|
|
196
183
|
});
|
|
197
184
|
} catch (error) {
|
|
198
185
|
// Store contention or corruption must not kill the host's loop.
|
|
@@ -215,7 +202,15 @@ export class WorkflowHost {
|
|
|
215
202
|
let workflow: WorkflowDefinition;
|
|
216
203
|
let workflowSource: import("../workflows/types.js").WorkflowSource;
|
|
217
204
|
try {
|
|
218
|
-
const bundle =
|
|
205
|
+
const bundle = this.childRunStore.readRun(runId);
|
|
206
|
+
if (bundle?.state.status === "waiting") {
|
|
207
|
+
if (this.store.parkWorkflowRun({ runId, claimToken })) {
|
|
208
|
+
this.store.settleRunEffect(runId, "run.park_queue");
|
|
209
|
+
this.skippedRuns.add(runId);
|
|
210
|
+
this.log(`left ${record.workflowName} run ${runId} waiting for its decision owner`);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
219
214
|
if (bundle?.state.workflowSource === undefined) {
|
|
220
215
|
throw new Error(`Workflow run ${runId} has no canonical workflow source`);
|
|
221
216
|
}
|
|
@@ -260,12 +255,13 @@ export class WorkflowHost {
|
|
|
260
255
|
throw new ClaimLostError(runId);
|
|
261
256
|
}
|
|
262
257
|
};
|
|
263
|
-
const fencedStore = new WorkflowRunStore(this.childRunStore.
|
|
264
|
-
|
|
258
|
+
const fencedStore = new WorkflowRunStore(this.childRunStore.databasePath, {
|
|
259
|
+
authorityProvider: () => store.workflowRunAuthority(runId, claimToken),
|
|
265
260
|
});
|
|
266
261
|
const executor = new RpcStepExecutor({
|
|
267
262
|
cwd: this.options.cwd,
|
|
268
263
|
registry: this.registry,
|
|
264
|
+
...(record.originSessionId !== null ? { assistantMessageMode: "park" as const } : {}),
|
|
269
265
|
...(this.options.piArgs !== undefined ? { piArgs: this.options.piArgs } : {}),
|
|
270
266
|
...(this.options.env !== undefined ? { env: this.options.env } : {}),
|
|
271
267
|
});
|
|
@@ -308,16 +304,39 @@ export class WorkflowHost {
|
|
|
308
304
|
|
|
309
305
|
this.recordEvent(runId, record.workflowName, "resumed", { runnerId: this.runnerId });
|
|
310
306
|
try {
|
|
311
|
-
const
|
|
307
|
+
const humanDecision =
|
|
308
|
+
record.parentRunId === null ? undefined : hostHumanDecision(record.launchOptions);
|
|
309
|
+
const result = record.initialized
|
|
310
|
+
? await engine.resumeRun(workflow, runId, { workflowSource })
|
|
311
|
+
: record.parentRunId === null
|
|
312
|
+
? await engine.run(workflow, record.input, { workflowSource, runId })
|
|
313
|
+
: await engine.continueRun(workflow, record.parentRunId, record.input, {
|
|
314
|
+
workflowSource,
|
|
315
|
+
runId,
|
|
316
|
+
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
317
|
+
});
|
|
312
318
|
clearInterval(renewTimer);
|
|
313
|
-
if (result.state.status === "running") {
|
|
314
|
-
//
|
|
319
|
+
if (result.state.status === "running" || result.state.status === "waiting") {
|
|
320
|
+
// A drained run, visible-assistant handoff, or new checkpoint stays
|
|
321
|
+
// available to its next owner. Skip waits in this host process so it
|
|
322
|
+
// cannot reclaim the same run in a tight loop.
|
|
315
323
|
this.store.parkWorkflowRun({ runId, claimToken });
|
|
316
|
-
this.
|
|
317
|
-
|
|
324
|
+
this.store.settleRunEffect(runId, "run.park_queue");
|
|
325
|
+
const waitingForOrigin =
|
|
326
|
+
result.state.status === "running" &&
|
|
327
|
+
result.state.statusDetail === "waiting for origin Pi session";
|
|
328
|
+
if (result.state.status === "waiting" || waitingForOrigin) this.skippedRuns.add(runId);
|
|
329
|
+
this.recordEvent(runId, record.workflowName, "parked", {
|
|
330
|
+
status: result.state.status,
|
|
331
|
+
...(waitingForOrigin ? { reason: "waiting for origin Pi session" } : {}),
|
|
332
|
+
});
|
|
333
|
+
this.log(
|
|
334
|
+
`parked ${record.workflowName} run ${runId}${waitingForOrigin ? ": waiting for origin Pi session" : ""}`,
|
|
335
|
+
);
|
|
318
336
|
return;
|
|
319
337
|
}
|
|
320
338
|
this.store.completeWorkflowRun({ runId, claimToken });
|
|
339
|
+
this.store.settleRunEffect(runId, "run.settle_queue");
|
|
321
340
|
this.recordEvent(runId, record.workflowName, result.state.status, {
|
|
322
341
|
...(result.state.error !== undefined ? { error: result.state.error } : {}),
|
|
323
342
|
...(result.state.waitingOn !== undefined ? { waitingOn: result.state.waitingOn } : {}),
|
|
@@ -352,6 +371,7 @@ export class WorkflowHost {
|
|
|
352
371
|
if (index !== -1) {
|
|
353
372
|
this.parkedEngines.splice(index, 1);
|
|
354
373
|
}
|
|
374
|
+
fencedStore.close();
|
|
355
375
|
await executor.close().catch(() => undefined);
|
|
356
376
|
}
|
|
357
377
|
}
|
|
@@ -365,17 +385,22 @@ export class WorkflowHost {
|
|
|
365
385
|
let heldClaim = false;
|
|
366
386
|
try {
|
|
367
387
|
// The interruption write obeys the same fencing rule as every other
|
|
368
|
-
//
|
|
388
|
+
// state write: without a live claim, the current owner decides.
|
|
369
389
|
heldClaim = this.store.verifyWorkflowRunClaim({ runId: record.runId, claimToken });
|
|
370
390
|
if (heldClaim) {
|
|
371
|
-
const
|
|
391
|
+
const claimedStore = new WorkflowRunStore(this.store.filePath, {
|
|
392
|
+
state: this.store.state,
|
|
393
|
+
authorityProvider: () => this.store.workflowRunAuthority(record.runId, claimToken),
|
|
394
|
+
});
|
|
395
|
+
const bundle = await claimedStore.markRunInterrupted(record.runId, message);
|
|
372
396
|
actualStatus = bundle?.state.status;
|
|
373
397
|
}
|
|
374
398
|
} catch {
|
|
375
|
-
// The
|
|
399
|
+
// The run state may be unreadable; the queue row still needs closure.
|
|
376
400
|
}
|
|
377
401
|
try {
|
|
378
402
|
this.store.completeWorkflowRun({ runId: record.runId, claimToken });
|
|
403
|
+
this.store.settleRunEffect(record.runId, "run.settle_queue");
|
|
379
404
|
} catch {
|
|
380
405
|
// Best-effort.
|
|
381
406
|
}
|
|
@@ -384,8 +409,8 @@ export class WorkflowHost {
|
|
|
384
409
|
this.log(`run ${record.runId} continues under another runner`);
|
|
385
410
|
return;
|
|
386
411
|
}
|
|
387
|
-
// Report the
|
|
388
|
-
// no-op (the
|
|
412
|
+
// Report the run's real terminal state when the interruption was a
|
|
413
|
+
// no-op (the run was already waiting or completed), so the feed
|
|
389
414
|
// stays truthful for sessions syncing from it.
|
|
390
415
|
if (actualStatus !== undefined && actualStatus !== "failed") {
|
|
391
416
|
this.recordEvent(record.runId, record.workflowName, actualStatus, {});
|
|
@@ -404,6 +429,33 @@ export class WorkflowHost {
|
|
|
404
429
|
}
|
|
405
430
|
}
|
|
406
431
|
|
|
432
|
+
function hostHumanDecision(value: unknown): ResolvedHumanDecision | undefined {
|
|
433
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
434
|
+
throw new Error("Stored workflow launch options are invalid");
|
|
435
|
+
}
|
|
436
|
+
const decision = Reflect.get(value, "humanDecision");
|
|
437
|
+
if (decision === undefined) return undefined;
|
|
438
|
+
if (
|
|
439
|
+
typeof decision !== "object" ||
|
|
440
|
+
decision === null ||
|
|
441
|
+
Array.isArray(decision) ||
|
|
442
|
+
Reflect.get(decision, "schema") !== "pi-workflows.human-decision-accepted.v1"
|
|
443
|
+
) {
|
|
444
|
+
throw new Error("Stored human decision launch option is invalid");
|
|
445
|
+
}
|
|
446
|
+
return decision as ResolvedHumanDecision;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function hostProjectScope(cwd: string): string {
|
|
450
|
+
let canonical: string;
|
|
451
|
+
try {
|
|
452
|
+
canonical = fs.realpathSync.native(cwd);
|
|
453
|
+
} catch {
|
|
454
|
+
canonical = path.resolve(cwd);
|
|
455
|
+
}
|
|
456
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 24);
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
function hostLockPath(stateDir: string): string {
|
|
408
460
|
return path.join(stateDir, "host.lock");
|
|
409
461
|
}
|
|
@@ -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;
|
|
@@ -341,15 +340,22 @@ function renderCellText(
|
|
|
341
340
|
: selected !== undefined
|
|
342
341
|
? `human: ${sanitizeText(selected.label)}`
|
|
343
342
|
: undefined;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
(atLatestStep && state.currentNode === nodeId && state.statusDetail
|
|
343
|
+
const configuredDetail =
|
|
344
|
+
atLatestStep && state.currentNode === nodeId && state.statusDetail
|
|
347
345
|
? sanitizeText(state.statusDetail)
|
|
348
346
|
: node?.statusDetail
|
|
349
347
|
? sanitizeText(node.statusDetail)
|
|
350
348
|
: node?.summary
|
|
351
349
|
? sanitizeText(node.summary)
|
|
352
|
-
: ""
|
|
350
|
+
: "";
|
|
351
|
+
const assistantDetail =
|
|
352
|
+
node?.nodeType === "agent" &&
|
|
353
|
+
typeof node.expectedOutput === "object" &&
|
|
354
|
+
node.expectedOutput.kind === "assistant-message"
|
|
355
|
+
? "assistant response"
|
|
356
|
+
: "";
|
|
357
|
+
const nodeDetail = [assistantDetail, configuredDetail].filter(Boolean).join(" · ");
|
|
358
|
+
const detail = humanDetail ?? nodeDetail;
|
|
353
359
|
const branchLines = Array.from({ length: metrics.branchRows }, (_, index) =>
|
|
354
360
|
labels[index] ? `◇ ${labels[index]}` : "",
|
|
355
361
|
);
|