@osolmaz/pi-workflows 0.15.2 → 0.16.0

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 (124) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
@@ -2,18 +2,30 @@ import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { BUILTIN_WORKFLOW_METADATA } from "../builtins/metadata.js";
5
- import { SqliteControllerStore, type WorkflowRunQueueRecord } from "../controllers/sqlite.js";
6
- import { WorkflowHostClient } from "../host/client.js";
7
- import type { HostResponse } from "../host/protocol.js";
8
- import { HostStateStore, type InteractiveRequestRecord } from "../host/state.js";
9
- import { workflowStatePath } from "../state/database.js";
5
+ import { ORIGIN_ACTIVITY_REFRESH_MS } from "../client/activity.js";
6
+ import { WorkflowClient } from "../client/client.js";
7
+ import { materializeSessionView } from "../client/materialize.js";
8
+ import type { ClientResponse } from "../client/protocol.js";
9
+ import type {
10
+ ClientInteractiveRequest,
11
+ WorkflowRunQueueView,
12
+ WorkflowSessionView,
13
+ } from "../client/view.js";
10
14
  import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
11
15
  import { errorMessage } from "../workflows/errors.js";
12
16
  import { discoverWorkflows } from "../workflows/loader.js";
13
- import { createRunId, WorkflowRunStore } from "../workflows/store.js";
17
+ import { createRunId } from "../workflows/store.js";
14
18
  import type { AgentStepContract, HumanDecisionResponse } from "../workflows/types.js";
15
19
  import { parseControllerArgs, type ParsedControllerArgs } from "./controller-command.js";
20
+ import {
21
+ HerdrWorkflowViewer,
22
+ PIW_SHORTCUT,
23
+ PIW_SHORTCUT_HINT,
24
+ VIEWER_PLACEMENTS,
25
+ type ViewerPlacement,
26
+ } from "./herdr-viewer.js";
16
27
  import { SessionDeliveryCoordinator, type ClaimedSessionDelivery } from "./session-delivery.js";
