@osolmaz/pi-workflows 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { WorkflowClient } from "../client/client.js";
|
|
3
|
+
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
4
|
+
import type {
|
|
5
|
+
WorkflowSessionBinding,
|
|
6
|
+
WorkflowSessionCapture,
|
|
7
|
+
WorkflowSessionEventRecord,
|
|
8
|
+
} from "../workflows/types.js";
|
|
9
|
+
import type { SessionRecordingStore } from "./recorder.js";
|
|
10
|
+
|
|
11
|
+
type SessionCommandContext = {
|
|
12
|
+
targetSessionId: string;
|
|
13
|
+
coordinatorEpoch: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Sends bounded session recording batches to the single workflow host writer. */
|
|
17
|
+
export class RemoteSessionRecordingStore implements SessionRecordingStore {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly client: WorkflowClient,
|
|
20
|
+
private readonly context: () => SessionCommandContext,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async hasSessionBinding(runId: string): Promise<boolean> {
|
|
24
|
+
const receipt = await this.request(runId, "status");
|
|
25
|
+
if (typeof receipt.bound !== "boolean") throw new Error("Session recording status is invalid");
|
|
26
|
+
return receipt.bound;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async writeSessionBinding(
|
|
30
|
+
runId: string,
|
|
31
|
+
binding: WorkflowSessionBinding,
|
|
32
|
+
attemptId?: string,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
await this.request(runId, "bind", {
|
|
35
|
+
binding: toJson(binding),
|
|
36
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async writeSessionCapture(
|
|
41
|
+
runId: string,
|
|
42
|
+
capture: WorkflowSessionCapture,
|
|
43
|
+
attemptId?: string,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
await this.request(runId, "capture", {
|
|
46
|
+
capture: toJson(capture),
|
|
47
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async appendSessionEntry(
|
|
52
|
+
runId: string,
|
|
53
|
+
entry: Record<string, unknown>,
|
|
54
|
+
attemptId?: string,
|
|
55
|
+
): Promise<number> {
|
|
56
|
+
const receipt = await this.request(runId, "entries", {
|
|
57
|
+
entries: [toJson(entry)],
|
|
58
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
59
|
+
});
|
|
60
|
+
if (!Number.isSafeInteger(receipt.entrySequence) || (receipt.entrySequence as number) <= 0) {
|
|
61
|
+
throw new Error("Session recording entry receipt is invalid");
|
|
62
|
+
}
|
|
63
|
+
return receipt.entrySequence as number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async appendSessionEventBatch(
|
|
67
|
+
runId: string,
|
|
68
|
+
events: WorkflowSessionEventRecord[],
|
|
69
|
+
attemptId?: string,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
await this.request(runId, "events", {
|
|
72
|
+
events: toJson(events),
|
|
73
|
+
...(attemptId === undefined ? {} : { attemptId }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async sessionCounts(
|
|
78
|
+
runId: string,
|
|
79
|
+
attemptId?: string,
|
|
80
|
+
): Promise<{ eventCount: number; entryCount: number; lastEventSeq: number }> {
|
|
81
|
+
const receipt = await this.request(
|
|
82
|
+
runId,
|
|
83
|
+
"status",
|
|
84
|
+
attemptId === undefined ? {} : { attemptId },
|
|
85
|
+
);
|
|
86
|
+
const eventCount = positiveOrZero(receipt.eventCount, "eventCount");
|
|
87
|
+
const entryCount = positiveOrZero(receipt.entryCount, "entryCount");
|
|
88
|
+
const lastEventSeq = positiveOrZero(receipt.lastEventSeq, "lastEventSeq");
|
|
89
|
+
return { eventCount, entryCount, lastEventSeq };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async request(
|
|
93
|
+
runId: string,
|
|
94
|
+
action: "status" | "bind" | "entries" | "events" | "capture",
|
|
95
|
+
extra: Record<string, JsonValue> = {},
|
|
96
|
+
): Promise<Record<string, unknown>> {
|
|
97
|
+
const idempotencyKey = `session-record-${randomUUID()}`;
|
|
98
|
+
const response = await this.client.requestDurable({
|
|
99
|
+
operation: "session.record",
|
|
100
|
+
requestId: idempotencyKey,
|
|
101
|
+
idempotencyKey,
|
|
102
|
+
runId,
|
|
103
|
+
payload: { ...this.context(), action, ...extra },
|
|
104
|
+
});
|
|
105
|
+
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
106
|
+
throw new Error(response.error ?? "Workflow host rejected the session recording batch");
|
|
107
|
+
}
|
|
108
|
+
if (!isRecord(response.receipt)) throw new Error("Session recording receipt is invalid");
|
|
109
|
+
return response.receipt;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toJson(value: unknown): JsonValue {
|
|
114
|
+
return JSON.parse(canonicalJson(value)) as JsonValue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function positiveOrZero(value: unknown, name: string): number {
|
|
118
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
119
|
+
throw new Error(`Session recording ${name} is invalid`);
|
|
120
|
+
}
|
|
121
|
+
return value as number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
126
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export type PublicAssistantMessageEvent =
|
|
2
|
+
| { type: "start"; partial: unknown }
|
|
3
|
+
| { type: "text_start"; contentIndex: number; partial: unknown }
|
|
4
|
+
| { type: "text_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
5
|
+
| { type: "text_end"; contentIndex: number; content: string; partial: unknown }
|
|
6
|
+
| { type: "thinking_start"; contentIndex: number; partial: unknown }
|
|
7
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
8
|
+
| { type: "thinking_end"; contentIndex: number; content: string; partial: unknown }
|
|
9
|
+
| { type: "toolcall_start"; contentIndex: number; partial: unknown }
|
|
10
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
11
|
+
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
|
|
12
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
|
|
13
|
+
| { type: "error"; reason: "aborted" | "error"; error: unknown };
|
|
14
|
+
|
|
15
|
+
export type TurnStartEventLike = { turnIndex: number };
|
|
16
|
+
export type TurnEndEventLike = { turnIndex: number; message: unknown };
|
|
17
|
+
export type MessageStartEventLike = { message: unknown };
|
|
18
|
+
export type MessageUpdateEventLike = {
|
|
19
|
+
message: unknown;
|
|
20
|
+
assistantMessageEvent: PublicAssistantMessageEvent;
|
|
21
|
+
};
|
|
22
|
+
export type MessageEndEventLike = { message: unknown };
|
|
23
|
+
export type ToolExecutionStartEventLike = {
|
|
24
|
+
toolCallId: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
args: unknown;
|
|
27
|
+
};
|
|
28
|
+
export type ToolExecutionUpdateEventLike = { toolCallId: string };
|
|
29
|
+
export type ToolExecutionEndEventLike = {
|
|
30
|
+
toolCallId: string;
|
|
31
|
+
toolName: string;
|
|
32
|
+
result: unknown;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type NormalizedAssistantEvent =
|
|
37
|
+
| { type: "start" }
|
|
38
|
+
| { type: "text_start"; contentIndex: number }
|
|
39
|
+
| { type: "text_delta"; contentIndex: number; delta: string }
|
|
40
|
+
| { type: "text_end"; contentIndex: number; content: string }
|
|
41
|
+
| { type: "thinking_start"; contentIndex: number }
|
|
42
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string }
|
|
43
|
+
| { type: "thinking_end"; contentIndex: number; content: string }
|
|
44
|
+
| { type: "toolcall_start"; contentIndex: number }
|
|
45
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string }
|
|
46
|
+
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
|
|
47
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
|
|
48
|
+
| { type: "error"; reason: "aborted" | "error" };
|
|
49
|
+
|
|
50
|
+
/** Remove cumulative snapshots while preserving every semantic stream event. */
|
|
51
|
+
export function normalizeAssistantEvent(
|
|
52
|
+
event: PublicAssistantMessageEvent,
|
|
53
|
+
): NormalizedAssistantEvent {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "start":
|
|
56
|
+
return { type: event.type };
|
|
57
|
+
case "text_start":
|
|
58
|
+
case "thinking_start":
|
|
59
|
+
case "toolcall_start":
|
|
60
|
+
return { type: event.type, contentIndex: event.contentIndex };
|
|
61
|
+
case "text_delta":
|
|
62
|
+
case "thinking_delta":
|
|
63
|
+
case "toolcall_delta":
|
|
64
|
+
return { type: event.type, contentIndex: event.contentIndex, delta: event.delta };
|
|
65
|
+
case "text_end":
|
|
66
|
+
case "thinking_end":
|
|
67
|
+
return { type: event.type, contentIndex: event.contentIndex, content: event.content };
|
|
68
|
+
case "toolcall_end":
|
|
69
|
+
return {
|
|
70
|
+
type: event.type,
|
|
71
|
+
contentIndex: event.contentIndex,
|
|
72
|
+
toolCall: event.toolCall,
|
|
73
|
+
};
|
|
74
|
+
case "done":
|
|
75
|
+
return { type: event.type, reason: event.reason };
|
|
76
|
+
case "error":
|
|
77
|
+
return { type: event.type, reason: event.reason };
|
|
78
|
+
default: {
|
|
79
|
+
const exhaustive: never = event;
|
|
80
|
+
return exhaustive;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function messageRole(message: unknown): string {
|
|
86
|
+
if (typeof message !== "object" || message === null || !("role" in message)) {
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
const role = (message as { role?: unknown }).role;
|
|
90
|
+
return typeof role === "string" && role.length > 0 ? role : "unknown";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function toolCallIdFromAssistantEvent(event: NormalizedAssistantEvent): string | null {
|
|
94
|
+
if (event.type !== "toolcall_end" || typeof event.toolCall !== "object" || !event.toolCall) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const id = (event.toolCall as { id?: unknown }).id;
|
|
98
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function toolStartedPayload(event: ToolExecutionStartEventLike): Record<string, unknown> {
|
|
102
|
+
return { toolName: event.toolName, args: event.args };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toolFinishedPayload(event: ToolExecutionEndEventLike): Record<string, unknown> {
|
|
106
|
+
return { toolName: event.toolName, isError: event.isError, result: event.result };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function turnFinishedPayload(
|
|
110
|
+
event: TurnEndEventLike,
|
|
111
|
+
messageId: string | undefined,
|
|
112
|
+
toolCallIds: string[],
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
return {
|
|
115
|
+
turnIndex: event.turnIndex,
|
|
116
|
+
...(messageId === undefined ? {} : { messageId }),
|
|
117
|
+
toolCallIds,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import type { WorkflowSessionView } from "../client/view.js";
|
|
3
|
+
import type {
|
|
4
|
+
WorkflowDefinitionSnapshot,
|
|
5
|
+
WorkflowRunState,
|
|
6
|
+
WorkflowUpdateRecord,
|
|
7
|
+
} from "../workflows/types.js";
|
|
5
8
|
import { buildWidgetView } from "./widget.js";
|
|
6
9
|
|
|
7
10
|
const WIDGET_KEY = "pi-workflows";
|
|
8
11
|
const WIDGET_SCROLL_STEP = 3;
|
|
9
12
|
|
|
10
|
-
/**
|
|
13
|
+
/** Client-backed projection of the host-owned run into the origin Pi session. */
|
|
11
14
|
export class SessionWorkflowView {
|
|
12
|
-
private
|
|
15
|
+
private session: WorkflowSessionView | null = null;
|
|
13
16
|
private scroll: number | null = null;
|
|
14
17
|
private shownScroll = 0;
|
|
15
18
|
private maxScroll = 0;
|
|
16
19
|
private stepCount = 0;
|
|
17
20
|
private visible = false;
|
|
21
|
+
private actionHint: string | undefined;
|
|
22
|
+
private lastNoticeKey: string | null = null;
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
24
|
+
update(session: WorkflowSessionView, ctx: ExtensionContext): void {
|
|
25
|
+
const run = session.run;
|
|
26
|
+
if (
|
|
27
|
+
run === null ||
|
|
28
|
+
!isWorkflowRunState(run.state) ||
|
|
29
|
+
!isWorkflowDefinitionSnapshot(run.workflow)
|
|
30
|
+
) {
|
|
31
|
+
this.session = session;
|
|
32
|
+
this.clearWidget(ctx);
|
|
23
33
|
return;
|
|
24
34
|
}
|
|
25
|
-
|
|
35
|
+
const previousRun = this.session?.run;
|
|
36
|
+
if (previousRun?.runId !== run.runId || this.stepCount !== run.state.steps.length) {
|
|
26
37
|
this.scroll = null;
|
|
27
|
-
this.stepCount =
|
|
38
|
+
this.stepCount = run.state.steps.length;
|
|
39
|
+
}
|
|
40
|
+
this.session = session;
|
|
41
|
+
this.notifyTransition(previousRun, session, ctx);
|
|
42
|
+
this.render(ctx);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setActionHint(hint: string | undefined, ctx: ExtensionContext): void {
|
|
46
|
+
this.actionHint = hint;
|
|
47
|
+
if (this.session?.run !== null) this.render(ctx);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
refresh(ctx: ExtensionContext): void {
|
|
51
|
+
if (this.session?.run === null || this.session === null) {
|
|
52
|
+
this.clearWidget(ctx);
|
|
53
|
+
return;
|
|
28
54
|
}
|
|
29
|
-
this.loaded = loaded;
|
|
30
55
|
this.render(ctx);
|
|
31
56
|
}
|
|
32
57
|
|
|
@@ -39,42 +64,54 @@ export class SessionWorkflowView {
|
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
clear(ctx: ExtensionContext): void {
|
|
42
|
-
this.
|
|
67
|
+
this.session = null;
|
|
43
68
|
this.scroll = null;
|
|
44
69
|
this.shownScroll = 0;
|
|
45
70
|
this.maxScroll = 0;
|
|
46
71
|
this.stepCount = 0;
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
safelyUpdateUi(ctx, () => {
|
|
50
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
51
|
-
ctx.ui.setStatus(WIDGET_KEY, undefined);
|
|
52
|
-
});
|
|
72
|
+
this.lastNoticeKey = null;
|
|
73
|
+
this.clearWidget(ctx);
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
private scrollBy(ctx: ExtensionContext, delta: number): void {
|
|
56
|
-
if (this.
|
|
77
|
+
if (this.session?.run === null || this.session === null) return;
|
|
57
78
|
const current = this.scroll ?? this.shownScroll;
|
|
58
79
|
this.scroll = Math.max(0, Math.min(this.maxScroll, current + delta));
|
|
59
80
|
this.render(ctx);
|
|
60
81
|
}
|
|
61
82
|
|
|
83
|
+
private clearWidget(ctx: ExtensionContext): void {
|
|
84
|
+
if (!this.visible) return;
|
|
85
|
+
this.visible = false;
|
|
86
|
+
safelyUpdateUi(ctx, () => {
|
|
87
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
88
|
+
ctx.ui.setStatus(WIDGET_KEY, undefined);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
private render(ctx: ExtensionContext): void {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
93
|
+
const run = this.session?.run;
|
|
94
|
+
if (run === null || run === undefined) return;
|
|
95
|
+
if (!isWorkflowRunState(run.state) || !isWorkflowDefinitionSnapshot(run.workflow)) return;
|
|
96
|
+
const state = run.state;
|
|
97
|
+
const snapshot = run.workflow;
|
|
65
98
|
const render = (
|
|
66
99
|
width = Number.POSITIVE_INFINITY,
|
|
67
100
|
theme?: Parameters<typeof buildWidgetView>[6],
|
|
68
101
|
) => {
|
|
69
102
|
const view = buildWidgetView(
|
|
70
|
-
|
|
71
|
-
|
|
103
|
+
state,
|
|
104
|
+
snapshot,
|
|
72
105
|
new Date(),
|
|
73
106
|
this.scroll,
|
|
74
|
-
|
|
107
|
+
run.display.status === "paused",
|
|
75
108
|
width,
|
|
76
109
|
theme,
|
|
77
|
-
|
|
110
|
+
workflowUpdates(run.updates),
|
|
111
|
+
this.actionHint,
|
|
112
|
+
run.display.status,
|
|
113
|
+
run.display.reason,
|
|
114
|
+
run.display.controls,
|
|
78
115
|
);
|
|
79
116
|
this.shownScroll = view.scroll;
|
|
80
117
|
this.maxScroll = view.maxScroll;
|
|
@@ -90,42 +127,111 @@ export class SessionWorkflowView {
|
|
|
90
127
|
} else {
|
|
91
128
|
ctx.ui.setWidget(WIDGET_KEY, render());
|
|
92
129
|
}
|
|
93
|
-
const
|
|
94
|
-
ctx.ui.setStatus(
|
|
130
|
+
const focus = state.currentNode ?? state.waitingOn;
|
|
131
|
+
ctx.ui.setStatus(
|
|
132
|
+
WIDGET_KEY,
|
|
133
|
+
`${state.workflowName} [${run.display.status}]${focus === undefined ? "" : ` ${focus}`}`,
|
|
134
|
+
);
|
|
95
135
|
this.visible = true;
|
|
96
136
|
});
|
|
97
137
|
}
|
|
98
|
-
}
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
private notifyTransition(
|
|
140
|
+
previousRun: WorkflowSessionView["run"] | undefined,
|
|
141
|
+
session: WorkflowSessionView,
|
|
142
|
+
ctx: ExtensionContext,
|
|
143
|
+
): void {
|
|
144
|
+
const run = session.run;
|
|
145
|
+
if (run === null || !isWorkflowRunState(run.state)) return;
|
|
146
|
+
const status = run.display.status;
|
|
147
|
+
const decision = session.pendingInteractions.some(
|
|
148
|
+
(value) => isRecord(value) && value.kind === "decision" && value.status === "pending",
|
|
149
|
+
);
|
|
150
|
+
const shouldNotify =
|
|
151
|
+
isTerminalStatus(status) ||
|
|
152
|
+
(status === "waiting" && (decision || previousRun?.display.status !== "waiting")) ||
|
|
153
|
+
(status === "paused" && previousRun?.display.status !== "paused");
|
|
154
|
+
if (!shouldNotify) return;
|
|
155
|
+
const key = `${run.runId}:${status}:${decision ? "decision" : "workflow"}`;
|
|
156
|
+
if (key === this.lastNoticeKey) return;
|
|
157
|
+
this.lastNoticeKey = key;
|
|
158
|
+
const reason = run.display.reason?.trim();
|
|
159
|
+
const message = decision
|
|
160
|
+
? `Workflow ${run.state.workflowName} needs a human decision.`
|
|
161
|
+
: reason && reason.length > 0
|
|
162
|
+
? reason
|
|
163
|
+
: `Workflow ${run.state.workflowName} ${status.replace("_", " ")}.`;
|
|
164
|
+
safelyUpdateUi(ctx, () => {
|
|
165
|
+
ctx.ui.notify(
|
|
166
|
+
message,
|
|
167
|
+
status === "failed" || status === "timed_out" || status === "ambiguous"
|
|
168
|
+
? "error"
|
|
169
|
+
: status === "waiting" || status === "paused"
|
|
170
|
+
? "warning"
|
|
171
|
+
: "info",
|
|
172
|
+
);
|
|
105
173
|
});
|
|
106
|
-
let runId: string | undefined;
|
|
107
|
-
try {
|
|
108
|
-
runId = queue.findSessionReservation(sessionId)?.runId;
|
|
109
|
-
} finally {
|
|
110
|
-
queue.close();
|
|
111
|
-
}
|
|
112
|
-
if (runId === undefined) return undefined;
|
|
113
|
-
|
|
114
|
-
const runs = new WorkflowRunStore(workflowStatePath(), { readOnly: true });
|
|
115
|
-
try {
|
|
116
|
-
return runs.readRun(runId) ?? undefined;
|
|
117
|
-
} finally {
|
|
118
|
-
runs.close();
|
|
119
|
-
}
|
|
120
|
-
} catch {
|
|
121
|
-
return undefined;
|
|
122
174
|
}
|
|
123
175
|
}
|
|
124
176
|
|
|
177
|
+
function isWorkflowRunState(value: unknown): value is WorkflowRunState {
|
|
178
|
+
return (
|
|
179
|
+
typeof value === "object" &&
|
|
180
|
+
value !== null &&
|
|
181
|
+
!Array.isArray(value) &&
|
|
182
|
+
(value as { schema?: unknown }).schema === "pi-workflows.run-state.v1" &&
|
|
183
|
+
typeof (value as { workflowName?: unknown }).workflowName === "string" &&
|
|
184
|
+
Array.isArray((value as { steps?: unknown }).steps)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function workflowUpdates(values: readonly unknown[]): WorkflowUpdateRecord[] {
|
|
189
|
+
return values.filter(isWorkflowUpdateRecord);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isWorkflowUpdateRecord(value: unknown): value is WorkflowUpdateRecord {
|
|
193
|
+
return (
|
|
194
|
+
isRecord(value) &&
|
|
195
|
+
typeof value.updateId === "string" &&
|
|
196
|
+
typeof value.seq === "number" &&
|
|
197
|
+
typeof value.at === "string" &&
|
|
198
|
+
typeof value.runId === "string" &&
|
|
199
|
+
typeof value.nodeId === "string" &&
|
|
200
|
+
typeof value.attemptId === "string" &&
|
|
201
|
+
typeof value.type === "string" &&
|
|
202
|
+
typeof value.key === "string" &&
|
|
203
|
+
isRecord(value.data)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isWorkflowDefinitionSnapshot(value: unknown): value is WorkflowDefinitionSnapshot {
|
|
208
|
+
return (
|
|
209
|
+
typeof value === "object" &&
|
|
210
|
+
value !== null &&
|
|
211
|
+
!Array.isArray(value) &&
|
|
212
|
+
(value as { schema?: unknown }).schema === "pi-workflows.definition-snapshot.v1" &&
|
|
213
|
+
typeof (value as { nodes?: unknown }).nodes === "object"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isTerminalStatus(status: string): boolean {
|
|
218
|
+
return (
|
|
219
|
+
status === "completed" ||
|
|
220
|
+
status === "failed" ||
|
|
221
|
+
status === "timed_out" ||
|
|
222
|
+
status === "cancelled" ||
|
|
223
|
+
status === "ambiguous"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
228
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
125
231
|
function safelyUpdateUi(ctx: ExtensionContext, update: () => void): void {
|
|
126
232
|
try {
|
|
127
233
|
if (ctx.hasUI) update();
|
|
128
234
|
} catch {
|
|
129
|
-
// A session replacement can make a captured context stale between
|
|
235
|
+
// A session replacement can make a captured context stale between updates.
|
|
130
236
|
}
|
|
131
237
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AgentStepPresentation,
|
|
7
7
|
AgentStepSubmission,
|
|
8
8
|
} from "../workflows/types.js";
|
|
9
|
+
import { WORKFLOW_STEP_MESSAGE_TYPE } from "../workflows/workflow-message-content.js";
|
|
9
10
|
import {
|
|
10
11
|
cleanOptionalSingleLine,
|
|
11
12
|
cleanSingleLine,
|
|
@@ -15,17 +16,19 @@ import {
|
|
|
15
16
|
type MessageCardView,
|
|
16
17
|
} from "./message-card.js";
|
|
17
18
|
|
|
18
|
-
export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE =
|
|
19
|
+
export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE = WORKFLOW_STEP_MESSAGE_TYPE;
|
|
19
20
|
export const WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA = "pi-workflows.agent-step-message.v1";
|
|
20
21
|
|
|
21
|
-
type
|
|
22
|
+
type PromptDeliveryReason = "initial" | "reminder" | "resumed";
|
|
22
23
|
|
|
23
24
|
export type WorkflowAgentStepMessageDetails = {
|
|
24
25
|
schema: typeof WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA;
|
|
25
|
-
kind:
|
|
26
|
+
kind: "step";
|
|
27
|
+
reason: PromptDeliveryReason;
|
|
26
28
|
contract: AgentStepContract;
|
|
27
29
|
presentation?: AgentStepPresentation;
|
|
28
|
-
|
|
30
|
+
requestId: string;
|
|
31
|
+
workflowMessageId: string;
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
type WorkflowAgentStepMessage = {
|
|
@@ -51,14 +54,14 @@ export function buildWorkflowAgentStepView(
|
|
|
51
54
|
): WorkflowAgentStepView {
|
|
52
55
|
const details = parseDetails(message.details);
|
|
53
56
|
const contract = details?.contract;
|
|
54
|
-
const
|
|
57
|
+
const reason = details?.reason ?? "initial";
|
|
55
58
|
const workflowName = cleanSingleLine(contract?.workflowName ?? "Workflow");
|
|
56
59
|
const nodeId = cleanSingleLine(contract?.nodeId ?? "step");
|
|
57
60
|
const runTitle = cleanOptionalSingleLine(details?.presentation?.runTitle);
|
|
58
61
|
const statusDetail = cleanOptionalSingleLine(details?.presentation?.statusDetail);
|
|
59
62
|
const label = runTitle ?? workflowName;
|
|
60
|
-
const suffix =
|
|
61
|
-
const glyph =
|
|
63
|
+
const suffix = reason === "initial" ? "" : ` · ${reason}`;
|
|
64
|
+
const glyph = reason === "initial" ? "▶" : "↻";
|
|
62
65
|
const title = paintMessageCard(theme, "accent", `${glyph} ${label} › ${nodeId}${suffix}`);
|
|
63
66
|
|
|
64
67
|
if (!expanded) {
|
|
@@ -80,7 +83,7 @@ export function buildWorkflowAgentStepView(
|
|
|
80
83
|
`Run id: ${cleanSingleLine(contract?.runId ?? "unknown")}`,
|
|
81
84
|
`Node id: ${nodeId}`,
|
|
82
85
|
`Attempt id: ${cleanSingleLine(contract?.attemptId ?? "unknown")}`,
|
|
83
|
-
`
|
|
86
|
+
`Reason: ${reason}`,
|
|
84
87
|
`Completion: ${assistant ? "assistant response" : "workflow submission"}`,
|
|
85
88
|
`Expected output: ${cleanSingleLine(expectedOutput)}`,
|
|
86
89
|
];
|
|
@@ -220,7 +223,14 @@ function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefin
|
|
|
220
223
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
221
224
|
const candidate = value as Partial<WorkflowAgentStepMessageDetails>;
|
|
222
225
|
if (candidate.schema !== WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA) return undefined;
|
|
223
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
candidate.kind !== "step" ||
|
|
228
|
+
(candidate.reason !== "initial" &&
|
|
229
|
+
candidate.reason !== "reminder" &&
|
|
230
|
+
candidate.reason !== "resumed") ||
|
|
231
|
+
typeof candidate.requestId !== "string" ||
|
|
232
|
+
typeof candidate.workflowMessageId !== "string"
|
|
233
|
+
) {
|
|
224
234
|
return undefined;
|
|
225
235
|
}
|
|
226
236
|
const contract = candidate.contract;
|