@osolmaz/pi-workflows 0.16.5 → 0.16.7
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 +116 -166
- 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/widget.js +1 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -3
- package/dist/extension/workflow-message-coordinator.js +108 -94
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/render/graph-render.js +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 +9 -8
- package/dist/server/server.js +364 -541
- 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 +6 -5
- package/dist/server/view.js +68 -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.js +0 -7
- 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 -16
- package/dist/workflows/engine.js +128 -252
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +1 -1
- package/dist/workflows/errors.js +2 -2
- package/dist/workflows/errors.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 +177 -174
- 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/2026-09-07-workflow-handoff-plan.md +84 -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 +68 -29
- package/docs/WORKFLOW_SERVER.md +21 -10
- package/docs/WORKFLOW_STEP_MESSAGES.md +83 -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/workflows.md +111 -45
- 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 +118 -190
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/widget.ts +1 -1
- package/src/extension/workflow-message-coordinator.ts +133 -95
- package/src/render/graph-render.ts +1 -1
- 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 +416 -641
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +73 -58
- 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 +0 -13
- 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 +136 -299
- package/src/workflows/errors.ts +4 -2
- 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 +226 -236
- 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,8 @@ 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),
|
|
777
|
+
requestDeliveryConfirmed: this.requestDeliveryConfirmed(queue.runId),
|
|
798
778
|
errorMessage: state?.error ?? queue.errorMessage,
|
|
799
779
|
});
|
|
800
780
|
}
|
|
@@ -805,7 +785,7 @@ export class ServerViewStore {
|
|
|
805
785
|
`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
|
|
806
786
|
JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
|
|
807
787
|
WHERE t.run_id = ? AND t.state = 'started'
|
|
808
|
-
AND m.kind IN ('step', '
|
|
788
|
+
AND m.kind IN ('step', 'followUp') LIMIT 1`,
|
|
809
789
|
)
|
|
810
790
|
.get(runId);
|
|
811
791
|
return (
|
|
@@ -815,14 +795,25 @@ export class ServerViewStore {
|
|
|
815
795
|
);
|
|
816
796
|
}
|
|
817
797
|
|
|
818
|
-
private
|
|
798
|
+
private pendingRequestKind(runId: string): WorkflowDisplayFacts["pendingRequestKind"] {
|
|
799
|
+
const row = this.state.connection
|
|
800
|
+
.prepare("SELECT kind FROM interactive_requests WHERE run_id = ? AND status = 'pending'")
|
|
801
|
+
.get(runId) as
|
|
802
|
+
| { kind: Exclude<WorkflowDisplayFacts["pendingRequestKind"], null> }
|
|
803
|
+
| undefined;
|
|
804
|
+
return row?.kind ?? null;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private requestDeliveryConfirmed(runId: string): boolean {
|
|
819
808
|
const row = this.state.connection
|
|
820
809
|
.prepare(
|
|
821
|
-
`SELECT
|
|
822
|
-
|
|
810
|
+
`SELECT m.status FROM interactive_requests i
|
|
811
|
+
JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
|
|
812
|
+
WHERE i.run_id = ? AND i.status = 'pending'
|
|
813
|
+
ORDER BY m.order_number DESC LIMIT 1`,
|
|
823
814
|
)
|
|
824
|
-
.get(runId);
|
|
825
|
-
return row
|
|
815
|
+
.get(runId) as { status: string } | undefined;
|
|
816
|
+
return row?.status === "sent";
|
|
826
817
|
}
|
|
827
818
|
|
|
828
819
|
private hasAmbiguousEffect(runId: string): boolean {
|
|
@@ -857,7 +848,8 @@ export class ServerViewStore {
|
|
|
857
848
|
this.workflowActivityRevision(),
|
|
858
849
|
this.hasLiveRunner(runId),
|
|
859
850
|
this.hasActivity(runId),
|
|
860
|
-
this.
|
|
851
|
+
this.pendingRequestKind(runId),
|
|
852
|
+
this.requestDeliveryConfirmed(runId),
|
|
861
853
|
this.hasAmbiguousEffect(runId),
|
|
862
854
|
].join(":");
|
|
863
855
|
}
|
|
@@ -890,35 +882,54 @@ export type WorkflowDisplayFacts = {
|
|
|
890
882
|
ambiguous: boolean;
|
|
891
883
|
runnerActive: boolean;
|
|
892
884
|
originTurnActive: boolean;
|
|
893
|
-
|
|
885
|
+
pendingRequestKind: "agent" | "assistant" | "checkpoint" | "decision" | null;
|
|
886
|
+
requestDeliveryConfirmed: boolean;
|
|
894
887
|
errorMessage: string | null;
|
|
895
888
|
};
|
|
896
889
|
|
|
897
890
|
export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisplay {
|
|
898
891
|
let status: WorkflowDisplayStatus;
|
|
899
|
-
|
|
892
|
+
const activity: WorkflowDisplay["activity"] = facts.runnerActive
|
|
893
|
+
? "supervised_runner"
|
|
894
|
+
: facts.originTurnActive
|
|
895
|
+
? "origin_turn"
|
|
896
|
+
: null;
|
|
900
897
|
let reason: string | null = null;
|
|
901
898
|
|
|
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 (
|
|
899
|
+
if (
|
|
909
900
|
facts.durableStatus === "completed" ||
|
|
910
901
|
facts.durableStatus === "failed" ||
|
|
911
902
|
facts.durableStatus === "timed_out" ||
|
|
912
903
|
facts.durableStatus === "cancelled"
|
|
913
904
|
) {
|
|
914
905
|
status = facts.durableStatus;
|
|
915
|
-
reason = facts.
|
|
906
|
+
reason = facts.ambiguous
|
|
907
|
+
? "The run is terminal, but an external effect still needs explicit recovery."
|
|
908
|
+
: facts.errorMessage;
|
|
909
|
+
} else if (facts.ambiguous) {
|
|
910
|
+
status = "ambiguous";
|
|
911
|
+
reason = "An external effect needs explicit recovery.";
|
|
916
912
|
} else if (facts.paused) {
|
|
917
913
|
status = "paused";
|
|
918
914
|
reason = "The workflow is durably paused.";
|
|
919
|
-
} else if (facts.
|
|
915
|
+
} else if (facts.pendingRequestKind !== null || facts.durableStatus === "waiting") {
|
|
920
916
|
status = "waiting";
|
|
921
|
-
reason =
|
|
917
|
+
reason =
|
|
918
|
+
facts.pendingRequestKind === "decision"
|
|
919
|
+
? "The workflow needs a protected human decision."
|
|
920
|
+
: facts.pendingRequestKind === "checkpoint"
|
|
921
|
+
? "The workflow needs a checkpoint answer."
|
|
922
|
+
: facts.pendingRequestKind === "agent"
|
|
923
|
+
? "The workflow needs its assigned agent result."
|
|
924
|
+
: facts.pendingRequestKind === "assistant"
|
|
925
|
+
? "The workflow needs its assigned visible response."
|
|
926
|
+
: "The workflow is waiting.";
|
|
927
|
+
if (facts.pendingRequestKind === "agent" || facts.pendingRequestKind === "assistant") {
|
|
928
|
+
if (facts.originTurnActive) reason = "The agent is working on the workflow step.";
|
|
929
|
+
else if (!facts.requestDeliveryConfirmed) reason = "Workflow step delivery is not confirmed.";
|
|
930
|
+
}
|
|
931
|
+
} else if (activity !== null) {
|
|
932
|
+
status = "running";
|
|
922
933
|
} else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
|
|
923
934
|
status = "queued";
|
|
924
935
|
reason =
|
|
@@ -941,8 +952,12 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
|
|
|
941
952
|
if (facts.queueStatus === "parked") controls.push("resume");
|
|
942
953
|
controls.push("cancel");
|
|
943
954
|
}
|
|
944
|
-
if (status === "waiting")
|
|
945
|
-
|
|
955
|
+
if (status === "waiting") {
|
|
956
|
+
if (facts.pendingRequestKind === "checkpoint") controls.push("answer");
|
|
957
|
+
if (facts.pendingRequestKind === "decision") controls.push("human-answer");
|
|
958
|
+
if (facts.pendingRequestKind === "agent") controls.push("update", "submit");
|
|
959
|
+
}
|
|
960
|
+
if (facts.ambiguous) controls.push("review");
|
|
946
961
|
return { status, activity, controls, reason };
|
|
947
962
|
}
|
|
948
963
|
|
|
@@ -983,7 +998,7 @@ function projectQueue(
|
|
|
983
998
|
rootRunId: run.rootRunId,
|
|
984
999
|
lineageKind: run.lineageKind,
|
|
985
1000
|
restartNumber: run.restartNumber,
|
|
986
|
-
|
|
1001
|
+
parentRunRevision: run.parentRunRevision,
|
|
987
1002
|
errorCode: run.errorCode,
|
|
988
1003
|
createdAt: run.createdAt,
|
|
989
1004
|
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));
|