@osolmaz/pi-workflows 0.16.0 → 0.16.2
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 +18 -2
- 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/client.d.ts +1 -0
- package/dist/client/client.js +5 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +11 -5
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +48 -16
- package/dist/client/view.js +1 -0
- package/dist/client/view.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +20 -165
- package/dist/controllers/sqlite.js +178 -436
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +637 -534
- 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 +2 -0
- package/dist/extension/session-view.js +60 -2
- 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 +2 -2
- package/dist/extension/widget.js +28 -18
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +28 -0
- package/dist/extension/workflow-message-coordinator.js +346 -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/runner.d.ts +50 -8
- package/dist/host/runner.js +1632 -341
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +19 -16
- package/dist/host/state.js +251 -63
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +19 -9
- package/dist/host/view.js +227 -119
- package/dist/host/view.js.map +1 -1
- package/dist/host/worker-entry.d.ts +4 -1
- package/dist/host/worker-entry.js +23 -24
- package/dist/host/worker-entry.js.map +1 -1
- package/dist/host/worker-protocol.d.ts +15 -0
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/prune.js +3 -25
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +76 -97
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +98 -0
- package/dist/state/workflow-messages.js +366 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +1 -1
- package/dist/viewer/tui.js +65 -8
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/composition.js +0 -4
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.js +0 -8
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +4 -5
- package/dist/workflows/engine.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/schema.js +0 -4
- package/dist/workflows/schema.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 +20 -19
- package/dist/workflows/store.js +163 -354
- 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 +6 -8
- 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 +5 -1
- package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +363 -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 +25 -17
- package/docs/WORKFLOW_HOST.md +105 -31
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/live-replay-protocol.md +8 -4
- package/docs/tui-viewer.md +3 -1
- package/docs/workflows.md +44 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +14 -5
- 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/client.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/client/protocol.ts +11 -5
- package/src/client/view.ts +51 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +247 -845
- package/src/extension/index.ts +712 -647
- 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 +86 -3
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +37 -17
- package/src/extension/workflow-message-coordinator.ts +398 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2163 -525
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +254 -122
- package/src/host/worker-entry.ts +38 -30
- package/src/host/worker-protocol.ts +11 -0
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +599 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- package/src/workflows/composition.ts +0 -5
- package/src/workflows/definition.ts +0 -8
- package/src/workflows/engine.ts +4 -6
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/schema.ts +0 -6
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +237 -489
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +6 -8
- 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/src/extension/decision-channels.ts +0 -1826
|
@@ -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,6 +1,10 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { WorkflowSessionView } from "../client/view.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
WorkflowDefinitionSnapshot,
|
|
5
|
+
WorkflowRunState,
|
|
6
|
+
WorkflowUpdateRecord,
|
|
7
|
+
} from "../workflows/types.js";
|
|
4
8
|
import { buildWidgetView } from "./widget.js";
|
|
5
9
|
|
|
6
10
|
const WIDGET_KEY = "pi-workflows";
|
|
@@ -15,6 +19,7 @@ export class SessionWorkflowView {
|
|
|
15
19
|
private stepCount = 0;
|
|
16
20
|
private visible = false;
|
|
17
21
|
private actionHint: string | undefined;
|
|
22
|
+
private lastNoticeKey: string | null = null;
|
|
18
23
|
|
|
19
24
|
update(session: WorkflowSessionView, ctx: ExtensionContext): void {
|
|
20
25
|
const run = session.run;
|
|
@@ -33,6 +38,7 @@ export class SessionWorkflowView {
|
|
|
33
38
|
this.stepCount = run.state.steps.length;
|
|
34
39
|
}
|
|
35
40
|
this.session = session;
|
|
41
|
+
this.notifyTransition(previousRun, session, ctx);
|
|
36
42
|
this.render(ctx);
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -63,6 +69,7 @@ export class SessionWorkflowView {
|
|
|
63
69
|
this.shownScroll = 0;
|
|
64
70
|
this.maxScroll = 0;
|
|
65
71
|
this.stepCount = 0;
|
|
72
|
+
this.lastNoticeKey = null;
|
|
66
73
|
this.clearWidget(ctx);
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -100,9 +107,11 @@ export class SessionWorkflowView {
|
|
|
100
107
|
run.display.status === "paused",
|
|
101
108
|
width,
|
|
102
109
|
theme,
|
|
103
|
-
|
|
110
|
+
workflowUpdates(run.updates),
|
|
104
111
|
this.actionHint,
|
|
105
112
|
run.display.status,
|
|
113
|
+
run.display.reason,
|
|
114
|
+
run.display.controls,
|
|
106
115
|
);
|
|
107
116
|
this.shownScroll = view.scroll;
|
|
108
117
|
this.maxScroll = view.maxScroll;
|
|
@@ -118,10 +127,51 @@ export class SessionWorkflowView {
|
|
|
118
127
|
} else {
|
|
119
128
|
ctx.ui.setWidget(WIDGET_KEY, render());
|
|
120
129
|
}
|
|
121
|
-
|
|
130
|
+
const focus = state.currentNode ?? state.waitingOn;
|
|
131
|
+
ctx.ui.setStatus(
|
|
132
|
+
WIDGET_KEY,
|
|
133
|
+
`${state.workflowName} [${run.display.status}]${focus === undefined ? "" : ` ${focus}`}`,
|
|
134
|
+
);
|
|
122
135
|
this.visible = true;
|
|
123
136
|
});
|
|
124
137
|
}
|
|
138
|
+
|
|
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
|
+
);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
125
175
|
}
|
|
126
176
|
|
|
127
177
|
function isWorkflowRunState(value: unknown): value is WorkflowRunState {
|
|
@@ -135,6 +185,25 @@ function isWorkflowRunState(value: unknown): value is WorkflowRunState {
|
|
|
135
185
|
);
|
|
136
186
|
}
|
|
137
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
|
+
|
|
138
207
|
function isWorkflowDefinitionSnapshot(value: unknown): value is WorkflowDefinitionSnapshot {
|
|
139
208
|
return (
|
|
140
209
|
typeof value === "object" &&
|
|
@@ -145,6 +214,20 @@ function isWorkflowDefinitionSnapshot(value: unknown): value is WorkflowDefiniti
|
|
|
145
214
|
);
|
|
146
215
|
}
|
|
147
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
|
+
|
|
148
231
|
function safelyUpdateUi(ctx: ExtensionContext, update: () => void): void {
|
|
149
232
|
try {
|
|
150
233
|
if (ctx.hasUI) update();
|
|
@@ -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;
|
package/src/extension/widget.ts
CHANGED
|
@@ -38,17 +38,28 @@ const STATUS_GLYPHS: Record<WorkflowDisplayStatus, string> = {
|
|
|
38
38
|
const PI_MAX_WIDGET_LINES = 10;
|
|
39
39
|
const MAX_NODE_ERROR_CHARS = 120;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
function displayedRunningNode(
|
|
42
|
+
state: WorkflowRunState,
|
|
43
|
+
displayStatus: WorkflowDisplayStatus | undefined,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
return state.currentNode ?? (displayStatus === "running" ? state.waitingOn : undefined);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function nodeGlyph(
|
|
49
|
+
state: WorkflowRunState,
|
|
50
|
+
nodeId: string,
|
|
51
|
+
displayStatus?: WorkflowDisplayStatus,
|
|
52
|
+
): string {
|
|
53
|
+
if (displayedRunningNode(state, displayStatus) === nodeId) {
|
|
43
54
|
return "◐";
|
|
44
55
|
}
|
|
56
|
+
if (state.waitingOn === nodeId) {
|
|
57
|
+
return "⏸";
|
|
58
|
+
}
|
|
45
59
|
const result = state.results[nodeId];
|
|
46
60
|
if (!result) {
|
|
47
61
|
return "·";
|
|
48
62
|
}
|
|
49
|
-
if (state.waitingOn === nodeId) {
|
|
50
|
-
return "⏸";
|
|
51
|
-
}
|
|
52
63
|
return result.outcome === "ok" ? "✓" : "✗";
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -83,6 +94,8 @@ export function buildWidgetView(
|
|
|
83
94
|
updateHistory?: WorkflowUpdateRecord[],
|
|
84
95
|
actionHint?: string,
|
|
85
96
|
displayStatus?: WorkflowDisplayStatus,
|
|
97
|
+
displayReason?: string | null,
|
|
98
|
+
controls?: readonly string[],
|
|
86
99
|
): WidgetView {
|
|
87
100
|
const availableWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : width;
|
|
88
101
|
if (availableWidth === 0) return { lines: [], scroll: 0, maxScroll: 0 };
|
|
@@ -100,19 +113,23 @@ export function buildWidgetView(
|
|
|
100
113
|
const header = `${paint(theme, headerTone, glyph)} workflow ${sanitizeText(state.workflowName)}${title} ${paint(theme, headerTone, `[${statusText}]`)}`;
|
|
101
114
|
|
|
102
115
|
const footer: string[] = [];
|
|
116
|
+
if (displayReason) {
|
|
117
|
+
footer.push(paint(theme, "dim", ` ${sanitizeText(displayReason)}`));
|
|
118
|
+
}
|
|
119
|
+
if (controls !== undefined && controls.length > 0) {
|
|
120
|
+
footer.push(paint(theme, "dim", ` controls: ${controls.join(", ")}`));
|
|
121
|
+
}
|
|
103
122
|
if (state.error) {
|
|
104
123
|
footer.push(paint(theme, "error", ` error: ${truncate(sanitizeText(state.error), 120)}`));
|
|
105
124
|
}
|
|
106
125
|
if (status === "waiting" && state.waitingOn) {
|
|
107
|
-
footer.push(
|
|
108
|
-
paint(theme, "warning", ` waiting on checkpoint: ${sanitizeText(state.waitingOn)}`),
|
|
109
|
-
);
|
|
126
|
+
footer.push(paint(theme, "warning", ` waiting on step: ${sanitizeText(state.waitingOn)}`));
|
|
110
127
|
}
|
|
111
128
|
const hint = actionHint?.trim() ? sanitizeText(actionHint) : undefined;
|
|
112
129
|
const progress = progressLines(state, now, updateHistory).slice(0, 4);
|
|
113
130
|
const baseBudget = PI_MAX_WIDGET_LINES - 1 - footer.length - progress.length;
|
|
114
131
|
const nodes = displayNodeIds(snapshot).map((nodeId) =>
|
|
115
|
-
compactNodeLine(state, snapshot, nodeId, now, paused, theme),
|
|
132
|
+
compactNodeLine(state, snapshot, nodeId, now, paused, status, theme),
|
|
116
133
|
);
|
|
117
134
|
const combineHintWithWindow =
|
|
118
135
|
hint !== undefined && nodes.length > 0 && nodes.length + 1 > baseBudget;
|
|
@@ -260,15 +277,17 @@ function compactNodeLine(
|
|
|
260
277
|
nodeId: string,
|
|
261
278
|
now: Date,
|
|
262
279
|
paused: boolean,
|
|
280
|
+
displayStatus: WorkflowDisplayStatus,
|
|
263
281
|
theme?: WidgetTheme,
|
|
264
282
|
): string {
|
|
265
283
|
const node = snapshot.nodes[nodeId];
|
|
266
|
-
const
|
|
284
|
+
const runningNode = displayedRunningNode(state, displayStatus);
|
|
285
|
+
const isCurrent = runningNode === nodeId;
|
|
286
|
+
const isWaiting = state.waitingOn === nodeId && !isCurrent;
|
|
287
|
+
const glyph = nodeGlyph(state, nodeId, displayStatus);
|
|
267
288
|
const typeGlyph = node ? nodeTypeGlyph(node.nodeType, node.actionExecution) : "?";
|
|
268
289
|
const name = sanitizeText(nodeId);
|
|
269
|
-
const segments = nodeRuntimeSegments(state, snapshot, nodeId, now);
|
|
270
|
-
const isCurrent = state.currentNode === nodeId;
|
|
271
|
-
const isWaiting = state.waitingOn === nodeId;
|
|
290
|
+
const segments = nodeRuntimeSegments(state, snapshot, nodeId, now, isCurrent);
|
|
272
291
|
|
|
273
292
|
if (isCurrent || isWaiting) {
|
|
274
293
|
const tone = paused || isWaiting ? "warning" : "accent";
|
|
@@ -301,6 +320,7 @@ function nodeRuntimeSegments(
|
|
|
301
320
|
snapshot: WorkflowDefinitionSnapshot,
|
|
302
321
|
nodeId: string,
|
|
303
322
|
now: Date,
|
|
323
|
+
isCurrent: boolean,
|
|
304
324
|
): string[] {
|
|
305
325
|
const segments: string[] = [];
|
|
306
326
|
const node = snapshot.nodes[nodeId];
|
|
@@ -312,11 +332,11 @@ function nodeRuntimeSegments(
|
|
|
312
332
|
segments.push("assistant response");
|
|
313
333
|
}
|
|
314
334
|
const completedAttempts = state.steps.filter((step) => step.nodeId === nodeId).length;
|
|
315
|
-
const attempts = completedAttempts + (
|
|
335
|
+
const attempts = completedAttempts + (isCurrent ? 1 : 0);
|
|
316
336
|
if (attempts > 1) {
|
|
317
337
|
segments.push(`↻${attempts}`);
|
|
318
338
|
}
|
|
319
|
-
if (
|
|
339
|
+
if (isCurrent && state.currentSettingsChangeNumber !== undefined) {
|
|
320
340
|
segments.push(`settings ${state.currentSettingsChangeNumber}`);
|
|
321
341
|
} else {
|
|
322
342
|
const latest = state.steps.findLast((step) => step.nodeId === nodeId);
|
|
@@ -326,8 +346,8 @@ function nodeRuntimeSegments(
|
|
|
326
346
|
}
|
|
327
347
|
|
|
328
348
|
const result = state.results[nodeId];
|
|
329
|
-
if (
|
|
330
|
-
if (state.statusDetail) {
|
|
349
|
+
if (isCurrent) {
|
|
350
|
+
if (state.currentNode === nodeId && state.statusDetail) {
|
|
331
351
|
segments.push(sanitizeText(state.statusDetail));
|
|
332
352
|
}
|
|
333
353
|
const elapsed = elapsedSince(state.currentNodeStartedAt, now);
|