@pasko70/pibo 2.4.1 → 2.4.2
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/agent-runtime/routed-session.js +150 -7
- package/dist/agent-runtimes/pi/routed-session.js +62 -16
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.1.vsix → pibo-vscode-ext-2.4.2.vsix} +0 -0
- package/dist/core/session-router.js +153 -63
- package/dist/loops/service.js +5 -1
- package/dist/loops/store.js +17 -5
- package/dist/loops/tools.js +3 -1
- package/dist/reliability/store.js +11 -6
- package/dist/runs/lifecycle.js +12 -0
- package/dist/runs/registry.js +23 -4
- package/dist/runs/tools.js +12 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,6 +9,20 @@ const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
|
|
|
9
9
|
"pibo_run_cancel",
|
|
10
10
|
"pibo_run_ack",
|
|
11
11
|
]);
|
|
12
|
+
// Run reminders are autonomous service wakeups, so their provider/tool loop needs a deterministic boundary.
|
|
13
|
+
const RUN_REMINDER_MAX_TOOL_EXECUTIONS = 64;
|
|
14
|
+
const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
|
|
15
|
+
const RUN_REMINDER_MAX_TOTAL_TOKENS = 2_000_000;
|
|
16
|
+
const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
|
|
17
|
+
const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
|
|
18
|
+
function serializedToolArgs(value) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(value) ?? String(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
12
26
|
function errorMessage(error) {
|
|
13
27
|
return error instanceof Error ? error.message : String(error);
|
|
14
28
|
}
|
|
@@ -82,9 +96,11 @@ export class RuntimeRoutedSession {
|
|
|
82
96
|
runtimeDisposePromise;
|
|
83
97
|
forceDisposalStarted = false;
|
|
84
98
|
drainPromise;
|
|
99
|
+
inFlightMessage;
|
|
85
100
|
activeMessage;
|
|
86
101
|
activeExecutionEvent;
|
|
87
102
|
activeMessageFailed = false;
|
|
103
|
+
runReminderTurnGuard;
|
|
88
104
|
activeAssistantIndex;
|
|
89
105
|
nextAssistantIndex = 0;
|
|
90
106
|
activeThinkingIndex;
|
|
@@ -409,6 +425,28 @@ export class RuntimeRoutedSession {
|
|
|
409
425
|
this.notifyState();
|
|
410
426
|
return true;
|
|
411
427
|
}
|
|
428
|
+
const inFlight = this.inFlightMessage;
|
|
429
|
+
if (inFlight?.event.id === eventId) {
|
|
430
|
+
if (!inFlight.cancellation) {
|
|
431
|
+
inFlight.cancelled = true;
|
|
432
|
+
this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
|
|
433
|
+
const active = this.activeMessage?.id === eventId;
|
|
434
|
+
inFlight.cancellation = (async () => {
|
|
435
|
+
try {
|
|
436
|
+
if (active)
|
|
437
|
+
await this.runtimeSession.abort();
|
|
438
|
+
await inFlight.settled;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
inFlight.cancelled = false;
|
|
442
|
+
inFlight.cancellation = undefined;
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
}
|
|
447
|
+
await inFlight.cancellation;
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
412
450
|
if (this.activeMessage?.id === eventId) {
|
|
413
451
|
this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
|
|
414
452
|
await this.runtimeSession.abort();
|
|
@@ -462,6 +500,7 @@ export class RuntimeRoutedSession {
|
|
|
462
500
|
return;
|
|
463
501
|
case "tool_execution_started":
|
|
464
502
|
this.emit(this.withActiveMessage({ ...event, type: "tool_execution_started", piboSessionId: this.piboSessionId }));
|
|
503
|
+
this.trackRunReminderTurnGuard("tool_execution_started", { toolName: event.toolName, args: event.args });
|
|
465
504
|
return;
|
|
466
505
|
case "tool_execution_updated":
|
|
467
506
|
this.emit(this.withActiveMessage({ ...event, type: "tool_execution_updated", piboSessionId: this.piboSessionId }));
|
|
@@ -479,7 +518,9 @@ export class RuntimeRoutedSession {
|
|
|
479
518
|
cacheWriteTokens: event.usage.cacheWriteTokens,
|
|
480
519
|
reasoningTokens: event.usage.reasoningTokens,
|
|
481
520
|
totalTokens: event.usage.totalTokens,
|
|
521
|
+
provenance: this.activeMessage?.provenance,
|
|
482
522
|
}));
|
|
523
|
+
this.trackRunReminderTurnGuard("usage", { totalTokens: event.usage.totalTokens });
|
|
483
524
|
return;
|
|
484
525
|
case "compaction_start":
|
|
485
526
|
this.emit(this.withActiveMessage({
|
|
@@ -593,9 +634,10 @@ export class RuntimeRoutedSession {
|
|
|
593
634
|
}
|
|
594
635
|
}
|
|
595
636
|
async processQueuedMessage(event) {
|
|
637
|
+
const inFlight = this.beginInFlightMessage(event);
|
|
596
638
|
try {
|
|
597
639
|
const preflight = await this.options.messagePreflight?.(event);
|
|
598
|
-
if (this.disposed)
|
|
640
|
+
if (this.disposed || inFlight.cancelled)
|
|
599
641
|
return;
|
|
600
642
|
if (preflight && !preflight.allowed) {
|
|
601
643
|
this.emit({
|
|
@@ -614,6 +656,10 @@ export class RuntimeRoutedSession {
|
|
|
614
656
|
});
|
|
615
657
|
return;
|
|
616
658
|
}
|
|
659
|
+
this.activeMessage = event;
|
|
660
|
+
this.activeMessageFailed = false;
|
|
661
|
+
this.beginRunReminderTurnGuard(event);
|
|
662
|
+
this.resetContentIndices();
|
|
617
663
|
this.emit({
|
|
618
664
|
type: "message_started",
|
|
619
665
|
piboSessionId: this.piboSessionId,
|
|
@@ -622,15 +668,14 @@ export class RuntimeRoutedSession {
|
|
|
622
668
|
source: event.source,
|
|
623
669
|
provenance: event.provenance,
|
|
624
670
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
this.resetContentIndices();
|
|
671
|
+
if (inFlight.cancelled)
|
|
672
|
+
return;
|
|
628
673
|
await this.runtimeSession.prompt({
|
|
629
674
|
text: event.text,
|
|
630
675
|
source: promptSource(event.source),
|
|
631
676
|
capabilityScope: event.capabilityScope,
|
|
632
677
|
});
|
|
633
|
-
if (this.disposed)
|
|
678
|
+
if (this.disposed || inFlight.cancelled)
|
|
634
679
|
return;
|
|
635
680
|
if (!this.activeMessageFailed) {
|
|
636
681
|
this.emit({
|
|
@@ -643,7 +688,7 @@ export class RuntimeRoutedSession {
|
|
|
643
688
|
}
|
|
644
689
|
}
|
|
645
690
|
catch (error) {
|
|
646
|
-
if (this.disposed)
|
|
691
|
+
if (this.disposed || inFlight.cancelled)
|
|
647
692
|
return;
|
|
648
693
|
if (!this.activeMessageFailed) {
|
|
649
694
|
const message = errorMessage(error);
|
|
@@ -661,7 +706,102 @@ export class RuntimeRoutedSession {
|
|
|
661
706
|
this.activeMessage = undefined;
|
|
662
707
|
this.activeMessageFailed = false;
|
|
663
708
|
this.resetContentIndices();
|
|
709
|
+
this.clearRunReminderTurnGuard();
|
|
710
|
+
if (this.inFlightMessage === inFlight)
|
|
711
|
+
this.inFlightMessage = undefined;
|
|
712
|
+
inFlight.resolveSettled();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
beginInFlightMessage(event) {
|
|
716
|
+
let resolveSettled;
|
|
717
|
+
const inFlight = {
|
|
718
|
+
event,
|
|
719
|
+
cancelled: false,
|
|
720
|
+
settled: new Promise((resolve) => { resolveSettled = resolve; }),
|
|
721
|
+
resolveSettled: () => { resolveSettled?.(); },
|
|
722
|
+
};
|
|
723
|
+
this.inFlightMessage = inFlight;
|
|
724
|
+
return inFlight;
|
|
725
|
+
}
|
|
726
|
+
beginRunReminderTurnGuard(event) {
|
|
727
|
+
this.clearRunReminderTurnGuard();
|
|
728
|
+
if (event.source !== "service" || !event.text.startsWith("<pibo_run_notification>"))
|
|
729
|
+
return;
|
|
730
|
+
const guard = {
|
|
731
|
+
eventId: event.id,
|
|
732
|
+
toolExecutions: 0,
|
|
733
|
+
providerRounds: 0,
|
|
734
|
+
totalTokens: 0,
|
|
735
|
+
toolSignatures: new Map(),
|
|
736
|
+
tripped: false,
|
|
737
|
+
};
|
|
738
|
+
guard.timer = setTimeout(() => {
|
|
739
|
+
this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_DURATION_MS / 60_000} minutes`);
|
|
740
|
+
}, RUN_REMINDER_MAX_DURATION_MS);
|
|
741
|
+
guard.timer.unref?.();
|
|
742
|
+
this.runReminderTurnGuard = guard;
|
|
743
|
+
}
|
|
744
|
+
clearRunReminderTurnGuard() {
|
|
745
|
+
const guard = this.runReminderTurnGuard;
|
|
746
|
+
if (!guard)
|
|
747
|
+
return;
|
|
748
|
+
if (guard.timer)
|
|
749
|
+
clearTimeout(guard.timer);
|
|
750
|
+
this.runReminderTurnGuard = undefined;
|
|
751
|
+
}
|
|
752
|
+
trackRunReminderTurnGuard(type, payload) {
|
|
753
|
+
const guard = this.runReminderTurnGuard;
|
|
754
|
+
if (!guard || guard.tripped || guard.eventId !== this.activeMessage?.id)
|
|
755
|
+
return;
|
|
756
|
+
if (type === "usage") {
|
|
757
|
+
const totalTokens = payload.totalTokens ?? 0;
|
|
758
|
+
guard.totalTokens += totalTokens;
|
|
759
|
+
guard.providerRounds += 1;
|
|
760
|
+
if (guard.totalTokens > RUN_REMINDER_MAX_TOTAL_TOKENS) {
|
|
761
|
+
this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_TOTAL_TOKENS} total tokens`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (guard.providerRounds > RUN_REMINDER_MAX_PROVIDER_ROUNDS) {
|
|
765
|
+
this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_PROVIDER_ROUNDS} provider rounds`);
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
664
768
|
}
|
|
769
|
+
const toolName = payload.toolName;
|
|
770
|
+
const args = payload.args;
|
|
771
|
+
guard.toolExecutions += 1;
|
|
772
|
+
if (guard.toolExecutions > RUN_REMINDER_MAX_TOOL_EXECUTIONS) {
|
|
773
|
+
this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_TOOL_EXECUTIONS} tool executions`);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const signature = `${String(toolName ?? "unknown")}:${serializedToolArgs(args)}`;
|
|
777
|
+
const repeated = (guard.toolSignatures.get(signature) ?? 0) + 1;
|
|
778
|
+
guard.toolSignatures.set(signature, repeated);
|
|
779
|
+
if (repeated > RUN_REMINDER_MAX_REPEATED_TOOL_CALLS) {
|
|
780
|
+
this.tripRunReminderTurnGuard(guard, `repeated the same tool call more than ${RUN_REMINDER_MAX_REPEATED_TOOL_CALLS} times`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
tripRunReminderTurnGuard(guard, reason) {
|
|
784
|
+
if (guard.tripped || this.runReminderTurnGuard !== guard)
|
|
785
|
+
return;
|
|
786
|
+
guard.tripped = true;
|
|
787
|
+
this.activeMessageFailed = true;
|
|
788
|
+
const error = `Run-reminder turn stopped because it ${reason}.`;
|
|
789
|
+
this.emit({
|
|
790
|
+
type: "session_error",
|
|
791
|
+
piboSessionId: this.piboSessionId,
|
|
792
|
+
eventId: guard.eventId,
|
|
793
|
+
error,
|
|
794
|
+
errorDetails: {
|
|
795
|
+
category: "runtime_abort",
|
|
796
|
+
errorClass: "runtime_abort",
|
|
797
|
+
code: "run_reminder_limit_exceeded",
|
|
798
|
+
origin: "runtime",
|
|
799
|
+
retryable: false,
|
|
800
|
+
userMessage: error,
|
|
801
|
+
},
|
|
802
|
+
provenance: this.activeMessage?.provenance,
|
|
803
|
+
});
|
|
804
|
+
void Promise.resolve(this.runtimeSession.abort()).catch(() => undefined);
|
|
665
805
|
}
|
|
666
806
|
async processQueuedCompact(event) {
|
|
667
807
|
this.activeExecutionEvent = event;
|
|
@@ -851,7 +991,10 @@ export class RuntimeRoutedSession {
|
|
|
851
991
|
}
|
|
852
992
|
activeAndQueuedMessages() {
|
|
853
993
|
const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
854
|
-
|
|
994
|
+
const inFlight = this.inFlightMessage?.event;
|
|
995
|
+
if (this.activeMessage)
|
|
996
|
+
return [this.activeMessage, ...messages];
|
|
997
|
+
return inFlight ? [inFlight, ...messages] : messages;
|
|
855
998
|
}
|
|
856
999
|
notifyMessagesInterrupted(messages, reason) {
|
|
857
1000
|
if (messages.length === 0)
|
|
@@ -451,6 +451,7 @@ export class RoutedSession {
|
|
|
451
451
|
runtimeDisposePromise;
|
|
452
452
|
forceDisposalStarted = false;
|
|
453
453
|
drainPromise;
|
|
454
|
+
inFlightMessage;
|
|
454
455
|
fastMode = false;
|
|
455
456
|
fastModePatchedAgents = new WeakSet();
|
|
456
457
|
activeMessage;
|
|
@@ -1101,6 +1102,31 @@ export class RoutedSession {
|
|
|
1101
1102
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
1102
1103
|
return true;
|
|
1103
1104
|
}
|
|
1105
|
+
const inFlight = this.inFlightMessage;
|
|
1106
|
+
if (inFlight?.event.id === eventId) {
|
|
1107
|
+
if (!inFlight.cancellation) {
|
|
1108
|
+
inFlight.cancelled = true;
|
|
1109
|
+
this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
|
|
1110
|
+
const active = this.activeMessage?.id === eventId;
|
|
1111
|
+
inFlight.cancellation = (async () => {
|
|
1112
|
+
try {
|
|
1113
|
+
if (active) {
|
|
1114
|
+
this.cancelProviderRecovery();
|
|
1115
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled with the active message");
|
|
1116
|
+
await this.runtime.session.abort();
|
|
1117
|
+
}
|
|
1118
|
+
await inFlight.settled;
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
inFlight.cancelled = false;
|
|
1122
|
+
inFlight.cancellation = undefined;
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
})();
|
|
1126
|
+
}
|
|
1127
|
+
await inFlight.cancellation;
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1104
1130
|
if (this.activeMessage?.id === eventId) {
|
|
1105
1131
|
this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
|
|
1106
1132
|
this.cancelProviderRecovery();
|
|
@@ -1143,9 +1169,10 @@ export class RoutedSession {
|
|
|
1143
1169
|
}
|
|
1144
1170
|
}
|
|
1145
1171
|
async processQueuedMessage(event) {
|
|
1172
|
+
const inFlight = this.beginInFlightMessage(event);
|
|
1146
1173
|
try {
|
|
1147
1174
|
const preflight = await this.messagePreflight?.(event);
|
|
1148
|
-
if (this.disposed)
|
|
1175
|
+
if (this.disposed || inFlight.cancelled)
|
|
1149
1176
|
return;
|
|
1150
1177
|
if (preflight && !preflight.allowed) {
|
|
1151
1178
|
this.emit({
|
|
@@ -1166,16 +1193,8 @@ export class RoutedSession {
|
|
|
1166
1193
|
}
|
|
1167
1194
|
const session = this.runtime.session;
|
|
1168
1195
|
await this.resumeTranscriptIntegrityRecovery(session);
|
|
1169
|
-
if (this.disposed)
|
|
1196
|
+
if (this.disposed || inFlight.cancelled)
|
|
1170
1197
|
return;
|
|
1171
|
-
this.emit({
|
|
1172
|
-
type: "message_started",
|
|
1173
|
-
piboSessionId: this.piboSessionId,
|
|
1174
|
-
eventId: event.id,
|
|
1175
|
-
text: event.text,
|
|
1176
|
-
source: event.source,
|
|
1177
|
-
provenance: event.provenance,
|
|
1178
|
-
});
|
|
1179
1198
|
this.activeMessage = event;
|
|
1180
1199
|
this.providerRecoveryCancelled = false;
|
|
1181
1200
|
this.pendingAssistantError = undefined;
|
|
@@ -1185,19 +1204,29 @@ export class RoutedSession {
|
|
|
1185
1204
|
this.nextAssistantIndex = 0;
|
|
1186
1205
|
this.activeThinkingIndex = undefined;
|
|
1187
1206
|
this.nextThinkingIndex = 0;
|
|
1207
|
+
this.emit({
|
|
1208
|
+
type: "message_started",
|
|
1209
|
+
piboSessionId: this.piboSessionId,
|
|
1210
|
+
eventId: event.id,
|
|
1211
|
+
text: event.text,
|
|
1212
|
+
source: event.source,
|
|
1213
|
+
provenance: event.provenance,
|
|
1214
|
+
});
|
|
1215
|
+
if (inFlight.cancelled)
|
|
1216
|
+
return;
|
|
1188
1217
|
this.applyMessageCapabilityScope(event, session);
|
|
1189
1218
|
const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
|
|
1190
1219
|
await session.prompt(expandedText, { source: promptSource(event.source) });
|
|
1191
|
-
if (this.disposed)
|
|
1220
|
+
if (this.disposed || inFlight.cancelled)
|
|
1192
1221
|
return;
|
|
1193
1222
|
await this.waitForPiAgentSettlement(session);
|
|
1194
|
-
if (this.disposed)
|
|
1223
|
+
if (this.disposed || inFlight.cancelled)
|
|
1195
1224
|
return;
|
|
1196
1225
|
await this.resumeContextGuardRecovery(session);
|
|
1197
|
-
if (this.disposed)
|
|
1226
|
+
if (this.disposed || inFlight.cancelled)
|
|
1198
1227
|
return;
|
|
1199
1228
|
await this.recoverTransientProviderErrors(session);
|
|
1200
|
-
if (this.disposed)
|
|
1229
|
+
if (this.disposed || inFlight.cancelled)
|
|
1201
1230
|
return;
|
|
1202
1231
|
this.flushPendingAssistantError();
|
|
1203
1232
|
if (!this.activeMessageFailed) {
|
|
@@ -1211,7 +1240,7 @@ export class RoutedSession {
|
|
|
1211
1240
|
}
|
|
1212
1241
|
}
|
|
1213
1242
|
catch (error) {
|
|
1214
|
-
if (error instanceof PiboProviderRecoveryCancelledError || this.disposed)
|
|
1243
|
+
if (error instanceof PiboProviderRecoveryCancelledError || this.disposed || inFlight.cancelled)
|
|
1215
1244
|
return;
|
|
1216
1245
|
const message = errorMessage(error);
|
|
1217
1246
|
this.emit({
|
|
@@ -1234,8 +1263,22 @@ export class RoutedSession {
|
|
|
1234
1263
|
this.nextAssistantIndex = 0;
|
|
1235
1264
|
this.activeThinkingIndex = undefined;
|
|
1236
1265
|
this.nextThinkingIndex = 0;
|
|
1266
|
+
if (this.inFlightMessage === inFlight)
|
|
1267
|
+
this.inFlightMessage = undefined;
|
|
1268
|
+
inFlight.resolveSettled();
|
|
1237
1269
|
}
|
|
1238
1270
|
}
|
|
1271
|
+
beginInFlightMessage(event) {
|
|
1272
|
+
let resolveSettled;
|
|
1273
|
+
const inFlight = {
|
|
1274
|
+
event,
|
|
1275
|
+
cancelled: false,
|
|
1276
|
+
settled: new Promise((resolve) => { resolveSettled = resolve; }),
|
|
1277
|
+
resolveSettled: () => { resolveSettled?.(); },
|
|
1278
|
+
};
|
|
1279
|
+
this.inFlightMessage = inFlight;
|
|
1280
|
+
return inFlight;
|
|
1281
|
+
}
|
|
1239
1282
|
applyMessageCapabilityScope(event, session) {
|
|
1240
1283
|
if (event.capabilityScope !== "run-reminder")
|
|
1241
1284
|
return;
|
|
@@ -1395,7 +1438,10 @@ export class RoutedSession {
|
|
|
1395
1438
|
}
|
|
1396
1439
|
activeAndQueuedMessages() {
|
|
1397
1440
|
const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
1398
|
-
|
|
1441
|
+
const inFlight = this.inFlightMessage?.event;
|
|
1442
|
+
if (this.activeMessage)
|
|
1443
|
+
return [this.activeMessage, ...messages];
|
|
1444
|
+
return inFlight ? [inFlight, ...messages] : messages;
|
|
1399
1445
|
}
|
|
1400
1446
|
notifyMessagesInterrupted(messages, reason) {
|
|
1401
1447
|
if (messages.length === 0)
|
|
Binary file
|
package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.1.vsix → pibo-vscode-ext-2.4.2.vsix}
RENAMED
|
Binary file
|
|
@@ -5,7 +5,7 @@ import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-
|
|
|
5
5
|
import { runtimeSessionErrorDetails } from "./session-errors.js";
|
|
6
6
|
import { normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
|
|
7
7
|
import { PiboRunRegistry } from "../runs/registry.js";
|
|
8
|
-
import { PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
|
|
8
|
+
import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
|
|
9
9
|
import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
|
|
10
10
|
import { createPiboSignalRegistry } from "../signals/registry.js";
|
|
11
11
|
import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
|
|
@@ -150,7 +150,29 @@ function formatRunReminderMessage(notification) {
|
|
|
150
150
|
].join("\n");
|
|
151
151
|
}
|
|
152
152
|
function isRunReminderServiceMessage(event) {
|
|
153
|
-
return event.source === "service" && event.
|
|
153
|
+
return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
|
|
154
|
+
}
|
|
155
|
+
function yieldedRunOrigin(event) {
|
|
156
|
+
if (!event?.id || event.provenance?.kind !== "loop-run")
|
|
157
|
+
return undefined;
|
|
158
|
+
return {
|
|
159
|
+
eventId: event.provenance.rootEventId ?? event.id,
|
|
160
|
+
provenance: {
|
|
161
|
+
kind: event.provenance.kind,
|
|
162
|
+
jobId: event.provenance.jobId,
|
|
163
|
+
runId: event.provenance.runId,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function runReminderProvenance(notification) {
|
|
168
|
+
const origin = notification.origin;
|
|
169
|
+
if (!origin || origin.provenance.kind !== "loop-run")
|
|
170
|
+
return undefined;
|
|
171
|
+
return {
|
|
172
|
+
...origin.provenance,
|
|
173
|
+
cause: "run-reminder",
|
|
174
|
+
rootEventId: origin.eventId,
|
|
175
|
+
};
|
|
154
176
|
}
|
|
155
177
|
function isTerminalRunStatus(status) {
|
|
156
178
|
return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
|
|
@@ -235,7 +257,7 @@ export class PiboSessionRouter {
|
|
|
235
257
|
portableHistoryProvider;
|
|
236
258
|
runtimeResourceSessions = new Map();
|
|
237
259
|
runtimeAuthFingerprints = new Map();
|
|
238
|
-
|
|
260
|
+
activeSubagentRequests = new Map();
|
|
239
261
|
agentObservations = [];
|
|
240
262
|
agentObservationEvictedThroughByParent = new Map();
|
|
241
263
|
nextAgentObservationSequence = 1;
|
|
@@ -388,11 +410,24 @@ export class PiboSessionRouter {
|
|
|
388
410
|
teardownCompleted = true;
|
|
389
411
|
return output;
|
|
390
412
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
413
|
+
let output;
|
|
414
|
+
if (event.action === "abort") {
|
|
415
|
+
const [sessionAbort, childAbort] = await Promise.allSettled([
|
|
416
|
+
session.executeAction(event),
|
|
417
|
+
this.abortActiveSubagentSessions(event.piboSessionId),
|
|
418
|
+
]);
|
|
419
|
+
if (sessionAbort.status === "rejected" && childAbort.status === "rejected") {
|
|
420
|
+
throw new AggregateError([sessionAbort.reason, childAbort.reason], "Failed to abort the session and its active subagent requests.");
|
|
421
|
+
}
|
|
422
|
+
if (sessionAbort.status === "rejected")
|
|
423
|
+
throw sessionAbort.reason;
|
|
424
|
+
if (childAbort.status === "rejected")
|
|
425
|
+
throw childAbort.reason;
|
|
426
|
+
output = sessionAbort.value;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
output = await session.executeAction(event);
|
|
430
|
+
}
|
|
396
431
|
if (event.action === "kill" || event.action === "kill_all") {
|
|
397
432
|
await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action`, { cancelRuns: event.action === "kill_all" });
|
|
398
433
|
teardownCompleted = true;
|
|
@@ -837,20 +872,10 @@ export class PiboSessionRouter {
|
|
|
837
872
|
const eventWithId = { ...event, id: event.id ?? randomUUID() };
|
|
838
873
|
return await new Promise((resolve, reject) => {
|
|
839
874
|
let settled = false;
|
|
840
|
-
let
|
|
875
|
+
let dispatchPromise;
|
|
841
876
|
let lastAssistantMessage;
|
|
842
877
|
let timeout;
|
|
843
|
-
const
|
|
844
|
-
if (!messageDispatched)
|
|
845
|
-
return;
|
|
846
|
-
void this.emit({
|
|
847
|
-
type: "execution",
|
|
848
|
-
piboSessionId: eventWithId.piboSessionId,
|
|
849
|
-
action: "abort",
|
|
850
|
-
id: randomUUID(),
|
|
851
|
-
}).catch(() => { });
|
|
852
|
-
};
|
|
853
|
-
const finish = (result) => {
|
|
878
|
+
const claimSettlement = () => {
|
|
854
879
|
if (settled)
|
|
855
880
|
return false;
|
|
856
881
|
settled = true;
|
|
@@ -858,16 +883,33 @@ export class PiboSessionRouter {
|
|
|
858
883
|
clearTimeout(timeout);
|
|
859
884
|
signal?.removeEventListener("abort", onAbort);
|
|
860
885
|
unsubscribe();
|
|
886
|
+
return true;
|
|
887
|
+
};
|
|
888
|
+
const finish = (result) => {
|
|
889
|
+
if (!claimSettlement())
|
|
890
|
+
return;
|
|
861
891
|
if (result instanceof Error)
|
|
862
892
|
reject(result);
|
|
863
893
|
else
|
|
864
894
|
resolve(result);
|
|
865
|
-
return true;
|
|
866
895
|
};
|
|
867
|
-
const
|
|
868
|
-
if (!
|
|
896
|
+
const rejectAfterMessageCancellation = (error) => {
|
|
897
|
+
if (!claimSettlement())
|
|
869
898
|
return;
|
|
870
|
-
|
|
899
|
+
void (async () => {
|
|
900
|
+
try {
|
|
901
|
+
await dispatchPromise;
|
|
902
|
+
await this.cancelSessionMessage(eventWithId.piboSessionId, eventWithId.id);
|
|
903
|
+
}
|
|
904
|
+
catch (cancellationError) {
|
|
905
|
+
reject(new PiboRunCancellationError(`Failed to cancel subagent request "${eventWithId.id}" in Pibo session "${eventWithId.piboSessionId}".`, { cause: cancellationError }));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
reject(error);
|
|
909
|
+
})();
|
|
910
|
+
};
|
|
911
|
+
const onAbort = () => {
|
|
912
|
+
rejectAfterMessageCancellation(subagentAbortError());
|
|
871
913
|
};
|
|
872
914
|
const unsubscribe = this.subscribe((output) => {
|
|
873
915
|
if (output.piboSessionId !== eventWithId.piboSessionId ||
|
|
@@ -886,20 +928,25 @@ export class PiboSessionRouter {
|
|
|
886
928
|
}
|
|
887
929
|
});
|
|
888
930
|
if (signal?.aborted) {
|
|
889
|
-
|
|
931
|
+
finish(subagentAbortError());
|
|
890
932
|
return;
|
|
891
933
|
}
|
|
892
934
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
893
935
|
timeout = setTimeout(() => {
|
|
894
|
-
|
|
895
|
-
if (!finish(timeoutError))
|
|
896
|
-
return;
|
|
897
|
-
abortChild();
|
|
936
|
+
rejectAfterMessageCancellation(new PiboRunExecutionTimeoutError(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`, "lifetime"));
|
|
898
937
|
}, timeoutMs);
|
|
899
|
-
|
|
900
|
-
|
|
938
|
+
dispatchPromise = this.emit(eventWithId);
|
|
939
|
+
dispatchPromise.catch((error) => {
|
|
940
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
941
|
+
});
|
|
901
942
|
});
|
|
902
943
|
}
|
|
944
|
+
async cancelSessionMessage(piboSessionId, eventId) {
|
|
945
|
+
const session = this.sessions.get(piboSessionId);
|
|
946
|
+
if (!session || !await session.cancelMessage(eventId)) {
|
|
947
|
+
throw new Error(`Pibo session "${piboSessionId}" no longer owns message "${eventId}".`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
903
950
|
async disposeAll() {
|
|
904
951
|
if (this.disposePromise)
|
|
905
952
|
return this.disposePromise;
|
|
@@ -946,7 +993,7 @@ export class PiboSessionRouter {
|
|
|
946
993
|
await this.portableToolService.dispose();
|
|
947
994
|
await this.runtimeResourceService.dispose();
|
|
948
995
|
this.runtimeResourceSessions.clear();
|
|
949
|
-
this.
|
|
996
|
+
this.activeSubagentRequests.clear();
|
|
950
997
|
this.agentObservations.length = 0;
|
|
951
998
|
this.agentObservationEvictedThroughByParent.clear();
|
|
952
999
|
await this.telemetryWriter?.dispose();
|
|
@@ -1562,44 +1609,40 @@ export class PiboSessionRouter {
|
|
|
1562
1609
|
this.signalRegistry.project({ type: "session_created", session: created });
|
|
1563
1610
|
return created;
|
|
1564
1611
|
}
|
|
1565
|
-
trackActiveSubagent(parentPiboSessionId,
|
|
1566
|
-
let
|
|
1567
|
-
if (!
|
|
1568
|
-
|
|
1569
|
-
this.
|
|
1612
|
+
trackActiveSubagent(parentPiboSessionId, request) {
|
|
1613
|
+
let requests = this.activeSubagentRequests.get(parentPiboSessionId);
|
|
1614
|
+
if (!requests) {
|
|
1615
|
+
requests = new Set();
|
|
1616
|
+
this.activeSubagentRequests.set(parentPiboSessionId, requests);
|
|
1570
1617
|
}
|
|
1571
|
-
|
|
1618
|
+
requests.add(request);
|
|
1572
1619
|
let active = true;
|
|
1573
1620
|
return () => {
|
|
1574
1621
|
if (!active)
|
|
1575
1622
|
return;
|
|
1576
1623
|
active = false;
|
|
1577
|
-
const current = this.
|
|
1624
|
+
const current = this.activeSubagentRequests.get(parentPiboSessionId);
|
|
1578
1625
|
if (!current)
|
|
1579
1626
|
return;
|
|
1580
|
-
|
|
1581
|
-
if (remaining > 0)
|
|
1582
|
-
current.set(childPiboSessionId, remaining);
|
|
1583
|
-
else
|
|
1584
|
-
current.delete(childPiboSessionId);
|
|
1627
|
+
current.delete(request);
|
|
1585
1628
|
if (current.size === 0)
|
|
1586
|
-
this.
|
|
1629
|
+
this.activeSubagentRequests.delete(parentPiboSessionId);
|
|
1587
1630
|
};
|
|
1588
1631
|
}
|
|
1589
1632
|
async abortActiveSubagentSessions(parentPiboSessionId) {
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
await Promise.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
id: randomUUID(),
|
|
1598
|
-
})));
|
|
1633
|
+
const requests = [...(this.activeSubagentRequests.get(parentPiboSessionId) ?? [])];
|
|
1634
|
+
for (const request of requests)
|
|
1635
|
+
request.abortController.abort();
|
|
1636
|
+
const settlements = await Promise.all(requests.map(async (request) => await request.settled));
|
|
1637
|
+
const failures = settlements.flatMap((settlement) => settlement.status === "rejected" ? [settlement.reason] : []);
|
|
1638
|
+
if (failures.length > 0)
|
|
1639
|
+
throw new AggregateError(failures, "Failed to cancel active subagent requests.");
|
|
1599
1640
|
}
|
|
1600
1641
|
createAgentsController(parentPiboSessionId) {
|
|
1601
1642
|
return {
|
|
1602
1643
|
sendMessage: async ({ subagent, message, threadKey, toolCallId, signal }) => {
|
|
1644
|
+
if (signal?.aborted)
|
|
1645
|
+
throw subagentAbortError();
|
|
1603
1646
|
this.assertSubagentDepth(parentPiboSessionId, subagent);
|
|
1604
1647
|
const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
|
|
1605
1648
|
const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
|
|
@@ -1619,9 +1662,21 @@ export class PiboSessionRouter {
|
|
|
1619
1662
|
childPiboSessionId: child.id,
|
|
1620
1663
|
threadKey: resolvedThreadKey,
|
|
1621
1664
|
});
|
|
1622
|
-
const
|
|
1665
|
+
const parentAbortController = new AbortController();
|
|
1666
|
+
const requestSignal = signal
|
|
1667
|
+
? AbortSignal.any([signal, parentAbortController.signal])
|
|
1668
|
+
: parentAbortController.signal;
|
|
1669
|
+
let resolveSettled;
|
|
1670
|
+
const settled = new Promise((resolve) => {
|
|
1671
|
+
resolveSettled = resolve;
|
|
1672
|
+
});
|
|
1673
|
+
const untrack = this.trackActiveSubagent(parentPiboSessionId, {
|
|
1674
|
+
abortController: parentAbortController,
|
|
1675
|
+
settled,
|
|
1676
|
+
});
|
|
1677
|
+
let settlement = { status: "fulfilled" };
|
|
1623
1678
|
try {
|
|
1624
|
-
const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS,
|
|
1679
|
+
const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, requestSignal);
|
|
1625
1680
|
return {
|
|
1626
1681
|
agentId: child.id,
|
|
1627
1682
|
name: subagent.name,
|
|
@@ -1631,8 +1686,17 @@ export class PiboSessionRouter {
|
|
|
1631
1686
|
reply,
|
|
1632
1687
|
};
|
|
1633
1688
|
}
|
|
1689
|
+
catch (error) {
|
|
1690
|
+
const confirmedParentCancellation = parentAbortController.signal.aborted
|
|
1691
|
+
&& error instanceof Error
|
|
1692
|
+
&& error.name === "AbortError";
|
|
1693
|
+
if (!confirmedParentCancellation)
|
|
1694
|
+
settlement = { status: "rejected", reason: error };
|
|
1695
|
+
throw error;
|
|
1696
|
+
}
|
|
1634
1697
|
finally {
|
|
1635
1698
|
untrack();
|
|
1699
|
+
resolveSettled?.(settlement);
|
|
1636
1700
|
}
|
|
1637
1701
|
},
|
|
1638
1702
|
listAgents: () => this.listManagedAgents(parentPiboSessionId),
|
|
@@ -1767,8 +1831,9 @@ export class PiboSessionRouter {
|
|
|
1767
1831
|
const cancel = this.runCancellationHandlers.get(run.runId);
|
|
1768
1832
|
if (!cancel)
|
|
1769
1833
|
return;
|
|
1770
|
-
this.runCancellationHandlers.delete(run.runId);
|
|
1771
1834
|
await cancel();
|
|
1835
|
+
if (this.runCancellationHandlers.get(run.runId) === cancel)
|
|
1836
|
+
this.runCancellationHandlers.delete(run.runId);
|
|
1772
1837
|
}));
|
|
1773
1838
|
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1774
1839
|
if (failures.length > 0)
|
|
@@ -1796,14 +1861,32 @@ export class PiboSessionRouter {
|
|
|
1796
1861
|
timeoutMs,
|
|
1797
1862
|
serviceWarning,
|
|
1798
1863
|
resources,
|
|
1864
|
+
origin: yieldedRunOrigin(this.sessions.get(parentPiboSessionId)?.getActiveMessage?.()),
|
|
1799
1865
|
});
|
|
1800
1866
|
}
|
|
1801
1867
|
catch (error) {
|
|
1802
1868
|
admission.release();
|
|
1803
1869
|
throw error;
|
|
1804
1870
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1871
|
+
const cancellation = { state: "none" };
|
|
1872
|
+
if (cancel) {
|
|
1873
|
+
this.runCancellationHandlers.set(run.runId, async () => {
|
|
1874
|
+
cancellation.state = "pending";
|
|
1875
|
+
let resolveDecision;
|
|
1876
|
+
cancellation.decision = new Promise((resolve) => { resolveDecision = resolve; });
|
|
1877
|
+
try {
|
|
1878
|
+
await cancel();
|
|
1879
|
+
cancellation.state = "confirmed";
|
|
1880
|
+
}
|
|
1881
|
+
catch (error) {
|
|
1882
|
+
cancellation.state = "failed";
|
|
1883
|
+
throw error;
|
|
1884
|
+
}
|
|
1885
|
+
finally {
|
|
1886
|
+
resolveDecision?.();
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1807
1890
|
void (async () => {
|
|
1808
1891
|
try {
|
|
1809
1892
|
const result = await execute();
|
|
@@ -1814,6 +1897,12 @@ export class PiboSessionRouter {
|
|
|
1814
1897
|
this.handleTerminalRunReminder(parentPiboSessionId, completed.runId, reminderGeneration);
|
|
1815
1898
|
}
|
|
1816
1899
|
catch (error) {
|
|
1900
|
+
if (error instanceof PiboRunCancelledError) {
|
|
1901
|
+
if (cancellation.state === "pending")
|
|
1902
|
+
await cancellation.decision;
|
|
1903
|
+
if (cancellation.state === "confirmed")
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1817
1906
|
const message = error instanceof Error ? error.message : String(error);
|
|
1818
1907
|
if (resources)
|
|
1819
1908
|
this.runRegistry.updateResources(run.runId, resources);
|
|
@@ -1842,14 +1931,15 @@ export class PiboSessionRouter {
|
|
|
1842
1931
|
return run;
|
|
1843
1932
|
},
|
|
1844
1933
|
cancelRun: async (runId) => {
|
|
1845
|
-
const
|
|
1934
|
+
const current = this.runRegistry.status(parentPiboSessionId, runId);
|
|
1846
1935
|
try {
|
|
1847
|
-
|
|
1936
|
+
if (!isTerminalRunStatus(current.status))
|
|
1937
|
+
await this.invokeRunCancellationHandlers([current]);
|
|
1938
|
+
return this.runRegistry.cancel(parentPiboSessionId, runId);
|
|
1848
1939
|
}
|
|
1849
1940
|
finally {
|
|
1850
1941
|
this.refreshQueuedRunReminders(parentPiboSessionId);
|
|
1851
1942
|
}
|
|
1852
|
-
return cancelled;
|
|
1853
1943
|
},
|
|
1854
1944
|
ackRun: (runId) => {
|
|
1855
1945
|
const run = this.runRegistry.ack(parentPiboSessionId, runId);
|
|
@@ -2067,8 +2157,8 @@ export class PiboSessionRouter {
|
|
|
2067
2157
|
piboSessionId,
|
|
2068
2158
|
text: formatRunReminderMessage(notification),
|
|
2069
2159
|
source: "service",
|
|
2070
|
-
capabilityScope: "run-reminder",
|
|
2071
2160
|
id: randomUUID(),
|
|
2161
|
+
provenance: runReminderProvenance(notification),
|
|
2072
2162
|
});
|
|
2073
2163
|
}
|
|
2074
2164
|
catch (error) {
|
package/dist/loops/service.js
CHANGED
|
@@ -531,9 +531,13 @@ export class PiboLoopService {
|
|
|
531
531
|
this.store.updateRunMessageState(eventId, 'invalidated');
|
|
532
532
|
if (event.type !== 'assistant_usage')
|
|
533
533
|
return;
|
|
534
|
-
const
|
|
534
|
+
const provenance = event.provenance?.kind === 'loop-run' ? event.provenance : undefined;
|
|
535
|
+
const run = this.store.getRunByMessageEventId(eventId) ?? (provenance ? this.store.getRun(provenance.runId) : undefined);
|
|
535
536
|
if (!run || run.piboSessionId !== event.piboSessionId)
|
|
536
537
|
return;
|
|
538
|
+
if (provenance && (run.jobId !== provenance.jobId
|
|
539
|
+
|| (provenance.cause === 'run-reminder' && run.messageEventId !== provenance.rootEventId)))
|
|
540
|
+
return;
|
|
537
541
|
const job = this.store.getJob(run.jobId);
|
|
538
542
|
if (!job || job.mode !== 'goal')
|
|
539
543
|
return;
|
package/dist/loops/store.js
CHANGED
|
@@ -636,7 +636,11 @@ export class PiboLoopStore {
|
|
|
636
636
|
const completedIterations = (job.state.completedIterations ?? 0) + 1;
|
|
637
637
|
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
638
638
|
const currentGoalStatus = goalStatus(job);
|
|
639
|
-
const nextGoalStatus = job.mode === 'goal'
|
|
639
|
+
const nextGoalStatus = job.mode === 'goal'
|
|
640
|
+
? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
|
|
641
|
+
? currentGoalStatus
|
|
642
|
+
: input.goalStatus ?? currentGoalStatus
|
|
643
|
+
: undefined;
|
|
640
644
|
const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
|
|
641
645
|
const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
642
646
|
const state = {
|
|
@@ -833,15 +837,23 @@ export function createLoopMessagePreflight(options = {}) {
|
|
|
833
837
|
const job = store.getJob(jobId);
|
|
834
838
|
const run = store.getRun(runId);
|
|
835
839
|
const status = job?.mode === 'goal' ? goalStatus(job) ?? (job.enabled ? 'active' : 'paused') : undefined;
|
|
840
|
+
const causalReminder = event.provenance.cause === 'run-reminder';
|
|
841
|
+
const validMessageBinding = causalReminder
|
|
842
|
+
? event.source === 'service'
|
|
843
|
+
&& event.text.startsWith('<pibo_run_notification>')
|
|
844
|
+
&& typeof event.provenance.rootEventId === 'string'
|
|
845
|
+
&& run?.messageEventId === event.provenance.rootEventId
|
|
846
|
+
: run?.messageEventId === event.id;
|
|
847
|
+
const validRunState = causalReminder
|
|
848
|
+
? true
|
|
849
|
+
: run?.status === 'running' && Boolean(job?.state.runningAt) && job?.state.lastRunId === runId;
|
|
836
850
|
const allowed = Boolean(job
|
|
837
851
|
&& run
|
|
838
852
|
&& run.jobId === jobId
|
|
839
|
-
&&
|
|
840
|
-
&&
|
|
853
|
+
&& validMessageBinding
|
|
854
|
+
&& validRunState
|
|
841
855
|
&& (!run.piboSessionId || run.piboSessionId === event.piboSessionId)
|
|
842
856
|
&& job.enabled
|
|
843
|
-
&& job.state.runningAt
|
|
844
|
-
&& job.state.lastRunId === runId
|
|
845
857
|
&& (job.mode !== 'goal' || status === 'active'));
|
|
846
858
|
if (allowed)
|
|
847
859
|
return { allowed: true };
|
package/dist/loops/tools.js
CHANGED
|
@@ -34,7 +34,9 @@ function resolveGoalForTurn(store, context, piboSessionId) {
|
|
|
34
34
|
if (provenance?.kind !== 'loop-run')
|
|
35
35
|
return store.getSessionGoalOwner(piboSessionId) ?? store.getLatestGoalForSession(piboSessionId);
|
|
36
36
|
const run = store.getRun(provenance.runId);
|
|
37
|
-
|
|
37
|
+
const expectedEventId = provenance.cause === 'run-reminder' ? provenance.rootEventId : activeMessage?.id;
|
|
38
|
+
const validReminder = provenance.cause !== 'run-reminder' || (activeMessage?.source === 'service' && typeof provenance.rootEventId === 'string');
|
|
39
|
+
if (!run || !validReminder || run.jobId !== provenance.jobId || run.piboSessionId !== piboSessionId || run.messageEventId !== expectedEventId) {
|
|
38
40
|
throw new Error('cannot resolve goal because this turn has stale or invalid Loop provenance');
|
|
39
41
|
}
|
|
40
42
|
const job = store.getJob(provenance.jobId);
|
|
@@ -149,7 +149,8 @@ export class PiboReliabilityStore {
|
|
|
149
149
|
timeout_at TEXT,
|
|
150
150
|
timeout_phase TEXT,
|
|
151
151
|
service_warning TEXT,
|
|
152
|
-
resource_json TEXT
|
|
152
|
+
resource_json TEXT,
|
|
153
|
+
origin_json TEXT
|
|
153
154
|
);
|
|
154
155
|
CREATE INDEX IF NOT EXISTS idx_pibo_runs_controller_updated
|
|
155
156
|
ON pibo_runs(controller_pibo_session_id, updated_at);
|
|
@@ -161,6 +162,7 @@ export class PiboReliabilityStore {
|
|
|
161
162
|
ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
|
|
162
163
|
ensurePiboRunColumn(this.db, "service_warning", "TEXT");
|
|
163
164
|
ensurePiboRunColumn(this.db, "resource_json", "TEXT");
|
|
165
|
+
ensurePiboRunColumn(this.db, "origin_json", "TEXT");
|
|
164
166
|
this.appendEventStatement = this.db.prepare(`
|
|
165
167
|
INSERT INTO pibo_event_stream (topic, key, event_id, idempotency_key, created_at, retention_class, payload_json)
|
|
166
168
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -502,10 +504,10 @@ export class PiboReliabilityStore {
|
|
|
502
504
|
INSERT INTO pibo_runs (
|
|
503
505
|
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
504
506
|
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
505
|
-
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json
|
|
506
|
-
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
507
|
+
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
|
|
508
|
+
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
507
509
|
`)
|
|
508
|
-
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null);
|
|
510
|
+
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
|
|
509
511
|
this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
|
|
510
512
|
return this.requireRun(runId);
|
|
511
513
|
}
|
|
@@ -534,10 +536,11 @@ export class PiboReliabilityStore {
|
|
|
534
536
|
timeout_at = ?,
|
|
535
537
|
timeout_phase = ?,
|
|
536
538
|
service_warning = ?,
|
|
537
|
-
resource_json =
|
|
539
|
+
resource_json = ?,
|
|
540
|
+
origin_json = ?
|
|
538
541
|
WHERE run_id = ?
|
|
539
542
|
`)
|
|
540
|
-
.run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, runId);
|
|
543
|
+
.run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, next.origin ? JSON.stringify(next.origin) : null, runId);
|
|
541
544
|
return this.requireRun(runId);
|
|
542
545
|
}
|
|
543
546
|
getRun(runId) {
|
|
@@ -810,6 +813,8 @@ function runFromRow(row) {
|
|
|
810
813
|
output.serviceWarning = row.service_warning;
|
|
811
814
|
if (row.resource_json)
|
|
812
815
|
output.resources = JSON.parse(row.resource_json);
|
|
816
|
+
if (row.origin_json)
|
|
817
|
+
output.origin = JSON.parse(row.origin_json);
|
|
813
818
|
return output;
|
|
814
819
|
}
|
|
815
820
|
function retryDelayMs(attempts, input) {
|
package/dist/runs/lifecycle.js
CHANGED
|
@@ -6,6 +6,18 @@ export class PiboRunExecutionTimeoutError extends Error {
|
|
|
6
6
|
this.name = "PiboRunExecutionTimeoutError";
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
export class PiboRunCancellationError extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "PiboRunCancellationError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class PiboRunCancelledError extends Error {
|
|
16
|
+
constructor(message = "Yielded run was cancelled.", options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "PiboRunCancelledError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
9
21
|
export function resolveRunTimeoutMs(toolName, params) {
|
|
10
22
|
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
11
23
|
return undefined;
|
package/dist/runs/registry.js
CHANGED
|
@@ -7,6 +7,16 @@ function now() {
|
|
|
7
7
|
function runTimeoutAt(createdAt, timeoutMs) {
|
|
8
8
|
return timeoutMs === undefined ? undefined : new Date(Date.parse(createdAt) + timeoutMs).toISOString();
|
|
9
9
|
}
|
|
10
|
+
function sameOrigin(left, right) {
|
|
11
|
+
if (!left || !right)
|
|
12
|
+
return left === right;
|
|
13
|
+
return left.eventId === right.eventId
|
|
14
|
+
&& left.provenance.kind === right.provenance.kind
|
|
15
|
+
&& left.provenance.jobId === right.provenance.jobId
|
|
16
|
+
&& left.provenance.runId === right.provenance.runId
|
|
17
|
+
&& left.provenance.cause === right.provenance.cause
|
|
18
|
+
&& left.provenance.rootEventId === right.provenance.rootEventId;
|
|
19
|
+
}
|
|
10
20
|
function formatTimeout(timeoutMs) {
|
|
11
21
|
if (timeoutMs === undefined)
|
|
12
22
|
return "its configured timeout";
|
|
@@ -83,6 +93,7 @@ export class PiboRunRegistry {
|
|
|
83
93
|
serviceWarning: input.serviceWarning,
|
|
84
94
|
resources: input.resources,
|
|
85
95
|
workerId: this.workerId,
|
|
96
|
+
origin: input.origin,
|
|
86
97
|
});
|
|
87
98
|
const record = recordFromStored(stored);
|
|
88
99
|
this.runs.set(record.runId, record);
|
|
@@ -108,6 +119,7 @@ export class PiboRunRegistry {
|
|
|
108
119
|
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: runTimeoutAt(timestamp, input.timeoutMs) } : {}),
|
|
109
120
|
...(input.serviceWarning ? { serviceWarning: input.serviceWarning } : {}),
|
|
110
121
|
...(input.resources ? { resources: structuredClone(input.resources) } : {}),
|
|
122
|
+
...(input.origin ? { origin: structuredClone(input.origin) } : {}),
|
|
111
123
|
};
|
|
112
124
|
this.runs.set(runId, record);
|
|
113
125
|
const output = snapshot(record);
|
|
@@ -245,7 +257,7 @@ export class PiboRunRegistry {
|
|
|
245
257
|
}
|
|
246
258
|
read(controllerPiboSessionId, runId) {
|
|
247
259
|
const record = this.requireRunForController(controllerPiboSessionId, runId);
|
|
248
|
-
if (terminal(record.status)) {
|
|
260
|
+
if (terminal(record.status) && !record.consumed) {
|
|
249
261
|
record.consumed = true;
|
|
250
262
|
record.updatedAt = now();
|
|
251
263
|
this.options.store?.updateRun(runId, record);
|
|
@@ -277,6 +289,9 @@ export class PiboRunRegistry {
|
|
|
277
289
|
}
|
|
278
290
|
ack(controllerPiboSessionId, runId) {
|
|
279
291
|
const record = this.requireRunForController(controllerPiboSessionId, runId);
|
|
292
|
+
const consumesTerminalRun = terminal(record.status) && !record.consumed;
|
|
293
|
+
if (record.acknowledgedStatus === record.status && !consumesTerminalRun)
|
|
294
|
+
return { ...snapshot(record), changed: false };
|
|
280
295
|
record.acknowledgedStatus = record.status;
|
|
281
296
|
if (terminal(record.status))
|
|
282
297
|
record.consumed = true;
|
|
@@ -284,7 +299,7 @@ export class PiboRunRegistry {
|
|
|
284
299
|
this.options.store?.updateRun(runId, record);
|
|
285
300
|
const output = snapshot(record);
|
|
286
301
|
this.notify({ type: "run_acknowledged", run: output });
|
|
287
|
-
return output;
|
|
302
|
+
return { ...output, changed: true };
|
|
288
303
|
}
|
|
289
304
|
suppressNotification(controllerPiboSessionId, runId) {
|
|
290
305
|
const record = this.requireRunForController(controllerPiboSessionId, runId);
|
|
@@ -306,14 +321,17 @@ export class PiboRunRegistry {
|
|
|
306
321
|
return suppressed;
|
|
307
322
|
}
|
|
308
323
|
createNotification(controllerPiboSessionId, options = {}) {
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
324
|
+
const pendingRecords = [...this.runs.values()].filter((record) => this.needsNotification(record, controllerPiboSessionId, options));
|
|
325
|
+
if (pendingRecords.length === 0)
|
|
311
326
|
return undefined;
|
|
327
|
+
const origin = pendingRecords[0].origin;
|
|
328
|
+
const records = pendingRecords.filter((record) => sameOrigin(record.origin, origin));
|
|
312
329
|
for (const record of records) {
|
|
313
330
|
record.notifiedStatus = record.status;
|
|
314
331
|
this.options.store?.updateRun(record.runId, record);
|
|
315
332
|
}
|
|
316
333
|
const notification = {
|
|
334
|
+
...(origin ? { origin: structuredClone(origin) } : {}),
|
|
317
335
|
completed: [],
|
|
318
336
|
failed: [],
|
|
319
337
|
timedOut: [],
|
|
@@ -461,5 +479,6 @@ function recordFromStored(record) {
|
|
|
461
479
|
timeoutPhase: record.timeoutPhase,
|
|
462
480
|
serviceWarning: record.serviceWarning,
|
|
463
481
|
resources: record.resources,
|
|
482
|
+
origin: record.origin,
|
|
464
483
|
};
|
|
465
484
|
}
|
package/dist/runs/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { piboStringEnum } from "../tools/schema.js";
|
|
3
3
|
import { definePiboTool } from "../tools/contract.js";
|
|
4
|
-
import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
|
|
4
|
+
import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
|
|
5
5
|
import { PiboRunResourceLimitError, prepareYieldedRunExecution } from "./resource-isolation.js";
|
|
6
6
|
function resultText(prefix, value) {
|
|
7
7
|
return `${prefix}\n${JSON.stringify(value, null, 2)}`;
|
|
@@ -73,6 +73,7 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
73
73
|
resolveExecutionSettled = resolve;
|
|
74
74
|
});
|
|
75
75
|
let observedOutput = false;
|
|
76
|
+
let cancellationFailure;
|
|
76
77
|
const run = controller.startToolRun({
|
|
77
78
|
toolName: tool.name,
|
|
78
79
|
params: params.arguments,
|
|
@@ -95,6 +96,8 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
95
96
|
throw processCancellationError;
|
|
96
97
|
if (executionStarted)
|
|
97
98
|
await waitForRunCancellationSettlement(executionSettled);
|
|
99
|
+
if (cancellationFailure)
|
|
100
|
+
throw cancellationFailure;
|
|
98
101
|
},
|
|
99
102
|
async execute() {
|
|
100
103
|
executionStarted = true;
|
|
@@ -113,8 +116,13 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
113
116
|
return { text, details: resultObject.details ?? result };
|
|
114
117
|
}
|
|
115
118
|
catch (error) {
|
|
116
|
-
if (error instanceof
|
|
119
|
+
if (error instanceof PiboRunCancellationError)
|
|
120
|
+
cancellationFailure = error;
|
|
121
|
+
if (error instanceof PiboRunExecutionTimeoutError || error instanceof PiboRunResourceLimitError || error instanceof PiboRunCancellationError)
|
|
117
122
|
throw error;
|
|
123
|
+
if (runAbortController.signal.aborted) {
|
|
124
|
+
throw new PiboRunCancelledError("Yielded run was cancelled; execution ended after cancellation.", { cause: error });
|
|
125
|
+
}
|
|
118
126
|
if (timeoutMs !== undefined && isConfiguredTimeoutError(error))
|
|
119
127
|
throw new PiboRunExecutionTimeoutError(error instanceof Error ? error.message : String(error), observedOutput ? "lifetime" : "startup");
|
|
120
128
|
throw error;
|
|
@@ -240,8 +248,9 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
240
248
|
}),
|
|
241
249
|
async execute(_toolCallId, params) {
|
|
242
250
|
const run = controller.ackRun(params.runId);
|
|
251
|
+
const prefix = run.changed ? `Acknowledged run ${run.runId}.` : `Run ${run.runId} was already acknowledged in state ${run.status}; no state changed.`;
|
|
243
252
|
return {
|
|
244
|
-
content: [{ type: "text", text: resultText(
|
|
253
|
+
content: [{ type: "text", text: resultText(prefix, run) }],
|
|
245
254
|
details: run,
|
|
246
255
|
};
|
|
247
256
|
},
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pasko70/pibo",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@pasko70/pibo",
|
|
9
|
-
"version": "2.4.
|
|
9
|
+
"version": "2.4.2",
|
|
10
10
|
"workspaces": [
|
|
11
11
|
"packages/workflows"
|
|
12
12
|
],
|