larkin 0.4.9 → 0.4.10
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.
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function escapeXml(value) {
|
|
2
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3
|
+
}
|
|
4
|
+
function extractTag(content, tagName) {
|
|
5
|
+
const match = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`).exec(content);
|
|
6
|
+
return match ? match[1].trim() : null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Pi subagents emit terminal <status> as Done or a display label
|
|
10
|
+
* (Error: ..., Aborted (...), Stopped, Wrapped up (turn limit)).
|
|
11
|
+
* Raw tokens completed/error/aborted/stopped/steered/turn-limit are also accepted.
|
|
12
|
+
*/
|
|
13
|
+
const TERMINAL_STATUS_TOKENS = new Set([
|
|
14
|
+
"done",
|
|
15
|
+
"completed",
|
|
16
|
+
"error",
|
|
17
|
+
"aborted",
|
|
18
|
+
"stopped",
|
|
19
|
+
"steered",
|
|
20
|
+
"turn-limit",
|
|
21
|
+
"turn_limit",
|
|
22
|
+
]);
|
|
23
|
+
export function isCanonicalTerminalPiSubagentStatus(status) {
|
|
24
|
+
const normalized = status.trim().toLowerCase();
|
|
25
|
+
if (!normalized)
|
|
26
|
+
return false;
|
|
27
|
+
if (TERMINAL_STATUS_TOKENS.has(normalized))
|
|
28
|
+
return true;
|
|
29
|
+
if (normalized.startsWith("error:") || normalized.startsWith("error "))
|
|
30
|
+
return true;
|
|
31
|
+
if (normalized.startsWith("aborted"))
|
|
32
|
+
return true;
|
|
33
|
+
if (normalized.startsWith("stopped"))
|
|
34
|
+
return true;
|
|
35
|
+
if (normalized.includes("turn limit") || normalized.includes("turn-limit") || normalized.includes("turn_limit")) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (normalized.includes("steered"))
|
|
39
|
+
return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function parseTaskNotificationBlock(block) {
|
|
43
|
+
const taskId = extractTag(block, "task-id");
|
|
44
|
+
const status = extractTag(block, "status");
|
|
45
|
+
const summary = extractTag(block, "summary");
|
|
46
|
+
const result = extractTag(block, "result");
|
|
47
|
+
if (!taskId || !status || !summary || !result)
|
|
48
|
+
return null;
|
|
49
|
+
if (!isCanonicalTerminalPiSubagentStatus(status))
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
taskId,
|
|
53
|
+
toolUseId: extractTag(block, "tool-use-id") ?? undefined,
|
|
54
|
+
outputFile: extractTag(block, "output-file") ?? undefined,
|
|
55
|
+
status,
|
|
56
|
+
summary,
|
|
57
|
+
result,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function parseCanonicalNotificationContent(content) {
|
|
61
|
+
const blocks = [...content.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)]
|
|
62
|
+
.map((match) => match[1]);
|
|
63
|
+
if (blocks.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const notifications = [];
|
|
67
|
+
for (const block of blocks) {
|
|
68
|
+
const parsed = parseTaskNotificationBlock(block);
|
|
69
|
+
if (!parsed || seen.has(parsed.taskId))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(parsed.taskId);
|
|
72
|
+
notifications.push(parsed);
|
|
73
|
+
}
|
|
74
|
+
if (notifications.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const taskIds = notifications.map((notification) => notification.taskId);
|
|
77
|
+
return { taskIds, notifications, content, key: taskIds.join("|") };
|
|
78
|
+
}
|
|
79
|
+
function collectCandidateContents(node, found = new Set()) {
|
|
80
|
+
const candidates = [];
|
|
81
|
+
const visit = (value) => {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
return;
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
for (const item of value)
|
|
86
|
+
visit(item);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value !== "object")
|
|
90
|
+
return;
|
|
91
|
+
const record = value;
|
|
92
|
+
if (record.customType === "subagent-notification" && typeof record.content === "string") {
|
|
93
|
+
if (!found.has(record.content)) {
|
|
94
|
+
found.add(record.content);
|
|
95
|
+
candidates.push(record.content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (Array.isArray(record.content)) {
|
|
99
|
+
visit(record.content);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(record.messages))
|
|
102
|
+
visit(record.messages);
|
|
103
|
+
if (Array.isArray(record.parts))
|
|
104
|
+
visit(record.parts);
|
|
105
|
+
};
|
|
106
|
+
visit(node);
|
|
107
|
+
return candidates;
|
|
108
|
+
}
|
|
109
|
+
export function buildCanonicalPiSubagentNotificationContent(options) {
|
|
110
|
+
const status = options.status ?? "Done";
|
|
111
|
+
const summary = options.summary ?? `Agent ${JSON.stringify("Fixture")} completed`;
|
|
112
|
+
const result = options.result ?? "Fixture result.";
|
|
113
|
+
const totalTokens = options.totalTokens ?? 12;
|
|
114
|
+
const toolUses = options.toolUses ?? 1;
|
|
115
|
+
const durationMs = options.durationMs ?? 34;
|
|
116
|
+
return [
|
|
117
|
+
"<task-notification>",
|
|
118
|
+
`<task-id>${escapeXml(options.taskId)}</task-id>`,
|
|
119
|
+
options.toolUseId ? `<tool-use-id>${escapeXml(options.toolUseId)}</tool-use-id>` : null,
|
|
120
|
+
options.outputFile ? `<output-file>${escapeXml(options.outputFile)}</output-file>` : null,
|
|
121
|
+
`<status>${escapeXml(status)}</status>`,
|
|
122
|
+
`<summary>${escapeXml(summary)}</summary>`,
|
|
123
|
+
`<result>${escapeXml(result)}</result>`,
|
|
124
|
+
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${toolUses}</tool_uses><duration_ms>${durationMs}</duration_ms></usage>`,
|
|
125
|
+
"</task-notification>",
|
|
126
|
+
].filter(Boolean).join("\n");
|
|
127
|
+
}
|
|
128
|
+
export function buildCanonicalPiSubagentAssistantMessage(options) {
|
|
129
|
+
return {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: [{
|
|
132
|
+
type: "custom",
|
|
133
|
+
customType: "subagent-notification",
|
|
134
|
+
content: buildCanonicalPiSubagentNotificationContent(options),
|
|
135
|
+
}],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function extractCanonicalPiSubagentNotification(messages) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const notifications = [];
|
|
141
|
+
const contents = [];
|
|
142
|
+
for (const content of collectCandidateContents(messages)) {
|
|
143
|
+
const parsed = parseCanonicalNotificationContent(content);
|
|
144
|
+
if (!parsed)
|
|
145
|
+
continue;
|
|
146
|
+
contents.push(parsed.content);
|
|
147
|
+
for (const notification of parsed.notifications) {
|
|
148
|
+
if (seen.has(notification.taskId))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(notification.taskId);
|
|
151
|
+
notifications.push(notification);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (notifications.length === 0)
|
|
155
|
+
return null;
|
|
156
|
+
const taskIds = notifications.map((notification) => notification.taskId);
|
|
157
|
+
return { taskIds, notifications, content: contents.join("\n"), key: taskIds.join("|") };
|
|
158
|
+
}
|
|
159
|
+
export function extractCanonicalPiSubagentCompletionKeyFromMessages(messages) {
|
|
160
|
+
return extractCanonicalPiSubagentNotification(messages)?.key ?? null;
|
|
161
|
+
}
|
|
@@ -10,6 +10,7 @@ import { BUNDLED_PI_VERSION, piAgentDirectory } from "./pi-provider-config.mjs";
|
|
|
10
10
|
import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
|
|
11
11
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
12
12
|
import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
|
|
13
|
+
import { extractCanonicalPiSubagentCompletionKeyFromMessages } from "./pi-subagents-notification.mjs";
|
|
13
14
|
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
14
15
|
import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
15
16
|
import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
|
|
@@ -451,6 +452,8 @@ class PiSession extends EventSession {
|
|
|
451
452
|
observedSubmitEpochs = new Set();
|
|
452
453
|
observedAcceptedEpochs = new Set();
|
|
453
454
|
observedCompletedEpochs = new Set();
|
|
455
|
+
observedBackgroundCompletionKeys = new Set();
|
|
456
|
+
pendingUnownedCompletionKeys = new Set();
|
|
454
457
|
observedAgentEndEpochs = new Set();
|
|
455
458
|
firstOutputObserved = false;
|
|
456
459
|
toolCallOpen = false;
|
|
@@ -551,6 +554,8 @@ class PiSession extends EventSession {
|
|
|
551
554
|
this.observedSubmitEpochs.clear();
|
|
552
555
|
this.observedAcceptedEpochs.clear();
|
|
553
556
|
this.observedCompletedEpochs.clear();
|
|
557
|
+
this.observedBackgroundCompletionKeys.clear();
|
|
558
|
+
this.pendingUnownedCompletionKeys.clear();
|
|
554
559
|
this.observedAgentEndEpochs.clear();
|
|
555
560
|
this.activeEpoch = null;
|
|
556
561
|
this.settleArmedEpoch = null;
|
|
@@ -600,11 +605,25 @@ class PiSession extends EventSession {
|
|
|
600
605
|
this.observedCompletedEpochs.add(this.activeEpoch);
|
|
601
606
|
this.emitObservation("completed");
|
|
602
607
|
}
|
|
608
|
+
const completionNotificationKey = extractCanonicalPiSubagentCompletionKeyFromMessages(event.messages);
|
|
609
|
+
if (completionNotificationKey
|
|
610
|
+
&& this.activeEpoch === null
|
|
611
|
+
&& !this.observedBackgroundCompletionKeys.has(completionNotificationKey)) {
|
|
612
|
+
// agent_end can still have an active Pi session. Prompting before
|
|
613
|
+
// unowned agent_settled is rejected as "Agent is already processing".
|
|
614
|
+
this.pendingUnownedCompletionKeys.add(completionNotificationKey);
|
|
615
|
+
}
|
|
603
616
|
}
|
|
604
617
|
else if (event?.type === "agent_settled") {
|
|
605
618
|
const epoch = this.activeEpoch;
|
|
606
|
-
if (epoch === null
|
|
619
|
+
if (epoch === null) {
|
|
620
|
+
this.flushPendingUnownedCompletions();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (this.settleArmedEpoch !== epoch) {
|
|
624
|
+
this.flushPendingUnownedCompletions();
|
|
607
625
|
return;
|
|
626
|
+
}
|
|
608
627
|
this.activeEpoch = null;
|
|
609
628
|
this.settleArmedEpoch = null;
|
|
610
629
|
const error = this.finalAssistantError;
|
|
@@ -645,6 +664,7 @@ class PiSession extends EventSession {
|
|
|
645
664
|
this.observedAcceptedEpochs.delete(epoch);
|
|
646
665
|
this.observedCompletedEpochs.delete(epoch);
|
|
647
666
|
this.observedAgentEndEpochs.delete(epoch);
|
|
667
|
+
this.flushPendingUnownedCompletions();
|
|
648
668
|
}
|
|
649
669
|
else if (event?.type === "tool_execution_start") {
|
|
650
670
|
if (!this.firstOutputObserved) {
|
|
@@ -672,6 +692,15 @@ class PiSession extends EventSession {
|
|
|
672
692
|
this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
|
|
673
693
|
}
|
|
674
694
|
}
|
|
695
|
+
flushPendingUnownedCompletions() {
|
|
696
|
+
for (const completionKey of this.pendingUnownedCompletionKeys) {
|
|
697
|
+
if (this.observedBackgroundCompletionKeys.has(completionKey))
|
|
698
|
+
continue;
|
|
699
|
+
this.observedBackgroundCompletionKeys.add(completionKey);
|
|
700
|
+
this.emitObservation("completed", { completionKey });
|
|
701
|
+
}
|
|
702
|
+
this.pendingUnownedCompletionKeys.clear();
|
|
703
|
+
}
|
|
675
704
|
emitObservation(phase, fields = {}) {
|
|
676
705
|
const inputId = this.oldestOwnedInput();
|
|
677
706
|
const observation = { type: "runtime-observation", runtime: "pi",
|
|
@@ -681,6 +710,8 @@ class PiSession extends EventSession {
|
|
|
681
710
|
Object.defineProperty(observation, "sessionId", { value: this.sessionId, enumerable: false });
|
|
682
711
|
if (inputId)
|
|
683
712
|
Object.defineProperty(observation, "inputId", { value: inputId, enumerable: false });
|
|
713
|
+
if (fields.completionKey)
|
|
714
|
+
Object.defineProperty(observation, "completionKey", { value: fields.completionKey, enumerable: false });
|
|
684
715
|
this.emit(observation);
|
|
685
716
|
}
|
|
686
717
|
oldestOwnedInput() {
|
|
@@ -39,6 +39,7 @@ class RuntimeSessionRecoveryError extends Error {
|
|
|
39
39
|
}
|
|
40
40
|
exports.RuntimeSessionRecoveryError = RuntimeSessionRecoveryError;
|
|
41
41
|
const MAX_DELIVERIES = 2048;
|
|
42
|
+
const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
|
|
42
43
|
const now = () => new Date().toISOString();
|
|
43
44
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
44
45
|
const REPLAY_FAILURE_CODES = new Set([
|
|
@@ -521,6 +522,12 @@ function createRuntimeHost(options) {
|
|
|
521
522
|
queueMicrotask(() => { void retryPending(agent); });
|
|
522
523
|
}
|
|
523
524
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
525
|
+
// A completion queued during this submit has no turn-end if prompt
|
|
526
|
+
// rejected/threw before turn-start. Drain now that submitting is clear.
|
|
527
|
+
if (!agent.busy && !agent.turnInProgress && agent.backgroundCompletionQueue.length > 0
|
|
528
|
+
&& !agent.backgroundCompletionRetryTimer) {
|
|
529
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
530
|
+
}
|
|
524
531
|
}
|
|
525
532
|
};
|
|
526
533
|
const retryPending = async (agent) => {
|
|
@@ -599,6 +606,210 @@ function createRuntimeHost(options) {
|
|
|
599
606
|
agent.retryAfterSubmit = true;
|
|
600
607
|
await retryPending(agent);
|
|
601
608
|
};
|
|
609
|
+
const clearBackgroundCompletionRetryTimer = (agent) => {
|
|
610
|
+
if (!agent.backgroundCompletionRetryTimer)
|
|
611
|
+
return;
|
|
612
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
613
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
614
|
+
};
|
|
615
|
+
const scheduleBackgroundCompletionDrain = (agent, delayMs = 0) => {
|
|
616
|
+
if (agent.stopped)
|
|
617
|
+
return;
|
|
618
|
+
if (delayMs > 0) {
|
|
619
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
620
|
+
return;
|
|
621
|
+
agent.backgroundCompletionRetryTimer = setTimeout(() => {
|
|
622
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
623
|
+
if (agent.stopped)
|
|
624
|
+
return;
|
|
625
|
+
void drainBackgroundCompletionQueue(agent);
|
|
626
|
+
}, delayMs);
|
|
627
|
+
agent.backgroundCompletionRetryTimer.unref?.();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
631
|
+
queueMicrotask(() => { void drainBackgroundCompletionQueue(agent); });
|
|
632
|
+
};
|
|
633
|
+
const dropBackgroundCompletion = (agent, completionKey) => {
|
|
634
|
+
if (!completionKey)
|
|
635
|
+
return;
|
|
636
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
637
|
+
agent.backgroundCompletionQueue.shift();
|
|
638
|
+
else {
|
|
639
|
+
const index = agent.backgroundCompletionQueue.indexOf(completionKey);
|
|
640
|
+
if (index >= 0)
|
|
641
|
+
agent.backgroundCompletionQueue.splice(index, 1);
|
|
642
|
+
}
|
|
643
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
644
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
645
|
+
agent.backgroundCompletionInFlight = null;
|
|
646
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
647
|
+
}
|
|
648
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
649
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
650
|
+
};
|
|
651
|
+
const scheduleBackgroundCompletionRetry = (agent) => {
|
|
652
|
+
const streak = agent.backgroundCompletionRejectStreak;
|
|
653
|
+
if (streak < BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT) {
|
|
654
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const delayedAttempt = streak - BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT;
|
|
658
|
+
if (delayedAttempt >= retryPolicy.maxAttempts) {
|
|
659
|
+
const exhausted = agent.backgroundCompletionQueue[0];
|
|
660
|
+
if (exhausted) {
|
|
661
|
+
log("background completion wake retries exhausted", exhausted);
|
|
662
|
+
dropBackgroundCompletion(agent, exhausted);
|
|
663
|
+
}
|
|
664
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
665
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const delay = Math.min(retryPolicy.maxDelayMs, retryPolicy.baseDelayMs * 2 ** delayedAttempt);
|
|
669
|
+
scheduleBackgroundCompletionDrain(agent, delay);
|
|
670
|
+
};
|
|
671
|
+
const isPersistentBackgroundWakeFailure = (event) => {
|
|
672
|
+
if (!event)
|
|
673
|
+
return false;
|
|
674
|
+
const category = event.errorCategory;
|
|
675
|
+
if (category === "auth" || category === "billing" || category === "quota")
|
|
676
|
+
return true;
|
|
677
|
+
return category === "provider" && event.retryable === false;
|
|
678
|
+
};
|
|
679
|
+
const failBackgroundCompletionWake = (agent, event) => {
|
|
680
|
+
const completionKey = agent.backgroundCompletionInFlight ?? agent.backgroundCompletionQueue[0];
|
|
681
|
+
if (!completionKey)
|
|
682
|
+
return;
|
|
683
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
684
|
+
if (isPersistentBackgroundWakeFailure(event)) {
|
|
685
|
+
log("background completion wake dropped", event?.errorCategory ?? "persistent");
|
|
686
|
+
dropBackgroundCompletion(agent, completionKey);
|
|
687
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
688
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
692
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
693
|
+
};
|
|
694
|
+
const rearmBackgroundCompletion = (agent, completionKey = agent.backgroundCompletionInFlight) => {
|
|
695
|
+
if (!completionKey)
|
|
696
|
+
return;
|
|
697
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
698
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
699
|
+
agent.backgroundCompletionInFlight = null;
|
|
700
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
701
|
+
}
|
|
702
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
703
|
+
agent.backgroundCompletionQueue.unshift(completionKey);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
const commitBackgroundCompletion = (agent) => {
|
|
707
|
+
const completionKey = agent.backgroundCompletionInFlight;
|
|
708
|
+
if (!completionKey)
|
|
709
|
+
return;
|
|
710
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
711
|
+
agent.backgroundCompletionQueue.shift();
|
|
712
|
+
agent.backgroundCompletionInFlight = null;
|
|
713
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
714
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
715
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
716
|
+
};
|
|
717
|
+
const wakeOnBackgroundCompletion = async (agent) => {
|
|
718
|
+
// Caller already reserved submitting/busy so a concurrent idle drain cannot
|
|
719
|
+
// dequeue another head while ensureSession/prompt yields.
|
|
720
|
+
try {
|
|
721
|
+
const session = await ensureSession(agent);
|
|
722
|
+
if (agent.session !== session || agent.stopped)
|
|
723
|
+
return { status: "dropped" };
|
|
724
|
+
const input = options.promptBuilder.buildRuntimeInput("wake", node_crypto_1.default.randomUUID(), { wakeReason: "background subagent completed" });
|
|
725
|
+
const result = await session.prompt(input);
|
|
726
|
+
if (agent.session !== session || agent.stopped)
|
|
727
|
+
return { status: "dropped" };
|
|
728
|
+
if (result.status === "accepted") {
|
|
729
|
+
markSessionStable(agent, session);
|
|
730
|
+
return { status: "accepted", inputId: input.inputId };
|
|
731
|
+
}
|
|
732
|
+
agent.busy = false;
|
|
733
|
+
return { status: "retry" };
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
agent.busy = false;
|
|
737
|
+
log("background completion wake failed", error instanceof Error ? error.message : String(error));
|
|
738
|
+
return { status: "retry" };
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const drainBackgroundCompletionQueue = async (agent) => {
|
|
742
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
743
|
+
return;
|
|
744
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
745
|
+
return;
|
|
746
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
747
|
+
const proactive = agent.piProactiveCompaction
|
|
748
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
749
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
750
|
+
? agent.piProactiveCompaction : null;
|
|
751
|
+
if (proactive && await proactive === "failed")
|
|
752
|
+
return;
|
|
753
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
754
|
+
return;
|
|
755
|
+
if (agent.adapter.id === "pi" && !agent.busy && !agent.turnInProgress && agent.session) {
|
|
756
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
757
|
+
if (idleGate && await idleGate === "failed")
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
761
|
+
return;
|
|
762
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
763
|
+
return;
|
|
764
|
+
agent.submitting = true;
|
|
765
|
+
agent.busy = true;
|
|
766
|
+
const completionKey = agent.backgroundCompletionQueue[0];
|
|
767
|
+
try {
|
|
768
|
+
const outcome = await wakeOnBackgroundCompletion(agent);
|
|
769
|
+
if (outcome.status === "accepted") {
|
|
770
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
771
|
+
agent.backgroundCompletionInFlight = completionKey;
|
|
772
|
+
agent.backgroundCompletionWakeInputId = outcome.inputId;
|
|
773
|
+
// Keep the reject streak until the wake turn actually succeeds. An accepted
|
|
774
|
+
// prompt that later terminal-fails must not reset the bounded retry budget.
|
|
775
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
776
|
+
}
|
|
777
|
+
else if (outcome.status === "dropped") {
|
|
778
|
+
if (agent.stopped) {
|
|
779
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
780
|
+
agent.backgroundCompletionQueue.shift();
|
|
781
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
782
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
786
|
+
agent.busy = false;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
agent.busy = false;
|
|
791
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
792
|
+
// Leave the item queued+deduped. Immediate microtask retries cover a
|
|
793
|
+
// brief reject; after that a bounded timer still drains because Pi will
|
|
794
|
+
// not re-emit the same completion.
|
|
795
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
agent.submitting = false;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const noteBackgroundCompletion = (agent, completionKey) => {
|
|
803
|
+
if (agent.backgroundCompletionKeys.has(completionKey))
|
|
804
|
+
return;
|
|
805
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
806
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
807
|
+
agent.backgroundCompletionQueue.push(completionKey);
|
|
808
|
+
}
|
|
809
|
+
if (!agent.busy && !agent.turnInProgress && !agent.submitting && !agent.backgroundCompletionInFlight) {
|
|
810
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
602
813
|
const scheduleRecreate = (agent, reason) => {
|
|
603
814
|
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
604
815
|
return;
|
|
@@ -621,6 +832,8 @@ function createRuntimeHost(options) {
|
|
|
621
832
|
if (proactive && await proactive === "failed")
|
|
622
833
|
return;
|
|
623
834
|
await retryPending(agent);
|
|
835
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
836
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
624
837
|
}).catch((error) => {
|
|
625
838
|
const readiness = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? error.readiness
|
|
626
839
|
: (0, runtime_readiness_js_1.classifyRuntimePrerequisite)(agent.adapter.id, error);
|
|
@@ -643,6 +856,7 @@ function createRuntimeHost(options) {
|
|
|
643
856
|
agent.busy = false;
|
|
644
857
|
agent.submitting = false;
|
|
645
858
|
agent.generation += 1;
|
|
859
|
+
rearmBackgroundCompletion(agent);
|
|
646
860
|
agent.recreateReason = reason;
|
|
647
861
|
if (agent.stabilityTimer)
|
|
648
862
|
clearTimeout(agent.stabilityTimer);
|
|
@@ -663,6 +877,7 @@ function createRuntimeHost(options) {
|
|
|
663
877
|
agent.busy = false;
|
|
664
878
|
agent.submitting = false;
|
|
665
879
|
agent.generation += 1;
|
|
880
|
+
rearmBackgroundCompletion(agent);
|
|
666
881
|
if (agent.stabilityTimer)
|
|
667
882
|
clearTimeout(agent.stabilityTimer);
|
|
668
883
|
agent.stabilityTimer = null;
|
|
@@ -919,6 +1134,15 @@ function createRuntimeHost(options) {
|
|
|
919
1134
|
agent.busy = false;
|
|
920
1135
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
921
1136
|
reconcileAcceptedAtTurnEnd(agent);
|
|
1137
|
+
if (agent.backgroundCompletionInFlight) {
|
|
1138
|
+
if (agent.turnHadFailure)
|
|
1139
|
+
failBackgroundCompletionWake(agent);
|
|
1140
|
+
else
|
|
1141
|
+
commitBackgroundCompletion(agent);
|
|
1142
|
+
}
|
|
1143
|
+
if (agent.backgroundCompletionQueue.length > 0 && !agent.backgroundCompletionRetryTimer) {
|
|
1144
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
1145
|
+
}
|
|
922
1146
|
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
923
1147
|
if (recoveredAuthentication) {
|
|
924
1148
|
agent.authFailureActive = false;
|
|
@@ -938,12 +1162,24 @@ function createRuntimeHost(options) {
|
|
|
938
1162
|
});
|
|
939
1163
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
940
1164
|
}
|
|
1165
|
+
else if (event.type === "runtime-observation") {
|
|
1166
|
+
if (event.phase === "completed" && typeof event.completionKey === "string") {
|
|
1167
|
+
noteBackgroundCompletion(agent, event.completionKey);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
941
1170
|
else if (event.type === "activity") {
|
|
942
1171
|
if (agent.turnInProgress && event.activity !== "internal")
|
|
943
1172
|
agent.turnHadAuthenticatedOutput = true;
|
|
944
1173
|
emit({ type: "activity", agentId: agent.config.agentId, activity: event.activity, activityKind: event.activity });
|
|
945
1174
|
}
|
|
946
1175
|
else if (event.type === "input-error") {
|
|
1176
|
+
if (agent.backgroundCompletionInFlight
|
|
1177
|
+
&& (!event.inputId || !agent.backgroundCompletionWakeInputId
|
|
1178
|
+
|| event.inputId === agent.backgroundCompletionWakeInputId)
|
|
1179
|
+
&& event.willRetry !== true) {
|
|
1180
|
+
agent.turnHadFailure = true;
|
|
1181
|
+
failBackgroundCompletionWake(agent, event);
|
|
1182
|
+
}
|
|
947
1183
|
const record = event.inputId
|
|
948
1184
|
? agent.records.get(event.inputId) ?? [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId)
|
|
949
1185
|
: undefined;
|
|
@@ -1270,10 +1506,13 @@ function createRuntimeHost(options) {
|
|
|
1270
1506
|
clearTimeout(previous.retryTimer);
|
|
1271
1507
|
if (previous.stabilityTimer)
|
|
1272
1508
|
clearTimeout(previous.stabilityTimer);
|
|
1509
|
+
if (previous.backgroundCompletionRetryTimer)
|
|
1510
|
+
clearTimeout(previous.backgroundCompletionRetryTimer);
|
|
1273
1511
|
const candidate = {
|
|
1274
1512
|
...previous, config, adapter, session, launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false,
|
|
1275
1513
|
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
1276
1514
|
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
1515
|
+
backgroundCompletionRetryTimer: null,
|
|
1277
1516
|
disabledReason: null, configurationRecovery: null,
|
|
1278
1517
|
readiness: previous.authFailureActive ? previous.readiness : readiness,
|
|
1279
1518
|
};
|
|
@@ -1290,6 +1529,11 @@ function createRuntimeHost(options) {
|
|
|
1290
1529
|
if (!candidate.authFailureActive) {
|
|
1291
1530
|
emit({ type: "agent-status", agentId: config.agentId, status: "active", readiness: candidate.readiness ?? readiness });
|
|
1292
1531
|
}
|
|
1532
|
+
// Commit clears any inherited backoff timer; reschedule so a queued
|
|
1533
|
+
// completion still drains on the new session.
|
|
1534
|
+
if (candidate.backgroundCompletionQueue.length > 0) {
|
|
1535
|
+
scheduleBackgroundCompletionDrain(candidate);
|
|
1536
|
+
}
|
|
1293
1537
|
await previous.session?.close("runtime candidate committed")
|
|
1294
1538
|
.catch((error) => log("previous runtime close after candidate commit failed", String(error)));
|
|
1295
1539
|
},
|
|
@@ -1622,7 +1866,10 @@ function createRuntimeHost(options) {
|
|
|
1622
1866
|
piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
|
|
1623
1867
|
piProactiveCompactionFailedGeneration: null, readiness: null,
|
|
1624
1868
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
|
|
1625
|
-
promotedInboxUpdateIds: new Set()
|
|
1869
|
+
promotedInboxUpdateIds: new Set(),
|
|
1870
|
+
backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
|
|
1871
|
+
backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
|
|
1872
|
+
backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null };
|
|
1626
1873
|
const startupConsumed = [];
|
|
1627
1874
|
const startupQuarantined = [];
|
|
1628
1875
|
for (const record of agent.records.values()) {
|
|
@@ -1773,6 +2020,8 @@ function createRuntimeHost(options) {
|
|
|
1773
2020
|
clearTimeout(agent.retryTimer);
|
|
1774
2021
|
if (agent.stabilityTimer)
|
|
1775
2022
|
clearTimeout(agent.stabilityTimer);
|
|
2023
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
2024
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
1776
2025
|
if (agent.busy)
|
|
1777
2026
|
await agent.session?.cancel(reason);
|
|
1778
2027
|
await agent.session?.close(reason);
|
|
@@ -30,6 +30,7 @@ export class RuntimeSessionRecoveryError extends Error {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
const MAX_DELIVERIES = 2048;
|
|
33
|
+
const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
|
|
33
34
|
const now = () => new Date().toISOString();
|
|
34
35
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
35
36
|
const REPLAY_FAILURE_CODES = new Set([
|
|
@@ -512,6 +513,12 @@ export function createRuntimeHost(options) {
|
|
|
512
513
|
queueMicrotask(() => { void retryPending(agent); });
|
|
513
514
|
}
|
|
514
515
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
516
|
+
// A completion queued during this submit has no turn-end if prompt
|
|
517
|
+
// rejected/threw before turn-start. Drain now that submitting is clear.
|
|
518
|
+
if (!agent.busy && !agent.turnInProgress && agent.backgroundCompletionQueue.length > 0
|
|
519
|
+
&& !agent.backgroundCompletionRetryTimer) {
|
|
520
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
521
|
+
}
|
|
515
522
|
}
|
|
516
523
|
};
|
|
517
524
|
const retryPending = async (agent) => {
|
|
@@ -590,6 +597,210 @@ export function createRuntimeHost(options) {
|
|
|
590
597
|
agent.retryAfterSubmit = true;
|
|
591
598
|
await retryPending(agent);
|
|
592
599
|
};
|
|
600
|
+
const clearBackgroundCompletionRetryTimer = (agent) => {
|
|
601
|
+
if (!agent.backgroundCompletionRetryTimer)
|
|
602
|
+
return;
|
|
603
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
604
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
605
|
+
};
|
|
606
|
+
const scheduleBackgroundCompletionDrain = (agent, delayMs = 0) => {
|
|
607
|
+
if (agent.stopped)
|
|
608
|
+
return;
|
|
609
|
+
if (delayMs > 0) {
|
|
610
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
611
|
+
return;
|
|
612
|
+
agent.backgroundCompletionRetryTimer = setTimeout(() => {
|
|
613
|
+
agent.backgroundCompletionRetryTimer = null;
|
|
614
|
+
if (agent.stopped)
|
|
615
|
+
return;
|
|
616
|
+
void drainBackgroundCompletionQueue(agent);
|
|
617
|
+
}, delayMs);
|
|
618
|
+
agent.backgroundCompletionRetryTimer.unref?.();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
622
|
+
queueMicrotask(() => { void drainBackgroundCompletionQueue(agent); });
|
|
623
|
+
};
|
|
624
|
+
const dropBackgroundCompletion = (agent, completionKey) => {
|
|
625
|
+
if (!completionKey)
|
|
626
|
+
return;
|
|
627
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
628
|
+
agent.backgroundCompletionQueue.shift();
|
|
629
|
+
else {
|
|
630
|
+
const index = agent.backgroundCompletionQueue.indexOf(completionKey);
|
|
631
|
+
if (index >= 0)
|
|
632
|
+
agent.backgroundCompletionQueue.splice(index, 1);
|
|
633
|
+
}
|
|
634
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
635
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
636
|
+
agent.backgroundCompletionInFlight = null;
|
|
637
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
638
|
+
}
|
|
639
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
640
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
641
|
+
};
|
|
642
|
+
const scheduleBackgroundCompletionRetry = (agent) => {
|
|
643
|
+
const streak = agent.backgroundCompletionRejectStreak;
|
|
644
|
+
if (streak < BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT) {
|
|
645
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const delayedAttempt = streak - BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT;
|
|
649
|
+
if (delayedAttempt >= retryPolicy.maxAttempts) {
|
|
650
|
+
const exhausted = agent.backgroundCompletionQueue[0];
|
|
651
|
+
if (exhausted) {
|
|
652
|
+
log("background completion wake retries exhausted", exhausted);
|
|
653
|
+
dropBackgroundCompletion(agent, exhausted);
|
|
654
|
+
}
|
|
655
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
656
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const delay = Math.min(retryPolicy.maxDelayMs, retryPolicy.baseDelayMs * 2 ** delayedAttempt);
|
|
660
|
+
scheduleBackgroundCompletionDrain(agent, delay);
|
|
661
|
+
};
|
|
662
|
+
const isPersistentBackgroundWakeFailure = (event) => {
|
|
663
|
+
if (!event)
|
|
664
|
+
return false;
|
|
665
|
+
const category = event.errorCategory;
|
|
666
|
+
if (category === "auth" || category === "billing" || category === "quota")
|
|
667
|
+
return true;
|
|
668
|
+
return category === "provider" && event.retryable === false;
|
|
669
|
+
};
|
|
670
|
+
const failBackgroundCompletionWake = (agent, event) => {
|
|
671
|
+
const completionKey = agent.backgroundCompletionInFlight ?? agent.backgroundCompletionQueue[0];
|
|
672
|
+
if (!completionKey)
|
|
673
|
+
return;
|
|
674
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
675
|
+
if (isPersistentBackgroundWakeFailure(event)) {
|
|
676
|
+
log("background completion wake dropped", event?.errorCategory ?? "persistent");
|
|
677
|
+
dropBackgroundCompletion(agent, completionKey);
|
|
678
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
679
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
683
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
684
|
+
};
|
|
685
|
+
const rearmBackgroundCompletion = (agent, completionKey = agent.backgroundCompletionInFlight) => {
|
|
686
|
+
if (!completionKey)
|
|
687
|
+
return;
|
|
688
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
689
|
+
if (agent.backgroundCompletionInFlight === completionKey) {
|
|
690
|
+
agent.backgroundCompletionInFlight = null;
|
|
691
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
692
|
+
}
|
|
693
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
694
|
+
agent.backgroundCompletionQueue.unshift(completionKey);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const commitBackgroundCompletion = (agent) => {
|
|
698
|
+
const completionKey = agent.backgroundCompletionInFlight;
|
|
699
|
+
if (!completionKey)
|
|
700
|
+
return;
|
|
701
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
702
|
+
agent.backgroundCompletionQueue.shift();
|
|
703
|
+
agent.backgroundCompletionInFlight = null;
|
|
704
|
+
agent.backgroundCompletionWakeInputId = null;
|
|
705
|
+
agent.backgroundCompletionRejectStreak = 0;
|
|
706
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
707
|
+
};
|
|
708
|
+
const wakeOnBackgroundCompletion = async (agent) => {
|
|
709
|
+
// Caller already reserved submitting/busy so a concurrent idle drain cannot
|
|
710
|
+
// dequeue another head while ensureSession/prompt yields.
|
|
711
|
+
try {
|
|
712
|
+
const session = await ensureSession(agent);
|
|
713
|
+
if (agent.session !== session || agent.stopped)
|
|
714
|
+
return { status: "dropped" };
|
|
715
|
+
const input = options.promptBuilder.buildRuntimeInput("wake", crypto.randomUUID(), { wakeReason: "background subagent completed" });
|
|
716
|
+
const result = await session.prompt(input);
|
|
717
|
+
if (agent.session !== session || agent.stopped)
|
|
718
|
+
return { status: "dropped" };
|
|
719
|
+
if (result.status === "accepted") {
|
|
720
|
+
markSessionStable(agent, session);
|
|
721
|
+
return { status: "accepted", inputId: input.inputId };
|
|
722
|
+
}
|
|
723
|
+
agent.busy = false;
|
|
724
|
+
return { status: "retry" };
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
agent.busy = false;
|
|
728
|
+
log("background completion wake failed", error instanceof Error ? error.message : String(error));
|
|
729
|
+
return { status: "retry" };
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const drainBackgroundCompletionQueue = async (agent) => {
|
|
733
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
734
|
+
return;
|
|
735
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
736
|
+
return;
|
|
737
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
738
|
+
const proactive = agent.piProactiveCompaction
|
|
739
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
740
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
741
|
+
? agent.piProactiveCompaction : null;
|
|
742
|
+
if (proactive && await proactive === "failed")
|
|
743
|
+
return;
|
|
744
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
745
|
+
return;
|
|
746
|
+
if (agent.adapter.id === "pi" && !agent.busy && !agent.turnInProgress && agent.session) {
|
|
747
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
748
|
+
if (idleGate && await idleGate === "failed")
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
|
|
752
|
+
return;
|
|
753
|
+
if (agent.backgroundCompletionInFlight || !agent.backgroundCompletionQueue.length)
|
|
754
|
+
return;
|
|
755
|
+
agent.submitting = true;
|
|
756
|
+
agent.busy = true;
|
|
757
|
+
const completionKey = agent.backgroundCompletionQueue[0];
|
|
758
|
+
try {
|
|
759
|
+
const outcome = await wakeOnBackgroundCompletion(agent);
|
|
760
|
+
if (outcome.status === "accepted") {
|
|
761
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
762
|
+
agent.backgroundCompletionInFlight = completionKey;
|
|
763
|
+
agent.backgroundCompletionWakeInputId = outcome.inputId;
|
|
764
|
+
// Keep the reject streak until the wake turn actually succeeds. An accepted
|
|
765
|
+
// prompt that later terminal-fails must not reset the bounded retry budget.
|
|
766
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
767
|
+
}
|
|
768
|
+
else if (outcome.status === "dropped") {
|
|
769
|
+
if (agent.stopped) {
|
|
770
|
+
if (agent.backgroundCompletionQueue[0] === completionKey)
|
|
771
|
+
agent.backgroundCompletionQueue.shift();
|
|
772
|
+
agent.backgroundCompletionKeys.delete(completionKey);
|
|
773
|
+
clearBackgroundCompletionRetryTimer(agent);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
rearmBackgroundCompletion(agent, completionKey);
|
|
777
|
+
agent.busy = false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
agent.busy = false;
|
|
782
|
+
agent.backgroundCompletionRejectStreak += 1;
|
|
783
|
+
// Leave the item queued+deduped. Immediate microtask retries cover a
|
|
784
|
+
// brief reject; after that a bounded timer still drains because Pi will
|
|
785
|
+
// not re-emit the same completion.
|
|
786
|
+
scheduleBackgroundCompletionRetry(agent);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
agent.submitting = false;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const noteBackgroundCompletion = (agent, completionKey) => {
|
|
794
|
+
if (agent.backgroundCompletionKeys.has(completionKey))
|
|
795
|
+
return;
|
|
796
|
+
agent.backgroundCompletionKeys.add(completionKey);
|
|
797
|
+
if (!agent.backgroundCompletionQueue.includes(completionKey)) {
|
|
798
|
+
agent.backgroundCompletionQueue.push(completionKey);
|
|
799
|
+
}
|
|
800
|
+
if (!agent.busy && !agent.turnInProgress && !agent.submitting && !agent.backgroundCompletionInFlight) {
|
|
801
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
593
804
|
const scheduleRecreate = (agent, reason) => {
|
|
594
805
|
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
595
806
|
return;
|
|
@@ -612,6 +823,8 @@ export function createRuntimeHost(options) {
|
|
|
612
823
|
if (proactive && await proactive === "failed")
|
|
613
824
|
return;
|
|
614
825
|
await retryPending(agent);
|
|
826
|
+
if (agent.backgroundCompletionQueue.length > 0)
|
|
827
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
615
828
|
}).catch((error) => {
|
|
616
829
|
const readiness = error instanceof RuntimePrerequisiteError ? error.readiness
|
|
617
830
|
: classifyRuntimePrerequisite(agent.adapter.id, error);
|
|
@@ -634,6 +847,7 @@ export function createRuntimeHost(options) {
|
|
|
634
847
|
agent.busy = false;
|
|
635
848
|
agent.submitting = false;
|
|
636
849
|
agent.generation += 1;
|
|
850
|
+
rearmBackgroundCompletion(agent);
|
|
637
851
|
agent.recreateReason = reason;
|
|
638
852
|
if (agent.stabilityTimer)
|
|
639
853
|
clearTimeout(agent.stabilityTimer);
|
|
@@ -654,6 +868,7 @@ export function createRuntimeHost(options) {
|
|
|
654
868
|
agent.busy = false;
|
|
655
869
|
agent.submitting = false;
|
|
656
870
|
agent.generation += 1;
|
|
871
|
+
rearmBackgroundCompletion(agent);
|
|
657
872
|
if (agent.stabilityTimer)
|
|
658
873
|
clearTimeout(agent.stabilityTimer);
|
|
659
874
|
agent.stabilityTimer = null;
|
|
@@ -910,6 +1125,15 @@ export function createRuntimeHost(options) {
|
|
|
910
1125
|
agent.busy = false;
|
|
911
1126
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
912
1127
|
reconcileAcceptedAtTurnEnd(agent);
|
|
1128
|
+
if (agent.backgroundCompletionInFlight) {
|
|
1129
|
+
if (agent.turnHadFailure)
|
|
1130
|
+
failBackgroundCompletionWake(agent);
|
|
1131
|
+
else
|
|
1132
|
+
commitBackgroundCompletion(agent);
|
|
1133
|
+
}
|
|
1134
|
+
if (agent.backgroundCompletionQueue.length > 0 && !agent.backgroundCompletionRetryTimer) {
|
|
1135
|
+
scheduleBackgroundCompletionDrain(agent);
|
|
1136
|
+
}
|
|
913
1137
|
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
914
1138
|
if (recoveredAuthentication) {
|
|
915
1139
|
agent.authFailureActive = false;
|
|
@@ -929,12 +1153,24 @@ export function createRuntimeHost(options) {
|
|
|
929
1153
|
});
|
|
930
1154
|
queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
|
|
931
1155
|
}
|
|
1156
|
+
else if (event.type === "runtime-observation") {
|
|
1157
|
+
if (event.phase === "completed" && typeof event.completionKey === "string") {
|
|
1158
|
+
noteBackgroundCompletion(agent, event.completionKey);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
932
1161
|
else if (event.type === "activity") {
|
|
933
1162
|
if (agent.turnInProgress && event.activity !== "internal")
|
|
934
1163
|
agent.turnHadAuthenticatedOutput = true;
|
|
935
1164
|
emit({ type: "activity", agentId: agent.config.agentId, activity: event.activity, activityKind: event.activity });
|
|
936
1165
|
}
|
|
937
1166
|
else if (event.type === "input-error") {
|
|
1167
|
+
if (agent.backgroundCompletionInFlight
|
|
1168
|
+
&& (!event.inputId || !agent.backgroundCompletionWakeInputId
|
|
1169
|
+
|| event.inputId === agent.backgroundCompletionWakeInputId)
|
|
1170
|
+
&& event.willRetry !== true) {
|
|
1171
|
+
agent.turnHadFailure = true;
|
|
1172
|
+
failBackgroundCompletionWake(agent, event);
|
|
1173
|
+
}
|
|
938
1174
|
const record = event.inputId
|
|
939
1175
|
? agent.records.get(event.inputId) ?? [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId)
|
|
940
1176
|
: undefined;
|
|
@@ -1261,10 +1497,13 @@ export function createRuntimeHost(options) {
|
|
|
1261
1497
|
clearTimeout(previous.retryTimer);
|
|
1262
1498
|
if (previous.stabilityTimer)
|
|
1263
1499
|
clearTimeout(previous.stabilityTimer);
|
|
1500
|
+
if (previous.backgroundCompletionRetryTimer)
|
|
1501
|
+
clearTimeout(previous.backgroundCompletionRetryTimer);
|
|
1264
1502
|
const candidate = {
|
|
1265
1503
|
...previous, config, adapter, session, launchId: crypto.randomUUID(), busy: false, submitting: false,
|
|
1266
1504
|
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
1267
1505
|
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
1506
|
+
backgroundCompletionRetryTimer: null,
|
|
1268
1507
|
disabledReason: null, configurationRecovery: null,
|
|
1269
1508
|
readiness: previous.authFailureActive ? previous.readiness : readiness,
|
|
1270
1509
|
};
|
|
@@ -1281,6 +1520,11 @@ export function createRuntimeHost(options) {
|
|
|
1281
1520
|
if (!candidate.authFailureActive) {
|
|
1282
1521
|
emit({ type: "agent-status", agentId: config.agentId, status: "active", readiness: candidate.readiness ?? readiness });
|
|
1283
1522
|
}
|
|
1523
|
+
// Commit clears any inherited backoff timer; reschedule so a queued
|
|
1524
|
+
// completion still drains on the new session.
|
|
1525
|
+
if (candidate.backgroundCompletionQueue.length > 0) {
|
|
1526
|
+
scheduleBackgroundCompletionDrain(candidate);
|
|
1527
|
+
}
|
|
1284
1528
|
await previous.session?.close("runtime candidate committed")
|
|
1285
1529
|
.catch((error) => log("previous runtime close after candidate commit failed", String(error)));
|
|
1286
1530
|
},
|
|
@@ -1613,7 +1857,10 @@ export function createRuntimeHost(options) {
|
|
|
1613
1857
|
piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
|
|
1614
1858
|
piProactiveCompactionFailedGeneration: null, readiness: null,
|
|
1615
1859
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
|
|
1616
|
-
promotedInboxUpdateIds: new Set()
|
|
1860
|
+
promotedInboxUpdateIds: new Set(),
|
|
1861
|
+
backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
|
|
1862
|
+
backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
|
|
1863
|
+
backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null };
|
|
1617
1864
|
const startupConsumed = [];
|
|
1618
1865
|
const startupQuarantined = [];
|
|
1619
1866
|
for (const record of agent.records.values()) {
|
|
@@ -1764,6 +2011,8 @@ export function createRuntimeHost(options) {
|
|
|
1764
2011
|
clearTimeout(agent.retryTimer);
|
|
1765
2012
|
if (agent.stabilityTimer)
|
|
1766
2013
|
clearTimeout(agent.stabilityTimer);
|
|
2014
|
+
if (agent.backgroundCompletionRetryTimer)
|
|
2015
|
+
clearTimeout(agent.backgroundCompletionRetryTimer);
|
|
1767
2016
|
if (agent.busy)
|
|
1768
2017
|
await agent.session?.cancel(reason);
|
|
1769
2018
|
await agent.session?.close(reason);
|