@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.
Files changed (207) hide show
  1. package/README.md +24 -8
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/activity.d.ts +2 -0
  15. package/dist/client/activity.js +6 -0
  16. package/dist/client/activity.js.map +1 -0
  17. package/dist/client/client.d.ts +102 -0
  18. package/dist/client/client.js +737 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.js +3 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/materialize.d.ts +7 -0
  24. package/dist/client/materialize.js +177 -0
  25. package/dist/client/materialize.js.map +1 -0
  26. package/dist/client/protocol.d.ts +60 -0
  27. package/dist/client/protocol.js +275 -0
  28. package/dist/client/protocol.js.map +1 -0
  29. package/dist/client/resolver.d.ts +23 -0
  30. package/dist/client/resolver.js +2 -0
  31. package/dist/client/resolver.js.map +1 -0
  32. package/dist/client/view.d.ts +144 -0
  33. package/dist/client/view.js +3 -0
  34. package/dist/client/view.js.map +1 -0
  35. package/dist/controllers/index.d.ts +1 -1
  36. package/dist/controllers/index.js.map +1 -1
  37. package/dist/controllers/sqlite.d.ts +47 -157
  38. package/dist/controllers/sqlite.js +230 -406
  39. package/dist/controllers/sqlite.js.map +1 -1
  40. package/dist/extension/index.d.ts +23 -1
  41. package/dist/extension/index.js +734 -555
  42. package/dist/extension/index.js.map +1 -1
  43. package/dist/extension/recorder.d.ts +95 -0
  44. package/dist/extension/recorder.js +530 -0
  45. package/dist/extension/recorder.js.map +1 -0
  46. package/dist/extension/remote-recorder-store.d.ts +25 -0
  47. package/dist/extension/remote-recorder-store.js +81 -0
  48. package/dist/extension/remote-recorder-store.js.map +1 -0
  49. package/dist/extension/session-events.d.ts +134 -0
  50. package/dist/extension/session-events.js +60 -0
  51. package/dist/extension/session-events.js.map +1 -0
  52. package/dist/extension/session-view.d.ts +9 -2
  53. package/dist/extension/session-view.js +117 -51
  54. package/dist/extension/session-view.js.map +1 -1
  55. package/dist/extension/step-message.d.ts +6 -4
  56. package/dist/extension/step-message.js +12 -6
  57. package/dist/extension/step-message.js.map +1 -1
  58. package/dist/extension/widget.d.ts +3 -2
  59. package/dist/extension/widget.js +41 -24
  60. package/dist/extension/widget.js.map +1 -1
  61. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  62. package/dist/extension/workflow-message-coordinator.js +294 -0
  63. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  64. package/dist/host/channel-effects.d.ts +72 -0
  65. package/dist/host/channel-effects.js +271 -0
  66. package/dist/host/channel-effects.js.map +1 -0
  67. package/dist/host/channel-supervisor.d.ts +21 -0
  68. package/dist/host/channel-supervisor.js +54 -0
  69. package/dist/host/channel-supervisor.js.map +1 -0
  70. package/dist/host/child-worker-supervisor.js +1 -1
  71. package/dist/host/child-worker-supervisor.js.map +1 -1
  72. package/dist/host/resolver-entry.d.ts +2 -23
  73. package/dist/host/resolver-entry.js +1 -1
  74. package/dist/host/resolver-entry.js.map +1 -1
  75. package/dist/host/runner.d.ts +54 -8
  76. package/dist/host/runner.js +1997 -305
  77. package/dist/host/runner.js.map +1 -1
  78. package/dist/host/state.d.ts +25 -19
  79. package/dist/host/state.js +303 -89
  80. package/dist/host/state.js.map +1 -1
  81. package/dist/host/view.d.ts +83 -0
  82. package/dist/host/view.js +975 -0
  83. package/dist/host/view.js.map +1 -0
  84. package/dist/host/worker-protocol.js +1 -1
  85. package/dist/host/worker-protocol.js.map +1 -1
  86. package/dist/state/database.d.ts +1 -0
  87. package/dist/state/database.js +15 -0
  88. package/dist/state/database.js.map +1 -1
  89. package/dist/state/prune.d.ts +3 -1
  90. package/dist/state/prune.js +9 -33
  91. package/dist/state/prune.js.map +1 -1
  92. package/dist/state/schema.js +88 -98
  93. package/dist/state/schema.js.map +1 -1
  94. package/dist/state/workflow-messages.d.ts +96 -0
  95. package/dist/state/workflow-messages.js +335 -0
  96. package/dist/state/workflow-messages.js.map +1 -0
  97. package/dist/viewer/backup.d.ts +2 -0
  98. package/dist/viewer/backup.js +28 -0
  99. package/dist/viewer/backup.js.map +1 -0
  100. package/dist/viewer/cli.d.ts +4 -0
  101. package/dist/viewer/cli.js +150 -170
  102. package/dist/viewer/cli.js.map +1 -1
  103. package/dist/viewer/render.js +1 -1
  104. package/dist/viewer/render.js.map +1 -1
  105. package/dist/viewer/tui.d.ts +5 -7
  106. package/dist/viewer/tui.js +245 -108
  107. package/dist/viewer/tui.js.map +1 -1
  108. package/dist/workflows/human-decision.d.ts +2 -0
  109. package/dist/workflows/human-decision.js +8 -2
  110. package/dist/workflows/human-decision.js.map +1 -1
  111. package/dist/workflows/index.d.ts +1 -1
  112. package/dist/workflows/index.js.map +1 -1
  113. package/dist/workflows/settings.d.ts +1 -8
  114. package/dist/workflows/settings.js.map +1 -1
  115. package/dist/workflows/store.d.ts +67 -20
  116. package/dist/workflows/store.js +451 -378
  117. package/dist/workflows/store.js.map +1 -1
  118. package/dist/workflows/tool-input.d.ts +21 -0
  119. package/dist/workflows/tool-input.js +23 -1
  120. package/dist/workflows/tool-input.js.map +1 -1
  121. package/dist/workflows/types.d.ts +5 -5
  122. package/dist/workflows/workflow-message-content.d.ts +38 -0
  123. package/dist/workflows/workflow-message-content.js +157 -0
  124. package/dist/workflows/workflow-message-content.js.map +1 -0
  125. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  126. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  127. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  128. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  129. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  130. package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
  131. package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
  132. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  133. package/docs/DEFERRED_TURNS.md +66 -294
  134. package/docs/HUMAN_DECISIONS.md +29 -30
  135. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  136. package/docs/SQLITE_STATE.md +31 -25
  137. package/docs/WORKFLOW_HOST.md +155 -77
  138. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  139. package/docs/development.md +2 -1
  140. package/docs/live-replay-protocol.md +74 -132
  141. package/docs/tui-viewer.md +12 -14
  142. package/docs/workflows.md +78 -28
  143. package/herdr-plugin.toml +1 -1
  144. package/package.json +9 -3
  145. package/plugins/herdr/viewer.mjs +14 -5
  146. package/protocol/client.v1.schema.json +137 -0
  147. package/protocol/fixtures/client-v1.json +23 -0
  148. package/src/channels/adapter-entry.ts +220 -0
  149. package/src/channels/config.ts +296 -0
  150. package/src/channels/protocol.ts +333 -0
  151. package/src/channels/telegram.ts +335 -0
  152. package/src/client/activity.ts +6 -0
  153. package/src/client/client.ts +939 -0
  154. package/src/client/index.ts +25 -0
  155. package/src/client/materialize.ts +228 -0
  156. package/src/client/protocol.ts +333 -0
  157. package/src/client/resolver.ts +26 -0
  158. package/src/client/view.ts +163 -0
  159. package/src/controllers/index.ts +0 -1
  160. package/src/controllers/sqlite.ts +339 -808
  161. package/src/extension/index.ts +853 -665
  162. package/src/extension/recorder.ts +687 -0
  163. package/src/extension/remote-recorder-store.ts +126 -0
  164. package/src/extension/session-events.ts +119 -0
  165. package/src/extension/session-view.ts +157 -51
  166. package/src/extension/step-message.ts +19 -9
  167. package/src/extension/widget.ts +53 -25
  168. package/src/extension/workflow-message-coordinator.ts +334 -0
  169. package/src/host/channel-effects.ts +465 -0
  170. package/src/host/channel-supervisor.ts +73 -0
  171. package/src/host/child-worker-supervisor.ts +1 -1
  172. package/src/host/resolver-entry.ts +11 -26
  173. package/src/host/runner.ts +2639 -450
  174. package/src/host/state.ts +404 -137
  175. package/src/host/view.ts +1212 -0
  176. package/src/host/worker-protocol.ts +1 -1
  177. package/src/state/database.ts +13 -0
  178. package/src/state/prune.ts +14 -42
  179. package/src/state/schema.ts +88 -98
  180. package/src/state/workflow-messages.ts +548 -0
  181. package/src/viewer/backup.ts +29 -0
  182. package/src/viewer/cli.ts +171 -185
  183. package/src/viewer/render.ts +1 -5
  184. package/src/viewer/tui.ts +261 -124
  185. package/src/workflows/human-decision.ts +18 -2
  186. package/src/workflows/index.ts +0 -1
  187. package/src/workflows/settings.ts +1 -20
  188. package/src/workflows/store.ts +650 -511
  189. package/src/workflows/tool-input.ts +36 -1
  190. package/src/workflows/types.ts +5 -5
  191. package/src/workflows/workflow-message-content.ts +197 -0
  192. package/dist/extension/decision-channels.d.ts +0 -134
  193. package/dist/extension/decision-channels.js +0 -1307
  194. package/dist/extension/decision-channels.js.map +0 -1
  195. package/dist/host/client.d.ts +0 -48
  196. package/dist/host/client.js +0 -216
  197. package/dist/host/client.js.map +0 -1
  198. package/dist/host/protocol.d.ts +0 -38
  199. package/dist/host/protocol.js +0 -156
  200. package/dist/host/protocol.js.map +0 -1
  201. package/dist/viewer/watch.d.ts +0 -6
  202. package/dist/viewer/watch.js +0 -46
  203. package/dist/viewer/watch.js.map +0 -1
  204. package/src/extension/decision-channels.ts +0 -1826
  205. package/src/host/client.ts +0 -293
  206. package/src/host/protocol.ts +0 -196
  207. 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 { SqliteControllerStore } from "../controllers/sqlite.js";