28
+ import { SessionWorkflowView } from "./session-view.js";
17
29
  import {
18
30
  recoverAssistantStep,
19
31
  registerWorkflowAgentStepMessageRenderer,
@@ -46,10 +58,26 @@ export {
46
58
  } from "./decision-channels.js";
47
59
 
48
60
  const INTERACTION_POLL_MS = 1_000;
49
- const SUBMISSION_POLL_MS = 50;
50
61
  const WORKFLOW_INTERACTION_MESSAGE_TYPE = "pi-workflows-interaction";
51
62
  const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
52
63
  const WORKFLOW_PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
64
+ const sessionSnapshots = new Map<string, WorkflowSessionView>();
65
+
66
+ type PresentedOriginActivity = {
67
+ sessionId: string;
68
+ runId: string;
69
+ requestId: string;
70
+ deliveryId: string;
71
+ sessionEntryId: string;
72
+ };
73
+
74
+ export function activityStateForConnection(
75
+ state: "started" | "refresh" | "settled",
76
+ previousConnectionId: string | null,
77
+ connectionId: string,
78
+ ): "started" | "refresh" | "settled" {
79
+ return state === "refresh" && previousConnectionId !== connectionId ? "started" : state;
80
+ }
53
81
 
54
82
  export type ParsedWorkflowArgs =
55
83
  | { kind: "list" }
@@ -127,12 +155,64 @@ export function parseWorkflowArgs(args: string): ParsedWorkflowArgs {
127
155
 
128
156
  export default function piWorkflows(pi: ExtensionAPI): void {
129
157
  registerWorkflowAgentStepMessageRenderer(pi);
130
- const client = new WorkflowHostClient({ clientId: `pi-extension-${randomUUID()}` });
158
+ let client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
159
+ const herdrViewer = new HerdrWorkflowViewer(pi.exec);
131
160
  let sessionContext: ExtensionContext | null = null;
161
+ let sessionGeneration = 0;
162
+ let sessionUnsubscribe: (() => Promise<void>) | null = null;
132
163
  let pollTimer: ReturnType<typeof setInterval> | null = null;
133
164
  let presentationTail = Promise.resolve();
134
165
  let toolTail = Promise.resolve();
166
+ let originActivity: (PresentedOriginActivity & { sequence: number }) | null = null;
167
+ let originActivityConnectionId: string | null = null;
168
+ let activityTimer: ReturnType<typeof setInterval> | null = null;
135
169
  const sessionDelivery = new SessionDeliveryCoordinator();
170
+ const sessionView = new SessionWorkflowView();
171
+
172
+ const reportOriginActivity = async (
173
+ activity: PresentedOriginActivity & { sequence: number },
174
+ state: "started" | "refresh" | "settled",
175
+ ): Promise<void> => {
176
+ const connection = await client.connect();
177
+ const reportState = activityStateForConnection(
178
+ state,
179
+ originActivityConnectionId,
180
+ connection.connectionId,
181
+ );
182
+ const response = await client.request({
183
+ operation: "activity.report",
184
+ runId: activity.runId,
185
+ payload: { ...activity, state: reportState },
186
+ });
187
+ if (response.outcome !== "accepted" && response.outcome !== "adopted") {
188
+ throw new Error(response.error ?? "Workflow host rejected origin activity");
189
+ }
190
+ originActivityConnectionId =
191
+ reportState === "settled" ? null : (client.connectionId ?? connection.connectionId);
192
+ };
193
+
194
+ const startOriginActivity = (presented: PresentedOriginActivity): void => {
195
+ if (activityTimer !== null) clearInterval(activityTimer);
196
+ originActivityConnectionId = null;
197
+ originActivity = { ...presented, sequence: 0 };
198
+ void reportOriginActivity(originActivity, "started").catch(() => undefined);
199
+ activityTimer = setInterval(() => {
200
+ if (originActivity === null) return;
201
+ originActivity.sequence += 1;
202
+ void reportOriginActivity(originActivity, "refresh").catch(() => undefined);
203
+ }, ORIGIN_ACTIVITY_REFRESH_MS);
204
+ activityTimer.unref?.();
205
+ };
206
+
207
+ const settleOriginActivity = async (): Promise<void> => {
208
+ if (activityTimer !== null) clearInterval(activityTimer);
209
+ activityTimer = null;
210
+ const settled = originActivity;
211
+ originActivity = null;
212
+ if (settled === null) return;
213
+ settled.sequence += 1;
214
+ await reportOriginActivity(settled, "settled").catch(() => undefined);
215
+ };
136
216
 
137
217
  const presentInOrder = async (ctx: ExtensionContext): Promise<void> => {
138
218
  const prior = presentationTail;
@@ -142,16 +222,53 @@ export default function piWorkflows(pi: ExtensionAPI): void {
142
222
  });
143
223
  await prior;
144
224
  try {
225
+ const deliveries = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.deliveries;
145
226
  await sessionDelivery.synchronize(ctx, [
146
- () => claimPendingInteractionDelivery(pi, client, ctx),
147
- () => claimPendingNotificationDelivery(pi, client, ctx),
148
- () => claimPendingTurnDelivery(pi, client, ctx),
227
+ () => claimPendingInteractionDelivery(pi, client, ctx, startOriginActivity),
228
+ ...(deliveries?.notification === true
229
+ ? [() => claimPendingNotificationDelivery(pi, client, ctx)]
230
+ : []),
231
+ ...(deliveries?.turn === true ? [() => claimPendingTurnDelivery(pi, client, ctx)] : []),
149
232
  ]);
150
233
  } finally {
234
+ sessionView.refresh(ctx);
151
235
  release?.();
152
236
  }
153
237
  };
154
238
 
239
+ const openPiw = async (
240
+ ctx: ExtensionContext,
241
+ requestedPlacement?: ViewerPlacement,
242
+ ): Promise<void> => {
243
+ const run = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.run;
244
+ if (run === null || run === undefined || !isRecord(run.state)) {
245
+ ctx.ui.notify("No active workflow is available for piw.", "warning");
246
+ return;
247
+ }
248
+ const capability = await herdrViewer.probe();
249
+ if (!capability.available) {
250
+ ctx.ui.notify(capability.reason, "warning");
251
+ return;
252
+ }
253
+ const placement =
254
+ requestedPlacement ??
255
+ ((await ctx.ui.select("Open workflow viewer", [...VIEWER_PLACEMENTS])) as
256
+ | ViewerPlacement
257
+ | undefined);
258
+ if (placement === undefined) return;
259
+ const workflowName =
260
+ typeof run.state.workflowName === "string" ? run.state.workflowName : run.runId;
261
+ const opened = await herdrViewer.open(
262
+ { runId: run.runId, workflowName },
263
+ placement as ViewerPlacement,
264
+ ctx.cwd,
265
+ );
266
+ ctx.ui.notify(
267
+ opened.reused ? "Focused the existing piw view." : "Opened piw in Herdr.",
268
+ "info",
269
+ );
270
+ };
271
+
155
272
  const runToolInOrder = async <T>(operation: () => Promise<T>): Promise<T> => {
156
273
  const prior = toolTail;
157
274
  let release: (() => void) | undefined;
@@ -159,8 +276,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
159
276
  release = resolve;
160
277
  });
161
278
  await prior;
162
- await presentationTail;
163
279
  try {
280
+ await presentationTail;
281
+ const currentContext = sessionContext;
282
+ if (currentContext === null) throw new Error("Workflow session is unavailable");
283
+ await presentInOrder(currentContext);
164
284
  return await operation();
165
285
  } finally {
166
286
  release?.();
@@ -236,7 +356,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
236
356
  }
237
357
  const response = await requestAccepted(client, {
238
358
  operation: params.action === "update" ? "interaction.update" : "interaction.submit",
239
- requestId: `${params.action}-${toolCallId}`,
359
+ requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
240
360
  idempotencyKey: toolCallId,
241
361
  runId: interaction.runId,
242
362
  expectedRevision: interaction.revision,
@@ -248,10 +368,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
248
368
  value:
249
369
  params.action === "update" ? { update: params.update } : { output: params.output },
250
370
  }),
371
+ ...(signal === undefined ? {} : { signal }),
251
372
  });
252
- if (params.action === "submit") {
253
- await waitForInteractionSubmission(interaction.requestId, toolCallId, signal);
254
- }
255
373
  await presentInOrder(ctx);
