@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/dist/extension/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { BUILTIN_WORKFLOW_METADATA } from "../builtins/metadata.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { workflowStatePath } from "../state/database.js";
|
|
4
|
+
import { ORIGIN_ACTIVITY_REFRESH_MS } from "../client/activity.js";
|
|
5
|
+
import { WorkflowClient } from "../client/client.js";
|
|
6
|
+
import { materializeSessionView } from "../client/materialize.js";
|
|
8
7
|
import { canonicalJson, parseJson } from "../state/json.js";
|
|
9
8
|
import { errorMessage } from "../workflows/errors.js";
|
|
10
9
|
import { discoverWorkflows } from "../workflows/loader.js";
|
|
11
|
-
import { createRunId
|
|
10
|
+
import { createRunId } from "../workflows/store.js";
|
|
12
11
|
import { parseControllerArgs } from "./controller-command.js";
|
|
12
|
+
import { HerdrWorkflowViewer, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
|
|
13
13
|
import { SessionDeliveryCoordinator } from "./session-delivery.js";
|
|
14
14
|
import { SessionWorkflowView } from "./session-view.js";
|
|
15
15
|
import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
@@ -17,10 +17,13 @@ import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.
|
|
|
17
17
|
export { parseControllerArgs } from "./controller-command.js";
|
|
18
18
|
export { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, decisionConfigDir, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
|
|
19
19
|
const INTERACTION_POLL_MS = 1_000;
|
|
20
|
-
const SUBMISSION_POLL_MS = 50;
|
|
21
20
|
const WORKFLOW_INTERACTION_MESSAGE_TYPE = "pi-workflows-interaction";
|
|
22
21
|
const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
|
|
23
22
|
const WORKFLOW_PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
23
|
+
const sessionSnapshots = new Map();
|
|
24
|
+
export function activityStateForConnection(state, previousConnectionId, connectionId) {
|
|
25
|
+
return state === "refresh" && previousConnectionId !== connectionId ? "started" : state;
|
|
26
|
+
}
|
|
24
27
|
/** Parse `/workflow` arguments. Exported for tests. */
|
|
25
28
|
export function parseWorkflowArgs(args) {
|
|
26
29
|
const trimmed = args.trim();
|
|
@@ -90,13 +93,58 @@ export function parseWorkflowArgs(args) {
|
|
|
90
93
|
}
|
|
91
94
|
export default function piWorkflows(pi) {
|
|
92
95
|
registerWorkflowAgentStepMessageRenderer(pi);
|
|
93
|
-
|
|
96
|
+
let client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
|
|
97
|
+
const herdrViewer = new HerdrWorkflowViewer(pi.exec);
|
|
94
98
|
let sessionContext = null;
|
|
99
|
+
let sessionGeneration = 0;
|
|
100
|
+
let sessionUnsubscribe = null;
|
|
95
101
|
let pollTimer = null;
|
|
96
102
|
let presentationTail = Promise.resolve();
|
|
97
103
|
let toolTail = Promise.resolve();
|
|
104
|
+
let originActivity = null;
|
|
105
|
+
let originActivityConnectionId = null;
|
|
106
|
+
let activityTimer = null;
|
|
98
107
|
const sessionDelivery = new SessionDeliveryCoordinator();
|
|
99
108
|
const sessionView = new SessionWorkflowView();
|
|
109
|
+
const reportOriginActivity = async (activity, state) => {
|
|
110
|
+
const connection = await client.connect();
|
|
111
|
+
const reportState = activityStateForConnection(state, originActivityConnectionId, connection.connectionId);
|
|
112
|
+
const response = await client.request({
|
|
113
|
+
operation: "activity.report",
|
|
114
|
+
runId: activity.runId,
|
|
115
|
+
payload: { ...activity, state: reportState },
|
|
116
|
+
});
|
|
117
|
+
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
118
|
+
throw new Error(response.error ?? "Workflow host rejected origin activity");
|
|
119
|
+
}
|
|
120
|
+
originActivityConnectionId =
|
|
121
|
+
reportState === "settled" ? null : (client.connectionId ?? connection.connectionId);
|
|
122
|
+
};
|
|
123
|
+
const startOriginActivity = (presented) => {
|
|
124
|
+
if (activityTimer !== null)
|
|
125
|
+
clearInterval(activityTimer);
|
|
126
|
+
originActivityConnectionId = null;
|
|
127
|
+
originActivity = { ...presented, sequence: 0 };
|
|
128
|
+
void reportOriginActivity(originActivity, "started").catch(() => undefined);
|
|
129
|
+
activityTimer = setInterval(() => {
|
|
130
|
+
if (originActivity === null)
|
|
131
|
+
return;
|
|
132
|
+
originActivity.sequence += 1;
|
|
133
|
+
void reportOriginActivity(originActivity, "refresh").catch(() => undefined);
|
|
134
|
+
}, ORIGIN_ACTIVITY_REFRESH_MS);
|
|
135
|
+
activityTimer.unref?.();
|
|
136
|
+
};
|
|
137
|
+
const settleOriginActivity = async () => {
|
|
138
|
+
if (activityTimer !== null)
|
|
139
|
+
clearInterval(activityTimer);
|
|
140
|
+
activityTimer = null;
|
|
141
|
+
const settled = originActivity;
|
|
142
|
+
originActivity = null;
|
|
143
|
+
if (settled === null)
|
|
144
|
+
return;
|
|
145
|
+
settled.sequence += 1;
|
|
146
|
+
await reportOriginActivity(settled, "settled").catch(() => undefined);
|
|
147
|
+
};
|
|
100
148
|
const presentInOrder = async (ctx) => {
|
|
101
149
|
const prior = presentationTail;
|
|
102
150
|
let release;
|
|
@@ -105,10 +153,13 @@ export default function piWorkflows(pi) {
|
|
|
105
153
|
});
|
|
106
154
|
await prior;
|
|
107
155
|
try {
|
|
156
|
+
const deliveries = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.deliveries;
|
|
108
157
|
await sessionDelivery.synchronize(ctx, [
|
|
109
|
-
() => claimPendingInteractionDelivery(pi, client, ctx),
|
|
110
|
-
(
|
|
111
|
-
|
|
158
|
+
() => claimPendingInteractionDelivery(pi, client, ctx, startOriginActivity),
|
|
159
|
+
...(deliveries?.notification === true
|
|
160
|
+
? [() => claimPendingNotificationDelivery(pi, client, ctx)]
|
|
161
|
+
: []),
|
|
162
|
+
...(deliveries?.turn === true ? [() => claimPendingTurnDelivery(pi, client, ctx)] : []),
|
|
112
163
|
]);
|
|
113
164
|
}
|
|
114
165
|
finally {
|
|
@@ -116,6 +167,25 @@ export default function piWorkflows(pi) {
|
|
|
116
167
|
release?.();
|
|
117
168
|
}
|
|
118
169
|
};
|
|
170
|
+
const openPiw = async (ctx, requestedPlacement) => {
|
|
171
|
+
const run = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.run;
|
|
172
|
+
if (run === null || run === undefined || !isRecord(run.state)) {
|
|
173
|
+
ctx.ui.notify("No active workflow is available for piw.", "warning");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const capability = await herdrViewer.probe();
|
|
177
|
+
if (!capability.available) {
|
|
178
|
+
ctx.ui.notify(capability.reason, "warning");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const placement = requestedPlacement ??
|
|
182
|
+
(await ctx.ui.select("Open workflow viewer", [...VIEWER_PLACEMENTS]));
|
|
183
|
+
if (placement === undefined)
|
|
184
|
+
return;
|
|
185
|
+
const workflowName = typeof run.state.workflowName === "string" ? run.state.workflowName : run.runId;
|
|
186
|
+
const opened = await herdrViewer.open({ runId: run.runId, workflowName }, placement, ctx.cwd);
|
|
187
|
+
ctx.ui.notify(opened.reused ? "Focused the existing piw view." : "Opened piw in Herdr.", "info");
|
|
188
|
+
};
|
|
119
189
|
const runToolInOrder = async (operation) => {
|
|
120
190
|
const prior = toolTail;
|
|
121
191
|
let release;
|
|
@@ -123,8 +193,12 @@ export default function piWorkflows(pi) {
|
|
|
123
193
|
release = resolve;
|
|
124
194
|
});
|
|
125
195
|
await prior;
|
|
126
|
-
await presentationTail;
|
|
127
196
|
try {
|
|
197
|
+
await presentationTail;
|
|
198
|
+
const currentContext = sessionContext;
|
|
199
|
+
if (currentContext === null)
|
|
200
|
+
throw new Error("Workflow session is unavailable");
|
|
201
|
+
await presentInOrder(currentContext);
|
|
128
202
|
return await operation();
|
|
129
203
|
}
|
|
130
204
|
finally {
|
|
@@ -200,7 +274,7 @@ export default function piWorkflows(pi) {
|
|
|
200
274
|
}
|
|
201
275
|
const response = await requestAccepted(client, {
|
|
202
276
|
operation: params.action === "update" ? "interaction.update" : "interaction.submit",
|
|
203
|
-
requestId: `${params.action}-${toolCallId}`,
|
|
277
|
+
requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
|
|
204
278
|
idempotencyKey: toolCallId,
|
|
205
279
|
runId: interaction.runId,
|
|
206
280
|
expectedRevision: interaction.revision,
|
|
@@ -211,10 +285,8 @@ export default function piWorkflows(pi) {
|
|
|
211
285
|
attempt: params.attempt,
|
|
212
286
|
value: params.action === "update" ? { update: params.update } : { output: params.output },
|
|
213
287
|
}),
|
|
288
|
+
...(signal === undefined ? {} : { signal }),
|
|
214
289
|
});
|
|
215
|
-
if (params.action === "submit") {
|
|
216
|
-
await waitForInteractionSubmission(interaction.requestId, toolCallId, signal);
|
|
217
|
-
}
|
|
218
290
|
await presentInOrder(ctx);
|
|
219
291
|
return toolResult(params.action === "update"
|
|
220
292
|
? "Workflow update accepted; the step remains active."
|
|
@@ -227,6 +299,32 @@ export default function piWorkflows(pi) {
|
|
|
227
299
|
});
|
|
228
300
|
},
|
|
229
301
|
});
|
|
302
|
+
pi.registerCommand("piw", {
|
|
303
|
+
description: "Open the active workflow in Herdr",
|
|
304
|
+
handler: async (args, ctx) => {
|
|
305
|
+
try {
|
|
306
|
+
const requested = args.trim();
|
|
307
|
+
if (requested.length > 0 && !VIEWER_PLACEMENTS.includes(requested)) {
|
|
308
|
+
throw new Error(`Unknown piw placement: ${requested}`);
|
|
309
|
+
}
|
|
310
|
+
await openPiw(ctx, requested.length === 0 ? undefined : requested);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
ctx.ui.notify(`Could not open piw: ${errorMessage(error)}`, "warning");
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
pi.registerShortcut(PIW_SHORTCUT, {
|
|
318
|
+
description: "Open the active workflow in Herdr",
|
|
319
|
+
handler: async (ctx) => {
|
|
320
|
+
try {
|
|
321
|
+
await openPiw(ctx);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
ctx.ui.notify(`Could not open piw: ${errorMessage(error)}`, "warning");
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
230
328
|
pi.registerShortcut("shift+up", {
|
|
231
329
|
description: "Scroll the workflow widget up",
|
|
232
330
|
handler: (ctx) => sessionView.scrollUp(ctx),
|
|
@@ -235,20 +333,63 @@ export default function piWorkflows(pi) {
|
|
|
235
333
|
description: "Scroll the workflow widget down",
|
|
236
334
|
handler: (ctx) => sessionView.scrollDown(ctx),
|
|
237
335
|
});
|
|
238
|
-
pi.on("session_start",
|
|
336
|
+
pi.on("session_start", (_event, ctx) => {
|
|
239
337
|
sessionContext = ctx;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
catch (error) {
|
|
245
|
-
ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
|
|
246
|
-
}
|
|
338
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
339
|
+
const generation = ++sessionGeneration;
|
|
340
|
+
let snapshotGeneration = 0;
|
|
341
|
+
const sessionClient = client;
|
|
247
342
|
pollTimer = setInterval(() => {
|
|
248
343
|
if (sessionContext !== null)
|
|
249
344
|
void presentInOrder(sessionContext).catch(() => undefined);
|
|
250
345
|
}, INTERACTION_POLL_MS);
|
|
251
346
|
pollTimer.unref?.();
|
|
347
|
+
void (async () => {
|
|
348
|
+
try {
|
|
349
|
+
await sessionClient.ensureAvailable();
|
|
350
|
+
const unsubscribe = await sessionClient.watchSession(sessionId, (event) => {
|
|
351
|
+
if (generation !== sessionGeneration || sessionContext !== ctx)
|
|
352
|
+
return;
|
|
353
|
+
const currentSnapshotGeneration = ++snapshotGeneration;
|
|
354
|
+
if (event.event === "unavailable") {
|
|
355
|
+
sessionSnapshots.delete(sessionId);
|
|
356
|
+
sessionView.clear(ctx);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!isWorkflowSessionView(event.payload))
|
|
360
|
+
return;
|
|
361
|
+
void materializeSessionView(sessionClient, event.payload)
|
|
362
|
+
.then((session) => {
|
|
363
|
+
if (generation !== sessionGeneration ||
|
|
364
|
+
currentSnapshotGeneration !== snapshotGeneration ||
|
|
365
|
+
sessionContext !== ctx) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
sessionSnapshots.set(sessionId, session);
|
|
369
|
+
sessionView.update(session, ctx);
|
|
370
|
+
void presentInOrder(ctx).catch(() => undefined);
|
|
371
|
+
})
|
|
372
|
+
.catch(() => {
|
|
373
|
+
// A newer session revision retries from its own stable snapshot.
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
if (generation !== sessionGeneration || sessionContext !== ctx) {
|
|
377
|
+
await unsubscribe();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
sessionUnsubscribe = unsubscribe;
|
|
381
|
+
const capability = await herdrViewer.probe();
|
|
382
|
+
if (generation !== sessionGeneration || sessionContext !== ctx)
|
|
383
|
+
return;
|
|
384
|
+
sessionView.setActionHint(capability.available ? PIW_SHORTCUT_HINT : undefined, ctx);
|
|
385
|
+
await presentInOrder(ctx);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (generation === sessionGeneration && sessionContext === ctx) {
|
|
389
|
+
ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
252
393
|
});
|
|
253
394
|
pi.on("agent_end", async (event, ctx) => {
|
|
254
395
|
const interrupted = ctx.signal?.aborted === true ||
|
|
@@ -264,6 +405,7 @@ export default function piWorkflows(pi) {
|
|
|
264
405
|
const turnHasPrompt = event.messages.some((message) => interactionRequestId(message) === interaction.requestId);
|
|
265
406
|
if (!turnHasPrompt)
|
|
266
407
|
return;
|
|
408
|
+
await settleOriginActivity();
|
|
267
409
|
try {
|
|
268
410
|
const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
|
|
269
411
|
await requestAccepted(client, {
|
|
@@ -286,15 +428,27 @@ export default function piWorkflows(pi) {
|
|
|
286
428
|
catch (error) {
|
|
287
429
|
ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
|
|
288
430
|
}
|
|
431
|
+
await settleOriginActivity();
|
|
289
432
|
await presentInOrder(ctx).catch(() => undefined);
|
|
290
433
|
});
|
|
291
434
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
435
|
+
sessionGeneration += 1;
|
|
292
436
|
sessionContext = null;
|
|
293
437
|
sessionDelivery.clear();
|
|
294
438
|
sessionView.clear(ctx);
|
|
439
|
+
sessionSnapshots.delete(ctx.sessionManager.getSessionId());
|
|
440
|
+
if (activityTimer !== null)
|
|
441
|
+
clearInterval(activityTimer);
|
|
442
|
+
activityTimer = null;
|
|
443
|
+
originActivity = null;
|
|
295
444
|
if (pollTimer !== null)
|
|
296
445
|
clearInterval(pollTimer);
|
|
297
446
|
pollTimer = null;
|
|
447
|
+
if (sessionUnsubscribe !== null)
|
|
448
|
+
await sessionUnsubscribe().catch(() => undefined);
|
|
449
|
+
sessionUnsubscribe = null;
|
|
450
|
+
await client.close();
|
|
451
|
+
client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
|
|
298
452
|
});
|
|
299
453
|
}
|
|
300
454
|
async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(), authority = "human") {
|
|
@@ -310,7 +464,7 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
|
|
|
310
464
|
};
|
|
311
465
|
}
|
|
312
466
|
case "run": {
|
|
313
|
-
await client.
|
|
467
|
+
await client.ensureAvailable();
|
|
314
468
|
const resolved = await client.resolveWorkflow({ cwd: ctx.cwd, workflowRef: command.ref });
|
|
315
469
|
const runId = createRunId(resolved.workflowName);
|
|
316
470
|
const response = await requestAccepted(client, {
|
|
@@ -497,10 +651,15 @@ async function executeControllerCommand(client, ctx, command) {
|
|
|
497
651
|
details: { action: command.kind, resource: response.receipt ?? null },
|
|
498
652
|
};
|
|
499
653
|
}
|
|
500
|
-
async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
654
|
+
async function claimPendingInteractionDelivery(pi, client, ctx, onPresented) {
|
|
655
|
+
const storedInteraction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
656
|
+
if (storedInteraction === undefined || storedInteraction.presentationSessionEntryId !== null)
|
|
503
657
|
return undefined;
|
|
658
|
+
const interaction = {
|
|
659
|
+
...storedInteraction,
|
|
660
|
+
contract: await client.hydrateContent(storedInteraction.runId, storedInteraction.contract),
|
|
661
|
+
};
|
|
662
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), interaction);
|
|
504
663
|
const findSessionEntryId = (entries) => entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
|
|
505
664
|
const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
|
|
506
665
|
let presentationRevision = interaction.revision;
|
|
@@ -512,7 +671,7 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
512
671
|
Date.parse(interaction.presentationClaimExpiresAt) > Date.now()) {
|
|
513
672
|
return undefined;
|
|
514
673
|
}
|
|
515
|
-
await client.
|
|
674
|
+
await client.ensureAvailable();
|
|
516
675
|
const claim = await client.request({
|
|
517
676
|
operation: "interaction.update",
|
|
518
677
|
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
@@ -531,13 +690,14 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
531
690
|
const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
|
|
532
691
|
presentationRevision = claim.revision;
|
|
533
692
|
claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
|
|
693
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), claim.receipt);
|
|
534
694
|
}
|
|
535
695
|
const contract = interactionContract(interaction);
|
|
536
696
|
return {
|
|
537
697
|
deliveryId: `interaction:${interaction.requestId}`,
|
|
538
698
|
claimExpiresAt,
|
|
539
699
|
isStillDeliverable: () => existingEntryId === undefined &&
|
|
540
|
-
interactionPresentationClaimIsLive({
|
|
700
|
+
interactionPresentationClaimIsLive(client, {
|
|
541
701
|
requestId: interaction.requestId,
|
|
542
702
|
runId: interaction.runId,
|
|
543
703
|
presenterId: client.clientId,
|
|
@@ -579,7 +739,7 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
579
739
|
}, { triggerTurn: true });
|
|
580
740
|
},
|
|
581
741
|
settle: async (sessionEntryId) => {
|
|
582
|
-
await requestAccepted(client, {
|
|
742
|
+
const response = await requestAccepted(client, {
|
|
583
743
|
operation: "interaction.update",
|
|
584
744
|
requestId: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
585
745
|
idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
@@ -587,13 +747,21 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
587
747
|
expectedRevision: presentationRevision,
|
|
588
748
|
payload: { requestId: interaction.requestId, sessionEntryId },
|
|
589
749
|
});
|
|
750
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), response.receipt);
|
|
751
|
+
if (interaction.kind !== "decision") {
|
|
752
|
+
onPresented({
|
|
753
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
754
|
+
runId: interaction.runId,
|
|
755
|
+
requestId: interaction.requestId,
|
|
756
|
+
deliveryId: `interaction:${interaction.requestId}`,
|
|
757
|
+
sessionEntryId,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
590
760
|
},
|
|
591
761
|
};
|
|
592
762
|
}
|
|
593
763
|
async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
594
764
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
595
|
-
if (!hasClaimableNotification(sessionId))
|
|
596
|
-
return undefined;
|
|
597
765
|
const claimRequestId = randomUUID();
|
|
598
766
|
const claimed = await requestAccepted(client, {
|
|
599
767
|
operation: "notification.claim",
|
|
@@ -647,9 +815,8 @@ async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
|
647
815
|
}
|
|
648
816
|
async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
649
817
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
650
|
-
if (pendingInteractionForSession(sessionId) !== undefined
|
|
818
|
+
if (pendingInteractionForSession(sessionId) !== undefined)
|
|
651
819
|
return undefined;
|
|
652
|
-
}
|
|
653
820
|
const claimRequestId = randomUUID();
|
|
654
821
|
const claimed = await requestAccepted(client, {
|
|
655
822
|
operation: "turn.claim",
|
|
@@ -665,9 +832,12 @@ async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
|
665
832
|
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
|
|
666
833
|
const intentId = requireText(turn.intentId, "turn intentId");
|
|
667
834
|
const runId = requireText(turn.runId, "turn runId");
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
670
|
-
|
|
835
|
+
const claimedRun = await client.getRun(runId);
|
|
836
|
+
if (claimedRun === null)
|
|
837
|
+
throw new Error(`Claimed workflow turn run is missing: ${runId}`);
|
|
838
|
+
const state = await client.hydrateContent(runId, claimedRun.state);
|
|
839
|
+
if (!isRecord(state))
|
|
840
|
+
throw new Error("Workflow terminal state content is invalid");
|
|
671
841
|
return {
|
|
672
842
|
deliveryId: `turn:${intentId}`,
|
|
673
843
|
claimExpiresAt,
|
|
@@ -731,148 +901,57 @@ async function submitVisibleAssistantResponse(client, ctx) {
|
|
|
731
901
|
value: submission,
|
|
732
902
|
},
|
|
733
903
|
});
|
|
734
|
-
await waitForInteractionSubmission(interaction.requestId, `assistant-${responseId}`);
|
|
735
904
|
}
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
throw new Error(typeof receipt?.error === "string"
|
|
749
|
-
? receipt.error
|
|
750
|
-
: "Workflow step output failed validation");
|
|
751
|
-
}
|
|
752
|
-
await waitForSubmissionPoll(signal);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
finally {
|
|
756
|
-
store.close();
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
async function waitForSubmissionPoll(signal) {
|
|
760
|
-
if (signal?.aborted === true) {
|
|
761
|
-
throw signal.reason ?? new Error("Workflow submission was cancelled");
|
|
762
|
-
}
|
|
763
|
-
await new Promise((resolve, reject) => {
|
|
764
|
-
const timer = setTimeout(finish, SUBMISSION_POLL_MS);
|
|
765
|
-
const onAbort = () => finish(signal?.reason ?? new Error("Workflow submission was cancelled"));
|
|
766
|
-
function finish(error) {
|
|
767
|
-
clearTimeout(timer);
|
|
768
|
-
signal?.removeEventListener("abort", onAbort);
|
|
769
|
-
if (error === undefined)
|
|
770
|
-
resolve();
|
|
771
|
-
else
|
|
772
|
-
reject(error);
|
|
773
|
-
}
|
|
774
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
905
|
+
function rememberInteractiveRequest(sessionId, value) {
|
|
906
|
+
const current = parseInteractiveRequest(value);
|
|
907
|
+
const session = sessionSnapshots.get(sessionId);
|
|
908
|
+
if (current === undefined || session === undefined)
|
|
909
|
+
return;
|
|
910
|
+
let found = false;
|
|
911
|
+
const pendingInteractions = session.pendingInteractions.map((item) => {
|
|
912
|
+
const stored = parseInteractiveRequest(item);
|
|
913
|
+
if (stored?.requestId !== current.requestId)
|
|
914
|
+
return item;
|
|
915
|
+
found = true;
|
|
916
|
+
return current;
|
|
775
917
|
});
|
|
918
|
+
if (found)
|
|
919
|
+
sessionSnapshots.set(sessionId, { ...session, pendingInteractions });
|
|
776
920
|
}
|
|
777
921
|
function pendingInteractionForSession(sessionId) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
finally {
|
|
784
|
-
store.close();
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
catch {
|
|
788
|
-
return undefined;
|
|
789
|
-
}
|
|
922
|
+
return sessionSnapshots
|
|
923
|
+
.get(sessionId)
|
|
924
|
+
?.pendingInteractions.map(parseInteractiveRequest)
|
|
925
|
+
.find((request) => request !== undefined);
|
|
790
926
|
}
|
|
791
927
|
function pendingDecision(sessionId, runId) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
finally {
|
|
800
|
-
store.close();
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
return undefined;
|
|
805
|
-
}
|
|
928
|
+
return sessionSnapshots
|
|
929
|
+
.get(sessionId)
|
|
930
|
+
?.pendingInteractions.map(parseInteractiveRequest)
|
|
931
|
+
.find((request) => request !== undefined &&
|
|
932
|
+
request.kind === "decision" &&
|
|
933
|
+
(runId === undefined || request.runId === runId));
|
|
806
934
|
}
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
return (store.listPendingWorkflowNotifications({ targetSessionId: sessionId, limit: 1 }).length > 0);
|
|
812
|
-
}
|
|
813
|
-
finally {
|
|
814
|
-
store.close();
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
catch {
|
|
818
|
-
return false;
|
|
935
|
+
function workflowRunPaused(runId) {
|
|
936
|
+
for (const session of sessionSnapshots.values()) {
|
|
937
|
+
if (session.run?.runId === runId)
|
|
938
|
+
return session.run.display.status === "paused";
|
|
819
939
|
}
|
|
940
|
+
return false;
|
|
820
941
|
}
|
|
821
|
-
function
|
|
822
|
-
|
|
823
|
-
const store = new SqliteControllerStore(workflowStatePath(), { readOnly: true, global: true });
|
|
824
|
-
try {
|
|
825
|
-
const now = Date.now();
|
|
826
|
-
return store
|
|
827
|
-
.listWorkflowTurnIntents({ targetSessionId: sessionId, unresolvedOnly: true, limit: 10 })
|
|
828
|
-
.some((intent) => intent.eligibleAt !== null &&
|
|
829
|
-
Date.parse(intent.eligibleAt) <= now &&
|
|
830
|
-
(intent.deliveryClaimExpiresAt === null ||
|
|
831
|
-
Date.parse(intent.deliveryClaimExpiresAt) <= now));
|
|
832
|
-
}
|
|
833
|
-
finally {
|
|
834
|
-
store.close();
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
catch {
|
|
942
|
+
async function interactionPresentationClaimIsLive(client, options) {
|
|
943
|
+
if (options.claimExpiresAt <= Date.now() || options.presenterId !== client.clientId)
|
|
838
944
|
return false;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function terminalRunState(runId) {
|
|
842
945
|
try {
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
catch {
|
|
853
|
-
return undefined;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
function workflowRunPaused(runId) {
|
|
857
|
-
return terminalRunState(runId)?.paused === true;
|
|
858
|
-
}
|
|
859
|
-
function interactionPresentationClaimIsLive(options) {
|
|
860
|
-
try {
|
|
861
|
-
const store = new HostStateStore(workflowStatePath(), { readOnly: true });
|
|
862
|
-
try {
|
|
863
|
-
const interaction = store.getInteraction(options.requestId);
|
|
864
|
-
return (interaction?.runId === options.runId &&
|
|
865
|
-
interaction.status === "presenting" &&
|
|
866
|
-
interaction.presenterId === options.presenterId &&
|
|
867
|
-
interaction.revision === options.revision &&
|
|
868
|
-
interaction.presentationSessionEntryId === null &&
|
|
869
|
-
interaction.presentationClaimExpiresAt !== null &&
|
|
870
|
-
Date.parse(interaction.presentationClaimExpiresAt) === options.claimExpiresAt &&
|
|
871
|
-
!workflowRunPaused(options.runId));
|
|
872
|
-
}
|
|
873
|
-
finally {
|
|
874
|
-
store.close();
|
|
875
|
-
}
|
|
946
|
+
const response = await client.request({
|
|
947
|
+
operation: "interaction.update",
|
|
948
|
+
runId: options.runId,
|
|
949
|
+
expectedRevision: options.revision,
|
|
950
|
+
payload: { requestId: options.requestId, validatePresentation: true },
|
|
951
|
+
});
|
|
952
|
+
return ((response.outcome === "accepted" || response.outcome === "adopted") &&
|
|
953
|
+
isRecord(response.receipt) &&
|
|
954
|
+
response.receipt.live === true);
|
|
876
955
|
}
|
|
877
956
|
catch {
|
|
878
957
|
return false;
|
|
@@ -942,21 +1021,14 @@ function activeSessionRun(ctx) {
|
|
|
942
1021
|
return sessionRun(ctx);
|
|
943
1022
|
}
|
|
944
1023
|
function sessionRun(ctx, runId) {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
try {
|
|
948
|
-
const run = runId === undefined
|
|
949
|
-
? store.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
950
|
-
: store.getWorkflowRun(runId);
|
|
951
|
-
return run?.originSessionId === ctx.sessionManager.getSessionId() ? run : undefined;
|
|
952
|
-
}
|
|
953
|
-
finally {
|
|
954
|
-
store.close();
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
catch {
|
|
1024
|
+
const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
|
|
1025
|
+
if (session?.run === null || session?.run === undefined || !isRecord(session.run.queue)) {
|
|
958
1026
|
return undefined;
|
|
959
1027
|
}
|
|
1028
|
+
if (runId !== undefined && session.run.runId !== runId)
|
|
1029
|
+
return undefined;
|
|
1030
|
+
const queue = session.run.queue;
|
|
1031
|
+
return queue.originSessionId === ctx.sessionManager.getSessionId() ? queue : undefined;
|
|
960
1032
|
}
|
|
961
1033
|
async function listWorkflowMetadata(cwd) {
|
|
962
1034
|
const files = await discoverWorkflows({ cwd });
|
|
@@ -997,8 +1069,10 @@ function toolInputToCommand(params) {
|
|
|
997
1069
|
}
|
|
998
1070
|
}
|
|
999
1071
|
async function requestAccepted(client, options) {
|
|
1000
|
-
await client.
|
|
1001
|
-
const response =
|
|
1072
|
+
await client.ensureAvailable();
|
|
1073
|
+
const response = options.idempotencyKey === undefined
|
|
1074
|
+
? await client.request(options)
|
|
1075
|
+
: await client.requestDurable({ ...options, idempotencyKey: options.idempotencyKey });
|
|
1002
1076
|
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
1003
1077
|
throw new Error(response.error ?? `Workflow host rejected ${options.operation}`);
|
|
1004
1078
|
}
|
|
@@ -1122,6 +1196,27 @@ function toolResult(message, details) {
|
|
|
1122
1196
|
function validRunId(value) {
|
|
1123
1197
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u.test(value);
|
|
1124
1198
|
}
|
|
1199
|
+
function isWorkflowSessionView(value) {
|
|
1200
|
+
return (isRecord(value) &&
|
|
1201
|
+
value.schema === "pi-workflows.session-view.v1" &&
|
|
1202
|
+
typeof value.sessionId === "string" &&
|
|
1203
|
+
Array.isArray(value.pendingInteractions) &&
|
|
1204
|
+
isRecord(value.deliveries) &&
|
|
1205
|
+
typeof value.deliveries.notification === "boolean" &&
|
|
1206
|
+
typeof value.deliveries.turn === "boolean" &&
|
|
1207
|
+
(value.run === null || isRecord(value.run)));
|
|
1208
|
+
}
|
|
1209
|
+
function parseInteractiveRequest(value) {
|
|
1210
|
+
if (!isRecord(value) ||
|
|
1211
|
+
typeof value.requestId !== "string" ||
|
|
1212
|
+
typeof value.runId !== "string" ||
|
|
1213
|
+
typeof value.targetSessionId !== "string" ||
|
|
1214
|
+
typeof value.revision !== "number" ||
|
|
1215
|
+
(value.kind !== "agent" && value.kind !== "assistant" && value.kind !== "decision")) {
|
|
1216
|
+
return undefined;
|
|
1217
|
+
}
|
|
1218
|
+
return value;
|
|
1219
|
+
}
|
|
1125
1220
|
function isRecord(value) {
|
|
1126
1221
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1127
1222
|
}
|