3
- import { workflowStatePath } from "../state/database.js";
4
- import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
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
- /** Read-only projection of the host-owned run into the origin Pi session. */
13
+ /** Client-backed projection of the host-owned run into the origin Pi session. */
11
14
  export class SessionWorkflowView {
12
- private loaded: LoadedWorkflowRun | undefined;
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
- refresh(ctx: ExtensionContext): void {
20
- const loaded = loadSessionRun(ctx.sessionManager.getSessionId());
21
- if (loaded === undefined) {
22
- this.clear(ctx);
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
- if (this.loaded?.runId !== loaded.runId || this.stepCount !== loaded.state.steps.length) {
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 = loaded.state.steps.length;
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.loaded = undefined;
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
- if (!this.visible) return;
48
- this.visible = false;
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.loaded === undefined) return;
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 loaded = this.loaded;
64
- if (loaded === undefined) return;
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
- loaded.state,
71
- loaded.snapshot,
103
+ state,
104
+ snapshot,
72
105
  new Date(),
73
106
  this.scroll,
74
- loaded.state.paused === true,
107
+ run.display.status === "paused",
75
108
  width,
76
109
  theme,
77
- undefined,
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 label = loaded.state.paused === true ? "paused" : loaded.state.status;
94
- ctx.ui.setStatus(WIDGET_KEY, `${loaded.state.workflowName} [${label}]`);
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
- function loadSessionRun(sessionId: string): LoadedWorkflowRun | undefined {
101
- try {
102
- const queue = new SqliteControllerStore(workflowStatePath(), {
103
- readOnly: true,
104
- global: true,
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 polls.
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 = "pi-workflows-agent-step";
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 PromptDeliveryKind = "step" | "reminder" | "resume";
22
+ type PromptDeliveryReason = "initial" | "reminder" | "resumed";
22
23
 
23
24
  export type WorkflowAgentStepMessageDetails = {
24
25
  schema: typeof WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA;
25
- kind: PromptDeliveryKind;
26
+ kind: "step";
27
+ reason: PromptDeliveryReason;
26
28
  contract: AgentStepContract;
27
29
  presentation?: AgentStepPresentation;
28
- turnIntentId?: string;
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 kind = details?.kind ?? "step";
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 = kind === "step" ? "" : ` · ${kind}`;
61
- const glyph = kind === "step" ? "▶" : "↻";
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
- `Delivery: ${kind}`,
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 (candidate.kind !== "step" && candidate.kind !== "reminder" && candidate.kind !== "resume") {
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;