@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.
- package/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/src/extension/index.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
148
|
-
|
|
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.
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
769
|
+
client: WorkflowClient,
|
|
523
770
|
ctx: ExtensionContext,
|
|
771
|
+
onPresented: (activity: PresentedOriginActivity) => void,
|
|
524
772
|
): Promise<ClaimedSessionDelivery | undefined> {
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
960
|
+
client: WorkflowClient,
|
|
664
961
|
ctx: ExtensionContext,
|
|
665
962
|
): Promise<ClaimedSessionDelivery | undefined> {
|
|
666
963
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
667
|
-
if (pendingInteractionForSession(sessionId) !== undefined
|
|
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
|
|
684
|
-
if (
|
|
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:
|
|
1028
|
+
client: WorkflowClient,
|
|
723
1029
|
ctx: ExtensionContext,
|
|
724
1030
|
): Promise<void> {
|
|
725
1031
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
726
|
-
if (
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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):
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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):
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|
827
|
-
|
|
828
|
-
|
|
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
|
|
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
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
|
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
|
|
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):
|
|
937
|
-
|
|
938
|
-
|
|
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:
|
|
995
|
-
options: Parameters<
|
|
996
|
-
): Promise<
|
|
997
|
-
await client.
|
|
998
|
-
const response =
|
|
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:
|
|
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:
|
|
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 (
|
|
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
|
}
|