@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/dist/extension/index.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
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
|
+
import { SessionWorkflowView } from "./session-view.js";
|
|
14
15
|
import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
15
16
|
import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
|
|
16
17
|
export { parseControllerArgs } from "./controller-command.js";
|
|
17
18
|
export { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, decisionConfigDir, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
|
|
18
19
|
const INTERACTION_POLL_MS = 1_000;
|
|
19
|
-
const SUBMISSION_POLL_MS = 50;
|
|
20
20
|
const WORKFLOW_INTERACTION_MESSAGE_TYPE = "pi-workflows-interaction";
|
|
21
21
|
const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
|
|
22
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
|
+
}
|
|
23
27
|
/** Parse `/workflow` arguments. Exported for tests. */
|
|
24
28
|
export function parseWorkflowArgs(args) {
|
|
25
29
|
const trimmed = args.trim();
|
|
@@ -89,12 +93,58 @@ export function parseWorkflowArgs(args) {
|
|
|
89
93
|
}
|
|
90
94
|
export default function piWorkflows(pi) {
|
|
91
95
|
registerWorkflowAgentStepMessageRenderer(pi);
|
|
92
|
-
|
|
96
|
+
let client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
|
|
97
|
+
const herdrViewer = new HerdrWorkflowViewer(pi.exec);
|
|
93
98
|
let sessionContext = null;
|
|
99
|
+
let sessionGeneration = 0;
|
|
100
|
+
let sessionUnsubscribe = null;
|
|
94
101
|
let pollTimer = null;
|
|
95
102
|
let presentationTail = Promise.resolve();
|
|
96
103
|
let toolTail = Promise.resolve();
|
|
104
|
+
let originActivity = null;
|
|
105
|
+
let originActivityConnectionId = null;
|
|
106
|
+
let activityTimer = null;
|
|
97
107
|
const sessionDelivery = new SessionDeliveryCoordinator();
|
|
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
|
+
};
|
|
98
148
|
const presentInOrder = async (ctx) => {
|
|
99
149
|
const prior = presentationTail;
|
|
100
150
|
let release;
|
|
@@ -103,16 +153,39 @@ export default function piWorkflows(pi) {
|
|
|
103
153
|
});
|
|
104
154
|
await prior;
|
|
105
155
|
try {
|
|
156
|
+
const deliveries = sessionSnapshots.get(ctx.sessionManager.getSessionId())?.deliveries;
|
|
106
157
|
await sessionDelivery.synchronize(ctx, [
|
|
107
|
-
() => claimPendingInteractionDelivery(pi, client, ctx),
|
|
108
|
-
(
|
|
109
|
-
|
|
158
|
+
() => claimPendingInteractionDelivery(pi, client, ctx, startOriginActivity),
|
|
159
|
+
...(deliveries?.notification === true
|
|
160
|
+
? [() => claimPendingNotificationDelivery(pi, client, ctx)]
|
|
161
|
+
: []),
|
|
162
|
+
...(deliveries?.turn === true ? [() => claimPendingTurnDelivery(pi, client, ctx)] : []),
|
|
110
163
|
]);
|
|
111
164
|
}
|
|
112
165
|
finally {
|
|
166
|
+
sessionView.refresh(ctx);
|
|
113
167
|
release?.();
|
|
114
168
|
}
|
|
115
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
|
+
};
|
|
116
189
|
const runToolInOrder = async (operation) => {
|
|
117
190
|
const prior = toolTail;
|
|
118
191
|
let release;
|
|
@@ -120,8 +193,12 @@ export default function piWorkflows(pi) {
|
|
|
120
193
|
release = resolve;
|
|
121
194
|
});
|
|
122
195
|
await prior;
|
|
123
|
-
await presentationTail;
|
|
124
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);
|
|
125
202
|
return await operation();
|
|
126
203
|
}
|
|
127
204
|
finally {
|
|
@@ -197,7 +274,7 @@ export default function piWorkflows(pi) {
|
|
|
197
274
|
}
|
|
198
275
|
const response = await requestAccepted(client, {
|
|
199
276
|
operation: params.action === "update" ? "interaction.update" : "interaction.submit",
|
|
200
|
-
requestId: `${params.action}-${toolCallId}`,
|
|
277
|
+
requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
|
|
201
278
|
idempotencyKey: toolCallId,
|
|
202
279
|
runId: interaction.runId,
|
|
203
280
|
expectedRevision: interaction.revision,
|
|
@@ -208,10 +285,8 @@ export default function piWorkflows(pi) {
|
|
|
208
285
|
attempt: params.attempt,
|
|
209
286
|
value: params.action === "update" ? { update: params.update } : { output: params.output },
|
|
210
287
|
}),
|
|
288
|
+
...(signal === undefined ? {} : { signal }),
|
|
211
289
|
});
|
|
212
|
-
if (params.action === "submit") {
|
|
213
|
-
await waitForInteractionSubmission(interaction.requestId, toolCallId, signal);
|
|
214
|
-
}
|
|
215
290
|
await presentInOrder(ctx);
|
|
216
291
|
return toolResult(params.action === "update"
|
|
217
292
|
? "Workflow update accepted; the step remains active."
|
|
@@ -224,20 +299,127 @@ export default function piWorkflows(pi) {
|
|
|
224
299
|
});
|
|
225
300
|
},
|
|
226
301
|
});
|
|
227
|
-
pi.
|
|
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
|
+
});
|
|
328
|
+
pi.registerShortcut("shift+up", {
|
|
329
|
+
description: "Scroll the workflow widget up",
|
|
330
|
+
handler: (ctx) => sessionView.scrollUp(ctx),
|
|
331
|
+
});
|
|
332
|
+
pi.registerShortcut("shift+down", {
|
|
333
|
+
description: "Scroll the workflow widget down",
|
|
334
|
+
handler: (ctx) => sessionView.scrollDown(ctx),
|
|
335
|
+
});
|
|
336
|
+
pi.on("session_start", (_event, ctx) => {
|
|
228
337
|
sessionContext = ctx;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
catch (error) {
|
|
234
|
-
ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
|
|
235
|
-
}
|
|
338
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
339
|
+
const generation = ++sessionGeneration;
|
|
340
|
+
let snapshotGeneration = 0;
|
|
341
|
+
const sessionClient = client;
|
|
236
342
|
pollTimer = setInterval(() => {
|
|
237
343
|
if (sessionContext !== null)
|
|
238
344
|
void presentInOrder(sessionContext).catch(() => undefined);
|
|
239
345
|
}, INTERACTION_POLL_MS);
|
|
240
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
|
+
})();
|
|
393
|
+
});
|
|
394
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
395
|
+
const interrupted = ctx.signal?.aborted === true ||
|
|
396
|
+
event.messages.some((message) => isRecord(message) && "stopReason" in message && message.stopReason === "aborted");
|
|
397
|
+
if (!interrupted)
|
|
398
|
+
return;
|
|
399
|
+
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
400
|
+
if (interaction === undefined ||
|
|
401
|
+
interaction.kind === "decision" ||
|
|
402
|
+
workflowRunPaused(interaction.runId)) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const turnHasPrompt = event.messages.some((message) => interactionRequestId(message) === interaction.requestId);
|
|
406
|
+
if (!turnHasPrompt)
|
|
407
|
+
return;
|
|
408
|
+
await settleOriginActivity();
|
|
409
|
+
try {
|
|
410
|
+
const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
|
|
411
|
+
await requestAccepted(client, {
|
|
412
|
+
operation: "run.pause",
|
|
413
|
+
requestId: pauseId,
|
|
414
|
+
idempotencyKey: pauseId,
|
|
415
|
+
runId: interaction.runId,
|
|
416
|
+
});
|
|
417
|
+
sessionView.refresh(ctx);
|
|
418
|
+
ctx.ui.notify(`Workflow ${interaction.runId} paused because its model turn was interrupted. Use /workflow resume to continue.`, "info");
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
ctx.ui.notify(`Could not pause interrupted workflow: ${errorMessage(error)}`, "warning");
|
|
422
|
+
}
|
|
241
423
|
});
|
|
242
424
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
243
425
|
try {
|
|
@@ -246,14 +428,27 @@ export default function piWorkflows(pi) {
|
|
|
246
428
|
catch (error) {
|
|
247
429
|
ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
|
|
248
430
|
}
|
|
431
|
+
await settleOriginActivity();
|
|
249
432
|
await presentInOrder(ctx).catch(() => undefined);
|
|
250
433
|
});
|
|
251
|
-
pi.on("session_shutdown", async () => {
|
|
434
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
435
|
+
sessionGeneration += 1;
|
|
252
436
|
sessionContext = null;
|
|
253
437
|
sessionDelivery.clear();
|
|
438
|
+
sessionView.clear(ctx);
|
|
439
|
+
sessionSnapshots.delete(ctx.sessionManager.getSessionId());
|
|
440
|
+
if (activityTimer !== null)
|
|
441
|
+
clearInterval(activityTimer);
|
|
442
|
+
activityTimer = null;
|
|
443
|
+
originActivity = null;
|
|
254
444
|
if (pollTimer !== null)
|
|
255
445
|
clearInterval(pollTimer);
|
|
256
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()}` });
|
|
257
452
|
});
|
|
258
453
|
}
|
|
259
454
|
async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(), authority = "human") {
|
|
@@ -269,7 +464,7 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
|
|
|
269
464
|
};
|
|
270
465
|
}
|
|
271
466
|
case "run": {
|
|
272
|
-
await client.
|
|
467
|
+
await client.ensureAvailable();
|
|
273
468
|
const resolved = await client.resolveWorkflow({ cwd: ctx.cwd, workflowRef: command.ref });
|
|
274
469
|
const runId = createRunId(resolved.workflowName);
|
|
275
470
|
const response = await requestAccepted(client, {
|
|
@@ -456,15 +651,28 @@ async function executeControllerCommand(client, ctx, command) {
|
|
|
456
651
|
details: { action: command.kind, resource: response.receipt ?? null },
|
|
457
652
|
};
|
|
458
653
|
}
|
|
459
|
-
async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
460
|
-
const
|
|
461
|
-
if (
|
|
654
|
+
async function claimPendingInteractionDelivery(pi, client, ctx, onPresented) {
|
|
655
|
+
const storedInteraction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
656
|
+
if (storedInteraction === undefined || storedInteraction.presentationSessionEntryId !== null)
|
|
462
657
|
return undefined;
|
|
658
|
+
const interaction = {
|
|
659
|
+
...storedInteraction,
|
|
660
|
+
contract: await client.hydrateContent(storedInteraction.runId, storedInteraction.contract),
|
|
661
|
+
};
|
|
662
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), interaction);
|
|
463
663
|
const findSessionEntryId = (entries) => entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
|
|
464
664
|
const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
|
|
465
665
|
let presentationRevision = interaction.revision;
|
|
666
|
+
let claimExpiresAt = Number.POSITIVE_INFINITY;
|
|
466
667
|
if (existingEntryId === undefined) {
|
|
467
|
-
|
|
668
|
+
if (workflowRunPaused(interaction.runId))
|
|
669
|
+
return undefined;
|
|
670
|
+
if (interaction.presentationClaimExpiresAt !== null &&
|
|
671
|
+
Date.parse(interaction.presentationClaimExpiresAt) > Date.now()) {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
await client.ensureAvailable();
|
|
675
|
+
const claim = await client.request({
|
|
468
676
|
operation: "interaction.update",
|
|
469
677
|
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
470
678
|
idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
@@ -472,13 +680,30 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
472
680
|
expectedRevision: interaction.revision,
|
|
473
681
|
payload: { requestId: interaction.requestId, claimPresentation: true },
|
|
474
682
|
});
|
|
683
|
+
if (claim.outcome === "conflict")
|
|
684
|
+
return undefined;
|
|
685
|
+
if (claim.outcome !== "accepted" && claim.outcome !== "adopted") {
|
|
686
|
+
throw new Error(claim.error ?? "Workflow host rejected interaction.update");
|
|
687
|
+
}
|
|
475
688
|
if (claim.revision === undefined)
|
|
476
689
|
throw new Error("Presentation claim has no revision");
|
|
690
|
+
const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
|
|
477
691
|
presentationRevision = claim.revision;
|
|
692
|
+
claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
|
|
693
|
+
rememberInteractiveRequest(ctx.sessionManager.getSessionId(), claim.receipt);
|
|
478
694
|
}
|
|
479
695
|
const contract = interactionContract(interaction);
|
|
480
696
|
return {
|
|
481
697
|
deliveryId: `interaction:${interaction.requestId}`,
|
|
698
|
+
claimExpiresAt,
|
|
699
|
+
isStillDeliverable: () => existingEntryId === undefined &&
|
|
700
|
+
interactionPresentationClaimIsLive(client, {
|
|
701
|
+
requestId: interaction.requestId,
|
|
702
|
+
runId: interaction.runId,
|
|
703
|
+
presenterId: client.clientId,
|
|
704
|
+
revision: presentationRevision,
|
|
705
|
+
claimExpiresAt,
|
|
706
|
+
}),
|
|
482
707
|
findSessionEntryId,
|
|
483
708
|
send: () => {
|
|
484
709
|
if (interaction.kind === "decision") {
|
|
@@ -514,7 +739,7 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
514
739
|
}, { triggerTurn: true });
|
|
515
740
|
},
|
|
516
741
|
settle: async (sessionEntryId) => {
|
|
517
|
-
await requestAccepted(client, {
|
|
742
|
+
const response = await requestAccepted(client, {
|
|
518
743
|
operation: "interaction.update",
|
|
519
744
|
requestId: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
520
745
|
idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
@@ -522,13 +747,21 @@ async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
|
522
747
|
expectedRevision: presentationRevision,
|
|
523
748
|
payload: { requestId: interaction.requestId, sessionEntryId },
|
|
524
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
|
+
}
|
|
525
760
|
},
|
|
526
761
|
};
|
|
527
762
|
}
|
|
528
763
|
async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
529
764
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
530
|
-
if (!hasClaimableNotification(sessionId))
|
|
531
|
-
return undefined;
|
|
532
765
|
const claimRequestId = randomUUID();
|
|
533
766
|
const claimed = await requestAccepted(client, {
|
|
534
767
|
operation: "notification.claim",
|
|
@@ -541,9 +774,17 @@ async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
|
541
774
|
if (notification === undefined)
|
|
542
775
|
return undefined;
|
|
543
776
|
const claimId = requireText(receipt?.claimId, "notification claimId");
|
|
777
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "notification claim");
|
|
544
778
|
const notificationId = requireText(notification.notificationId, "notificationId");
|
|
545
779
|
return {
|
|
546
780
|
deliveryId: `notification:${notificationId}`,
|
|
781
|
+
claimExpiresAt,
|
|
782
|
+
isStillDeliverable: () => hostDeliveryClaimIsLive(client, {
|
|
783
|
+
kind: "notification",
|
|
784
|
+
resourceId: notificationId,
|
|
785
|
+
targetSessionId: sessionId,
|
|
786
|
+
claimId,
|
|
787
|
+
}),
|
|
547
788
|
findSessionEntryId: (entries) => entryIdentifier(entries.find((entry) => customMessageDetail(entry, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
|
|
548
789
|
notificationId)),
|
|
549
790
|
send: () => {
|
|
@@ -574,9 +815,8 @@ async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
|
574
815
|
}
|
|
575
816
|
async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
576
817
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
577
|
-
if (pendingInteractionForSession(sessionId) !== undefined
|
|
818
|
+
if (pendingInteractionForSession(sessionId) !== undefined)
|
|
578
819
|
return undefined;
|
|
579
|
-
}
|
|
580
820
|
const claimRequestId = randomUUID();
|
|
581
821
|
const claimed = await requestAccepted(client, {
|
|
582
822
|
operation: "turn.claim",
|
|
@@ -589,13 +829,24 @@ async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
|
589
829
|
if (turn === undefined)
|
|
590
830
|
return undefined;
|
|
591
831
|
const claimId = requireText(receipt?.claimId, "turn claimId");
|
|
832
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
|
|
592
833
|
const intentId = requireText(turn.intentId, "turn intentId");
|
|
593
834
|
const runId = requireText(turn.runId, "turn runId");
|
|
594
|
-
const
|
|
595
|
-
if (
|
|
596
|
-
|
|
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");
|
|
597
841
|
return {
|
|
598
842
|
deliveryId: `turn:${intentId}`,
|
|
843
|
+
claimExpiresAt,
|
|
844
|
+
isStillDeliverable: () => hostDeliveryClaimIsLive(client, {
|
|
845
|
+
kind: "turn",
|
|
846
|
+
resourceId: intentId,
|
|
847
|
+
targetSessionId: sessionId,
|
|
848
|
+
claimId,
|
|
849
|
+
}),
|
|
599
850
|
findSessionEntryId: (entries) => entryIdentifier(entries.find((entry) => customMessageDetail(entry, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") === intentId)),
|
|
600
851
|
send: () => {
|
|
601
852
|
pi.sendMessage({
|
|
@@ -622,8 +873,11 @@ async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
|
622
873
|
}
|
|
623
874
|
async function submitVisibleAssistantResponse(client, ctx) {
|
|
624
875
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
625
|
-
if (interaction === undefined ||
|
|
876
|
+
if (interaction === undefined ||
|
|
877
|
+
interaction.kind !== "assistant" ||
|
|
878
|
+
workflowRunPaused(interaction.runId)) {
|
|
626
879
|
return;
|
|
880
|
+
}
|
|
627
881
|
const contract = agentContract(interaction);
|
|
628
882
|
if (contract === undefined)
|
|
629
883
|
return;
|
|
@@ -647,126 +901,76 @@ async function submitVisibleAssistantResponse(client, ctx) {
|
|
|
647
901
|
value: submission,
|
|
648
902
|
},
|
|
649
903
|
});
|
|
650
|
-
await waitForInteractionSubmission(interaction.requestId, `assistant-${responseId}`);
|
|
651
|
-
}
|
|
652
|
-
async function waitForInteractionSubmission(requestId, submissionId, signal) {
|
|
653
|
-
const store = new HostStateStore(workflowStatePath(), { readOnly: true });
|
|
654
|
-
try {
|
|
655
|
-
for (;;) {
|
|
656
|
-
if (signal?.aborted === true) {
|
|
657
|
-
throw signal.reason ?? new Error("Workflow submission validation was cancelled");
|
|
658
|
-
}
|
|
659
|
-
const submission = store.interactionSubmission(requestId, submissionId);
|
|
660
|
-
if (submission?.outcome === "accepted" || submission?.outcome === "adopted")
|
|
661
|
-
return;
|
|
662
|
-
if (submission?.outcome === "rejected") {
|
|
663
|
-
const receipt = isRecord(submission.receipt) ? submission.receipt : undefined;
|
|
664
|
-
throw new Error(typeof receipt?.error === "string"
|
|
665
|
-
? receipt.error
|
|
666
|
-
: "Workflow step output failed validation");
|
|
667
|
-
}
|
|
668
|
-
await waitForSubmissionPoll(signal);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
finally {
|
|
672
|
-
store.close();
|
|
673
|
-
}
|
|
674
904
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
else
|
|
688
|
-
reject(error);
|
|
689
|
-
}
|
|
690
|
-
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;
|
|
691
917
|
});
|
|
918
|
+
if (found)
|
|
919
|
+
sessionSnapshots.set(sessionId, { ...session, pendingInteractions });
|
|
692
920
|
}
|
|
693
921
|
function pendingInteractionForSession(sessionId) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
finally {
|
|
700
|
-
store.close();
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
catch {
|
|
704
|
-
return undefined;
|
|
705
|
-
}
|
|
922
|
+
return sessionSnapshots
|
|
923
|
+
.get(sessionId)
|
|
924
|
+
?.pendingInteractions.map(parseInteractiveRequest)
|
|
925
|
+
.find((request) => request !== undefined);
|
|
706
926
|
}
|
|
707
927
|
function pendingDecision(sessionId, runId) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
finally {
|
|
716
|
-
store.close();
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
return undefined;
|
|
721
|
-
}
|
|
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));
|
|
722
934
|
}
|
|
723
|
-
function
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
return (store.listPendingWorkflowNotifications({ targetSessionId: sessionId, limit: 1 }).length > 0);
|
|
728
|
-
}
|
|
729
|
-
finally {
|
|
730
|
-
store.close();
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch {
|
|
734
|
-
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";
|
|
735
939
|
}
|
|
940
|
+
return false;
|
|
736
941
|
}
|
|
737
|
-
function
|
|
942
|
+
async function interactionPresentationClaimIsLive(client, options) {
|
|
943
|
+
if (options.claimExpiresAt <= Date.now() || options.presenterId !== client.clientId)
|
|
944
|
+
return false;
|
|
738
945
|
try {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
finally {
|
|
750
|
-
store.close();
|
|
751
|
-
}
|
|
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);
|
|
752
955
|
}
|
|
753
956
|
catch {
|
|
754
957
|
return false;
|
|
755
958
|
}
|
|
756
959
|
}
|
|
757
|
-
function
|
|
960
|
+
async function hostDeliveryClaimIsLive(client, options) {
|
|
758
961
|
try {
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
962
|
+
const validationId = randomUUID();
|
|
963
|
+
const response = await client.request({
|
|
964
|
+
operation: options.kind === "notification" ? "notification.claim" : "turn.claim",
|
|
965
|
+
requestId: `delivery-validate-${options.claimId}-${validationId}`,
|
|
966
|
+
idempotencyKey: validationId,
|
|
967
|
+
payload: { ...options, validateClaim: true },
|
|
968
|
+
});
|
|
969
|
+
const receipt = isRecord(response.receipt) ? response.receipt : undefined;
|
|
970
|
+
return ((response.outcome === "accepted" || response.outcome === "adopted") && receipt?.live === true);
|
|
767
971
|
}
|
|
768
972
|
catch {
|
|
769
|
-
return
|
|
973
|
+
return false;
|
|
770
974
|
}
|
|
771
975
|
}
|
|
772
976
|
function presentationMessage(turn, state) {
|
|
@@ -807,25 +1011,24 @@ function requireText(value, name) {
|
|
|
807
1011
|
throw new Error(`${name} must be text`);
|
|
808
1012
|
return value;
|
|
809
1013
|
}
|
|
1014
|
+
function claimExpiry(value, name) {
|
|
1015
|
+
const expiry = Date.parse(requireText(value, `${name} expiry`));
|
|
1016
|
+
if (!Number.isFinite(expiry))
|
|
1017
|
+
throw new Error(`${name} expiry must be a timestamp`);
|
|
1018
|
+
return expiry;
|
|
1019
|
+
}
|
|
810
1020
|
function activeSessionRun(ctx) {
|
|
811
1021
|
return sessionRun(ctx);
|
|
812
1022
|
}
|
|
813
1023
|
function sessionRun(ctx, runId) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
try {
|
|
817
|
-
const run = runId === undefined
|
|
818
|
-
? store.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
819
|
-
: store.getWorkflowRun(runId);
|
|
820
|
-
return run?.originSessionId === ctx.sessionManager.getSessionId() ? run : undefined;
|
|
821
|
-
}
|
|
822
|
-
finally {
|
|
823
|
-
store.close();
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
catch {
|
|
1024
|
+
const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
|
|
1025
|
+
if (session?.run === null || session?.run === undefined || !isRecord(session.run.queue)) {
|
|
827
1026
|
return undefined;
|
|
828
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;
|
|
829
1032
|
}
|
|
830
1033
|
async function listWorkflowMetadata(cwd) {
|
|
831
1034
|
const files = await discoverWorkflows({ cwd });
|
|
@@ -866,8 +1069,10 @@ function toolInputToCommand(params) {
|
|
|
866
1069
|
}
|
|
867
1070
|
}
|
|
868
1071
|
async function requestAccepted(client, options) {
|
|
869
|
-
await client.
|
|
870
|
-
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 });
|
|
871
1076
|
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
872
1077
|
throw new Error(response.error ?? `Workflow host rejected ${options.operation}`);
|
|
873
1078
|
}
|
|
@@ -891,7 +1096,9 @@ function agentContract(interaction) {
|
|
|
891
1096
|
return value;
|
|
892
1097
|
}
|
|
893
1098
|
function interactionRequestId(value) {
|
|
894
|
-
if (!isRecord(value) ||
|
|
1099
|
+
if (!isRecord(value) ||
|
|
1100
|
+
(value.type !== "custom_message" && value.role !== "custom") ||
|
|
1101
|
+
!isRecord(value.details)) {
|
|
895
1102
|
return undefined;
|
|
896
1103
|
}
|
|
897
1104
|
return typeof value.details.requestId === "string" ? value.details.requestId : undefined;
|
|
@@ -989,6 +1196,27 @@ function toolResult(message, details) {
|
|
|
989
1196
|
function validRunId(value) {
|
|
990
1197
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u.test(value);
|
|
991
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
|
+
}
|
|
992
1220
|
function isRecord(value) {
|
|
993
1221
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
994
1222
|
}
|