@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/src/extension/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { discoverWorkflows } from "../workflows/loader.js";
|
|
|
13
13
|
import { createRunId, WorkflowRunStore } from "../workflows/store.js";
|
|
14
14
|
import type { AgentStepContract, HumanDecisionResponse } from "../workflows/types.js";
|
|
15
15
|
import { parseControllerArgs, type ParsedControllerArgs } from "./controller-command.js";
|
|
16
|
+
import { SessionDeliveryCoordinator, type ClaimedSessionDelivery } from "./session-delivery.js";
|
|
17
|
+
import { SessionWorkflowView } from "./session-view.js";
|
|
16
18
|
import {
|
|
17
19
|
recoverAssistantStep,
|
|
18
20
|
registerWorkflowAgentStepMessageRenderer,
|
|
@@ -131,6 +133,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
131
133
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
132
134
|
let presentationTail = Promise.resolve();
|
|
133
135
|
let toolTail = Promise.resolve();
|
|
136
|
+
const sessionDelivery = new SessionDeliveryCoordinator();
|
|
137
|
+
const sessionView = new SessionWorkflowView();
|
|
134
138
|
|
|
135
139
|
const presentInOrder = async (ctx: ExtensionContext): Promise<void> => {
|
|
136
140
|
const prior = presentationTail;
|
|
@@ -140,12 +144,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
140
144
|
});
|
|
141
145
|
await prior;
|
|
142
146
|
try {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
await sessionDelivery.synchronize(ctx, [
|
|
148
|
+
() => claimPendingInteractionDelivery(pi, client, ctx),
|
|
149
|
+
() => claimPendingNotificationDelivery(pi, client, ctx),
|
|
150
|
+
() => claimPendingTurnDelivery(pi, client, ctx),
|
|
151
|
+
]);
|
|
148
152
|
} finally {
|
|
153
|
+
sessionView.refresh(ctx);
|
|
149
154
|
release?.();
|
|
150
155
|
}
|
|
151
156
|
};
|
|
@@ -266,6 +271,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
266
271
|
},
|
|
267
272
|
});
|
|
268
273
|
|
|
274
|
+
pi.registerShortcut("shift+up", {
|
|
275
|
+
description: "Scroll the workflow widget up",
|
|
276
|
+
handler: (ctx) => sessionView.scrollUp(ctx),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
pi.registerShortcut("shift+down", {
|
|
280
|
+
description: "Scroll the workflow widget down",
|
|
281
|
+
handler: (ctx) => sessionView.scrollDown(ctx),
|
|
282
|
+
});
|
|
283
|
+
|
|
269
284
|
pi.on("session_start", async (_event, ctx) => {
|
|
270
285
|
sessionContext = ctx;
|
|
271
286
|
try {
|
|
@@ -280,6 +295,44 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
280
295
|
pollTimer.unref?.();
|
|
281
296
|
});
|
|
282
297
|
|
|
298
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
299
|
+
const interrupted =
|
|
300
|
+
ctx.signal?.aborted === true ||
|
|
301
|
+
event.messages.some(
|
|
302
|
+
(message) =>
|
|
303
|
+
isRecord(message) && "stopReason" in message && message.stopReason === "aborted",
|
|
304
|
+
);
|
|
305
|
+
if (!interrupted) return;
|
|
306
|
+
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
307
|
+
if (
|
|
308
|
+
interaction === undefined ||
|
|
309
|
+
interaction.kind === "decision" ||
|
|
310
|
+
workflowRunPaused(interaction.runId)
|
|
311
|
+
) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const turnHasPrompt = event.messages.some(
|
|
315
|
+
(message) => interactionRequestId(message) === interaction.requestId,
|
|
316
|
+
);
|
|
317
|
+
if (!turnHasPrompt) return;
|
|
318
|
+
try {
|
|
319
|
+
const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
|
|
320
|
+
await requestAccepted(client, {
|
|
321
|
+
operation: "run.pause",
|
|
322
|
+
requestId: pauseId,
|
|
323
|
+
idempotencyKey: pauseId,
|
|
324
|
+
runId: interaction.runId,
|
|
325
|
+
});
|
|
326
|
+
sessionView.refresh(ctx);
|
|
327
|
+
ctx.ui.notify(
|
|
328
|
+
`Workflow ${interaction.runId} paused because its model turn was interrupted. Use /workflow resume to continue.`,
|
|
329
|
+
"info",
|
|
330
|
+
);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
ctx.ui.notify(`Could not pause interrupted workflow: ${errorMessage(error)}`, "warning");
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
283
336
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
284
337
|
try {
|
|
285
338
|
await submitVisibleAssistantResponse(client, ctx);
|
|
@@ -289,8 +342,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
289
342
|
await presentInOrder(ctx).catch(() => undefined);
|
|
290
343
|
});
|
|
291
344
|
|
|
292
|
-
pi.on("session_shutdown", async () => {
|
|
345
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
293
346
|
sessionContext = null;
|
|
347
|
+
sessionDelivery.clear();
|
|
348
|
+
sessionView.clear(ctx);
|
|
294
349
|
if (pollTimer !== null) clearInterval(pollTimer);
|
|
295
350
|
pollTimer = null;
|
|
296
351
|
});
|
|
@@ -514,94 +569,120 @@ async function executeControllerCommand(
|
|
|
514
569
|
};
|
|
515
570
|
}
|
|
516
571
|
|
|
517
|
-
async function
|
|
572
|
+
async function claimPendingInteractionDelivery(
|
|
518
573
|
pi: ExtensionAPI,
|
|
519
574
|
client: WorkflowHostClient,
|
|
520
575
|
ctx: ExtensionContext,
|
|
521
|
-
): Promise<
|
|
576
|
+
): Promise<ClaimedSessionDelivery | undefined> {
|
|
522
577
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
523
|
-
if (interaction === undefined)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
578
|
+
if (interaction === undefined || interaction.presentationSessionEntryId !== null)
|
|
579
|
+
return undefined;
|
|
580
|
+
|
|
581
|
+
const findSessionEntryId = (entries: readonly unknown[]): string | undefined =>
|
|
582
|
+
entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
|
|
583
|
+
const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
|
|
584
|
+
let presentationRevision = interaction.revision;
|
|
585
|
+
let claimExpiresAt = Number.POSITIVE_INFINITY;
|
|
586
|
+
if (existingEntryId === undefined) {
|
|
587
|
+
if (workflowRunPaused(interaction.runId)) return undefined;
|
|
588
|
+
if (
|
|
589
|
+
interaction.presentationClaimExpiresAt !== null &&
|
|
590
|
+
Date.parse(interaction.presentationClaimExpiresAt) > Date.now()
|
|
591
|
+
) {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
await client.ensureRunning();
|
|
595
|
+
const claim = await client.request({
|
|
596
|
+
operation: "interaction.update",
|
|
597
|
+
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
598
|
+
idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
599
|
+
runId: interaction.runId,
|
|
600
|
+
expectedRevision: interaction.revision,
|
|
601
|
+
payload: { requestId: interaction.requestId, claimPresentation: true },
|
|
602
|
+
});
|
|
603
|
+
if (claim.outcome === "conflict") return undefined;
|
|
604
|
+
if (claim.outcome !== "accepted" && claim.outcome !== "adopted") {
|
|
605
|
+
throw new Error(claim.error ?? "Workflow host rejected interaction.update");
|
|
606
|
+
}
|
|
607
|
+
if (claim.revision === undefined) throw new Error("Presentation claim has no revision");
|
|
608
|
+
const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
|
|
609
|
+
presentationRevision = claim.revision;
|
|
610
|
+
claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const contract = interactionContract(interaction);
|
|
614
|
+
return {
|
|
615
|
+
deliveryId: `interaction:${interaction.requestId}`,
|
|
616
|
+
claimExpiresAt,
|
|
617
|
+
isStillDeliverable: () =>
|
|
618
|
+
existingEntryId === undefined &&
|
|
619
|
+
interactionPresentationClaimIsLive({
|
|
620
|
+
requestId: interaction.requestId,
|
|
621
|
+
runId: interaction.runId,
|
|
622
|
+
presenterId: client.clientId,
|
|
623
|
+
revision: presentationRevision,
|
|
624
|
+
claimExpiresAt,
|
|
625
|
+
}),
|
|
626
|
+
findSessionEntryId,
|
|
627
|
+
send: () => {
|
|
628
|
+
if (interaction.kind === "decision") {
|
|
629
|
+
pi.sendMessage(
|
|
630
|
+
{
|
|
631
|
+
customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
|
|
632
|
+
content: decisionPrompt(contract),
|
|
633
|
+
display: true,
|
|
634
|
+
details: {
|
|
635
|
+
requestId: interaction.requestId,
|
|
636
|
+
runId: interaction.runId,
|
|
637
|
+
kind: "decision",
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{ triggerTurn: false },
|
|
641
|
+
);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const agent = agentContract(interaction);
|
|
645
|
+
if (agent === undefined) throw new Error("Stored workflow agent contract is invalid");
|
|
646
|
+
const details: WorkflowAgentStepMessageDetails & { requestId: string } = {
|
|
647
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
648
|
+
kind: "step",
|
|
649
|
+
contract: agent,
|
|
650
|
+
requestId: interaction.requestId,
|
|
651
|
+
...(isRecord(contract.presentation)
|
|
652
|
+
? { presentation: contract.presentation as never }
|
|
653
|
+
: {}),
|
|
654
|
+
};
|
|
655
|
+
pi.sendMessage(
|
|
656
|
+
{
|
|
657
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
658
|
+
content:
|
|
659
|
+
typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
|
|
660
|
+
display: true,
|
|
661
|
+
details,
|
|
662
|
+
},
|
|
663
|
+
{ triggerTurn: true },
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
settle: async (sessionEntryId) => {
|
|
529
667
|
await requestAccepted(client, {
|
|
530
668
|
operation: "interaction.update",
|
|
531
|
-
requestId: `present-${interaction.requestId}-${
|
|
532
|
-
idempotencyKey: `present-${interaction.requestId}-${
|
|
669
|
+
requestId: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
670
|
+
idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
|
|
533
671
|
runId: interaction.runId,
|
|
534
|
-
expectedRevision:
|
|
535
|
-
payload: { requestId: interaction.requestId, sessionEntryId
|
|
672
|
+
expectedRevision: presentationRevision,
|
|
673
|
+
payload: { requestId: interaction.requestId, sessionEntryId },
|
|
536
674
|
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
if (interaction.presentationSessionEntryId !== null) return;
|
|
541
|
-
const claim = await requestAccepted(client, {
|
|
542
|
-
operation: "interaction.update",
|
|
543
|
-
requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
544
|
-
idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
|
|
545
|
-
runId: interaction.runId,
|
|
546
|
-
expectedRevision: interaction.revision,
|
|
547
|
-
payload: { requestId: interaction.requestId, claimPresentation: true },
|
|
548
|
-
});
|
|
549
|
-
if (claim.revision === undefined) throw new Error("Presentation claim has no revision");
|
|
550
|
-
const presentationRevision = claim.revision;
|
|
551
|
-
const contract = interactionContract(interaction);
|
|
552
|
-
if (interaction.kind === "decision") {
|
|
553
|
-
pi.sendMessage(
|
|
554
|
-
{
|
|
555
|
-
customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
|
|
556
|
-
content: decisionPrompt(contract),
|
|
557
|
-
display: true,
|
|
558
|
-
details: { requestId: interaction.requestId, runId: interaction.runId, kind: "decision" },
|
|
559
|
-
},
|
|
560
|
-
{ triggerTurn: false },
|
|
561
|
-
);
|
|
562
|
-
} else {
|
|
563
|
-
const agent = agentContract(interaction);
|
|
564
|
-
if (agent === undefined) throw new Error("Stored workflow agent contract is invalid");
|
|
565
|
-
const details: WorkflowAgentStepMessageDetails & { requestId: string } = {
|
|
566
|
-
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
567
|
-
kind: "step",
|
|
568
|
-
contract: agent,
|
|
569
|
-
requestId: interaction.requestId,
|
|
570
|
-
...(isRecord(contract.presentation) ? { presentation: contract.presentation as never } : {}),
|
|
571
|
-
};
|
|
572
|
-
pi.sendMessage(
|
|
573
|
-
{
|
|
574
|
-
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
575
|
-
content:
|
|
576
|
-
typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
|
|
577
|
-
display: true,
|
|
578
|
-
details,
|
|
579
|
-
},
|
|
580
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
const inserted = ctx.sessionManager
|
|
584
|
-
.getBranch()
|
|
585
|
-
.find((entry) => interactionRequestId(entry) === interaction.requestId);
|
|
586
|
-
const entryId = entryIdentifier(inserted);
|
|
587
|
-
if (entryId === undefined) return;
|
|
588
|
-
await requestAccepted(client, {
|
|
589
|
-
operation: "interaction.update",
|
|
590
|
-
requestId: `present-${interaction.requestId}-${entryId}`,
|
|
591
|
-
idempotencyKey: `present-${interaction.requestId}-${entryId}`,
|
|
592
|
-
runId: interaction.runId,
|
|
593
|
-
expectedRevision: presentationRevision,
|
|
594
|
-
payload: { requestId: interaction.requestId, sessionEntryId: entryId },
|
|
595
|
-
});
|
|
675
|
+
},
|
|
676
|
+
};
|
|
596
677
|
}
|
|
597
678
|
|
|
598
|
-
async function
|
|
679
|
+
async function claimPendingNotificationDelivery(
|
|
599
680
|
pi: ExtensionAPI,
|
|
600
681
|
client: WorkflowHostClient,
|
|
601
682
|
ctx: ExtensionContext,
|
|
602
|
-
): Promise<
|
|
683
|
+
): Promise<ClaimedSessionDelivery | undefined> {
|
|
603
684
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
604
|
-
if (!hasClaimableNotification(sessionId)) return;
|
|
685
|
+
if (!hasClaimableNotification(sessionId)) return undefined;
|
|
605
686
|
const claimRequestId = randomUUID();
|
|
606
687
|
const claimed = await requestAccepted(client, {
|
|
607
688
|
operation: "notification.claim",
|
|
@@ -611,57 +692,67 @@ async function deliverPendingNotification(
|
|
|
611
692
|
});
|
|
612
693
|
const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
|
|
613
694
|
const notification = isRecord(receipt?.notification) ? receipt.notification : undefined;
|
|
614
|
-
if (notification === undefined) return;
|
|
695
|
+
if (notification === undefined) return undefined;
|
|
615
696
|
const claimId = requireText(receipt?.claimId, "notification claimId");
|
|
697
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "notification claim");
|
|
616
698
|
const notificationId = requireText(notification.notificationId, "notificationId");
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
699
|
+
return {
|
|
700
|
+
deliveryId: `notification:${notificationId}`,
|
|
701
|
+
claimExpiresAt,
|
|
702
|
+
isStillDeliverable: () =>
|
|
703
|
+
hostDeliveryClaimIsLive(client, {
|
|
704
|
+
kind: "notification",
|
|
705
|
+
resourceId: notificationId,
|
|
706
|
+
targetSessionId: sessionId,
|
|
707
|
+
claimId,
|
|
708
|
+
}),
|
|
709
|
+
findSessionEntryId: (entries) =>
|
|
710
|
+
entryIdentifier(
|
|
711
|
+
entries.find(
|
|
712
|
+
(entry) =>
|
|
713
|
+
customMessageDetail(entry, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
|
|
714
|
+
notificationId,
|
|
715
|
+
),
|
|
716
|
+
),
|
|
717
|
+
send: () => {
|
|
718
|
+
pi.sendMessage(
|
|
719
|
+
{
|
|
720
|
+
customType: WORKFLOW_NOTIFICATION_MESSAGE_TYPE,
|
|
721
|
+
content: requireText(notification.content, "notification content"),
|
|
722
|
+
display: true,
|
|
723
|
+
details: {
|
|
724
|
+
notificationId,
|
|
725
|
+
runId: requireText(notification.runId, "notification runId"),
|
|
726
|
+
kind: notification.kind,
|
|
727
|
+
},
|
|
633
728
|
},
|
|
634
|
-
|
|
635
|
-
{ triggerTurn: false },
|
|
636
|
-
);
|
|
637
|
-
entry = ctx.sessionManager
|
|
638
|
-
.getBranch()
|
|
639
|
-
.find(
|
|
640
|
-
(candidate) =>
|
|
641
|
-
customMessageDetail(candidate, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
|
|
642
|
-
notificationId,
|
|
729
|
+
{ triggerTurn: false },
|
|
643
730
|
);
|
|
644
|
-
}
|
|
645
|
-
if (entry === undefined) return;
|
|
646
|
-
await requestAccepted(client, {
|
|
647
|
-
operation: "notification.deliver",
|
|
648
|
-
requestId: `notification-deliver-${notificationId}-${claimId}`,
|
|
649
|
-
idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
|
|
650
|
-
payload: {
|
|
651
|
-
notificationId,
|
|
652
|
-
targetSessionId: sessionId,
|
|
653
|
-
claimId,
|
|
654
731
|
},
|
|
655
|
-
|
|
732
|
+
settle: async () => {
|
|
733
|
+
await requestAccepted(client, {
|
|
734
|
+
operation: "notification.deliver",
|
|
735
|
+
requestId: `notification-deliver-${notificationId}-${claimId}`,
|
|
736
|
+
idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
|
|
737
|
+
payload: {
|
|
738
|
+
notificationId,
|
|
739
|
+
targetSessionId: sessionId,
|
|
740
|
+
claimId,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
},
|
|
744
|
+
};
|
|
656
745
|
}
|
|
657
746
|
|
|
658
|
-
async function
|
|
747
|
+
async function claimPendingTurnDelivery(
|
|
659
748
|
pi: ExtensionAPI,
|
|
660
749
|
client: WorkflowHostClient,
|
|
661
750
|
ctx: ExtensionContext,
|
|
662
|
-
): Promise<
|
|
751
|
+
): Promise<ClaimedSessionDelivery | undefined> {
|
|
663
752
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
664
|
-
if (!hasClaimableTurn(sessionId))
|
|
753
|
+
if (pendingInteractionForSession(sessionId) !== undefined || !hasClaimableTurn(sessionId)) {
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
665
756
|
const claimRequestId = randomUUID();
|
|
666
757
|
const claimed = await requestAccepted(client, {
|
|
667
758
|
operation: "turn.claim",
|
|
@@ -671,49 +762,55 @@ async function presentPendingTurn(
|
|
|
671
762
|
});
|
|
672
763
|
const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
|
|
673
764
|
const turn = isRecord(receipt?.turn) ? receipt.turn : undefined;
|
|
674
|
-
if (turn === undefined) return;
|
|
765
|
+
if (turn === undefined) return undefined;
|
|
675
766
|
const claimId = requireText(receipt?.claimId, "turn claimId");
|
|
767
|
+
const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
|
|
676
768
|
const intentId = requireText(turn.intentId, "turn intentId");
|
|
677
769
|
const runId = requireText(turn.runId, "turn runId");
|
|
678
770
|
const state = terminalRunState(runId);
|
|
679
|
-
if (state === undefined) return;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
771
|
+
if (state === undefined) return undefined;
|
|
772
|
+
return {
|
|
773
|
+
deliveryId: `turn:${intentId}`,
|
|
774
|
+
claimExpiresAt,
|
|
775
|
+
isStillDeliverable: () =>
|
|
776
|
+
hostDeliveryClaimIsLive(client, {
|
|
777
|
+
kind: "turn",
|
|
778
|
+
resourceId: intentId,
|
|
779
|
+
targetSessionId: sessionId,
|
|
780
|
+
claimId,
|
|
781
|
+
}),
|
|
782
|
+
findSessionEntryId: (entries) =>
|
|
783
|
+
entryIdentifier(
|
|
784
|
+
entries.find(
|
|
785
|
+
(entry) =>
|
|
786
|
+
customMessageDetail(entry, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") === intentId,
|
|
787
|
+
),
|
|
788
|
+
),
|
|
789
|
+
send: () => {
|
|
790
|
+
pi.sendMessage(
|
|
791
|
+
{
|
|
792
|
+
customType: WORKFLOW_PRESENTATION_MESSAGE_TYPE,
|
|
793
|
+
content: presentationMessage(turn, state),
|
|
794
|
+
display: false,
|
|
795
|
+
details: { intentId, runId },
|
|
796
|
+
},
|
|
797
|
+
{ triggerTurn: true },
|
|
702
798
|
);
|
|
703
|
-
}
|
|
704
|
-
const messageId = entryIdentifier(entry);
|
|
705
|
-
if (messageId === undefined) return;
|
|
706
|
-
await requestAccepted(client, {
|
|
707
|
-
operation: "turn.resolve",
|
|
708
|
-
requestId: `turn-resolve-${intentId}-${messageId}`,
|
|
709
|
-
idempotencyKey: `turn-resolve-${intentId}-${messageId}`,
|
|
710
|
-
payload: {
|
|
711
|
-
intentId,
|
|
712
|
-
targetSessionId: sessionId,
|
|
713
|
-
claimId,
|
|
714
|
-
messageId,
|
|
715
799
|
},
|
|
716
|
-
|
|
800
|
+
settle: async (sessionEntryId) => {
|
|
801
|
+
await requestAccepted(client, {
|
|
802
|
+
operation: "turn.resolve",
|
|
803
|
+
requestId: `turn-resolve-${intentId}-${sessionEntryId}`,
|
|
804
|
+
idempotencyKey: `turn-resolve-${intentId}-${sessionEntryId}`,
|
|
805
|
+
payload: {
|
|
806
|
+
intentId,
|
|
807
|
+
targetSessionId: sessionId,
|
|
808
|
+
claimId,
|
|
809
|
+
messageId: sessionEntryId,
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
},
|
|
813
|
+
};
|
|
717
814
|
}
|
|
718
815
|
|
|
719
816
|
async function submitVisibleAssistantResponse(
|
|
@@ -721,7 +818,13 @@ async function submitVisibleAssistantResponse(
|
|
|
721
818
|
ctx: ExtensionContext,
|
|
722
819
|
): Promise<void> {
|
|
723
820
|
const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
|
|
724
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
interaction === undefined ||
|
|
823
|
+
interaction.kind !== "assistant" ||
|
|
824
|
+
workflowRunPaused(interaction.runId)
|
|
825
|
+
) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
725
828
|
const contract = agentContract(interaction);
|
|
726
829
|
if (contract === undefined) return;
|
|
727
830
|
const submission = recoverAssistantStep(ctx.sessionManager.getBranch(), contract);
|
|
@@ -872,6 +975,65 @@ function terminalRunState(runId: string): Record<string, unknown> | undefined {
|
|
|
872
975
|
}
|
|
873
976
|
}
|
|
874
977
|
|
|
978
|
+
function workflowRunPaused(runId: string): boolean {
|
|
979
|
+
return terminalRunState(runId)?.paused === true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function interactionPresentationClaimIsLive(options: {
|
|
983
|
+
requestId: string;
|
|
984
|
+
runId: string;
|
|
985
|
+
presenterId: string;
|
|
986
|
+
revision: number;
|
|
987
|
+
claimExpiresAt: number;
|
|
988
|
+
}): boolean {
|
|
989
|
+
try {
|
|
990
|
+
const store = new HostStateStore(workflowStatePath(), { readOnly: true });
|
|
991
|
+
try {
|
|
992
|
+
const interaction = store.getInteraction(options.requestId);
|
|
993
|
+
return (
|
|
994
|
+
interaction?.runId === options.runId &&
|
|
995
|
+
interaction.status === "presenting" &&
|
|
996
|
+
interaction.presenterId === options.presenterId &&
|
|
997
|
+
interaction.revision === options.revision &&
|
|
998
|
+
interaction.presentationSessionEntryId === null &&
|
|
999
|
+
interaction.presentationClaimExpiresAt !== null &&
|
|
1000
|
+
Date.parse(interaction.presentationClaimExpiresAt) === options.claimExpiresAt &&
|
|
1001
|
+
!workflowRunPaused(options.runId)
|
|
1002
|
+
);
|
|
1003
|
+
} finally {
|
|
1004
|
+
store.close();
|
|
1005
|
+
}
|
|
1006
|
+
} catch {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function hostDeliveryClaimIsLive(
|
|
1012
|
+
client: WorkflowHostClient,
|
|
1013
|
+
options: {
|
|
1014
|
+
kind: "notification" | "turn";
|
|
1015
|
+
resourceId: string;
|
|
1016
|
+
targetSessionId: string;
|
|
1017
|
+
claimId: string;
|
|
1018
|
+
},
|
|
1019
|
+
): Promise<boolean> {
|
|
1020
|
+
try {
|
|
1021
|
+
const validationId = randomUUID();
|
|
1022
|
+
const response = await client.request({
|
|
1023
|
+
operation: options.kind === "notification" ? "notification.claim" : "turn.claim",
|
|
1024
|
+
requestId: `delivery-validate-${options.claimId}-${validationId}`,
|
|
1025
|
+
idempotencyKey: validationId,
|
|
1026
|
+
payload: { ...options, validateClaim: true },
|
|
1027
|
+
});
|
|
1028
|
+
const receipt = isRecord(response.receipt) ? response.receipt : undefined;
|
|
1029
|
+
return (
|
|
1030
|
+
(response.outcome === "accepted" || response.outcome === "adopted") && receipt?.live === true
|
|
1031
|
+
);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
875
1037
|
function presentationMessage(
|
|
876
1038
|
turn: Record<string, unknown>,
|
|
877
1039
|
state: Record<string, unknown>,
|
|
@@ -927,6 +1089,12 @@ function requireText(value: unknown, name: string): string {
|
|
|
927
1089
|
return value;
|
|
928
1090
|
}
|
|
929
1091
|
|
|
1092
|
+
function claimExpiry(value: unknown, name: string): number {
|
|
1093
|
+
const expiry = Date.parse(requireText(value, `${name} expiry`));
|
|
1094
|
+
if (!Number.isFinite(expiry)) throw new Error(`${name} expiry must be a timestamp`);
|
|
1095
|
+
return expiry;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
930
1098
|
function activeSessionRun(ctx: ExtensionContext): WorkflowRunQueueRecord | undefined {
|
|
931
1099
|
return sessionRun(ctx);
|
|
932
1100
|
}
|
|
@@ -1021,7 +1189,11 @@ function agentContract(interaction: InteractiveRequestRecord): AgentStepContract
|
|
|
1021
1189
|
}
|
|
1022
1190
|
|
|
1023
1191
|
function interactionRequestId(value: unknown): string | undefined {
|
|
1024
|
-
if (
|
|
1192
|
+
if (
|
|
1193
|
+
!isRecord(value) ||
|
|
1194
|
+
(value.type !== "custom_message" && value.role !== "custom") ||
|
|
1195
|
+
!isRecord(value.details)
|
|
1196
|
+
) {
|
|
1025
1197
|
return undefined;
|
|
1026
1198
|
}
|
|
1027
1199
|
return typeof value.details.requestId === "string" ? value.details.requestId : undefined;
|