@osolmaz/pi-workflows 0.15.1 → 0.15.3
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/dist/controllers/sqlite.d.ts +21 -0
- package/dist/controllers/sqlite.js +82 -1
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.js +264 -139
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +28 -0
- package/dist/extension/session-delivery.js +138 -0
- package/dist/extension/session-delivery.js.map +1 -0
- package/dist/extension/session-view.d.ts +16 -0
- package/dist/extension/session-view.js +119 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.js +2 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +92 -26
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +2 -0
- package/dist/host/state.js +9 -3
- package/dist/host/state.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/WORKFLOW_HOST.md +12 -5
- package/docs/WORKFLOW_STEP_MESSAGES.md +12 -5
- package/docs/workflows.md +12 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/controllers/sqlite.ts +129 -1
- package/src/extension/index.ts +331 -159
- package/src/extension/session-delivery.ts +174 -0
- package/src/extension/session-view.ts +131 -0
- package/src/extension/widget.ts +3 -2
- package/src/host/runner.ts +103 -29
- package/src/host/state.ts +12 -3
package/dist/extension/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { errorMessage } from "../workflows/errors.js";
|
|
|
10
10
|
import { discoverWorkflows } from "../workflows/loader.js";
|
|
11
11
|
import { createRunId, WorkflowRunStore } from "../workflows/store.js";
|
|
12
12
|
import { parseControllerArgs } from "./controller-command.js";
|
|
13
|
+
import { SessionDeliveryCoordinator } from "./session-delivery.js";
|
|
14
|
+
import { SessionWorkflowView } from "./session-view.js";
|
|
13
15
|
import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
|
|
14
16
|
import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
|
|
15
17
|
export { parseControllerArgs } from "./controller-command.js";
|
|
@@ -93,6 +95,8 @@ export default function piWorkflows(pi) {
|
|
|
93
95
|
let pollTimer = null;
|
|
94
96
|
let presentationTail = Promise.resolve();
|
|
95
97
|
let toolTail = Promise.resolve();
|
|
98
|
+
const sessionDelivery = new SessionDeliveryCoordinator();
|
|
99
|
+
const sessionView = new SessionWorkflowView();
|
|
96
100
|
const presentInOrder = async (ctx) => {
|
|
97
101
|
const prior = presentationTail;
|
|
98
102
|
let release;
|
|
@@ -101,13 +105,14 @@ export default function piWorkflows(pi) {
|
|
|
101
105
|
});
|
|
102
106
|
await prior;
|
|
103
107
|
try {
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
await sessionDelivery.synchronize(ctx, [
|
|
109
|
+
() => claimPendingInteractionDelivery(pi, client, ctx),
|
|
110
|
+
() => claimPendingNotificationDelivery(pi, client, ctx),
|
|
111
|
+
() => claimPendingTurnDelivery(pi, client, ctx),
|
|
112
|
+
]);
|
|
109
113
|
}
|
|
110
114
|
finally {
|
|
115
|
+
sessionView.refresh(ctx);
|
|
111
116
|
release?.();
|
|
112
117
|
}
|
|
113
118
|
};
|
|
@@ -222,6 +227,14 @@ export default function piWorkflows(pi) {
|
|
|
222
227
|
});
|
|
223
228
|
},
|
|
224
229
|
});
|
|
230
|
+
pi.registerShortcut("shift+up", {
|
|
231
|
+
description: "Scroll the workflow widget up",
|
|
232
|
+
handler: (ctx) => sessionView.scrollUp(ctx),
|
|
233
|
+
});
|
|
234
|
+
pi.registerShortcut("shift+down", {
|
|
235
|
+
description: "Scroll the workflow widget down",
|
|
236
|
+
handler: (ctx) => sessionView.scrollDown(ctx),
|
|
237
|
+
});
|
|
225
238
|
pi.on("session_start", async (_event, ctx) => {
|
|
226
239
|
sessionContext = ctx;
|
|
227
240
|
try {
|
|
@@ -237,6 +250,35 @@ export default function piWorkflows(pi) {
|
|
|
237
250
|
}, INTERACTION_POLL_MS);
|
|
238
251
|
pollTimer.unref?.();
|
|
239
252
|
});
|
|
253
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
254
|
+
const interrupted = ctx.signal?.aborted === true ||
|
|
255
|
+
event.messages.some((message) => isRecord(message) && "stopReason" in message && message.stopReason === "aborted");
|
|
256
|
+
if (!interrupted)
|
|
257
|
+
return;
|
|
258
|
+
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
259
|
+
if (interaction === undefined ||
|
|
260
|
+
interaction.kind === "decision" ||
|
|
261
|
+
workflowRunPaused(interaction.runId)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const turnHasPrompt = event.messages.some((message) => interactionRequestId(message) === interaction.requestId);
|
|
265
|
+
if (!turnHasPrompt)
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
|
|
269
|
+
await requestAccepted(client, {
|
|
270
|
+
operation: "run.pause",
|
|
271
|
+
requestId: pauseId,
|
|
272
|
+
idempotencyKey: pauseId,
|
|
273
|
+
runId: interaction.runId,
|
|
274
|
+
});
|
|
275
|
+
sessionView.refresh(ctx);
|
|
276
|
+
ctx.ui.notify(`Workflow ${interaction.runId} paused because its model turn was interrupted. Use /workflow resume to continue.`, "info");
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
ctx.ui.notify(`Could not pause interrupted workflow: ${errorMessage(error)}`, "warning");
|
|
280
|
+
}
|
|
281
|
+
});
|
|
240
282
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
241
283
|
try {
|
|
242
284
|
await submitVisibleAssistantResponse(client, ctx);
|
|
@@ -246,8 +288,10 @@ export default function piWorkflows(pi) {
|
|
|
246
288
|
}
|
|
247
289
|
await presentInOrder(ctx).catch(() => undefined);
|
|
248
290
|
});
|
|
249
|
-
pi.on("session_shutdown", async () => {
|
|
291
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
250
292
|
sessionContext = null;
|
|
293
|
+
sessionDelivery.clear();
|
|
294
|
+
sessionView.clear(ctx);
|
|
251
295
|
if (pollTimer !== null)
|
|
252
296
|
clearInterval(pollTimer);
|
|
253
297
|
pollTimer = null;
|
|
@@ -453,85 +497,103 @@ async function executeControllerCommand(client, ctx, command) {
|
|
|
453
497
|
details: { action: command.kind, resource: response.receipt ?? null },
|
|
454
498
|
};
|
|
455
499
|
}
|
|
456
|
-
async function
|
|
500
|
+
async function claimPendingInteractionDelivery(pi, client, ctx) {
|
|
457
501
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
458
|
-
if (interaction === undefined)
|
|
459
|
-
return;
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
502
|
+
if (interaction === undefined || interaction.presentationSessionEntryId !== null)
|
|
503
|
+
return undefined;
|
|
504
|
+
const findSessionEntryId = (entries) => entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
|
|
505
|
+
const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
|
|
506
|
+
let presentationRevision = interaction.revision;
|
|
507
|
+
let claimExpiresAt = Number.POSITIVE_INFINITY;
|
|
508
|
+
if (existingEntryId === undefined) {
|
|
509
|
+
if (workflowRunPaused(interaction.runId))
|
|
510
|
+
return undefined;
|
|
511
|
+
if (interaction.presentationClaimExpiresAt !== null &&
|
|
512
|
+
Date.parse(interaction.presentationClaimExpiresAt) > Date.now()) {
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
await client.ensureRunning();
|
|
516
|
+
const claim = await client.request({
|
|
517
|
+
operation: "interaction.update",
|
|
518
|
+
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
519
|
+
idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
520
|
+
runId: interaction.runId,
|
|
521
|
+
expectedRevision: interaction.revision,
|
|
522
|
+
payload: { requestId: interaction.requestId, claimPresentation: true },
|
|
523
|
+
});
|
|
524
|
+
if (claim.outcome === "conflict")
|
|
525
|
+
return undefined;
|
|
526
|
+
if (claim.outcome !== "accepted" && claim.outcome !== "adopted") {
|
|
527
|
+
throw new Error(claim.error ?? "Workflow host rejected interaction.update");
|
|
528
|
+
}
|
|
529
|
+
if (claim.revision === undefined)
|
|
530
|
+
throw new Error("Presentation claim has no revision");
|
|
531
|
+
const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
|
|
532
|
+
presentationRevision = claim.revision;
|
|
533
|
+
claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
|
|
534
|
+
}
|
|
535
|
+
const contract = interactionContract(interaction);
|
|
536
|
+
return {
|
|
537
|
+
deliveryId: `interaction:${interaction.requestId}`,
|
|
538
|
+
claimExpiresAt,
|
|
539
|
+
isStillDeliverable: () => existingEntryId === undefined &&
|
|
540
|
+
interactionPresentationClaimIsLive({
|
|
541
|
+
requestId: interaction.requestId,
|
|
542
|
+
runId: interaction.runId,
|
|
543
|
+
presenterId: client.clientId,
|
|
544
|
+
revision: presentationRevision,
|
|
545
|
+
claimExpiresAt,
|
|
546
|
+
}),
|
|
547
|
+
findSessionEntryId,
|
|
548
|
+
send: () => {
|
|
549
|
+
if (interaction.kind === "decision") {
|
|
550
|
+
pi.sendMessage({
|
|
551
|
+
customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
|
|
552
|
+
content: decisionPrompt(contract),
|
|
553
|
+
display: true,
|
|
554
|
+
details: {
|
|
555
|
+
requestId: interaction.requestId,
|
|
556
|
+
runId: interaction.runId,
|
|
557
|
+
kind: "decision",
|
|
558
|
+
},
|
|
559
|
+
}, { triggerTurn: false });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const agent = agentContract(interaction);
|
|
563
|
+
if (agent === undefined)
|
|
564
|
+
throw new Error("Stored workflow agent contract is invalid");
|
|
565
|
+
const details = {
|
|
566
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
567
|
+
kind: "step",
|
|
568
|
+
contract: agent,
|
|
569
|
+
requestId: interaction.requestId,
|
|
570
|
+
...(isRecord(contract.presentation)
|
|
571
|
+
? { presentation: contract.presentation }
|
|
572
|
+
: {}),
|
|
573
|
+
};
|
|
574
|
+
pi.sendMessage({
|
|
575
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
576
|
+
content: typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
|
|
577
|
+
display: true,
|
|
578
|
+
details,
|
|
579
|
+
}, { triggerTurn: true });
|
|
580
|
+
},
|
|
581
|
+
settle: async (sessionEntryId) => {
|
|
465
582
|
await requestAccepted(client, {
|
|
466
583
|
operation: "interaction.update",
|
|
467
|
-
requestId: `present-${interaction.requestId}-${
|
|
468
|
-
idempotencyKey: `present-${interaction.requestId}-${
|
|
584
|
+
requestId: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
585
|
+
idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
469
586
|
runId: interaction.runId,
|
|
470
|
-
expectedRevision:
|
|
471
|
-
payload: { requestId: interaction.requestId, sessionEntryId
|
|
587
|
+
expectedRevision: presentationRevision,
|
|
588
|
+
payload: { requestId: interaction.requestId, sessionEntryId },
|
|
472
589
|
});
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
if (interaction.presentationSessionEntryId !== null)
|
|
477
|
-
return;
|
|
478
|
-
const claim = await requestAccepted(client, {
|
|
479
|
-
operation: "interaction.update",
|
|
480
|
-
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
481
|
-
idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
482
|
-
runId: interaction.runId,
|
|
483
|
-
expectedRevision: interaction.revision,
|
|
484
|
-
payload: { requestId: interaction.requestId, claimPresentation: true },
|
|
485
|
-
});
|
|
486
|
-
if (claim.revision === undefined)
|
|
487
|
-
throw new Error("Presentation claim has no revision");
|
|
488
|
-
const presentationRevision = claim.revision;
|
|
489
|
-
const contract = interactionContract(interaction);
|
|
490
|
-
if (interaction.kind === "decision") {
|
|
491
|
-
pi.sendMessage({
|
|
492
|
-
customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
|
|
493
|
-
content: decisionPrompt(contract),
|
|
494
|
-
display: true,
|
|
495
|
-
details: { requestId: interaction.requestId, runId: interaction.runId, kind: "decision" },
|
|
496
|
-
}, { triggerTurn: false });
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
const agent = agentContract(interaction);
|
|
500
|
-
if (agent === undefined)
|
|
501
|
-
throw new Error("Stored workflow agent contract is invalid");
|
|
502
|
-
const details = {
|
|
503
|
-
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
504
|
-
kind: "step",
|
|
505
|
-
contract: agent,
|
|
506
|
-
requestId: interaction.requestId,
|
|
507
|
-
...(isRecord(contract.presentation) ? { presentation: contract.presentation } : {}),
|
|
508
|
-
};
|
|
509
|
-
pi.sendMessage({
|
|
510
|
-
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
511
|
-
content: typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
|
|
512
|
-
display: true,
|
|
513
|
-
details,
|
|
514
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
515
|
-
}
|
|
516
|
-
const inserted = ctx.sessionManager
|
|
517
|
-
.getBranch()
|
|
518
|
-
.find((entry) => interactionRequestId(entry) === interaction.requestId);
|
|
519
|
-
const entryId = entryIdentifier(inserted);
|
|
520
|
-
if (entryId === undefined)
|
|
521
|
-
return;
|
|
522
|
-
await requestAccepted(client, {
|
|
523
|
-
operation: "interaction.update",
|
|
524
|
-
requestId: `present-${interaction.requestId}-${entryId}`,
|
|
525
|
-
idempotencyKey: `present-${interaction.requestId}-${entryId}`,
|
|
526
|
-
runId: interaction.runId,
|
|
527
|
-
expectedRevision: presentationRevision,
|
|
528
|
-
payload: { requestId: interaction.requestId, sessionEntryId: entryId },
|
|
529
|
-
});
|
|
590
|
+
},
|
|
591
|
+
};
|
|
530
592
|
}
|
|
531
|
-
async function
|
|
593
|
+
async function claimPendingNotificationDelivery(pi, client, ctx) {
|
|
532
594
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
533
595
|
if (!hasClaimableNotification(sessionId))
|
|
534
|
-
return;
|
|
596
|
+
return undefined;
|
|
535
597
|
const claimRequestId = randomUUID();
|
|
536
598
|
const claimed = await requestAccepted(client, {
|
|
537
599
|
operation: "notification.claim",
|
|
@@ -542,45 +604,52 @@ async function deliverPendingNotification(pi, client, ctx) {
|
|
|
542
604
|
const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
|
|
543
605
|
const notification = isRecord(receipt?.notification) ? receipt.notification : undefined;
|
|
544
606
|
if (notification === undefined)
|
|
545
|
-
return;
|
|
607
|
+
return undefined;
|
|
546
608
|
const claimId = requireText(receipt?.claimId, "notification claimId");
|
|
609
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "notification claim");
|
|
547
610
|
const notificationId = requireText(notification.notificationId, "notificationId");
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
content: requireText(notification.content, "notification content"),
|
|
555
|
-
display: true,
|
|
556
|
-
details: {
|
|
557
|
-
notificationId,
|
|
558
|
-
runId: requireText(notification.runId, "notification runId"),
|
|
559
|
-
kind: notification.kind,
|
|
560
|
-
},
|
|
561
|
-
}, { triggerTurn: false });
|
|
562
|
-
entry = ctx.sessionManager
|
|
563
|
-
.getBranch()
|
|
564
|
-
.find((candidate) => customMessageDetail(candidate, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
|
|
565
|
-
notificationId);
|
|
566
|
-
}
|
|
567
|
-
if (entry === undefined)
|
|
568
|
-
return;
|
|
569
|
-
await requestAccepted(client, {
|
|
570
|
-
operation: "notification.deliver",
|
|
571
|
-
requestId: `notification-deliver-${notificationId}-${claimId}`,
|
|
572
|
-
idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
|
|
573
|
-
payload: {
|
|
574
|
-
notificationId,
|
|
611
|
+
return {
|
|
612
|
+
deliveryId: `notification:${notificationId}`,
|
|
613
|
+
claimExpiresAt,
|
|
614
|
+
isStillDeliverable: () => hostDeliveryClaimIsLive(client, {
|
|
615
|
+
kind: "notification",
|
|
616
|
+
resourceId: notificationId,
|
|
575
617
|
targetSessionId: sessionId,
|
|
576
618
|
claimId,
|
|
619
|
+
}),
|
|
620
|
+
findSessionEntryId: (entries) => entryIdentifier(entries.find((entry) => customMessageDetail(entry, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
|
|
621
|
+
notificationId)),
|
|
622
|
+
send: () => {
|
|
623
|
+
pi.sendMessage({
|
|
624
|
+
customType: WORKFLOW_NOTIFICATION_MESSAGE_TYPE,
|
|
625
|
+
content: requireText(notification.content, "notification content"),
|
|
626
|
+
display: true,
|
|
627
|
+
details: {
|
|
628
|
+
notificationId,
|
|
629
|
+
runId: requireText(notification.runId, "notification runId"),
|
|
630
|
+
kind: notification.kind,
|
|
631
|
+
},
|
|
632
|
+
}, { triggerTurn: false });
|
|
577
633
|
},
|
|
578
|
-
|
|
634
|
+
settle: async () => {
|
|
635
|
+
await requestAccepted(client, {
|
|
636
|
+
operation: "notification.deliver",
|
|
637
|
+
requestId: `notification-deliver-${notificationId}-${claimId}`,
|
|
638
|
+
idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
|
|
639
|
+
payload: {
|
|
640
|
+
notificationId,
|
|
641
|
+
targetSessionId: sessionId,
|
|
642
|
+
claimId,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
};
|
|
579
647
|
}
|
|
580
|
-
async function
|
|
648
|
+
async function claimPendingTurnDelivery(pi, client, ctx) {
|
|
581
649
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
582
|
-
if (!hasClaimableTurn(sessionId))
|
|
583
|
-
return;
|
|
650
|
+
if (pendingInteractionForSession(sessionId) !== undefined || !hasClaimableTurn(sessionId)) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
584
653
|
const claimRequestId = randomUUID();
|
|
585
654
|
const claimed = await requestAccepted(client, {
|
|
586
655
|
operation: "turn.claim",
|
|
@@ -591,47 +660,54 @@ async function presentPendingTurn(pi, client, ctx) {
|
|
|
591
660
|
const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
|
|
592
661
|
const turn = isRecord(receipt?.turn) ? receipt.turn : undefined;
|
|
593
662
|
if (turn === undefined)
|
|
594
|
-
return;
|
|
663
|
+
return undefined;
|
|
595
664
|
const claimId = requireText(receipt?.claimId, "turn claimId");
|
|
665
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
|
|
596
666
|
const intentId = requireText(turn.intentId, "turn intentId");
|
|
597
667
|
const runId = requireText(turn.runId, "turn runId");
|
|
598
668
|
const state = terminalRunState(runId);
|
|
599
669
|
if (state === undefined)
|
|
600
|
-
return;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
content: presentationMessage(turn, state),
|
|
608
|
-
display: false,
|
|
609
|
-
details: { intentId, runId },
|
|
610
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
611
|
-
entry = ctx.sessionManager
|
|
612
|
-
.getBranch()
|
|
613
|
-
.find((candidate) => customMessageDetail(candidate, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") ===
|
|
614
|
-
intentId);
|
|
615
|
-
}
|
|
616
|
-
const messageId = entryIdentifier(entry);
|
|
617
|
-
if (messageId === undefined)
|
|
618
|
-
return;
|
|
619
|
-
await requestAccepted(client, {
|
|
620
|
-
operation: "turn.resolve",
|
|
621
|
-
requestId: `turn-resolve-${intentId}-${messageId}`,
|
|
622
|
-
idempotencyKey: `turn-resolve-${intentId}-${messageId}`,
|
|
623
|
-
payload: {
|
|
624
|
-
intentId,
|
|
670
|
+
return undefined;
|
|
671
|
+
return {
|
|
672
|
+
deliveryId: `turn:${intentId}`,
|
|
673
|
+
claimExpiresAt,
|
|
674
|
+
isStillDeliverable: () => hostDeliveryClaimIsLive(client, {
|
|
675
|
+
kind: "turn",
|
|
676
|
+
resourceId: intentId,
|
|
625
677
|
targetSessionId: sessionId,
|
|
626
678
|
claimId,
|
|
627
|
-
|
|
679
|
+
}),
|
|
680
|
+
findSessionEntryId: (entries) => entryIdentifier(entries.find((entry) => customMessageDetail(entry, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") === intentId)),
|
|
681
|
+
send: () => {
|
|
682
|
+
pi.sendMessage({
|
|
683
|
+
customType: WORKFLOW_PRESENTATION_MESSAGE_TYPE,
|
|
684
|
+
content: presentationMessage(turn, state),
|
|
685
|
+
display: false,
|
|
686
|
+
details: { intentId, runId },
|
|
687
|
+
}, { triggerTurn: true });
|
|
628
688
|
},
|
|
629
|
-
|
|
689
|
+
settle: async (sessionEntryId) => {
|
|
690
|
+
await requestAccepted(client, {
|
|
691
|
+
operation: "turn.resolve",
|
|
692
|
+
requestId: `turn-resolve-${intentId}-${sessionEntryId}`,
|
|
693
|
+
idempotencyKey: `turn-resolve-${intentId}-${sessionEntryId}`,
|
|
694
|
+
payload: {
|
|
695
|
+
intentId,
|
|
696
|
+
targetSessionId: sessionId,
|
|
697
|
+
claimId,
|
|
698
|
+
messageId: sessionEntryId,
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
},
|
|
702
|
+
};
|
|
630
703
|
}
|
|
631
704
|
async function submitVisibleAssistantResponse(client, ctx) {
|
|
632
705
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
633
|
-
if (interaction === undefined ||
|
|
706
|
+
if (interaction === undefined ||
|
|
707
|
+
interaction.kind !== "assistant" ||
|
|
708
|
+
workflowRunPaused(interaction.runId)) {
|
|
634
709
|
return;
|
|
710
|
+
}
|
|
635
711
|
const contract = agentContract(interaction);
|
|
636
712
|
if (contract === undefined)
|
|
637
713
|
return;
|
|
@@ -777,6 +853,47 @@ function terminalRunState(runId) {
|
|
|
777
853
|
return undefined;
|
|
778
854
|
}
|
|
779
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
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function hostDeliveryClaimIsLive(client, options) {
|
|
882
|
+
try {
|
|
883
|
+
const validationId = randomUUID();
|
|
884
|
+
const response = await client.request({
|
|
885
|
+
operation: options.kind === "notification" ? "notification.claim" : "turn.claim",
|
|
886
|
+
requestId: `delivery-validate-${options.claimId}-${validationId}`,
|
|
887
|
+
idempotencyKey: validationId,
|
|
888
|
+
payload: { ...options, validateClaim: true },
|
|
889
|
+
});
|
|
890
|
+
const receipt = isRecord(response.receipt) ? response.receipt : undefined;
|
|
891
|
+
return ((response.outcome === "accepted" || response.outcome === "adopted") && receipt?.live === true);
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
780
897
|
function presentationMessage(turn, state) {
|
|
781
898
|
const facts = isRecord(turn.fallbackFacts) ? turn.fallbackFacts : {};
|
|
782
899
|
const instructions = typeof facts.presentationPrompt === "string"
|
|
@@ -815,6 +932,12 @@ function requireText(value, name) {
|
|
|
815
932
|
throw new Error(`${name} must be text`);
|
|
816
933
|
return value;
|
|
817
934
|
}
|
|
935
|
+
function claimExpiry(value, name) {
|
|
936
|
+
const expiry = Date.parse(requireText(value, `${name} expiry`));
|
|
937
|
+
if (!Number.isFinite(expiry))
|
|
938
|
+
throw new Error(`${name} expiry must be a timestamp`);
|
|
939
|
+
return expiry;
|
|
940
|
+
}
|
|
818
941
|
function activeSessionRun(ctx) {
|
|
819
942
|
return sessionRun(ctx);
|
|
820
943
|
}
|
|
@@ -899,7 +1022,9 @@ function agentContract(interaction) {
|
|
|
899
1022
|
return value;
|
|
900
1023
|
}
|
|
901
1024
|
function interactionRequestId(value) {
|
|
902
|
-
if (!isRecord(value) ||
|
|
1025
|
+
if (!isRecord(value) ||
|
|
1026
|
+
(value.type !== "custom_message" && value.role !== "custom") ||
|
|
1027
|
+
!isRecord(value.details)) {
|
|
903
1028
|
return undefined;
|
|
904
1029
|
}
|
|
905
1030
|
return typeof value.details.requestId === "string" ? value.details.requestId : undefined;
|