256
374
  return toolResult(
257
375
  params.action === "update"
@@ -268,18 +386,136 @@ export default function piWorkflows(pi: ExtensionAPI): void {
268
386
  },
269
387
  });
270
388
 
271
- pi.on("session_start", async (_event, ctx) => {
389
+ pi.registerCommand("piw", {
390
+ description: "Open the active workflow in Herdr",
391
+ handler: async (args, ctx) => {
392
+ try {
393
+ const requested = args.trim();
394
+ if (requested.length > 0 && !VIEWER_PLACEMENTS.includes(requested as ViewerPlacement)) {
395
+ throw new Error(`Unknown piw placement: ${requested}`);
396
+ }
397
+ await openPiw(ctx, requested.length === 0 ? undefined : (requested as ViewerPlacement));
398
+ } catch (error) {
399
+ ctx.ui.notify(`Could not open piw: ${errorMessage(error)}`, "warning");
400
+ }
401
+ },
402
+ });
403
+
404
+ pi.registerShortcut(PIW_SHORTCUT, {
405
+ description: "Open the active workflow in Herdr",
406
+ handler: async (ctx) => {
407
+ try {
408
+ await openPiw(ctx);
409
+ } catch (error) {
410
+ ctx.ui.notify(`Could not open piw: ${errorMessage(error)}`, "warning");
411
+ }
412
+ },
413
+ });
414
+
415
+ pi.registerShortcut("shift+up", {
416
+ description: "Scroll the workflow widget up",
417
+ handler: (ctx) => sessionView.scrollUp(ctx),
418
+ });
419
+
420
+ pi.registerShortcut("shift+down", {
421
+ description: "Scroll the workflow widget down",
422
+ handler: (ctx) => sessionView.scrollDown(ctx),
423
+ });
424
+
425
+ pi.on("session_start", (_event, ctx) => {
272
426
  sessionContext = ctx;
273
- try {
274
- await client.ensureRunning();
275
- await presentInOrder(ctx);
276
- } catch (error) {
277
- ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
278
- }
427
+ const sessionId = ctx.sessionManager.getSessionId();
428
+ const generation = ++sessionGeneration;
429
+ let snapshotGeneration = 0;
430
+ const sessionClient = client;
279
431
  pollTimer = setInterval(() => {
280
432
  if (sessionContext !== null) void presentInOrder(sessionContext).catch(() => undefined);
281
433
  }, INTERACTION_POLL_MS);
282
434
  pollTimer.unref?.();
435
+
436
+ void (async () => {
437
+ try {
438
+ await sessionClient.ensureAvailable();
439
+ const unsubscribe = await sessionClient.watchSession(sessionId, (event) => {
440
+ if (generation !== sessionGeneration || sessionContext !== ctx) return;
441
+ const currentSnapshotGeneration = ++snapshotGeneration;
442
+ if (event.event === "unavailable") {
443
+ sessionSnapshots.delete(sessionId);
444
+ sessionView.clear(ctx);
445
+ return;
446
+ }
447
+ if (!isWorkflowSessionView(event.payload)) return;
448
+ void materializeSessionView(sessionClient, event.payload)
449
+ .then((session) => {
450
+ if (
451
+ generation !== sessionGeneration ||
452
+ currentSnapshotGeneration !== snapshotGeneration ||
453
+ sessionContext !== ctx
454
+ ) {
455
+ return;
456
+ }
457
+ sessionSnapshots.set(sessionId, session);
458
+ sessionView.update(session, ctx);
459
+ void presentInOrder(ctx).catch(() => undefined);
460
+ })
461
+ .catch(() => {
462
+ // A newer session revision retries from its own stable snapshot.
463
+ });
464
+ });
465
+ if (generation !== sessionGeneration || sessionContext !== ctx) {
466
+ await unsubscribe();
467
+ return;
468
+ }
469
+ sessionUnsubscribe = unsubscribe;
470
+ const capability = await herdrViewer.probe();
471
+ if (generation !== sessionGeneration || sessionContext !== ctx) return;
472
+ sessionView.setActionHint(capability.available ? PIW_SHORTCUT_HINT : undefined, ctx);
473
+ await presentInOrder(ctx);
474
+ } catch (error) {
475
+ if (generation === sessionGeneration && sessionContext === ctx) {
476
+ ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
477
+ }
478
+ }
479
+ })();
480
+ });
481
+
482
+ pi.on("agent_end", async (event, ctx) => {
483
+ const interrupted =
484
+ ctx.signal?.aborted === true ||
485
+ event.messages.some(
486
+ (message) =>
487
+ isRecord(message) && "stopReason" in message && message.stopReason === "aborted",
488
+ );
489
+ if (!interrupted) return;
490
+ const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
491
+ if (
492
+ interaction === undefined ||
493
+ interaction.kind === "decision" ||
494
+ workflowRunPaused(interaction.runId)
495
+ ) {
496
+ return;
497
+ }
498
+ const turnHasPrompt = event.messages.some(
499
+ (message) => interactionRequestId(message) === interaction.requestId,
500
+ );
501
+ if (!turnHasPrompt) return;
502
+ await settleOriginActivity();
503
+ try {
504
+ const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
505
+ await requestAccepted(client, {
506
+ operation: "run.pause",
507
+ requestId: pauseId,
508
+ idempotencyKey: pauseId,
509
+ runId: interaction.runId,
510
+ });
511
+ sessionView.refresh(ctx);
512
+ ctx.ui.notify(
513
+ `Workflow ${interaction.runId} paused because its model turn was interrupted. Use /workflow resume to continue.`,
514
+ "info",
515
+ );
516
+ } catch (error) {
517
+ ctx.ui.notify(`Could not pause interrupted workflow: ${errorMessage(error)}`, "warning");
518
+ }
283
519
  });
284
520
 
285
521
  pi.on("agent_settled", async (_event, ctx) => {
@@ -288,14 +524,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
288
524
  } catch (error) {
289
525
  ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
290
526
  }
527
+ await settleOriginActivity();
291
528
  await presentInOrder(ctx).catch(() => undefined);
292
529
  });
293
530
 
294
- pi.on("session_shutdown", async () => {
531
+ pi.on("session_shutdown", async (_event, ctx) => {
532
+ sessionGeneration += 1;
295
533
  sessionContext = null;
296
534
  sessionDelivery.clear();
535
+ sessionView.clear(ctx);
536
+ sessionSnapshots.delete(ctx.sessionManager.getSessionId());
537
+ if (activityTimer !== null) clearInterval(activityTimer);
538
+ activityTimer = null;
539
+ originActivity = null;
297
540
  if (pollTimer !== null) clearInterval(pollTimer);
298
541
  pollTimer = null;
542
+ if (sessionUnsubscribe !== null) await sessionUnsubscribe().catch(() => undefined);
543
+ sessionUnsubscribe = null;
544
+ await client.close();
545
+ client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
299
546
  });
300
547
  }
301
548
 
@@ -306,7 +553,7 @@ type CommandResult = {
306
553
  };
307
554
 
308
555
  async function executeCommand(
309
- client: WorkflowHostClient,
556
+ client: WorkflowClient,
310
557
  ctx: ExtensionContext,
311
558
  command: ParsedWorkflowArgs,
312
559
  idempotencyKey: string = randomUUID(),
@@ -325,7 +572,7 @@ async function executeCommand(
325
572
  };
326
573
  }
327
574
  case "run": {
328
- await client.ensureRunning();
575
+ await client.ensureAvailable();
329
576
  const resolved = await client.resolveWorkflow({ cwd: ctx.cwd, workflowRef: command.ref });
330
577
  const runId = createRunId(resolved.workflowName);
331
578
  const response = await requestAccepted(client, {
@@ -452,7 +699,7 @@ async function executeCommand(
452
699
  }
453
700
 
454
701
  async function executeControllerCommand(
455
- client: WorkflowHostClient,
702
+ client: WorkflowClient,
456
703
  ctx: ExtensionContext,
457
704
  command: ParsedControllerArgs,
458
705
  ): Promise<CommandResult> {
@@ -519,19 +766,34 @@ async function executeControllerCommand(
519
766
 
520
767
  async function claimPendingInteractionDelivery(
521
768
  pi: ExtensionAPI,
522
- client: WorkflowHostClient,
769
+ client: WorkflowClient,
523
770
  ctx: ExtensionContext,
771
+ onPresented: (activity: PresentedOriginActivity) => void,
524
772
  ): Promise<ClaimedSessionDelivery | undefined> {
525
- const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
526
- if (interaction === undefined || interaction.presentationSessionEntryId !== null)
773
+ const storedInteraction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
774
+ if (storedInteraction === undefined || storedInteraction.presentationSessionEntryId !== null)
527
775
  return undefined;
776
+ const interaction = {
777
+ ...storedInteraction,
778
+ contract: await client.hydrateContent(storedInteraction.runId, storedInteraction.contract),
779
+ };
780
+ rememberInteractiveRequest(ctx.sessionManager.getSessionId(), interaction);
528
781
 
529
782
  const findSessionEntryId = (entries: readonly unknown[]): string | undefined =>
530
783
  entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
531
784
  const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
532
785
  let presentationRevision = interaction.revision;
786
+ let claimExpiresAt = Number.POSITIVE_INFINITY;
533
787
  if (existingEntryId === undefined) {
534
- const claim = await requestAccepted(client, {
788
+ if (workflowRunPaused(interaction.runId)) return undefined;
789
+ if (
790
+ interaction.presentationClaimExpiresAt !== null &&
791
+ Date.parse(interaction.presentationClaimExpiresAt) > Date.now()
792
+ ) {
793
+ return undefined;
794
+ }
795
+ await client.ensureAvailable();
796
+ const claim = await client.request({
535
797
  operation: "interaction.update",
536
798
  requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
537
799
  idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
@@ -539,13 +801,30 @@ async function claimPendingInteractionDelivery(
539
801
  expectedRevision: interaction.revision,
540
802
  payload: { requestId: interaction.requestId, claimPresentation: true },
541
803
  });
804
+ if (claim.outcome === "conflict") return undefined;
805
+ if (claim.outcome !== "accepted" && claim.outcome !== "adopted") {
806
+ throw new Error(claim.error ?? "Workflow host rejected interaction.update");
807
+ }
542
808
  if (claim.revision === undefined) throw new Error("Presentation claim has no revision");
809
+ const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
543
810
  presentationRevision = claim.revision;
811
+ claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
812
+ rememberInteractiveRequest(ctx.sessionManager.getSessionId(), claim.receipt);
544
813
  }
545
814
 
546
815
  const contract = interactionContract(interaction);
547
816
  return {
548
817
  deliveryId: `interaction:${interaction.requestId}`,
818
+ claimExpiresAt,
819
+ isStillDeliverable: () =>
820
+ existingEntryId === undefined &&
821
+ interactionPresentationClaimIsLive(client, {
822
+ requestId: interaction.requestId,
823
+ runId: interaction.runId,
824
+ presenterId: client.clientId,
825
+ revision: presentationRevision,
826
+ claimExpiresAt,
827
+ }),
549
828
  findSessionEntryId,
550
829
  send: () => {
551
830
  if (interaction.kind === "decision") {
@@ -587,7 +866,7 @@ async function claimPendingInteractionDelivery(
587
866
  );
588
867
  },
589
868
  settle: async (sessionEntryId) => {
590
- await requestAccepted(client, {
869
+ const response = await requestAccepted(client, {
591
870
  operation: "interaction.update",
592
871
  requestId: `present-${interaction.requestId}-${sessionEntryId}`,
593
872
  idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
@@ -595,17 +874,26 @@ async function claimPendingInteractionDelivery(
595
874
  expectedRevision: presentationRevision,
596
875
  payload: { requestId: interaction.requestId, sessionEntryId },
597
876
  });
877
+ rememberInteractiveRequest(ctx.sessionManager.getSessionId(), response.receipt);
878
+ if (interaction.kind !== "decision") {
879
+ onPresented({
880
+ sessionId: ctx.sessionManager.getSessionId(),
881
+ runId: interaction.runId,
882
+ requestId: interaction.requestId,
883
+ deliveryId: `interaction:${interaction.requestId}`,
884
+ sessionEntryId,
885
+ });
886
+ }
598
887
  },
599
888
  };
600
889
  }
601
890
 
602
891
  async function claimPendingNotificationDelivery(
603
892
  pi: ExtensionAPI,
604
- client: WorkflowHostClient,
893
+ client: WorkflowClient,
605
894
  ctx: ExtensionContext,
606
895
  ): Promise<ClaimedSessionDelivery | undefined> {
607
896
  const sessionId = ctx.sessionManager.getSessionId();
608
- if (!hasClaimableNotification(sessionId)) return undefined;
609
897
  const claimRequestId = randomUUID();
610
898
  const claimed = await requestAccepted(client, {
611
899
  operation: "notification.claim",
@@ -617,9 +905,18 @@ async function claimPendingNotificationDelivery(
617
905
  const notification = isRecord(receipt?.notification) ? receipt.notification : undefined;
618
906
  if (notification === undefined) return undefined;
619
907
  const claimId = requireText(receipt?.claimId, "notification claimId");
908
+ const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "notification claim");
620
909
  const notificationId = requireText(notification.notificationId, "notificationId");
621
910
  return {
622
911
  deliveryId: `notification:${notificationId}`,
912
+ claimExpiresAt,
913
+ isStillDeliverable: () =>
914
+ hostDeliveryClaimIsLive(client, {
915
+ kind: "notification",
916
+ resourceId: notificationId,
917
+ targetSessionId: sessionId,
918
+ claimId,
919
+ }),
623
920
  findSessionEntryId: (entries) =>
624
921
  entryIdentifier(
625
922
  entries.find(
@@ -660,13 +957,11 @@ async function claimPendingNotificationDelivery(
660
957
 
661
958
  async function claimPendingTurnDelivery(
662
959
  pi: ExtensionAPI,
663
- client: WorkflowHostClient,
960
+ client: WorkflowClient,
664
961
  ctx: ExtensionContext,
665
962
  ): Promise<ClaimedSessionDelivery | undefined> {
666
963
  const sessionId = ctx.sessionManager.getSessionId();
667
- if (pendingInteractionForSession(sessionId) !== undefined || !hasClaimableTurn(sessionId)) {
668
- return undefined;
669
- }
964
+ if (pendingInteractionForSession(sessionId) !== undefined) return undefined;
670
965
  const claimRequestId = randomUUID();
671
966
  const claimed = await requestAccepted(client, {
672
967
  operation: "turn.claim",
@@ -678,12 +973,23 @@ async function claimPendingTurnDelivery(
678
973
  const turn = isRecord(receipt?.turn) ? receipt.turn : undefined;
679
974
  if (turn === undefined) return undefined;
680
975
  const claimId = requireText(receipt?.claimId, "turn claimId");
976
+ const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
681
977
  const intentId = requireText(turn.intentId, "turn intentId");
682
978
  const runId = requireText(turn.runId, "turn runId");
683
- const state = terminalRunState(runId);
684
- if (state === undefined) return undefined;
979
+ const claimedRun = await client.getRun(runId);
980
+ if (claimedRun === null) throw new Error(`Claimed workflow turn run is missing: ${runId}`);
981
+ const state = await client.hydrateContent(runId, claimedRun.state);
982
+ if (!isRecord(state)) throw new Error("Workflow terminal state content is invalid");
685
983
  return {
686
984
  deliveryId: `turn:${intentId}`,
985
+ claimExpiresAt,
986
+ isStillDeliverable: () =>
987
+ hostDeliveryClaimIsLive(client, {
988
+ kind: "turn",
989
+ resourceId: intentId,
990
+ targetSessionId: sessionId,
991
+ claimId,
992
+ }),
687
993
  findSessionEntryId: (entries) =>
688
994
  entryIdentifier(
689
995
  entries.find(
@@ -719,11 +1025,17 @@ async function claimPendingTurnDelivery(
719
1025
  }
720
1026
 
721
1027
  async function submitVisibleAssistantResponse(
722
- client: WorkflowHostClient,
1028
+ client: WorkflowClient,
723
1029
  ctx: ExtensionContext,
724
1030
  ): Promise<void> {
725
1031
  const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
726
- if (interaction === undefined || interaction.kind !== "assistant") return;
1032
+ if (
1033
+ interaction === undefined ||
1034
+ interaction.kind !== "assistant" ||
1035
+ workflowRunPaused(interaction.runId)
1036
+ ) {
1037
+ return;
1038
+ }
727
1039
  const contract = agentContract(interaction);
728
1040
  if (contract === undefined) return;
729
1041
  const submission = recoverAssistantStep(ctx.sessionManager.getBranch(), contract);
@@ -744,133 +1056,99 @@ async function submitVisibleAssistantResponse(
744
1056
  value: submission as unknown as JsonValue,
745
1057
  },
746
1058
  });
747
- await waitForInteractionSubmission(interaction.requestId, `assistant-${responseId}`);
748
- }
749
-
750
- async function waitForInteractionSubmission(
751
- requestId: string,
752
- submissionId: string,
753
- signal?: AbortSignal,
754
- ): Promise<void> {
755
- const store = new HostStateStore(workflowStatePath(), { readOnly: true });
756
- try {
757
- for (;;) {
758
- if (signal?.aborted === true) {
759
- throw signal.reason ?? new Error("Workflow submission validation was cancelled");
760
- }
761
- const submission = store.interactionSubmission(requestId, submissionId);
762
- if (submission?.outcome === "accepted" || submission?.outcome === "adopted") return;
763
- if (submission?.outcome === "rejected") {
764
- const receipt = isRecord(submission.receipt) ? submission.receipt : undefined;
765
- throw new Error(
766
- typeof receipt?.error === "string"
767
- ? receipt.error
768
- : "Workflow step output failed validation",
769
- );
770
- }
771
- await waitForSubmissionPoll(signal);
772
- }
773
- } finally {
774
- store.close();
775
- }
776
1059
  }
777
1060
 
778
- async function waitForSubmissionPoll(signal?: AbortSignal): Promise<void> {
779
- if (signal?.aborted === true) {
780
- throw signal.reason ?? new Error("Workflow submission was cancelled");
781
- }
782
- await new Promise<void>((resolve, reject) => {
783
- const timer = setTimeout(finish, SUBMISSION_POLL_MS);
784
- const onAbort = () => finish(signal?.reason ?? new Error("Workflow submission was cancelled"));
785
- function finish(error?: unknown) {
786
- clearTimeout(timer);
787
- signal?.removeEventListener("abort", onAbort);
788
- if (error === undefined) resolve();
789
- else reject(error);
790
- }
791
- signal?.addEventListener("abort", onAbort, { once: true });
1061
+ function rememberInteractiveRequest(sessionId: string, value: unknown): void {
1062
+ const current = parseInteractiveRequest(value);
1063
+ const session = sessionSnapshots.get(sessionId);
1064
+ if (current === undefined || session === undefined) return;
1065
+ let found = false;
1066
+ const pendingInteractions = session.pendingInteractions.map((item) => {
1067
+ const stored = parseInteractiveRequest(item);
1068
+ if (stored?.requestId !== current.requestId) return item;
1069
+ found = true;
1070
+ return current as unknown as JsonValue;
792
1071
  });
1072
+ if (found) sessionSnapshots.set(sessionId, { ...session, pendingInteractions });
793
1073
  }
794
1074
 
795
- function pendingInteractionForSession(sessionId: string): InteractiveRequestRecord | undefined {
796
- try {
797
- const store = new HostStateStore(workflowStatePath(), { readOnly: true });
798
- try {
799
- return store.listPendingInteractions(sessionId)[0];
800
- } finally {
801
- store.close();
802
- }
803
- } catch {
804
- return undefined;
805
- }
1075
+ function pendingInteractionForSession(sessionId: string): ClientInteractiveRequest | undefined {
1076
+ return sessionSnapshots
1077
+ .get(sessionId)
1078
+ ?.pendingInteractions.map(parseInteractiveRequest)
1079
+ .find((request): request is ClientInteractiveRequest => request !== undefined);
806
1080
  }
807
1081
 
808
- function pendingDecision(sessionId: string, runId?: string): InteractiveRequestRecord | undefined {
809
- try {
810
- const store = new HostStateStore(workflowStatePath(), { readOnly: true });
811
- try {
812
- return store
813
- .listPendingInteractions(sessionId)
814
- .find(
815
- (request) =>
816
- request.kind === "decision" && (runId === undefined || request.runId === runId),
817
- );
818
- } finally {
819
- store.close();
820
- }
821
- } catch {
822
- return undefined;
823
- }
1082
+ function pendingDecision(sessionId: string, runId?: string): ClientInteractiveRequest | undefined {
1083
+ return sessionSnapshots
1084
+ .get(sessionId)
1085
+ ?.pendingInteractions.map(parseInteractiveRequest)
1086
+ .find(
1087
+ (request): request is ClientInteractiveRequest =>
1088
+ request !== undefined &&
1089
+ request.kind === "decision" &&
1090
+ (runId === undefined || request.runId === runId),
1091
+ );
824
1092
  }
825
1093
 
826
- function hasClaimableNotification(sessionId: string): boolean {
827
- try {
828
- const store = new SqliteControllerStore(workflowStatePath(), { readOnly: true, global: true });
829
- try {
830
- return (
831
- store.listPendingWorkflowNotifications({ targetSessionId: sessionId, limit: 1 }).length > 0
832
- );
833
- } finally {
834
- store.close();
835
- }
836
- } catch {
837
- return false;
1094
+ function workflowRunPaused(runId: string): boolean {
1095
+ for (const session of sessionSnapshots.values()) {
1096
+ if (session.run?.runId === runId) return session.run.display.status === "paused";
838
1097
  }
1098
+ return false;
839
1099
  }
840
1100
 
841
- function hasClaimableTurn(sessionId: string): boolean {
1101
+ async function interactionPresentationClaimIsLive(
1102
+ client: WorkflowClient,
1103
+ options: {
1104
+ requestId: string;
1105
+ runId: string;
1106
+ presenterId: string;
1107
+ revision: number;
1108
+ claimExpiresAt: number;
1109
+ },
1110
+ ): Promise<boolean> {
1111
+ if (options.claimExpiresAt <= Date.now() || options.presenterId !== client.clientId) return false;
842
1112
  try {
843
- const store = new SqliteControllerStore(workflowStatePath(), { readOnly: true, global: true });
844
- try {
845
- const now = Date.now();
846
- return store
847
- .listWorkflowTurnIntents({ targetSessionId: sessionId, unresolvedOnly: true, limit: 10 })
848
- .some(
849
- (intent) =>
850
- intent.eligibleAt !== null &&
851
- Date.parse(intent.eligibleAt) <= now &&
852
- (intent.deliveryClaimExpiresAt === null ||
853
- Date.parse(intent.deliveryClaimExpiresAt) <= now),
854
- );
855
- } finally {
856
- store.close();
857
- }
1113
+ const response = await client.request({
1114
+ operation: "interaction.update",
1115
+ runId: options.runId,
1116
+ expectedRevision: options.revision,
1117
+ payload: { requestId: options.requestId, validatePresentation: true },
1118
+ });
1119
+ return (
1120
+ (response.outcome === "accepted" || response.outcome === "adopted") &&
1121
+ isRecord(response.receipt) &&
1122
+ response.receipt.live === true
1123
+ );
858
1124
  } catch {
859
1125
  return false;
860
1126
  }
861
1127
  }
862
1128
 
863
- function terminalRunState(runId: string): Record<string, unknown> | undefined {
1129
+ async function hostDeliveryClaimIsLive(
1130
+ client: WorkflowClient,
1131
+ options: {
1132
+ kind: "notification" | "turn";
1133
+ resourceId: string;
1134
+ targetSessionId: string;
1135
+ claimId: string;
1136
+ },
1137
+ ): Promise<boolean> {
864
1138
  try {
865
- const store = new WorkflowRunStore(workflowStatePath(), { readOnly: true });
866
- try {
867
- const loaded = store.readRun(runId);
868
- return loaded === null ? undefined : (loaded.state as unknown as Record<string, unknown>);
869
- } finally {
870
- store.close();
871
- }
1139
+ const validationId = randomUUID();
1140
+ const response = await client.request({
1141
+ operation: options.kind === "notification" ? "notification.claim" : "turn.claim",
1142
+ requestId: `delivery-validate-${options.claimId}-${validationId}`,
1143
+ idempotencyKey: validationId,
1144
+ payload: { ...options, validateClaim: true },
1145
+ });
1146
+ const receipt = isRecord(response.receipt) ? response.receipt : undefined;
1147
+ return (
1148
+ (response.outcome === "accepted" || response.outcome === "adopted") && receipt?.live === true
1149
+ );
872
1150
  } catch {
873
- return undefined;
1151
+ return false;
874
1152
  }
875
1153
  }
876
1154
 
@@ -929,25 +1207,24 @@ function requireText(value: unknown, name: string): string {
929
1207
  return value;
930
1208
  }
931
1209
 
932
- function activeSessionRun(ctx: ExtensionContext): WorkflowRunQueueRecord | undefined {
1210
+ function claimExpiry(value: unknown, name: string): number {
1211
+ const expiry = Date.parse(requireText(value, `${name} expiry`));
1212
+ if (!Number.isFinite(expiry)) throw new Error(`${name} expiry must be a timestamp`);
1213
+ return expiry;
1214
+ }
1215
+
1216
+ function activeSessionRun(ctx: ExtensionContext): WorkflowRunQueueView | undefined {
933
1217
  return sessionRun(ctx);
934
1218
  }
935
1219
 
936
- function sessionRun(ctx: ExtensionContext, runId?: string): WorkflowRunQueueRecord | undefined {
937
- try {
938
- const store = new SqliteControllerStore(workflowStatePath(), { readOnly: true, global: true });
939
- try {
940
- const run =
941
- runId === undefined
942
- ? store.findSessionReservation(ctx.sessionManager.getSessionId())
943
- : store.getWorkflowRun(runId);
944
- return run?.originSessionId === ctx.sessionManager.getSessionId() ? run : undefined;
945
- } finally {
946
- store.close();
947
- }
948
- } catch {
1220
+ function sessionRun(ctx: ExtensionContext, runId?: string): WorkflowRunQueueView | undefined {
1221
+ const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
1222
+ if (session?.run === null || session?.run === undefined || !isRecord(session.run.queue)) {
949
1223
  return undefined;
950
1224
  }
1225
+ if (runId !== undefined && session.run.runId !== runId) return undefined;
1226
+ const queue = session.run.queue;
1227
+ return queue.originSessionId === ctx.sessionManager.getSessionId() ? queue : undefined;
951
1228
  }
952
1229
 
953
1230
  async function listWorkflowMetadata(cwd: string): Promise<Array<{ name: string; source: string }>> {
@@ -991,23 +1268,26 @@ function toolInputToCommand(params: ReturnType<typeof parseWorkflowToolInput>):
991
1268
  }
992
1269
 
993
1270
  async function requestAccepted(
994
- client: WorkflowHostClient,
995
- options: Parameters<WorkflowHostClient["request"]>[0],
996
- ): Promise<HostResponse> {
997
- await client.ensureRunning();
998
- const response = await client.request(options);
1271
+ client: WorkflowClient,
1272
+ options: Parameters<WorkflowClient["request"]>[0],
1273
+ ): Promise<ClientResponse> {
1274
+ await client.ensureAvailable();
1275
+ const response =
1276
+ options.idempotencyKey === undefined
1277
+ ? await client.request(options)
1278
+ : await client.requestDurable({ ...options, idempotencyKey: options.idempotencyKey });
999
1279
  if (response.outcome !== "accepted" && response.outcome !== "adopted") {
1000
1280
  throw new Error(response.error ?? `Workflow host rejected ${options.operation}`);
1001
1281
  }
1002
1282
  return response;
1003
1283
  }
1004
1284
 
1005
- function interactionContract(interaction: InteractiveRequestRecord): Record<string, unknown> {
1285
+ function interactionContract(interaction: ClientInteractiveRequest): Record<string, unknown> {
1006
1286
  if (!isRecord(interaction.contract)) throw new Error("Stored interaction contract is invalid");
1007
1287
  return interaction.contract;
1008
1288
  }
1009
1289
 
1010
- function agentContract(interaction: InteractiveRequestRecord): AgentStepContract | undefined {
1290
+ function agentContract(interaction: ClientInteractiveRequest): AgentStepContract | undefined {
1011
1291
  const value = interactionContract(interaction).contract;
1012
1292
  if (
1013
1293
  !isRecord(value) ||
@@ -1023,7 +1303,11 @@ function agentContract(interaction: InteractiveRequestRecord): AgentStepContract
1023
1303
  }
1024
1304
 
1025
1305
  function interactionRequestId(value: unknown): string | undefined {
1026
- if (!isRecord(value) || value.type !== "custom_message" || !isRecord(value.details)) {
1306
+ if (
1307
+ !isRecord(value) ||
1308
+ (value.type !== "custom_message" && value.role !== "custom") ||
1309
+ !isRecord(value.details)
1310
+ ) {
1027
1311
  return undefined;
1028
1312
  }
1029
1313
  return typeof value.details.requestId === "string" ? value.details.requestId : undefined;
@@ -1126,6 +1410,33 @@ function validRunId(value: string): boolean {
1126
1410
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u.test(value);
1127
1411
  }
1128
1412
 
1413
+ function isWorkflowSessionView(value: unknown): value is WorkflowSessionView {
1414
+ return (
1415
+ isRecord(value) &&
1416
+ value.schema === "pi-workflows.session-view.v1" &&
1417
+ typeof value.sessionId === "string" &&
1418
+ Array.isArray(value.pendingInteractions) &&
1419
+ isRecord(value.deliveries) &&
1420
+ typeof value.deliveries.notification === "boolean" &&
1421
+ typeof value.deliveries.turn === "boolean" &&
1422
+ (value.run === null || isRecord(value.run))
1423
+ );
1424
+ }
1425
+
1426
+ function parseInteractiveRequest(value: unknown): ClientInteractiveRequest | undefined {
1427
+ if (
1428
+ !isRecord(value) ||
1429
+ typeof value.requestId !== "string" ||
1430
+ typeof value.runId !== "string" ||
1431
+ typeof value.targetSessionId !== "string" ||
1432
+ typeof value.revision !== "number" ||
1433
+ (value.kind !== "agent" && value.kind !== "assistant" && value.kind !== "decision")
1434
+ ) {
1435
+ return undefined;
1436
+ }
1437
+ return value as unknown as ClientInteractiveRequest;
1438
+ }
1439
+
1129
1440
  function isRecord(value: unknown): value is Record<string, unknown> {
1130
1441
  return typeof value === "object" && value !== null && !Array.isArray(value);
1131
1442
  }