@osolmaz/pi-workflows 0.15.3 → 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 +43 -0
- package/dist/controllers/sqlite.js +137 -2
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +278 -183
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-view.d.ts +7 -2
- package/dist/extension/session-view.js +60 -52
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +14 -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 +11 -0
- package/dist/host/runner.js +473 -17
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +6 -3
- package/dist/host/state.js +52 -26
- 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-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 +74 -61
- 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 +44 -1
- 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 +213 -2
- package/src/extension/index.ts +349 -208
- package/src/extension/session-view.ts +73 -50
- package/src/extension/widget.ts +16 -8
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +648 -48
- package/src/host/state.ts +71 -43
- 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,17 +2,28 @@ 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";
|
|
17
28
|
import { SessionWorkflowView } from "./session-view.js";
|
|
18
29
|
import {
|
|
@@ -47,10 +58,26 @@ export {
|
|
|
47
58
|
} from "./decision-channels.js";
|
|
48
59
|
|
|
49
60
|
const INTERACTION_POLL_MS = 1_000;
|
|
50
|
-
const SUBMISSION_POLL_MS = 50;
|
|
51
61
|
const WORKFLOW_INTERACTION_MESSAGE_TYPE = "pi-workflows-interaction";
|
|
52
62
|
const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
|
|
53
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
|
+
}
|
|
54
81
|
|
|
55
82
|
export type ParsedWorkflowArgs =
|
|
56
83
|
| { kind: "list" }
|
|
@@ -128,14 +155,65 @@ export function parseWorkflowArgs(args: string): ParsedWorkflowArgs {
|
|
|
128
155
|
|
|
129
156
|
export default function piWorkflows(pi: ExtensionAPI): void {
|
|
130
157
|
registerWorkflowAgentStepMessageRenderer(pi);
|
|
131
|
-
|
|
158
|
+
let client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
|
|
159
|
+
const herdrViewer = new HerdrWorkflowViewer(pi.exec);
|
|
132
160
|
let sessionContext: ExtensionContext | null = null;
|
|
161
|
+
let sessionGeneration = 0;
|
|
162
|
+
let sessionUnsubscribe: (() => Promise<void>) | null = null;
|
|
133
163
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
134
164
|
let presentationTail = Promise.resolve();
|
|
135
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;
|
|
136
169
|
const sessionDelivery = new SessionDeliveryCoordinator();
|
|
137
170
|
const sessionView = new SessionWorkflowView();
|
|
138
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
|
+
};
|
|
216
|
+
|
|
139
217
|
const presentInOrder = async (ctx: ExtensionContext): Promise<void> => {
|
|
140
218
|
const prior = presentationTail;
|
|
141
219
|
let release: (() => void) | undefined;
|
|
@@ -144,10 +222,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
144
222
|
});
|
|
145
223
|
await prior;
|
|
146
224
|
try {
|
|
225
|
+
const deliveries = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.deliveries;
|
|
147
226
|
await sessionDelivery.synchronize(ctx, [
|
|
148
|
-
() => claimPendingInteractionDelivery(pi, client, ctx),
|
|
149
|
-
(
|
|
150
|
-
|
|
227
|
+
() => claimPendingInteractionDelivery(pi, client, ctx, startOriginActivity),
|
|
228
|
+
...(deliveries?.notification === true
|
|
229
|
+
? [() => claimPendingNotificationDelivery(pi, client, ctx)]
|
|
230
|
+
: []),
|
|
231
|
+
...(deliveries?.turn === true ? [() => claimPendingTurnDelivery(pi, client, ctx)] : []),
|
|
151
232
|
]);
|
|
152
233
|
} finally {
|
|
153
234
|
sessionView.refresh(ctx);
|
|
@@ -155,6 +236,39 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
155
236
|
}
|
|
156
237
|
};
|
|
157
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
|
+
|
|
158
272
|
const runToolInOrder = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
159
273
|
const prior = toolTail;
|
|
160
274
|
let release: (() => void) | undefined;
|
|
@@ -162,8 +276,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
162
276
|
release = resolve;
|
|
163
277
|
});
|
|
164
278
|
await prior;
|
|
165
|
-
await presentationTail;
|
|
166
279
|
try {
|
|
280
|
+
await presentationTail;
|
|
281
|
+
const currentContext = sessionContext;
|
|
282
|
+
if (currentContext === null) throw new Error("Workflow session is unavailable");
|
|
283
|
+
await presentInOrder(currentContext);
|
|
167
284
|
return await operation();
|
|
168
285
|
} finally {
|
|
169
286
|
release?.();
|
|
@@ -239,7 +356,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
239
356
|
}
|
|
240
357
|
const response = await requestAccepted(client, {
|
|
241
358
|
operation: params.action === "update" ? "interaction.update" : "interaction.submit",
|
|
242
|
-
requestId: `${params.action}-${toolCallId}`,
|
|
359
|
+
requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
|
|
243
360
|
idempotencyKey: toolCallId,
|
|
244
361
|
runId: interaction.runId,
|
|
245
362
|
expectedRevision: interaction.revision,
|
|
@@ -251,10 +368,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
251
368
|
value:
|
|
252
369
|
params.action === "update" ? { update: params.update } : { output: params.output },
|
|
253
370
|
}),
|
|
371
|
+
...(signal === undefined ? {} : { signal }),
|
|
254
372
|
});
|
|
255
|
-
if (params.action === "submit") {
|
|
256
|
-
await waitForInteractionSubmission(interaction.requestId, toolCallId, signal);
|
|
257
|
-
}
|
|
258
373
|
await presentInOrder(ctx);
|
|
259
374
|
return toolResult(
|
|
260
375
|
params.action === "update"
|
|
@@ -271,6 +386,32 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
271
386
|
},
|
|
272
387
|
});
|
|
273
388
|
|
|
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
|
+
|
|
274
415
|
pi.registerShortcut("shift+up", {
|
|
275
416
|
description: "Scroll the workflow widget up",
|
|
276
417
|
handler: (ctx) => sessionView.scrollUp(ctx),
|
|
@@ -281,18 +422,61 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
281
422
|
handler: (ctx) => sessionView.scrollDown(ctx),
|
|
282
423
|
});
|
|
283
424
|
|
|
284
|
-
pi.on("session_start",
|
|
425
|
+
pi.on("session_start", (_event, ctx) => {
|
|
285
426
|
sessionContext = ctx;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
|
|
291
|
-
}
|
|
427
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
428
|
+
const generation = ++sessionGeneration;
|
|
429
|
+
let snapshotGeneration = 0;
|
|
430
|
+
const sessionClient = client;
|
|
292
431
|
pollTimer = setInterval(() => {
|
|
293
432
|
if (sessionContext !== null) void presentInOrder(sessionContext).catch(() => undefined);
|
|
294
433
|
}, INTERACTION_POLL_MS);
|
|
295
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
|
+
})();
|
|
296
480
|
});
|
|
297
481
|
|
|
298
482
|
pi.on("agent_end", async (event, ctx) => {
|
|
@@ -315,6 +499,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
315
499
|
(message) => interactionRequestId(message) === interaction.requestId,
|
|
316
500
|
);
|
|
317
501
|
if (!turnHasPrompt) return;
|
|
502
|
+
await settleOriginActivity();
|
|
318
503
|
try {
|
|
319
504
|
const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
|
|
320
505
|
await requestAccepted(client, {
|
|
@@ -339,15 +524,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
339
524
|
} catch (error) {
|
|
340
525
|
ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
|
|
341
526
|
}
|
|
527
|
+
await settleOriginActivity();
|
|
342
528
|
await presentInOrder(ctx).catch(() => undefined);
|
|
343
529
|
});
|
|
344
530
|
|
|
345
531
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
532
|
+
sessionGeneration += 1;
|
|
346
533
|
sessionContext = null;
|
|
347
534
|
sessionDelivery.clear();
|
|
348
535
|
sessionView.clear(ctx);
|
|
536
|
+
sessionSnapshots.delete(ctx.sessionManager.getSessionId());
|
|
537
|
+
if (activityTimer !== null) clearInterval(activityTimer);
|
|
538
|
+
activityTimer = null;
|
|
539
|
+
originActivity = null;
|
|
349
540
|
if (pollTimer !== null) clearInterval(pollTimer);
|
|
350
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()}` });
|
|
351
546
|
});
|
|
352
547
|
}
|
|
353
548
|
|
|
@@ -358,7 +553,7 @@ type CommandResult = {
|
|
|
358
553
|
};
|
|
359
554
|
|
|
360
555
|
async function executeCommand(
|
|
361
|
-
client:
|
|
556
|
+
client: WorkflowClient,
|
|
362
557
|
ctx: ExtensionContext,
|
|
363
558
|
command: ParsedWorkflowArgs,
|
|
364
559
|
idempotencyKey: string = randomUUID(),
|
|
@@ -377,7 +572,7 @@ async function executeCommand(
|
|
|
377
572
|
};
|
|
378
573
|
}
|
|
379
574
|
case "run": {
|
|
380
|
-
await client.
|
|
575
|
+
await client.ensureAvailable();
|
|
381
576
|
const resolved = await client.resolveWorkflow({ cwd: ctx.cwd, workflowRef: command.ref });
|
|
382
577
|
const runId = createRunId(resolved.workflowName);
|
|
383
578
|
const response = await requestAccepted(client, {
|
|
@@ -504,7 +699,7 @@ async function executeCommand(
|
|
|
504
699
|
}
|
|
505
700
|
|
|
506
701
|
async function executeControllerCommand(
|
|
507
|
-
client:
|
|
702
|
+
client: WorkflowClient,
|
|
508
703
|
ctx: ExtensionContext,
|
|
509
704
|
command: ParsedControllerArgs,
|
|
510
705
|
): Promise<CommandResult> {
|
|
@@ -571,12 +766,18 @@ async function executeControllerCommand(
|
|
|
571
766
|
|
|
572
767
|
async function claimPendingInteractionDelivery(
|
|
573
768
|
pi: ExtensionAPI,
|
|
574
|
-
client:
|
|
769
|
+
client: WorkflowClient,
|
|
575
770
|
ctx: ExtensionContext,
|
|
771
|
+
onPresented: (activity: PresentedOriginActivity) => void,
|
|
576
772
|
): Promise<ClaimedSessionDelivery | undefined> {
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
773
|
+
const storedInteraction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
774
|
+
if (storedInteraction === undefined || storedInteraction.presentationSessionEntryId !== null)
|
|
579
775
|
return undefined;
|
|
776
|
+
const interaction = {
|
|
777
|
+
...storedInteraction,
|
|
778
|
+
contract: await client.hydrateContent(storedInteraction.runId, storedInteraction.contract),
|
|
779
|
+
};
|
|
780
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), interaction);
|
|
580
781
|
|
|
581
782
|
const findSessionEntryId = (entries: readonly unknown[]): string | undefined =>
|
|
582
783
|
entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
|
|
@@ -591,7 +792,7 @@ async function claimPendingInteractionDelivery(
|
|
|
591
792
|
) {
|
|
592
793
|
return undefined;
|
|
593
794
|
}
|
|
594
|
-
await client.
|
|
795
|
+
await client.ensureAvailable();
|
|
595
796
|
const claim = await client.request({
|
|
596
797
|
operation: "interaction.update",
|
|
597
798
|
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
@@ -608,6 +809,7 @@ async function claimPendingInteractionDelivery(
|
|
|
608
809
|
const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
|
|
609
810
|
presentationRevision = claim.revision;
|
|
610
811
|
claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
|
|
812
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), claim.receipt);
|
|
611
813
|
}
|
|
612
814
|
|
|
613
815
|
const contract = interactionContract(interaction);
|
|
@@ -616,7 +818,7 @@ async function claimPendingInteractionDelivery(
|
|
|
616
818
|
claimExpiresAt,
|
|
617
819
|
isStillDeliverable: () =>
|
|
618
820
|
existingEntryId === undefined &&
|
|
619
|
-
interactionPresentationClaimIsLive({
|
|
821
|
+
interactionPresentationClaimIsLive(client, {
|
|
620
822
|
requestId: interaction.requestId,
|
|
621
823
|
runId: interaction.runId,
|
|
622
824
|
presenterId: client.clientId,
|
|
@@ -664,7 +866,7 @@ async function claimPendingInteractionDelivery(
|
|
|
664
866
|
);
|
|
665
867
|
},
|
|
666
868
|
settle: async (sessionEntryId) => {
|
|
667
|
-
await requestAccepted(client, {
|
|
869
|
+
const response = await requestAccepted(client, {
|
|
668
870
|
operation: "interaction.update",
|
|
669
871
|
requestId: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
670
872
|
idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
@@ -672,17 +874,26 @@ async function claimPendingInteractionDelivery(
|
|
|
672
874
|
expectedRevision: presentationRevision,
|
|
673
875
|
payload: { requestId: interaction.requestId, sessionEntryId },
|
|
674
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
|
+
}
|
|
675
887
|
},
|
|
676
888
|
};
|
|
677
889
|
}
|
|
678
890
|
|
|
679
891
|
async function claimPendingNotificationDelivery(
|
|
680
892
|
pi: ExtensionAPI,
|
|
681
|
-
client:
|
|
893
|
+
client: WorkflowClient,
|
|
682
894
|
ctx: ExtensionContext,
|
|
683
895
|
): Promise<ClaimedSessionDelivery | undefined> {
|
|
684
896
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
685
|
-
if (!hasClaimableNotification(sessionId)) return undefined;
|
|
686
897
|
const claimRequestId = randomUUID();
|
|
687
898
|
const claimed = await requestAccepted(client, {
|
|
688
899
|
operation: "notification.claim",
|
|
@@ -746,13 +957,11 @@ async function claimPendingNotificationDelivery(
|
|
|
746
957
|
|
|
747
958
|
async function claimPendingTurnDelivery(
|
|
748
959
|
pi: ExtensionAPI,
|
|
749
|
-
client:
|
|
960
|
+
client: WorkflowClient,
|
|
750
961
|
ctx: ExtensionContext,
|
|
751
962
|
): Promise<ClaimedSessionDelivery | undefined> {
|
|
752
963
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
753
|
-
if (pendingInteractionForSession(sessionId) !== undefined
|
|
754
|
-
return undefined;
|
|
755
|
-
}
|
|
964
|
+
if (pendingInteractionForSession(sessionId) !== undefined) return undefined;
|
|
756
965
|
const claimRequestId = randomUUID();
|
|
757
966
|
const claimed = await requestAccepted(client, {
|
|
758
967
|
operation: "turn.claim",
|
|
@@ -767,8 +976,10 @@ async function claimPendingTurnDelivery(
|
|
|
767
976
|
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
|
|
768
977
|
const intentId = requireText(turn.intentId, "turn intentId");
|
|
769
978
|
const runId = requireText(turn.runId, "turn runId");
|
|
770
|
-
const
|
|
771
|
-
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");
|
|
772
983
|
return {
|
|
773
984
|
deliveryId: `turn:${intentId}`,
|
|
774
985
|
claimExpiresAt,
|
|
@@ -814,7 +1025,7 @@ async function claimPendingTurnDelivery(
|
|
|
814
1025
|
}
|
|
815
1026
|
|
|
816
1027
|
async function submitVisibleAssistantResponse(
|
|
817
|
-
client:
|
|
1028
|
+
client: WorkflowClient,
|
|
818
1029
|
ctx: ExtensionContext,
|
|
819
1030
|
): Promise<void> {
|
|
820
1031
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
@@ -845,171 +1056,78 @@ async function submitVisibleAssistantResponse(
|
|
|
845
1056
|
value: submission as unknown as JsonValue,
|
|
846
1057
|
},
|
|
847
1058
|
});
|
|
848
|
-
await waitForInteractionSubmission(interaction.requestId, `assistant-${responseId}`);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
async function waitForInteractionSubmission(
|
|
852
|
-
requestId: string,
|
|
853
|
-
submissionId: string,
|
|
854
|
-
signal?: AbortSignal,
|
|
855
|
-
): Promise<void> {
|
|
856
|
-
const store = new HostStateStore(workflowStatePath(), { readOnly: true });
|
|
857
|
-
try {
|
|
858
|
-
for (;;) {
|
|
859
|
-
if (signal?.aborted === true) {
|
|
860
|
-
throw signal.reason ?? new Error("Workflow submission validation was cancelled");
|
|
861
|
-
}
|
|
862
|
-
const submission = store.interactionSubmission(requestId, submissionId);
|
|
863
|
-
if (submission?.outcome === "accepted" || submission?.outcome === "adopted") return;
|
|
864
|
-
if (submission?.outcome === "rejected") {
|
|
865
|
-
const receipt = isRecord(submission.receipt) ? submission.receipt : undefined;
|
|
866
|
-
throw new Error(
|
|
867
|
-
typeof receipt?.error === "string"
|
|
868
|
-
? receipt.error
|
|
869
|
-
: "Workflow step output failed validation",
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
await waitForSubmissionPoll(signal);
|
|
873
|
-
}
|
|
874
|
-
} finally {
|
|
875
|
-
store.close();
|
|
876
|
-
}
|
|
877
1059
|
}
|
|
878
1060
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (error === undefined) resolve();
|
|
890
|
-
else reject(error);
|
|
891
|
-
}
|
|
892
|
-
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;
|
|
893
1071
|
});
|
|
1072
|
+
if (found) sessionSnapshots.set(sessionId, { ...session, pendingInteractions });
|
|
894
1073
|
}
|
|
895
1074
|
|
|
896
|
-
function pendingInteractionForSession(sessionId: string):
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
} finally {
|
|
902
|
-
store.close();
|
|
903
|
-
}
|
|
904
|
-
} catch {
|
|
905
|
-
return undefined;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function pendingDecision(sessionId: string, runId?: string): InteractiveRequestRecord | undefined {
|
|
910
|
-
try {
|
|
911
|
-
const store = new HostStateStore(workflowStatePath(), { readOnly: true });
|
|
912
|
-
try {
|
|
913
|
-
return store
|
|
914
|
-
.listPendingInteractions(sessionId)
|
|
915
|
-
.find(
|
|
916
|
-
(request) =>
|
|
917
|
-
request.kind === "decision" && (runId === undefined || request.runId === runId),
|
|
918
|
-
);
|
|
919
|
-
} finally {
|
|
920
|
-
store.close();
|
|
921
|
-
}
|
|
922
|
-
} catch {
|
|
923
|
-
return undefined;
|
|
924
|
-
}
|
|
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);
|
|
925
1080
|
}
|
|
926
1081
|
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
} catch {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function hasClaimableTurn(sessionId: string): boolean {
|
|
943
|
-
try {
|
|
944
|
-
const store = new SqliteControllerStore(workflowStatePath(), { readOnly: true, global: true });
|
|
945
|
-
try {
|
|
946
|
-
const now = Date.now();
|
|
947
|
-
return store
|
|
948
|
-
.listWorkflowTurnIntents({ targetSessionId: sessionId, unresolvedOnly: true, limit: 10 })
|
|
949
|
-
.some(
|
|
950
|
-
(intent) =>
|
|
951
|
-
intent.eligibleAt !== null &&
|
|
952
|
-
Date.parse(intent.eligibleAt) <= now &&
|
|
953
|
-
(intent.deliveryClaimExpiresAt === null ||
|
|
954
|
-
Date.parse(intent.deliveryClaimExpiresAt) <= now),
|
|
955
|
-
);
|
|
956
|
-
} finally {
|
|
957
|
-
store.close();
|
|
958
|
-
}
|
|
959
|
-
} catch {
|
|
960
|
-
return false;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function terminalRunState(runId: string): Record<string, unknown> | undefined {
|
|
965
|
-
try {
|
|
966
|
-
const store = new WorkflowRunStore(workflowStatePath(), { readOnly: true });
|
|
967
|
-
try {
|
|
968
|
-
const loaded = store.readRun(runId);
|
|
969
|
-
return loaded === null ? undefined : (loaded.state as unknown as Record<string, unknown>);
|
|
970
|
-
} finally {
|
|
971
|
-
store.close();
|
|
972
|
-
}
|
|
973
|
-
} catch {
|
|
974
|
-
return undefined;
|
|
975
|
-
}
|
|
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
|
+
);
|
|
976
1092
|
}
|
|
977
1093
|
|
|
978
1094
|
function workflowRunPaused(runId: string): boolean {
|
|
979
|
-
|
|
1095
|
+
for (const session of sessionSnapshots.values()) {
|
|
1096
|
+
if (session.run?.runId === runId) return session.run.display.status === "paused";
|
|
1097
|
+
}
|
|
1098
|
+
return false;
|
|
980
1099
|
}
|
|
981
1100
|
|
|
982
|
-
function interactionPresentationClaimIsLive(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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;
|
|
989
1112
|
try {
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
!workflowRunPaused(options.runId)
|
|
1002
|
-
);
|
|
1003
|
-
} finally {
|
|
1004
|
-
store.close();
|
|
1005
|
-
}
|
|
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
|
+
);
|
|
1006
1124
|
} catch {
|
|
1007
1125
|
return false;
|
|
1008
1126
|
}
|
|
1009
1127
|
}
|
|
1010
1128
|
|
|
1011
1129
|
async function hostDeliveryClaimIsLive(
|
|
1012
|
-
client:
|
|
1130
|
+
client: WorkflowClient,
|
|
1013
1131
|
options: {
|
|
1014
1132
|
kind: "notification" | "turn";
|
|
1015
1133
|
resourceId: string;
|
|
@@ -1095,25 +1213,18 @@ function claimExpiry(value: unknown, name: string): number {
|
|
|
1095
1213
|
return expiry;
|
|
1096
1214
|
}
|
|
1097
1215
|
|
|
1098
|
-
function activeSessionRun(ctx: ExtensionContext):
|
|
1216
|
+
function activeSessionRun(ctx: ExtensionContext): WorkflowRunQueueView | undefined {
|
|
1099
1217
|
return sessionRun(ctx);
|
|
1100
1218
|
}
|
|
1101
1219
|
|
|
1102
|
-
function sessionRun(ctx: ExtensionContext, runId?: string):
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
try {
|
|
1106
|
-
const run =
|
|
1107
|
-
runId === undefined
|
|
1108
|
-
? store.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
1109
|
-
: store.getWorkflowRun(runId);
|
|
1110
|
-
return run?.originSessionId === ctx.sessionManager.getSessionId() ? run : undefined;
|
|
1111
|
-
} finally {
|
|
1112
|
-
store.close();
|
|
1113
|
-
}
|
|
1114
|
-
} 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)) {
|
|
1115
1223
|
return undefined;
|
|
1116
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;
|
|
1117
1228
|
}
|
|
1118
1229
|
|
|
1119
1230
|
async function listWorkflowMetadata(cwd: string): Promise<Array<{ name: string; source: string }>> {
|
|
@@ -1157,23 +1268,26 @@ function toolInputToCommand(params: ReturnType<typeof parseWorkflowToolInput>):
|
|
|
1157
1268
|
}
|
|
1158
1269
|
|
|
1159
1270
|
async function requestAccepted(
|
|
1160
|
-
client:
|
|
1161
|
-
options: Parameters<
|
|
1162
|
-
): Promise<
|
|
1163
|
-
await client.
|
|
1164
|
-
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 });
|
|
1165
1279
|
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
1166
1280
|
throw new Error(response.error ?? `Workflow host rejected ${options.operation}`);
|
|
1167
1281
|
}
|
|
1168
1282
|
return response;
|
|
1169
1283
|
}
|
|
1170
1284
|
|
|
1171
|
-
function interactionContract(interaction:
|
|
1285
|
+
function interactionContract(interaction: ClientInteractiveRequest): Record<string, unknown> {
|
|
1172
1286
|
if (!isRecord(interaction.contract)) throw new Error("Stored interaction contract is invalid");
|
|
1173
1287
|
return interaction.contract;
|
|
1174
1288
|
}
|
|
1175
1289
|
|
|
1176
|
-
function agentContract(interaction:
|
|
1290
|
+
function agentContract(interaction: ClientInteractiveRequest): AgentStepContract | undefined {
|
|
1177
1291
|
const value = interactionContract(interaction).contract;
|
|
1178
1292
|
if (
|
|
1179
1293
|
!isRecord(value) ||
|
|
@@ -1296,6 +1410,33 @@ function validRunId(value: string): boolean {
|
|
|
1296
1410
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u.test(value);
|
|
1297
1411
|
}
|
|
1298
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
|
+
|
|
1299
1440
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1300
1441
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1301
1442
|
}
|