@osolmaz/pi-workflows 0.16.4 → 0.16.6
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 +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +115 -165
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -1
- package/dist/extension/workflow-message-coordinator.js +65 -54
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +22 -8
- package/dist/server/server.js +498 -543
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +4 -5
- package/dist/server/view.js +51 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.d.ts +28 -0
- package/dist/state/prune.js +278 -83
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -15
- package/dist/workflows/engine.js +125 -245
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +174 -169
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +87 -30
- package/docs/WORKFLOW_SERVER.md +50 -11
- package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
- package/docs/workflows.md +109 -44
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +117 -189
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/workflow-message-coordinator.ts +78 -50
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +568 -644
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +56 -60
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +408 -95
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +133 -291
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +223 -231
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
package/src/server/view.ts
CHANGED
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
type WorkflowRunView,
|
|
11
11
|
type WorkflowSessionView,
|
|
12
12
|
} from "../client/view.js";
|
|
13
|
-
import type {
|
|
14
|
-
SqliteResourceManagerStore,
|
|
15
|
-
WorkflowRunQueueRecord,
|
|
16
|
-
WorkflowRunQueueViewRecord,
|
|
17
|
-
} from "../resource-managers/sqlite.js";
|
|
18
13
|
import type { StateDatabase } from "../state/database.js";
|
|
19
14
|
import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
20
15
|
import { WorkflowMessageStore, type WorkflowMessage } from "../state/workflow-messages.js";
|
|
16
|
+
import type {
|
|
17
|
+
WorkflowRunQueueStore,
|
|
18
|
+
WorkflowRunQueueRecord,
|
|
19
|
+
WorkflowRunQueueViewRecord,
|
|
20
|
+
} from "../workflows/queue.js";
|
|
21
21
|
import type { WorkflowRunDisplayState, WorkflowRunStore } from "../workflows/store.js";
|
|
22
22
|
import type {
|
|
23
23
|
WorkflowRunState,
|
|
@@ -74,7 +74,7 @@ export class ServerViewStore {
|
|
|
74
74
|
|
|
75
75
|
constructor(
|
|
76
76
|
private readonly state: StateDatabase,
|
|
77
|
-
private readonly queue:
|
|
77
|
+
private readonly queue: WorkflowRunQueueStore,
|
|
78
78
|
private readonly serverState: ServerStateStore,
|
|
79
79
|
private readonly runs: WorkflowRunStore,
|
|
80
80
|
private readonly hasLiveRunner: (runId: string) => boolean,
|
|
@@ -274,6 +274,7 @@ export class ServerViewStore {
|
|
|
274
274
|
schema: RUN_VIEW_SCHEMA,
|
|
275
275
|
runId,
|
|
276
276
|
revision,
|
|
277
|
+
runRevision: this.runs.runRevision(runId),
|
|
277
278
|
display,
|
|
278
279
|
manifest: manifest(queue, display.status),
|
|
279
280
|
state: this.projectState(runId, loaded.state, stepPage.items, updatePage.items),
|
|
@@ -643,11 +644,7 @@ export class ServerViewStore {
|
|
|
643
644
|
}
|
|
644
645
|
if (message.status === "pending") return message.runId;
|
|
645
646
|
if (message.status !== "sent") continue;
|
|
646
|
-
|
|
647
|
-
if (turn === undefined || turn.state === "started") return message.runId;
|
|
648
|
-
if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
|
|
649
|
-
return message.runId;
|
|
650
|
-
}
|
|
647
|
+
if (Date.parse(message.updatedAt) + TERMINAL_VIEW_RETENTION_MS > now) return message.runId;
|
|
651
648
|
}
|
|
652
649
|
return undefined;
|
|
653
650
|
}
|
|
@@ -686,15 +683,15 @@ export class ServerViewStore {
|
|
|
686
683
|
) {
|
|
687
684
|
return false;
|
|
688
685
|
}
|
|
689
|
-
const
|
|
690
|
-
|
|
686
|
+
const run = this.state.connection
|
|
687
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
688
|
+
.get(source.runId);
|
|
689
|
+
if (!isObjectRecord(run) || run.status !== "completed") return false;
|
|
691
690
|
const terminal = this.workflowMessages
|
|
692
|
-
.listRun(
|
|
691
|
+
.listRun(source.runId)
|
|
693
692
|
.filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
|
|
694
693
|
.at(-1);
|
|
695
694
|
if (terminal === undefined) return false;
|
|
696
|
-
const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
|
|
697
|
-
if (terminalTurn?.state !== "ended") return false;
|
|
698
695
|
const prior = this.state.connection
|
|
699
696
|
.prepare(
|
|
700
697
|
`SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
|
|
@@ -729,24 +726,6 @@ export class ServerViewStore {
|
|
|
729
726
|
return reservation === undefined;
|
|
730
727
|
}
|
|
731
728
|
|
|
732
|
-
private terminalChainLeaf(runId: string): { runId: string; status: string } | undefined {
|
|
733
|
-
const row = this.state.connection
|
|
734
|
-
.prepare(
|
|
735
|
-
`WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
|
|
736
|
-
SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
|
|
737
|
-
UNION ALL
|
|
738
|
-
SELECT child.run_id, child.status, chain.depth + 1, child.created_at
|
|
739
|
-
FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
|
|
740
|
-
)
|
|
741
|
-
SELECT run_id AS runId, status FROM chain
|
|
742
|
-
ORDER BY depth DESC, created_at DESC LIMIT 1`,
|
|
743
|
-
)
|
|
744
|
-
.get(runId);
|
|
745
|
-
return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
|
|
746
|
-
? { runId: row.runId, status: row.status }
|
|
747
|
-
: undefined;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
729
|
private openWorkflowMessage(messages: readonly WorkflowMessage[]): WorkflowMessage | undefined {
|
|
751
730
|
for (const message of [...messages].reverse()) {
|
|
752
731
|
if (message.status !== "sent") continue;
|
|
@@ -760,7 +739,7 @@ export class ServerViewStore {
|
|
|
760
739
|
if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
|
|
761
740
|
return message;
|
|
762
741
|
}
|
|
763
|
-
} else if (message.kind === "
|
|
742
|
+
} else if (message.kind === "followUp") {
|
|
764
743
|
const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
|
|
765
744
|
if (turn === undefined || turn.state === "started") return message;
|
|
766
745
|
}
|
|
@@ -794,7 +773,7 @@ export class ServerViewStore {
|
|
|
794
773
|
ambiguous: this.hasAmbiguousEffect(queue.runId),
|
|
795
774
|
runnerActive: this.hasLiveRunner(queue.runId),
|
|
796
775
|
originTurnActive: this.hasActivity(queue.runId),
|
|
797
|
-
|
|
776
|
+
pendingRequestKind: this.pendingRequestKind(queue.runId),
|
|
798
777
|
errorMessage: state?.error ?? queue.errorMessage,
|
|
799
778
|
});
|
|
800
779
|
}
|
|
@@ -805,7 +784,7 @@ export class ServerViewStore {
|
|
|
805
784
|
`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
|
|
806
785
|
JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
|
|
807
786
|
WHERE t.run_id = ? AND t.state = 'started'
|
|
808
|
-
AND m.kind IN ('step', '
|
|
787
|
+
AND m.kind IN ('step', 'followUp') LIMIT 1`,
|
|
809
788
|
)
|
|
810
789
|
.get(runId);
|
|
811
790
|
return (
|
|
@@ -815,14 +794,13 @@ export class ServerViewStore {
|
|
|
815
794
|
);
|
|
816
795
|
}
|
|
817
796
|
|
|
818
|
-
private
|
|
797
|
+
private pendingRequestKind(runId: string): WorkflowDisplayFacts["pendingRequestKind"] {
|
|
819
798
|
const row = this.state.connection
|
|
820
|
-
.prepare(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return row !== undefined;
|
|
799
|
+
.prepare("SELECT kind FROM interactive_requests WHERE run_id = ? AND status = 'pending'")
|
|
800
|
+
.get(runId) as
|
|
801
|
+
| { kind: Exclude<WorkflowDisplayFacts["pendingRequestKind"], null> }
|
|
802
|
+
| undefined;
|
|
803
|
+
return row?.kind ?? null;
|
|
826
804
|
}
|
|
827
805
|
|
|
828
806
|
private hasAmbiguousEffect(runId: string): boolean {
|
|
@@ -857,7 +835,7 @@ export class ServerViewStore {
|
|
|
857
835
|
this.workflowActivityRevision(),
|
|
858
836
|
this.hasLiveRunner(runId),
|
|
859
837
|
this.hasActivity(runId),
|
|
860
|
-
this.
|
|
838
|
+
this.pendingRequestKind(runId),
|
|
861
839
|
this.hasAmbiguousEffect(runId),
|
|
862
840
|
].join(":");
|
|
863
841
|
}
|
|
@@ -890,35 +868,49 @@ export type WorkflowDisplayFacts = {
|
|
|
890
868
|
ambiguous: boolean;
|
|
891
869
|
runnerActive: boolean;
|
|
892
870
|
originTurnActive: boolean;
|
|
893
|
-
|
|
871
|
+
pendingRequestKind: "agent" | "assistant" | "checkpoint" | "decision" | null;
|
|
894
872
|
errorMessage: string | null;
|
|
895
873
|
};
|
|
896
874
|
|
|
897
875
|
export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisplay {
|
|
898
876
|
let status: WorkflowDisplayStatus;
|
|
899
|
-
|
|
877
|
+
const activity: WorkflowDisplay["activity"] = facts.runnerActive
|
|
878
|
+
? "supervised_runner"
|
|
879
|
+
: facts.originTurnActive
|
|
880
|
+
? "origin_turn"
|
|
881
|
+
: null;
|
|
900
882
|
let reason: string | null = null;
|
|
901
883
|
|
|
902
|
-
if (
|
|
903
|
-
status = "ambiguous";
|
|
904
|
-
reason = "An external effect needs explicit recovery.";
|
|
905
|
-
} else if (facts.runnerActive || facts.originTurnActive) {
|
|
906
|
-
status = "running";
|
|
907
|
-
activity = facts.runnerActive ? "supervised_runner" : "origin_turn";
|
|
908
|
-
} else if (
|
|
884
|
+
if (
|
|
909
885
|
facts.durableStatus === "completed" ||
|
|
910
886
|
facts.durableStatus === "failed" ||
|
|
911
887
|
facts.durableStatus === "timed_out" ||
|
|
912
888
|
facts.durableStatus === "cancelled"
|
|
913
889
|
) {
|
|
914
890
|
status = facts.durableStatus;
|
|
915
|
-
reason = facts.
|
|
891
|
+
reason = facts.ambiguous
|
|
892
|
+
? "The run is terminal, but an external effect still needs explicit recovery."
|
|
893
|
+
: facts.errorMessage;
|
|
894
|
+
} else if (facts.ambiguous) {
|
|
895
|
+
status = "ambiguous";
|
|
896
|
+
reason = "An external effect needs explicit recovery.";
|
|
916
897
|
} else if (facts.paused) {
|
|
917
898
|
status = "paused";
|
|
918
899
|
reason = "The workflow is durably paused.";
|
|
919
|
-
} else if (facts.
|
|
900
|
+
} else if (facts.pendingRequestKind !== null || facts.durableStatus === "waiting") {
|
|
920
901
|
status = "waiting";
|
|
921
|
-
reason =
|
|
902
|
+
reason =
|
|
903
|
+
facts.pendingRequestKind === "decision"
|
|
904
|
+
? "The workflow needs a protected human decision."
|
|
905
|
+
: facts.pendingRequestKind === "checkpoint"
|
|
906
|
+
? "The workflow needs a checkpoint answer."
|
|
907
|
+
: facts.pendingRequestKind === "agent"
|
|
908
|
+
? "The workflow needs its assigned agent result."
|
|
909
|
+
: facts.pendingRequestKind === "assistant"
|
|
910
|
+
? "The workflow needs its assigned visible response."
|
|
911
|
+
: "The workflow is waiting.";
|
|
912
|
+
} else if (activity !== null) {
|
|
913
|
+
status = "running";
|
|
922
914
|
} else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
|
|
923
915
|
status = "queued";
|
|
924
916
|
reason =
|
|
@@ -941,8 +933,12 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
941
933
|
if (facts.queueStatus === "parked") controls.push("resume");
|
|
942
934
|
controls.push("cancel");
|
|
943
935
|
}
|
|
944
|
-
if (status === "waiting")
|
|
945
|
-
|
|
936
|
+
if (status === "waiting") {
|
|
937
|
+
if (facts.pendingRequestKind === "checkpoint") controls.push("answer");
|
|
938
|
+
if (facts.pendingRequestKind === "decision") controls.push("human-answer");
|
|
939
|
+
if (facts.pendingRequestKind === "agent") controls.push("update", "submit");
|
|
940
|
+
}
|
|
941
|
+
if (facts.ambiguous) controls.push("review");
|
|
946
942
|
return { status, activity, controls, reason };
|
|
947
943
|
}
|
|
948
944
|
|
|
@@ -983,7 +979,7 @@ function projectQueue(
|
|
|
983
979
|
rootRunId: run.rootRunId,
|
|
984
980
|
lineageKind: run.lineageKind,
|
|
985
981
|
restartNumber: run.restartNumber,
|
|
986
|
-
|
|
982
|
+
parentRunRevision: run.parentRunRevision,
|
|
987
983
|
errorCode: run.errorCode,
|
|
988
984
|
createdAt: run.createdAt,
|
|
989
985
|
updatedAt: run.updatedAt,
|
|
@@ -18,10 +18,8 @@ import type {
|
|
|
18
18
|
AgentStepExecutor,
|
|
19
19
|
AgentStepRequest,
|
|
20
20
|
AgentStepSubmission,
|
|
21
|
-
ResolvedHumanDecision,
|
|
22
21
|
WorkflowDefinition,
|
|
23
22
|
WorkflowMountedSource,
|
|
24
|
-
WorkflowRunState,
|
|
25
23
|
WorkflowSource,
|
|
26
24
|
} from "../workflows/types.js";
|
|
27
25
|
import { RpcStepExecutor } from "./rpc-executor.js";
|
|
@@ -43,7 +41,6 @@ import {
|
|
|
43
41
|
} from "./workflow-runner-store.js";
|
|
44
42
|
|
|
45
43
|
const STARTUP_ENV = "PI_WORKFLOWS_WORKFLOW_RUNNER_LAUNCH";
|
|
46
|
-
const PRESENTATION_TIMEOUT_MS = 30_000;
|
|
47
44
|
|
|
48
45
|
type WorkflowRunnerBootstrap = {
|
|
49
46
|
command: WorkflowRunnerCommand;
|
|
@@ -198,7 +195,7 @@ class StdioRunnerTransport implements WorkflowRunnerStoreTransport {
|
|
|
198
195
|
|
|
199
196
|
class InteractiveExecutor implements AgentStepExecutor {
|
|
200
197
|
readonly assistantMessageMode = "visible" as const;
|
|
201
|
-
readonly
|
|
198
|
+
readonly preservesActiveTimeBudget = true;
|
|
202
199
|
|
|
203
200
|
constructor(
|
|
204
201
|
private readonly store: ServerBackedWorkflowStore,
|
|
@@ -317,7 +314,7 @@ export function validateAcceptedAssistantSubmission(
|
|
|
317
314
|
}
|
|
318
315
|
|
|
319
316
|
export async function executeRunnerRunCommand(
|
|
320
|
-
engine: Pick<WorkflowEngine, "run" | "resumeRun"
|
|
317
|
+
engine: Pick<WorkflowEngine, "run" | "resumeRun">,
|
|
321
318
|
workflow: WorkflowDefinition,
|
|
322
319
|
runId: string,
|
|
323
320
|
workflowSource: WorkflowSource,
|
|
@@ -334,14 +331,6 @@ export async function executeRunnerRunCommand(
|
|
|
334
331
|
? {}
|
|
335
332
|
: { resumeInteractionAttemptId: command.resumeInteractionAttemptId }),
|
|
336
333
|
});
|
|
337
|
-
case "continue":
|
|
338
|
-
return await engine.continueRun(workflow, command.parentRunId, command.input, {
|
|
339
|
-
runId,
|
|
340
|
-
workflowSource,
|
|
341
|
-
...(command.humanDecision === undefined
|
|
342
|
-
? {}
|
|
343
|
-
: { humanDecision: command.humanDecision as ResolvedHumanDecision }),
|
|
344
|
-
});
|
|
345
334
|
}
|
|
346
335
|
}
|
|
347
336
|
|
|
@@ -421,11 +410,6 @@ export async function runWorkflowRunner(): Promise<number> {
|
|
|
421
410
|
notificationSink: {
|
|
422
411
|
notify: async (request) => await store.requestNotification(request),
|
|
423
412
|
},
|
|
424
|
-
onRunFinishing: async (_runId, state) => {
|
|
425
|
-
if (state.status !== "completed" || workflow.presentationPrompt === undefined) return;
|
|
426
|
-
const instructions = await resolvePresentationInstructions(workflow, state);
|
|
427
|
-
await store.requestPresentation(instructions);
|
|
428
|
-
},
|
|
429
413
|
});
|
|
430
414
|
try {
|
|
431
415
|
const result = await executeRunnerRunCommand(
|
|
@@ -448,43 +432,6 @@ export async function runWorkflowRunner(): Promise<number> {
|
|
|
448
432
|
}
|
|
449
433
|
}
|
|
450
434
|
|
|
451
|
-
async function resolvePresentationInstructions(
|
|
452
|
-
workflow: WorkflowDefinition,
|
|
453
|
-
state: WorkflowRunState,
|
|
454
|
-
): Promise<string> {
|
|
455
|
-
const fallback = "Summarize the completed workflow result for the user in a normal response.";
|
|
456
|
-
if (typeof workflow.presentationPrompt === "string") {
|
|
457
|
-
return workflow.presentationPrompt.trim() || fallback;
|
|
458
|
-
}
|
|
459
|
-
if (workflow.presentationPrompt === undefined) return fallback;
|
|
460
|
-
const controller = new AbortController();
|
|
461
|
-
const timer = setTimeout(
|
|
462
|
-
() => controller.abort(new Error("Workflow presentation prompt timed out")),
|
|
463
|
-
PRESENTATION_TIMEOUT_MS,
|
|
464
|
-
);
|
|
465
|
-
timer.unref?.();
|
|
466
|
-
const snapshot = structuredClone(state);
|
|
467
|
-
try {
|
|
468
|
-
const instructions = await Promise.race([
|
|
469
|
-
workflow.presentationPrompt({
|
|
470
|
-
state: snapshot,
|
|
471
|
-
finalOutput: snapshot.finalOutput,
|
|
472
|
-
signal: controller.signal,
|
|
473
|
-
}),
|
|
474
|
-
new Promise<never>((_resolve, reject) => {
|
|
475
|
-
controller.signal.addEventListener("abort", () => reject(controller.signal.reason), {
|
|
476
|
-
once: true,
|
|
477
|
-
});
|
|
478
|
-
}),
|
|
479
|
-
]);
|
|
480
|
-
return instructions?.trim() || fallback;
|
|
481
|
-
} catch {
|
|
482
|
-
return fallback;
|
|
483
|
-
} finally {
|
|
484
|
-
clearTimeout(timer);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
435
|
type WorkflowRunnerSources = {
|
|
489
436
|
root: WorkflowSource;
|
|
490
437
|
mounted: WorkflowMountedSource[];
|
|
@@ -11,12 +11,6 @@ export const WORKFLOW_RUNNER_CONTENT_CHUNK_SCHEMA = "pi-workflows.worker-content
|
|
|
11
11
|
export type WorkflowRunnerCommand =
|
|
12
12
|
| { kind: "start"; input: JsonValue }
|
|
13
13
|
| { kind: "resume"; resumeInteractionAttemptId?: string }
|
|
14
|
-
| {
|
|
15
|
-
kind: "continue";
|
|
16
|
-
parentRunId: string;
|
|
17
|
-
input: JsonValue;
|
|
18
|
-
humanDecision?: JsonValue;
|
|
19
|
-
}
|
|
20
14
|
| { kind: "restart"; input: JsonValue };
|
|
21
15
|
|
|
22
16
|
export const WORKFLOW_RUNNER_MESSAGE_KINDS = [
|
|
@@ -31,7 +25,6 @@ export const WORKFLOW_RUNNER_MESSAGE_KINDS = [
|
|
|
31
25
|
"interaction.accepted",
|
|
32
26
|
"interaction.rejected",
|
|
33
27
|
"notification.requested",
|
|
34
|
-
"presentation.requested",
|
|
35
28
|
"effect.reserve",
|
|
36
29
|
"effect.settle",
|
|
37
30
|
"runner.progress",
|
|
@@ -43,13 +36,12 @@ export const WORKFLOW_RUNNER_STORE_OPERATIONS = [
|
|
|
43
36
|
"store.initializeRun",
|
|
44
37
|
"store.prepareRunResume",
|
|
45
38
|
"store.readRunState",
|
|
46
|
-
"store.
|
|
39
|
+
"store.commitTransition",
|
|
47
40
|
"store.publishUpdate",
|
|
48
41
|
"store.findSettingsScope",
|
|
49
42
|
"store.ensureSettingsScope",
|
|
50
43
|
"store.getSettingsScopeAtChange",
|
|
51
|
-
"store.
|
|
52
|
-
"store.readResolvedHumanDecision",
|
|
44
|
+
"store.readCheckpoint",
|
|
53
45
|
"store.reserveEffect",
|
|
54
46
|
"store.settleEffect",
|
|
55
47
|
"content.read",
|
|
@@ -59,7 +51,6 @@ export const WORKFLOW_RUNNER_STORE_OPERATIONS = [
|
|
|
59
51
|
"interaction.accept",
|
|
60
52
|
"interaction.reject",
|
|
61
53
|
"notification.request",
|
|
62
|
-
"presentation.request",
|
|
63
54
|
] as const;
|
|
64
55
|
export type WorkflowRunnerStoreOperation = (typeof WORKFLOW_RUNNER_STORE_OPERATIONS)[number];
|
|
65
56
|
const WORKFLOW_RUNNER_CONTROL_OPERATIONS = ["runner.ready", "runner.exiting"] as const;
|
|
@@ -142,15 +133,14 @@ export function runnerKindForOperation(
|
|
|
142
133
|
if (operation === "store.publishUpdate") return "node.update";
|
|
143
134
|
if (operation === "store.reserveEffect") return "effect.reserve";
|
|
144
135
|
if (operation === "store.settleEffect") return "effect.settle";
|
|
145
|
-
if (operation === "
|
|
136
|
+
if (operation === "interaction.request") {
|
|
146
137
|
return "interaction.requested";
|
|
147
138
|
}
|
|
148
139
|
if (operation === "interaction.accept") return "interaction.accepted";
|
|
149
140
|
if (operation === "interaction.reject") return "interaction.rejected";
|
|
150
141
|
if (operation === "notification.request") return "notification.requested";
|
|
151
|
-
if (operation === "
|
|
152
|
-
|
|
153
|
-
const event = payload.event;
|
|
142
|
+
if (operation === "store.commitTransition") {
|
|
143
|
+
const event = isRecord(payload.transition) ? payload.transition.event : undefined;
|
|
154
144
|
const eventType = isRecord(event) && typeof event.type === "string" ? event.type : "";
|
|
155
145
|
if (eventType === "node_started") return "node.started";
|
|
156
146
|
if (eventType === "node_finished") return "node.finished";
|
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
type InitializeWorkflowRunOptions,
|
|
7
7
|
type WorkflowExecutionStore,
|
|
8
8
|
} from "../workflows/store.js";
|
|
9
|
+
import type { WorkflowTransition } from "../workflows/transitions.js";
|
|
9
10
|
import type {
|
|
10
|
-
HumanDecisionRequest,
|
|
11
|
-
ResolvedHumanDecision,
|
|
12
11
|
WorkflowDefinition,
|
|
13
12
|
WorkflowEffectRecovery,
|
|
14
13
|
WorkflowEffectReservation,
|
|
@@ -16,7 +15,6 @@ import type {
|
|
|
16
15
|
WorkflowNotificationRequest,
|
|
17
16
|
WorkflowRunState,
|
|
18
17
|
WorkflowTraceEvent,
|
|
19
|
-
WorkflowTraceEventDraft,
|
|
20
18
|
WorkflowUpdateInput,
|
|
21
19
|
WorkflowUpdateRecord,
|
|
22
20
|
} from "../workflows/types.js";
|
|
@@ -71,28 +69,26 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
|
|
|
71
69
|
return await this.call<WorkflowRunState | null>("store.readRunState", { runId });
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
async
|
|
72
|
+
async commitTransition(
|
|
75
73
|
runId: string,
|
|
76
|
-
|
|
77
|
-
event: WorkflowTraceEventDraft,
|
|
74
|
+
transition: WorkflowTransition,
|
|
78
75
|
): Promise<WorkflowTraceEvent> {
|
|
79
76
|
return await this.call<WorkflowTraceEvent>(
|
|
80
|
-
"store.
|
|
81
|
-
{ runId,
|
|
82
|
-
event.attemptId,
|
|
77
|
+
"store.commitTransition",
|
|
78
|
+
{ runId, transition },
|
|
79
|
+
transition.event.attemptId,
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
async publishUpdate(
|
|
87
84
|
runId: string,
|
|
88
|
-
state: WorkflowRunState,
|
|
89
85
|
nodeId: string,
|
|
90
86
|
attemptId: string,
|
|
91
87
|
update: WorkflowUpdateInput,
|
|
92
88
|
): Promise<{ event: WorkflowTraceEvent; record: WorkflowUpdateRecord }> {
|
|
93
89
|
return await this.call<{ event: WorkflowTraceEvent; record: WorkflowUpdateRecord }>(
|
|
94
90
|
"store.publishUpdate",
|
|
95
|
-
{ runId,
|
|
91
|
+
{ runId, nodeId, attemptId, update },
|
|
96
92
|
attemptId,
|
|
97
93
|
);
|
|
98
94
|
}
|
|
@@ -128,18 +124,11 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
|
|
|
128
124
|
);
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
async
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async readResolvedHumanDecision(decisionId: string): Promise<ResolvedHumanDecision | null> {
|
|
140
|
-
return await this.call<ResolvedHumanDecision | null>("store.readResolvedHumanDecision", {
|
|
141
|
-
decisionId,
|
|
142
|
-
});
|
|
127
|
+
async readCheckpoint(runId: string, attemptId: string) {
|
|
128
|
+
const value = await this.call<
|
|
129
|
+
import("../workflows/requests.js").WorkflowCheckpointState | null
|
|
130
|
+
>("store.readCheckpoint", { runId, attemptId }, attemptId);
|
|
131
|
+
return value ?? undefined;
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
async reserveEffect(options: {
|
|
@@ -204,10 +193,6 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
|
|
|
204
193
|
);
|
|
205
194
|
}
|
|
206
195
|
|
|
207
|
-
async requestPresentation(instructions: string): Promise<void> {
|
|
208
|
-
await this.call("presentation.request", { instructions });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
196
|
private async call<T>(
|
|
212
197
|
operation: WorkflowRunnerStoreOperation,
|
|
213
198
|
payload: Record<string, unknown>,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import type { StateDatabase } from "./database.js";
|
|
3
|
+
|
|
4
|
+
export type AttemptClock = { now(): number; monotonic(): number };
|
|
5
|
+
const realClock: AttemptClock = { now: () => Date.now(), monotonic: () => performance.now() };
|
|
6
|
+
|
|
7
|
+
type Interval = { attemptId: string; intervalNumber: number; elapsedMs: number };
|
|
8
|
+
|
|
9
|
+
/** Records active execution in the existing database. Wall time never determines the budget. */
|
|
10
|
+
export class AttemptTime {
|
|
11
|
+
private readonly anchors = new Map<string, number>();
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly state: StateDatabase,
|
|
15
|
+
private readonly clock: AttemptClock = realClock,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
start(attemptId: string): void {
|
|
19
|
+
this.state.transaction(() => {
|
|
20
|
+
const open = this.openIntervals().find((interval) => interval.attemptId === attemptId);
|
|
21
|
+
if (open !== undefined) {
|
|
22
|
+
if (!this.anchors.has(intervalKey(open))) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Recover the previous host's active intervals before starting model work",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const row = this.state.connection
|
|
30
|
+
.prepare(
|
|
31
|
+
"SELECT COALESCE(MAX(interval_number), 0) + 1 AS next FROM attempt_active_intervals WHERE attempt_id = ?",
|
|
32
|
+
)
|
|
33
|
+
.get(attemptId) as { next: number };
|
|
34
|
+
const now = this.clock.now();
|
|
35
|
+
this.state.connection
|
|
36
|
+
.prepare(
|
|
37
|
+
"INSERT INTO attempt_active_intervals(attempt_id, interval_number, started_at, observed_at) VALUES (?, ?, ?, ?)",
|
|
38
|
+
)
|
|
39
|
+
.run(attemptId, row.next, now, now);
|
|
40
|
+
this.anchors.set(
|
|
41
|
+
intervalKey({ attemptId, intervalNumber: row.next }),
|
|
42
|
+
this.clock.monotonic(),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sample(): void {
|
|
48
|
+
const nested = this.state.connection.inTransaction;
|
|
49
|
+
const open = this.openIntervals();
|
|
50
|
+
this.state.transaction(() => {
|
|
51
|
+
for (const interval of open) this.sampleInterval(interval);
|
|
52
|
+
});
|
|
53
|
+
if (!nested) {
|
|
54
|
+
const active = new Set(open.map(intervalKey));
|
|
55
|
+
for (const key of this.anchors.keys()) {
|
|
56
|
+
if (!active.has(key)) this.anchors.delete(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
stop(attemptId: string): void {
|
|
62
|
+
this.state.transaction(() => {
|
|
63
|
+
const interval = this.openIntervals().find((candidate) => candidate.attemptId === attemptId);
|
|
64
|
+
if (interval === undefined) return;
|
|
65
|
+
this.sampleInterval(interval);
|
|
66
|
+
closeAttemptTime(this.state, attemptId);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
recover(): void {
|
|
71
|
+
// A crashed host can prove only the last saved sample, not time spent while offline.
|
|
72
|
+
this.state.connection
|
|
73
|
+
.prepare("UPDATE attempt_active_intervals SET ended_at = observed_at WHERE ended_at IS NULL")
|
|
74
|
+
.run();
|
|
75
|
+
this.anchors.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private sampleInterval(interval: Interval): void {
|
|
79
|
+
const anchor = this.anchors.get(intervalKey(interval));
|
|
80
|
+
if (anchor === undefined) {
|
|
81
|
+
throw new Error("Active interval belongs to a previous host; recover it before sampling");
|
|
82
|
+
}
|
|
83
|
+
const elapsedMs = Math.max(interval.elapsedMs, this.clock.monotonic() - anchor, 0);
|
|
84
|
+
this.state.connection
|
|
85
|
+
.prepare(
|
|
86
|
+
`UPDATE attempt_active_intervals SET elapsed_ms = ?, observed_at = ?
|
|
87
|
+
WHERE attempt_id = ? AND interval_number = ? AND ended_at IS NULL`,
|
|
88
|
+
)
|
|
89
|
+
.run(elapsedMs, this.clock.now(), interval.attemptId, interval.intervalNumber);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private openIntervals(): Interval[] {
|
|
93
|
+
return this.state.connection
|
|
94
|
+
.prepare(
|
|
95
|
+
`SELECT attempt_id AS attemptId, interval_number AS intervalNumber, elapsed_ms AS elapsedMs
|
|
96
|
+
FROM attempt_active_intervals WHERE ended_at IS NULL`,
|
|
97
|
+
)
|
|
98
|
+
.all() as Interval[];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function intervalKey(interval: Pick<Interval, "attemptId" | "intervalNumber">): string {
|
|
103
|
+
return JSON.stringify([interval.attemptId, interval.intervalNumber]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function closeRunTime(state: StateDatabase, runId: string): void {
|
|
107
|
+
state.connection
|
|
108
|
+
.prepare(
|
|
109
|
+
`UPDATE attempt_active_intervals SET ended_at = observed_at
|
|
110
|
+
WHERE ended_at IS NULL AND attempt_id IN (SELECT attempt_id FROM node_attempts WHERE run_id = ?)`,
|
|
111
|
+
)
|
|
112
|
+
.run(runId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Freeze the last host sample when an attempt settles outside its model turn. */
|
|
116
|
+
export function closeAttemptTime(state: StateDatabase, attemptId: string): void {
|
|
117
|
+
state.connection
|
|
118
|
+
.prepare(
|
|
119
|
+
"UPDATE attempt_active_intervals SET ended_at = observed_at WHERE attempt_id = ? AND ended_at IS NULL",
|
|
120
|
+
)
|
|
121
|
+
.run(attemptId);
|
|
122
|
+
}
|
package/src/state/json.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
-
export type JsonValue = JsonPrimitive | JsonValue[] |
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | JsonObject;
|
|
3
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
3
4
|
|
|
4
5
|
export function canonicalJson(value: unknown): string {
|
|
5
6
|
return JSON.stringify(normalizeJson(value));
|