@cydm/pie 2.6.5 → 2.6.7
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/builtin/extensions/ask-user/index.js +3 -3
- package/dist/builtin/extensions/mcp-adapter/index.js +141 -31
- package/dist/builtin/extensions/plan-mode/index.js +3 -3
- package/dist/builtin/extensions/subagent/index.js +362 -6
- package/dist/builtin/extensions/todo/index.js +3 -3
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-bridge.js +3 -2
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +1 -1
- package/dist/builtin/skills/chrome-devtools-axi/{chunk-UPATXALW.js → chunk-U75F4DPF.js} +60 -0
- package/dist/builtin/skills/chrome-devtools-axi/{cli-5Z4COIKQ.js → cli-4ITIR57L.js} +85 -36
- package/dist/builtin/skills/skill-creator/SKILL.md +7 -8
- package/dist/chunks/{chunk-5OXH6NUH.js → chunk-7B4IIBVS.js} +307 -6
- package/dist/chunks/{chunk-AETQBDEG.js → chunk-PAC62XU4.js} +1 -1
- package/dist/chunks/{chunk-QKSK5K5N.js → chunk-T6GMEDIE.js} +1207 -225
- package/dist/chunks/{chunk-TFRQE7UQ.js → chunk-VUKF7U7Q.js} +1 -1
- package/dist/chunks/{src-Z4KWMX6E.js → src-HPIBCE2I.js} +2 -2
- package/dist/chunks/{test-stream-3FG7FHHU.js → test-stream-Y7WXELAJ.js} +1 -1
- package/dist/cli.js +2619 -2510
- package/package.json +2 -2
|
@@ -2,8 +2,9 @@ import { createRequire as __createRequire } from "node:module"; const require =
|
|
|
2
2
|
import {
|
|
3
3
|
Agent,
|
|
4
4
|
getToolCallerIdentity
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-PAC62XU4.js";
|
|
6
6
|
import {
|
|
7
|
+
AssistantMessageEventStream,
|
|
7
8
|
FileSystemGateway,
|
|
8
9
|
completeSimple,
|
|
9
10
|
detectPlatform,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
getPlatformConfig,
|
|
12
13
|
hashText,
|
|
13
14
|
streamSimple
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-7B4IIBVS.js";
|
|
15
16
|
import {
|
|
16
17
|
Type
|
|
17
18
|
} from "./chunk-XXGYUYXI.js";
|
|
@@ -1070,6 +1071,141 @@ function createPlanCapability() {
|
|
|
1070
1071
|
});
|
|
1071
1072
|
}
|
|
1072
1073
|
|
|
1074
|
+
// ../../packages/shared-headless-capabilities/src/subagent-stream.ts
|
|
1075
|
+
function isRetryableSubagentFailure(message) {
|
|
1076
|
+
const remoteClosure = /SSE stream (?:aborted|closed before end|ended before response completed)/i.test(message);
|
|
1077
|
+
const classificationText = message.replace(/econnaborted/ig, "").replace(/SSE stream aborted/ig, "remote stream closed");
|
|
1078
|
+
if (/\b(?:400|401|403|404|422|429)\b|quota|weekly|monthly|limit reached|exhausted|unauthorized|authentication|invalid|protocol|stopReason=length|token limit|abort/i.test(classificationText)) return false;
|
|
1079
|
+
return remoteClosure || /Request timeout/i.test(message) || /econnreset|econnaborted|etimedout|eai_again|econnrefused|socket hang up|socket disconnected|secure TLS connection|fetch failed|network error|connection reset|premature close|\b50[0234]\b|bad gateway|service unavailable/i.test(message);
|
|
1080
|
+
}
|
|
1081
|
+
function cancellableBackoff(ms, signal) {
|
|
1082
|
+
return new Promise((resolve3) => {
|
|
1083
|
+
const finish = () => {
|
|
1084
|
+
clearTimeout(timer);
|
|
1085
|
+
signal?.removeEventListener("abort", finish);
|
|
1086
|
+
resolve3();
|
|
1087
|
+
};
|
|
1088
|
+
const timer = setTimeout(finish, ms);
|
|
1089
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
1090
|
+
if (signal?.aborted) finish();
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function withSubagentResponseRecovery(stream, backoffMs = 2e3) {
|
|
1094
|
+
return async (model, context, options) => {
|
|
1095
|
+
for (let attempt = 0; ; attempt++) {
|
|
1096
|
+
if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
|
|
1097
|
+
let message;
|
|
1098
|
+
try {
|
|
1099
|
+
const response = await stream(model, context, options);
|
|
1100
|
+
let terminal;
|
|
1101
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
1102
|
+
try {
|
|
1103
|
+
while (true) {
|
|
1104
|
+
const next = await iterator.next();
|
|
1105
|
+
if (next.done) break;
|
|
1106
|
+
if (next.value.type === "done") terminal = next.value.message;
|
|
1107
|
+
if (next.value.type === "error") terminal = next.value.error;
|
|
1108
|
+
}
|
|
1109
|
+
} finally {
|
|
1110
|
+
try {
|
|
1111
|
+
await iterator.return?.();
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
|
|
1116
|
+
message = terminal;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (!options?.signal?.aborted && attempt < 2 && isRetryableSubagentFailure(error instanceof Error ? error.message : String(error))) {
|
|
1119
|
+
await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
throw error;
|
|
1123
|
+
}
|
|
1124
|
+
if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
|
|
1125
|
+
if (message.stopReason === "error" && attempt < 2 && isRetryableSubagentFailure(message.errorMessage ?? "")) {
|
|
1126
|
+
await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const result = new AssistantMessageEventStream();
|
|
1130
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
1131
|
+
result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
|
|
1132
|
+
} else {
|
|
1133
|
+
result.push({ type: "done", reason: message.stopReason, message });
|
|
1134
|
+
}
|
|
1135
|
+
result.end();
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ../../packages/shared-headless-capabilities/src/subagent-sessions.ts
|
|
1142
|
+
var SUBAGENT_SESSION_RECORD_VERSION = 1;
|
|
1143
|
+
var DEFAULT_MAX_LIVE_SESSIONS = 8;
|
|
1144
|
+
var DEFAULT_SESSION_STORE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1145
|
+
var DEFAULT_SESSION_STORE_MAX_RECORDS = 64;
|
|
1146
|
+
var SESSION_TRANSCRIPT_WARN_CHARS = 24e4;
|
|
1147
|
+
var SESSION_RECORD_MAX_CHARS = 4e6;
|
|
1148
|
+
function touchLiveSession(registry, id) {
|
|
1149
|
+
const session = registry.get(id);
|
|
1150
|
+
if (!session) return void 0;
|
|
1151
|
+
registry.delete(id);
|
|
1152
|
+
registry.set(id, session);
|
|
1153
|
+
return session;
|
|
1154
|
+
}
|
|
1155
|
+
function retainLiveSession(registry, session, cap) {
|
|
1156
|
+
registry.delete(session.id);
|
|
1157
|
+
registry.set(session.id, session);
|
|
1158
|
+
while (registry.size > Math.max(1, cap)) {
|
|
1159
|
+
let evicted = false;
|
|
1160
|
+
for (const [key, candidate] of registry) {
|
|
1161
|
+
if (candidate.busy) continue;
|
|
1162
|
+
registry.delete(key);
|
|
1163
|
+
evicted = true;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
if (!evicted) {
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function toSessionRecord(session, messages) {
|
|
1172
|
+
return {
|
|
1173
|
+
version: SUBAGENT_SESSION_RECORD_VERSION,
|
|
1174
|
+
id: session.id,
|
|
1175
|
+
name: session.name,
|
|
1176
|
+
mode: session.mode,
|
|
1177
|
+
priority: session.priority,
|
|
1178
|
+
originalTask: session.originalTask,
|
|
1179
|
+
modelOverride: session.modelOverride,
|
|
1180
|
+
thinkingLevel: session.thinkingLevel,
|
|
1181
|
+
timeoutSeconds: session.timeoutSeconds,
|
|
1182
|
+
createdAt: session.createdAt,
|
|
1183
|
+
lastUsedAt: session.lastUsedAt,
|
|
1184
|
+
runCount: session.runCount,
|
|
1185
|
+
revision: session.revision,
|
|
1186
|
+
messages
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
function estimateTranscriptChars(messages) {
|
|
1190
|
+
let total = 0;
|
|
1191
|
+
for (const message of messages) {
|
|
1192
|
+
try {
|
|
1193
|
+
total += JSON.stringify(message)?.length ?? 0;
|
|
1194
|
+
} catch {
|
|
1195
|
+
total += SESSION_TRANSCRIPT_WARN_CHARS;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return total;
|
|
1199
|
+
}
|
|
1200
|
+
function transplantMessages(target, messages) {
|
|
1201
|
+
const mutable = target;
|
|
1202
|
+
if (typeof mutable.replaceMessages === "function") {
|
|
1203
|
+
mutable.replaceMessages(messages);
|
|
1204
|
+
} else {
|
|
1205
|
+
mutable.state.messages = messages;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1073
1209
|
// ../../packages/shared-headless-capabilities/src/bash-policy.ts
|
|
1074
1210
|
var DESTRUCTIVE_PATTERNS = [
|
|
1075
1211
|
{ pattern: /\bgit\s+reset\s+--hard\b/, warning: "may discard uncommitted changes", commandClass: "git_destructive" },
|
|
@@ -1404,6 +1540,10 @@ function clipHeartbeatLabel(text, maxLength = 60) {
|
|
|
1404
1540
|
function formatSkillsForPrompt(skills) {
|
|
1405
1541
|
return formatSkillSummariesForPrompt(skills);
|
|
1406
1542
|
}
|
|
1543
|
+
var SYNTHETIC_PROVIDER_TEXT_PREFIXES = ["[output truncated:", "[dropped "];
|
|
1544
|
+
function isSyntheticProviderText(text) {
|
|
1545
|
+
return SYNTHETIC_PROVIDER_TEXT_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
1546
|
+
}
|
|
1407
1547
|
function collectLastAssistantText(messages) {
|
|
1408
1548
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1409
1549
|
const message = messages[i];
|
|
@@ -1411,17 +1551,29 @@ function collectLastAssistantText(messages) {
|
|
|
1411
1551
|
continue;
|
|
1412
1552
|
}
|
|
1413
1553
|
if (Array.isArray(message.content)) {
|
|
1414
|
-
const
|
|
1554
|
+
const realBlocks = message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).filter((text) => !isSyntheticProviderText(text));
|
|
1555
|
+
const textContent = realBlocks.join("\n");
|
|
1415
1556
|
if (textContent.trim()) {
|
|
1416
1557
|
return textContent;
|
|
1417
1558
|
}
|
|
1418
1559
|
}
|
|
1419
|
-
if (typeof message.content === "string" && message.content.trim()) {
|
|
1560
|
+
if (typeof message.content === "string" && message.content.trim() && !isSyntheticProviderText(message.content)) {
|
|
1420
1561
|
return message.content;
|
|
1421
1562
|
}
|
|
1422
1563
|
}
|
|
1423
1564
|
return "";
|
|
1424
1565
|
}
|
|
1566
|
+
function visibleTextBlocks(message) {
|
|
1567
|
+
if (!Array.isArray(message.content)) return [];
|
|
1568
|
+
return message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).filter((text) => text.trim().length > 0 && !isSyntheticProviderText(text));
|
|
1569
|
+
}
|
|
1570
|
+
function messageHasOwnText(message) {
|
|
1571
|
+
if (!message) return false;
|
|
1572
|
+
if (Array.isArray(message.content)) {
|
|
1573
|
+
return visibleTextBlocks(message).length > 0;
|
|
1574
|
+
}
|
|
1575
|
+
return typeof message.content === "string" && message.content.trim().length > 0 && !isSyntheticProviderText(message.content);
|
|
1576
|
+
}
|
|
1425
1577
|
function inferComplexity(task, priority) {
|
|
1426
1578
|
const normalized = task.toLowerCase();
|
|
1427
1579
|
if (priority === "thorough" || task.length > 600 || /\b(across|architecture|refactor|review|investigate|debug|failing|test|compare|multiple|race|root cause)\b/.test(normalized)) {
|
|
@@ -1539,9 +1691,31 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOver
|
|
|
1539
1691
|
function subagentOutputBudget(model) {
|
|
1540
1692
|
return Math.min(model.maxTokens || 8192, 16384);
|
|
1541
1693
|
}
|
|
1694
|
+
function buildSubagentSystemPrompt(mode, skills) {
|
|
1695
|
+
return `You are a specialized subagent focused on one bounded task.
|
|
1696
|
+
|
|
1697
|
+
Task boundary:
|
|
1698
|
+
- Work only on the assigned task.
|
|
1699
|
+
- Use available tools to gather evidence before making claims.
|
|
1700
|
+
${mode === "edit" ? "- You may edit files and run tests using the inherited parent tools and permissions. This is a shared workspace, not an isolated sandbox. Preserve existing user changes. Do not commit or automatically roll back changes. Report changed files, tests actually run, and remaining work for parent review." : "- Do not edit files; if writing is needed, say what the parent agent should change."}
|
|
1701
|
+
- Return a concise answer with evidence and uncertainty.
|
|
1702
|
+
|
|
1703
|
+
Output:
|
|
1704
|
+
- Answer
|
|
1705
|
+
- Evidence
|
|
1706
|
+
- Uncertainty: low, medium, or high
|
|
1707
|
+
- Parent follow-up needed, if any
|
|
1708
|
+
|
|
1709
|
+
${formatSkillsForPrompt(skills ?? [])}`;
|
|
1710
|
+
}
|
|
1542
1711
|
function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingLevel) {
|
|
1712
|
+
const streamFn = withSubagentResponseRecovery((streamModel, context, options) => streamSimple(streamModel, context, {
|
|
1713
|
+
...options,
|
|
1714
|
+
maxTokens: options?.maxTokens ?? subagentOutputBudget(streamModel)
|
|
1715
|
+
}), deps.transientRetryBackoffMs);
|
|
1543
1716
|
if (deps.createAgent) {
|
|
1544
1717
|
return deps.createAgent({
|
|
1718
|
+
streamFn,
|
|
1545
1719
|
systemPrompt,
|
|
1546
1720
|
model,
|
|
1547
1721
|
tools,
|
|
@@ -1560,10 +1734,7 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
|
|
|
1560
1734
|
// Subagent turns are single-purpose report generations: request an
|
|
1561
1735
|
// explicit output budget (see subagentOutputBudget) instead of leaving
|
|
1562
1736
|
// it to endpoint defaults that silently truncate long reports.
|
|
1563
|
-
streamFn
|
|
1564
|
-
...options,
|
|
1565
|
-
maxTokens: options?.maxTokens ?? subagentOutputBudget(streamModel)
|
|
1566
|
-
})
|
|
1737
|
+
streamFn
|
|
1567
1738
|
});
|
|
1568
1739
|
}
|
|
1569
1740
|
function extractEvidenceFromToolArgs(toolName, args) {
|
|
@@ -1606,7 +1777,114 @@ function inferUncertainty(text, signals) {
|
|
|
1606
1777
|
function isParentHandoffRequired(task) {
|
|
1607
1778
|
return task.mode === "edit" || task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use" || signal.type === "no_text_report");
|
|
1608
1779
|
}
|
|
1609
|
-
|
|
1780
|
+
function shellFromRecord(record) {
|
|
1781
|
+
return {
|
|
1782
|
+
id: record.id,
|
|
1783
|
+
name: record.name,
|
|
1784
|
+
mode: record.mode,
|
|
1785
|
+
priority: record.priority,
|
|
1786
|
+
originalTask: record.originalTask,
|
|
1787
|
+
modelOverride: record.modelOverride,
|
|
1788
|
+
thinkingLevel: record.thinkingLevel,
|
|
1789
|
+
timeoutSeconds: record.timeoutSeconds,
|
|
1790
|
+
agent: null,
|
|
1791
|
+
pendingMessages: record.messages,
|
|
1792
|
+
createdAt: record.createdAt,
|
|
1793
|
+
lastUsedAt: record.lastUsedAt,
|
|
1794
|
+
runCount: record.runCount,
|
|
1795
|
+
// The restored record IS our durable base line: its revision (0 for
|
|
1796
|
+
// legacy records that predate the field) is both our current revision
|
|
1797
|
+
// and our known-persisted revision.
|
|
1798
|
+
revision: record.revision ?? 0,
|
|
1799
|
+
persistedRevision: record.revision ?? 0,
|
|
1800
|
+
busy: true
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
async function resolveRecallSession(deps, id, sessions, maxLiveSessions) {
|
|
1804
|
+
let live = sessions ? touchLiveSession(sessions, id) : void 0;
|
|
1805
|
+
if (live?.busy) throw new Error(`Subagent session "${id}" is busy with another task; wait for it to settle.`);
|
|
1806
|
+
let record;
|
|
1807
|
+
if (deps.sessionStore) {
|
|
1808
|
+
try {
|
|
1809
|
+
record = await deps.sessionStore.load(id);
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1812
|
+
deps.debugLog?.("session", "session store load failed", { id, error: detail });
|
|
1813
|
+
throw new Error(`Subagent session "${id}" could not be read from the session store (${detail}); refusing to recall without verifying session freshness. Retry the recall, or spawn a fresh task.`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (record && typeof record.version === "number" && record.version !== SUBAGENT_SESSION_RECORD_VERSION) {
|
|
1817
|
+
throw new Error(`Subagent session "${id}" is persisted in record format version ${record.version}, which this build cannot read (supported: ${SUBAGENT_SESSION_RECORD_VERSION}); refusing to recall so the newer record is not overwritten. Use the matching Pie version, or spawn a fresh task.`);
|
|
1818
|
+
}
|
|
1819
|
+
if (record && (record.version !== SUBAGENT_SESSION_RECORD_VERSION || record.id !== id || !Array.isArray(record.messages))) {
|
|
1820
|
+
record = void 0;
|
|
1821
|
+
}
|
|
1822
|
+
live = sessions ? touchLiveSession(sessions, id) : void 0;
|
|
1823
|
+
if (live?.busy) throw new Error(`Subagent session "${id}" is busy with another task; wait for it to settle.`);
|
|
1824
|
+
if (live) {
|
|
1825
|
+
const liveTranscriptLength = live.agent?.state.messages.length ?? live.pendingMessages?.length ?? 0;
|
|
1826
|
+
const recordIsNewer = record !== void 0 && (record.revision !== void 0 ? record.revision > live.revision : record.runCount > live.runCount || record.runCount === live.runCount && record.lastUsedAt > live.lastUsedAt || record.runCount === live.runCount && record.lastUsedAt === live.lastUsedAt && record.messages.length > liveTranscriptLength);
|
|
1827
|
+
const forked = record?.revision !== void 0 && record.revision > (live.persistedRevision ?? 0) && live.revision > (live.persistedRevision ?? 0);
|
|
1828
|
+
if (forked && record?.revision !== void 0) {
|
|
1829
|
+
deps.debugLog?.("session", "live and durable sessions diverged after failed persistence", {
|
|
1830
|
+
id,
|
|
1831
|
+
liveRevision: live.revision,
|
|
1832
|
+
persistedRevision: record.revision,
|
|
1833
|
+
lastSharedRevision: live.persistedRevision
|
|
1834
|
+
});
|
|
1835
|
+
sessions?.delete(id);
|
|
1836
|
+
throw new Error(`Subagent session "${id}" diverged across processes: this process holds unsaved work (revision ${live.revision}, last durable sync ${live.persistedRevision ?? 0}) while another process advanced the stored record (revision ${record.revision}). The diverged in-memory branch was discarded; recall again to continue from the last durable transcript, or spawn a fresh task.`);
|
|
1837
|
+
}
|
|
1838
|
+
if (recordIsNewer && record) {
|
|
1839
|
+
deps.debugLog?.("session", "live session behind the durable record; adopting the persisted record", {
|
|
1840
|
+
id,
|
|
1841
|
+
liveMessages: liveTranscriptLength,
|
|
1842
|
+
persistedMessages: record.messages.length,
|
|
1843
|
+
liveRevision: live.revision,
|
|
1844
|
+
persistedRevision: record.revision,
|
|
1845
|
+
liveRunCount: live.runCount,
|
|
1846
|
+
persistedRunCount: record.runCount,
|
|
1847
|
+
liveLastUsedAt: live.lastUsedAt,
|
|
1848
|
+
persistedLastUsedAt: record.lastUsedAt
|
|
1849
|
+
});
|
|
1850
|
+
const shell2 = shellFromRecord(record);
|
|
1851
|
+
if (sessions) {
|
|
1852
|
+
retainLiveSession(sessions, shell2, maxLiveSessions);
|
|
1853
|
+
}
|
|
1854
|
+
return { session: shell2, restored: true };
|
|
1855
|
+
}
|
|
1856
|
+
live.busy = true;
|
|
1857
|
+
return { session: live, restored: false };
|
|
1858
|
+
}
|
|
1859
|
+
if (!record) return void 0;
|
|
1860
|
+
const shell = shellFromRecord(record);
|
|
1861
|
+
if (sessions) {
|
|
1862
|
+
retainLiveSession(sessions, shell, maxLiveSessions);
|
|
1863
|
+
}
|
|
1864
|
+
return { session: shell, restored: true };
|
|
1865
|
+
}
|
|
1866
|
+
async function persistSubagentSession(deps, session, task) {
|
|
1867
|
+
const messages = session.agent?.state.messages ?? session.pendingMessages ?? [];
|
|
1868
|
+
const transcriptChars = estimateTranscriptChars(messages);
|
|
1869
|
+
if (task.status === "completed" && task.result && transcriptChars > SESSION_TRANSCRIPT_WARN_CHARS) {
|
|
1870
|
+
task.result += `
|
|
1871
|
+
|
|
1872
|
+
[Session growth warning: session ${session.id} now carries ~${Math.round(transcriptChars / 4)} tokens of transcript; every recall pays for the whole history. For the next round, consider a fresh subagent with a compact brief instead of recalling.]`;
|
|
1873
|
+
}
|
|
1874
|
+
if (!deps.sessionStore) return;
|
|
1875
|
+
if (transcriptChars > SESSION_RECORD_MAX_CHARS) {
|
|
1876
|
+
deps.debugLog?.("session", "skipping session persist: transcript over the size cap", { id: session.id, transcriptChars });
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
const saved = await deps.sessionStore.save(toSessionRecord(session, messages));
|
|
1881
|
+
task.sessionPersisted = saved === true;
|
|
1882
|
+
if (saved === true) session.persistedRevision = session.revision;
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
deps.debugLog?.("session", "session persist failed", { id: session.id, error: error instanceof Error ? error.message : String(error) });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function runSubagentWithProgress(deps, task, reportProgress, signal, sessions) {
|
|
1610
1888
|
const startTime = Date.now();
|
|
1611
1889
|
task.status = "running";
|
|
1612
1890
|
let timedOut = false;
|
|
@@ -1620,6 +1898,11 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
|
|
|
1620
1898
|
const heartbeatIntervalMs = deps.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
1621
1899
|
deps.debugLog?.("run", "runSubagentWithProgress START", { taskName: task.name, yoloMode: deps.yoloMode });
|
|
1622
1900
|
let heartbeat;
|
|
1901
|
+
let session;
|
|
1902
|
+
let runStarted = false;
|
|
1903
|
+
let pendingRun;
|
|
1904
|
+
let agentSettled = true;
|
|
1905
|
+
let releaseLease;
|
|
1623
1906
|
if (heartbeatIntervalMs > 0) {
|
|
1624
1907
|
heartbeat = setInterval(() => {
|
|
1625
1908
|
try {
|
|
@@ -1632,46 +1915,134 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
|
|
|
1632
1915
|
}
|
|
1633
1916
|
}, Math.max(50, Math.min(1e3, heartbeatIntervalMs / 2)));
|
|
1634
1917
|
}
|
|
1918
|
+
if (task.sessionId) task.wasRecall = true;
|
|
1635
1919
|
try {
|
|
1636
1920
|
report({ message: `[${task.name}] Starting...`, increment: 5 });
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1921
|
+
let subagent;
|
|
1922
|
+
let baseline = 0;
|
|
1923
|
+
let plan;
|
|
1924
|
+
let runTimeoutSeconds = task.timeoutSeconds;
|
|
1925
|
+
if (task.sessionId) {
|
|
1926
|
+
if (sessions?.get(task.sessionId)?.busy) {
|
|
1927
|
+
throw new Error(`Subagent session "${task.sessionId}" is busy with another task; wait for it to settle.`);
|
|
1643
1928
|
}
|
|
1929
|
+
releaseLease = await deps.sessionStore?.acquire?.(task.sessionId);
|
|
1930
|
+
const resolved = await resolveRecallSession(deps, task.sessionId, sessions, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
|
|
1931
|
+
if (!resolved) {
|
|
1932
|
+
throw new Error(`Unknown or expired subagent session "${task.sessionId}": it is neither live in this process nor in the session store (evicted, swept, or never persisted). Spawn a fresh task instead of recalling.`);
|
|
1933
|
+
}
|
|
1934
|
+
const owned = resolved.session;
|
|
1935
|
+
session = owned;
|
|
1936
|
+
if (task.requestedTimeoutSeconds !== void 0) {
|
|
1937
|
+
session.timeoutSeconds = task.requestedTimeoutSeconds;
|
|
1938
|
+
}
|
|
1939
|
+
const nextMode = task.requestedMode ?? session.mode;
|
|
1940
|
+
if (nextMode === "edit" && (task.batchSize ?? 1) > 1) {
|
|
1941
|
+
throw new Error(`Recalling session "${session.id}" resolves to edit mode, and an edit delegation must contain exactly one task (the workspace is shared). Retry it alone.`);
|
|
1942
|
+
}
|
|
1943
|
+
const nextModelOverride = task.modelOverride ?? session.modelOverride;
|
|
1944
|
+
const nextThinking = task.thinkingLevel ?? session.thinkingLevel;
|
|
1945
|
+
const mustRebuild = session.agent === null || task.requestedMode !== void 0 || task.modelOverride !== void 0 || task.thinkingLevel !== void 0;
|
|
1946
|
+
if (!mustRebuild) {
|
|
1947
|
+
subagent = session.agent;
|
|
1948
|
+
baseline = subagent.state.messages.length;
|
|
1949
|
+
} else {
|
|
1950
|
+
let built = buildSubagentPlan(deps, session.originalTask, session.name, session.priority, session.timeoutSeconds, nextModelOverride, nextThinking, nextMode);
|
|
1951
|
+
let effectiveOverride = nextModelOverride;
|
|
1952
|
+
if (task.modelOverride && !built.plan.modelOverrideApplied && session.modelOverride && session.modelOverride !== task.modelOverride) {
|
|
1953
|
+
const missReason = built.plan.modelFallbackReason ?? `model override "${task.modelOverride}" could not be resolved`;
|
|
1954
|
+
built = buildSubagentPlan(deps, session.originalTask, session.name, session.priority, session.timeoutSeconds, session.modelOverride, nextThinking, nextMode);
|
|
1955
|
+
built.plan.modelFallbackReason = built.plan.modelFallbackReason ? `${missReason}; ${built.plan.modelFallbackReason}` : missReason;
|
|
1956
|
+
effectiveOverride = session.modelOverride;
|
|
1957
|
+
}
|
|
1958
|
+
plan = built.plan;
|
|
1959
|
+
task.plan = plan;
|
|
1960
|
+
for (const excluded of plan.toolsExcluded) {
|
|
1961
|
+
if (excluded.reason === "unclassified_tool") {
|
|
1962
|
+
task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
if (plan.modelFallbackReason) {
|
|
1966
|
+
task.confidenceSignals.push({ type: "model_fallback", message: plan.modelFallbackReason });
|
|
1967
|
+
}
|
|
1968
|
+
if (!built.apiKey) {
|
|
1969
|
+
throw new Error("No API key available for subagent execution.");
|
|
1970
|
+
}
|
|
1971
|
+
if (!built.model) {
|
|
1972
|
+
throw new Error("No model configured for subagent execution.");
|
|
1973
|
+
}
|
|
1974
|
+
const priorMessages = session.agent ? session.agent.state.messages : session.pendingMessages ?? [];
|
|
1975
|
+
const rebuilt = createAgentInstance(deps, buildSubagentSystemPrompt(nextMode, deps.skills), built.tools, built.model, built.apiKey, plan.thinkingLevel);
|
|
1976
|
+
transplantMessages(rebuilt, priorMessages);
|
|
1977
|
+
baseline = priorMessages.length;
|
|
1978
|
+
subagent = rebuilt;
|
|
1979
|
+
session.agent = subagent;
|
|
1980
|
+
session.pendingMessages = void 0;
|
|
1981
|
+
session.mode = nextMode;
|
|
1982
|
+
session.modelOverride = effectiveOverride;
|
|
1983
|
+
session.thinkingLevel = plan.thinkingLevel;
|
|
1984
|
+
}
|
|
1985
|
+
if (plan) {
|
|
1986
|
+
runTimeoutSeconds = plan.timeoutSeconds;
|
|
1987
|
+
} else {
|
|
1988
|
+
runTimeoutSeconds = session.timeoutSeconds;
|
|
1989
|
+
}
|
|
1990
|
+
task.mode = session.mode;
|
|
1991
|
+
task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
|
|
1992
|
+
deps.log(`[Subagent ${task.name}] Recalling session ${session.id} (run ${session.runCount + 1}, mode=${session.mode}${resolved.restored ? ", restored from disk" : ""}${mustRebuild ? ", reconfigured" : ""})`);
|
|
1993
|
+
} else {
|
|
1994
|
+
const built = buildSubagentPlan(deps, task.task, task.name, task.priority, task.timeoutSeconds, task.modelOverride, task.thinkingLevel, task.mode);
|
|
1995
|
+
plan = built.plan;
|
|
1996
|
+
task.plan = plan;
|
|
1997
|
+
const { tools, apiKey, model } = built;
|
|
1998
|
+
task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
|
|
1999
|
+
for (const excluded of plan.toolsExcluded) {
|
|
2000
|
+
if (excluded.reason === "unclassified_tool") {
|
|
2001
|
+
task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (plan.modelFallbackReason) {
|
|
2005
|
+
task.confidenceSignals.push({ type: "model_fallback", message: plan.modelFallbackReason });
|
|
2006
|
+
}
|
|
2007
|
+
if (!apiKey) {
|
|
2008
|
+
throw new Error("No API key available for subagent execution.");
|
|
2009
|
+
}
|
|
2010
|
+
if (!model) {
|
|
2011
|
+
throw new Error("No model configured for subagent execution.");
|
|
2012
|
+
}
|
|
2013
|
+
if (signal?.aborted) {
|
|
2014
|
+
throw new Error("Subagent aborted by parent");
|
|
2015
|
+
}
|
|
2016
|
+
deps.log(`[Subagent ${task.name}] Plan: ${plan.complexity}/${plan.modelClass}${plan.modelOverride ? `, model=${plan.modelOverrideApplied ? plan.modelOverride : "override failed"}` : ""}, ${tools.length} tools`);
|
|
2017
|
+
deps.debugLog?.("plan", "SubagentPlan", plan);
|
|
2018
|
+
subagent = createAgentInstance(deps, buildSubagentSystemPrompt(task.mode, deps.skills), tools, model, apiKey, plan.thinkingLevel);
|
|
2019
|
+
session = {
|
|
2020
|
+
id: task.id,
|
|
2021
|
+
name: task.name,
|
|
2022
|
+
mode: task.mode,
|
|
2023
|
+
priority: task.priority,
|
|
2024
|
+
originalTask: task.task,
|
|
2025
|
+
modelOverride: task.modelOverride,
|
|
2026
|
+
thinkingLevel: plan.thinkingLevel,
|
|
2027
|
+
timeoutSeconds: task.timeoutSeconds,
|
|
2028
|
+
agent: subagent,
|
|
2029
|
+
createdAt: Date.now(),
|
|
2030
|
+
lastUsedAt: Date.now(),
|
|
2031
|
+
runCount: 0,
|
|
2032
|
+
revision: 0,
|
|
2033
|
+
busy: true
|
|
2034
|
+
};
|
|
2035
|
+
task.sessionId = session.id;
|
|
1644
2036
|
}
|
|
1645
|
-
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
throw new Error("No model configured for subagent execution.");
|
|
1653
|
-
}
|
|
1654
|
-
if (signal?.aborted) {
|
|
1655
|
-
throw new Error("Subagent aborted by parent");
|
|
2037
|
+
if (!task.wasRecall) {
|
|
2038
|
+
try {
|
|
2039
|
+
releaseLease = await deps.sessionStore?.acquire?.(session.id);
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
session = void 0;
|
|
2042
|
+
throw error;
|
|
2043
|
+
}
|
|
1656
2044
|
}
|
|
1657
|
-
|
|
1658
|
-
deps.debugLog?.("plan", "SubagentPlan", plan);
|
|
1659
|
-
const systemPrompt = `You are a specialized subagent focused on one bounded task.
|
|
1660
|
-
|
|
1661
|
-
Task boundary:
|
|
1662
|
-
- Work only on the assigned task.
|
|
1663
|
-
- Use available tools to gather evidence before making claims.
|
|
1664
|
-
${task.mode === "edit" ? "- You may edit files and run tests using the inherited parent tools and permissions. This is a shared workspace, not an isolated sandbox. Preserve existing user changes. Do not commit or automatically roll back changes. Report changed files, tests actually run, and remaining work for parent review." : "- Do not edit files; if writing is needed, say what the parent agent should change."}
|
|
1665
|
-
- Return a concise answer with evidence and uncertainty.
|
|
1666
|
-
|
|
1667
|
-
Output:
|
|
1668
|
-
- Answer
|
|
1669
|
-
- Evidence
|
|
1670
|
-
- Uncertainty: low, medium, or high
|
|
1671
|
-
- Parent follow-up needed, if any
|
|
1672
|
-
|
|
1673
|
-
${formatSkillsForPrompt(deps.skills ?? [])}`;
|
|
1674
|
-
const subagent = createAgentInstance(deps, systemPrompt, tools, model, apiKey, plan.thinkingLevel);
|
|
2045
|
+
if (sessions) retainLiveSession(sessions, session, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
|
|
1675
2046
|
let assistantTurns = 0;
|
|
1676
2047
|
let toolCalls = 0;
|
|
1677
2048
|
let rejectTimeout = () => void 0;
|
|
@@ -1683,10 +2054,10 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
|
|
|
1683
2054
|
});
|
|
1684
2055
|
timeout = setTimeout(() => {
|
|
1685
2056
|
timedOut = true;
|
|
1686
|
-
const timeoutMessage = `Subagent timed out after ${
|
|
2057
|
+
const timeoutMessage = `Subagent timed out after ${runTimeoutSeconds} seconds`;
|
|
1687
2058
|
subagent.abort?.();
|
|
1688
2059
|
rejectTimeout(new Error(timeoutMessage));
|
|
1689
|
-
},
|
|
2060
|
+
}, runTimeoutSeconds * 1e3);
|
|
1690
2061
|
const abortPromise = new Promise((_, reject) => {
|
|
1691
2062
|
if (!signal) return;
|
|
1692
2063
|
const abortSubagent = () => {
|
|
@@ -1723,17 +2094,33 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
|
|
|
1723
2094
|
}
|
|
1724
2095
|
});
|
|
1725
2096
|
let prompt = task.task;
|
|
2097
|
+
if (task.wasRecall && session) {
|
|
2098
|
+
prompt = `This task CONTINUES subagent session "${session.name}" (${session.id}); your earlier transcript is retained. Build on it instead of redoing completed investigation, and re-verify anything the workspace may have changed since your last run.
|
|
2099
|
+
|
|
2100
|
+
New instruction:
|
|
2101
|
+
${task.task}`;
|
|
2102
|
+
}
|
|
1726
2103
|
if (task.mode === "read") {
|
|
1727
2104
|
prompt += "\n\nThis delegation is read mode. Do not write files. If changes are needed, describe them for the parent.";
|
|
1728
2105
|
}
|
|
1729
2106
|
prompt += "\n\nReturn evidence for your conclusion and state uncertainty as low, medium, or high.";
|
|
1730
|
-
|
|
2107
|
+
const runLabel = plan ? `plan ${plan.complexity}/${plan.modelClass}` : `session ${session?.id ?? ""} (run ${(session?.runCount ?? 0) + 1})`;
|
|
2108
|
+
report({ message: `[${task.name}] Running ${runLabel}...`, increment: 20 });
|
|
2109
|
+
runStarted = true;
|
|
2110
|
+
agentSettled = false;
|
|
2111
|
+
const runPromise = (async () => {
|
|
2112
|
+
try {
|
|
2113
|
+
await subagent.prompt(prompt);
|
|
2114
|
+
await subagent.waitForIdle?.();
|
|
2115
|
+
} finally {
|
|
2116
|
+
agentSettled = true;
|
|
2117
|
+
}
|
|
2118
|
+
})();
|
|
2119
|
+
const settledRun = runPromise.catch(() => void 0);
|
|
2120
|
+
pendingRun = settledRun;
|
|
1731
2121
|
try {
|
|
1732
2122
|
await Promise.race([
|
|
1733
|
-
|
|
1734
|
-
await subagent.prompt(prompt);
|
|
1735
|
-
await subagent.waitForIdle?.();
|
|
1736
|
-
})(),
|
|
2123
|
+
runPromise,
|
|
1737
2124
|
timeoutPromise,
|
|
1738
2125
|
abortPromise
|
|
1739
2126
|
]);
|
|
@@ -1743,17 +2130,30 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
|
|
|
1743
2130
|
}
|
|
1744
2131
|
removeAbortListener?.();
|
|
1745
2132
|
unsubscribe?.();
|
|
2133
|
+
if (!agentSettled) {
|
|
2134
|
+
const graceMs = deps.sessionSettleGraceMs ?? 2e3;
|
|
2135
|
+
if (graceMs > 0) {
|
|
2136
|
+
await Promise.race([
|
|
2137
|
+
settledRun,
|
|
2138
|
+
new Promise((resolve3) => setTimeout(resolve3, graceMs))
|
|
2139
|
+
]);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
1746
2142
|
}
|
|
1747
|
-
const
|
|
1748
|
-
|
|
2143
|
+
const allMessages = subagent.state.messages;
|
|
2144
|
+
const messages = allMessages.slice(baseline);
|
|
2145
|
+
deps.log(`[Subagent ${task.name}] Total messages: ${allMessages.length} (${messages.length} new)`);
|
|
1749
2146
|
const lastAssistant = [...messages].reverse().find((m) => m?.role === "assistant");
|
|
1750
2147
|
const terminalStopReason = lastAssistant?.stopReason;
|
|
1751
2148
|
const terminalError = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
|
|
1752
2149
|
const collectedResponse = collectLastAssistantText(messages) || void 0;
|
|
1753
|
-
const streamInterrupted = terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse && terminalStopReason !== "stop";
|
|
2150
|
+
const streamInterrupted = terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse && terminalStopReason !== "stop" || terminalStopReason === "length" && !messageHasOwnText(lastAssistant);
|
|
1754
2151
|
if (streamInterrupted) {
|
|
2152
|
+
if (terminalStopReason === "length") {
|
|
2153
|
+
throw new Error(`Subagent output token limit reached (stopReason=length): the provider cut the final response before the report was produced${collectedResponse ? " (earlier-turn narration discarded)" : ", no text response"}. The output/thinking budget was too small for this task at its reasoning level. Rerun with a narrower task or a lower thinkingLevel.`);
|
|
2154
|
+
}
|
|
1755
2155
|
const detail = terminalError ? `: ${terminalError.slice(0, 200)}` : "";
|
|
1756
|
-
throw new Error(`Subagent
|
|
2156
|
+
throw new Error(`Subagent response failed (provider/transport failure before the report was produced, stopReason=${terminalStopReason ?? "none"}${collectedResponse ? ", partial transcript discarded" : ", no text response"})${detail}`);
|
|
1757
2157
|
}
|
|
1758
2158
|
let response = collectedResponse ?? "[No textual report: the subagent finished normally (stopReason=stop) but produced no text \u2014 every assistant message was tool calls only. Review the task's effects directly, or rerun it with an explicit instruction to summarize findings.]";
|
|
1759
2159
|
if (!collectedResponse) {
|
|
@@ -1802,6 +2202,29 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
|
|
|
1802
2202
|
if (heartbeat) {
|
|
1803
2203
|
clearInterval(heartbeat);
|
|
1804
2204
|
}
|
|
2205
|
+
const finalize = async () => {
|
|
2206
|
+
try {
|
|
2207
|
+
if (session) {
|
|
2208
|
+
if (runStarted) session.runCount++;
|
|
2209
|
+
session.lastUsedAt = Date.now();
|
|
2210
|
+
session.revision++;
|
|
2211
|
+
task.sessionRunCount = session.runCount;
|
|
2212
|
+
await persistSubagentSession(deps, session, task);
|
|
2213
|
+
}
|
|
2214
|
+
} finally {
|
|
2215
|
+
await releaseLease?.();
|
|
2216
|
+
if (session) {
|
|
2217
|
+
session.busy = false;
|
|
2218
|
+
if (sessions) retainLiveSession(sessions, session, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
if (!agentSettled && pendingRun) {
|
|
2223
|
+
task.sessionBusy = true;
|
|
2224
|
+
void pendingRun.then(finalize).catch((error) => deps.debugLog?.("session", "late session cleanup failed", { error: String(error) }));
|
|
2225
|
+
} else {
|
|
2226
|
+
await finalize();
|
|
2227
|
+
}
|
|
1805
2228
|
}
|
|
1806
2229
|
}
|
|
1807
2230
|
function buildParallelResult(tasks, totalDuration) {
|
|
@@ -1838,6 +2261,12 @@ function buildParallelResult(tasks, totalDuration) {
|
|
|
1838
2261
|
resultText += `Evidence: ${task.evidence.slice(0, 5).map((entry) => `${entry.type}:${entry.value}`).join("; ")}
|
|
1839
2262
|
`;
|
|
1840
2263
|
}
|
|
2264
|
+
if (task.sessionId) {
|
|
2265
|
+
resultText += task.sessionBusy ? `Session: ${task.sessionId} (busy \u2014 previous run is still settling; recall will fail until it settles` : `Session: ${task.sessionId} (resumable \u2014 pass as "sessionId" in a later spawn to continue this subagent with its accumulated context`;
|
|
2266
|
+
if (task.sessionRunCount) resultText += `; run ${task.sessionRunCount}`;
|
|
2267
|
+
if (task.sessionPersisted) resultText += "; persisted to disk, survives restarts";
|
|
2268
|
+
resultText += ")\n";
|
|
2269
|
+
}
|
|
1841
2270
|
if (task.mode === "edit") {
|
|
1842
2271
|
resultText += "Parent follow-up: review changes and validate reported tests. Partial edits are preserved on failure; no automatic rollback or task retry.\n";
|
|
1843
2272
|
} else if (task.needsParentWrite) {
|
|
@@ -1862,7 +2291,8 @@ function normalizeTaskDefs(args) {
|
|
|
1862
2291
|
return { error: "Invalid spawn_subagents_parallel arguments: tasks must be a non-empty array." };
|
|
1863
2292
|
}
|
|
1864
2293
|
const normalized = [];
|
|
1865
|
-
const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel", "mode"];
|
|
2294
|
+
const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel", "mode", "sessionId"];
|
|
2295
|
+
const seenSessionIds = /* @__PURE__ */ new Set();
|
|
1866
2296
|
for (const [index, def] of taskDefs.entries()) {
|
|
1867
2297
|
if (!def || typeof def !== "object") {
|
|
1868
2298
|
return { error: `Invalid task #${index + 1}: expected an object with the objective in "task" (aliases "description"/"prompt"/"objective" accepted).` };
|
|
@@ -1895,6 +2325,16 @@ function normalizeTaskDefs(args) {
|
|
|
1895
2325
|
if (def.thinkingLevel !== void 0 && (typeof def.thinkingLevel !== "string" || !SUBAGENT_THINKING_LEVELS.includes(def.thinkingLevel))) {
|
|
1896
2326
|
return { error: `Invalid task #${index + 1}: "thinkingLevel" must be one of ${SUBAGENT_THINKING_LEVELS.map((level) => `"${level}"`).join(", ")}.` };
|
|
1897
2327
|
}
|
|
2328
|
+
const sessionId = typeof def.sessionId === "string" && def.sessionId.trim() ? def.sessionId.trim() : void 0;
|
|
2329
|
+
if (def.sessionId !== void 0 && !sessionId) {
|
|
2330
|
+
return { error: `Invalid task #${index + 1}: "sessionId" must be a non-empty session id from an earlier result's Session line.` };
|
|
2331
|
+
}
|
|
2332
|
+
if (sessionId) {
|
|
2333
|
+
if (seenSessionIds.has(sessionId)) {
|
|
2334
|
+
return { error: `Invalid task #${index + 1}: duplicate "sessionId" "${sessionId}" \u2014 recalls to the same session must run in separate calls.` };
|
|
2335
|
+
}
|
|
2336
|
+
seenSessionIds.add(sessionId);
|
|
2337
|
+
}
|
|
1898
2338
|
normalized.push({
|
|
1899
2339
|
name: typeof def.name === "string" && def.name.trim() ? def.name.trim() : void 0,
|
|
1900
2340
|
task,
|
|
@@ -1902,7 +2342,10 @@ function normalizeTaskDefs(args) {
|
|
|
1902
2342
|
mode: def.mode ?? "read",
|
|
1903
2343
|
timeoutSeconds,
|
|
1904
2344
|
modelOverride,
|
|
1905
|
-
thinkingLevel: def.thinkingLevel
|
|
2345
|
+
thinkingLevel: def.thinkingLevel,
|
|
2346
|
+
sessionId,
|
|
2347
|
+
requestedMode: def.mode,
|
|
2348
|
+
requestedTimeoutSeconds: "timeout" in def ? timeoutSeconds : void 0
|
|
1906
2349
|
});
|
|
1907
2350
|
}
|
|
1908
2351
|
if (normalized.length !== 1 && normalized.some((task) => task.mode === "edit")) {
|
|
@@ -1911,25 +2354,7 @@ function normalizeTaskDefs(args) {
|
|
|
1911
2354
|
return normalized;
|
|
1912
2355
|
}
|
|
1913
2356
|
var DEFAULT_MAX_CONCURRENT_SUBAGENTS = 4;
|
|
1914
|
-
var
|
|
1915
|
-
var TRANSIENT_RETRY_BACKOFF_MS = 2e3;
|
|
1916
|
-
var TRANSIENT_ERROR_PATTERN = /econnreset|econnaborted|etimedout|timeout|enotfound|eai_again|econnrefused|socket hang up|sse stream aborted|fetch failed|status 5\d\d|bad gateway|service unavailable|overloaded/i;
|
|
1917
|
-
function isTransientNetworkError(message) {
|
|
1918
|
-
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
1919
|
-
}
|
|
1920
|
-
function sleep(ms, signal) {
|
|
1921
|
-
return new Promise((resolve3) => {
|
|
1922
|
-
const timer = setTimeout(resolve3, ms);
|
|
1923
|
-
signal?.addEventListener(
|
|
1924
|
-
"abort",
|
|
1925
|
-
() => {
|
|
1926
|
-
clearTimeout(timer);
|
|
1927
|
-
resolve3();
|
|
1928
|
-
},
|
|
1929
|
-
{ once: true }
|
|
1930
|
-
);
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
2357
|
+
var taskIdCounter = 0;
|
|
1933
2358
|
async function runWithConcurrencyLimit(jobs, limit) {
|
|
1934
2359
|
let next = 0;
|
|
1935
2360
|
const workers = Array.from({ length: Math.max(1, Math.min(limit, jobs.length)) }, async () => {
|
|
@@ -1941,6 +2366,13 @@ async function runWithConcurrencyLimit(jobs, limit) {
|
|
|
1941
2366
|
await Promise.all(workers);
|
|
1942
2367
|
}
|
|
1943
2368
|
function createSubagentCapability(deps) {
|
|
2369
|
+
const liveSessions = /* @__PURE__ */ new Map();
|
|
2370
|
+
if (deps.sessionStore?.sweep) {
|
|
2371
|
+
try {
|
|
2372
|
+
void Promise.resolve(deps.sessionStore.sweep({ maxAgeMs: DEFAULT_SESSION_STORE_MAX_AGE_MS, maxRecords: DEFAULT_SESSION_STORE_MAX_RECORDS })).catch(() => void 0);
|
|
2373
|
+
} catch {
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
1944
2376
|
const spawnParallelTool = {
|
|
1945
2377
|
name: "spawn_subagents_parallel",
|
|
1946
2378
|
label: "Spawn Subagents",
|
|
@@ -1953,7 +2385,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
1953
2385
|
|
|
1954
2386
|
"model" overrides the automatic model-class selection for that task when the user explicitly asks for a specific model (e.g. "anthropic/claude-..."). Prefer "provider/model-id"; a bare model id works only when it is unambiguous across providers. When the override cannot be resolved, the task falls back to automatic selection and reports why.
|
|
1955
2387
|
|
|
1956
|
-
"thinkingLevel" defaults to "medium"; set it only when the user asks (e.g. "xhigh" for deep review) or for cheap lookup tasks ("off"). Non-reasoning models ignore it
|
|
2388
|
+
"thinkingLevel" defaults to "medium"; set it only when the user asks (e.g. "xhigh" for deep review) or for cheap lookup tasks ("off"). Non-reasoning models ignore it.
|
|
2389
|
+
|
|
2390
|
+
Sessions: every task that starts execution returns a Session id (persisted to disk when the host supports it). Pass "sessionId" in a later call to continue that subagent with its accumulated context \u2014 cheaper than re-briefing a fresh agent for iterative work on the same area. A recall may override "mode" (including upgrading read\u2192edit), "model", and "thinkingLevel": the session is reconfigured with freshly resolved tools and credentials. Recalls never replay on failure, and a session runs one task at a time. Prefer a FRESH task for independent reviews \u2014 accumulated context anchors judgment, and each recall carries the whole transcript.`,
|
|
1957
2391
|
parameters: Type.Object({
|
|
1958
2392
|
tasks: Type.Array(
|
|
1959
2393
|
Type.Object(
|
|
@@ -1967,9 +2401,10 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
1967
2401
|
priority: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("thorough")], { description: "fast uses lower-cost defaults; thorough raises model class when needed." })),
|
|
1968
2402
|
model: Type.Optional(Type.String({ description: 'Explicit model override for this task, e.g. "provider/model-id". Only set when the user explicitly requests a specific model; otherwise Pie picks the model class automatically.' })),
|
|
1969
2403
|
thinkingLevel: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], { description: `Reasoning level for this task. Default: "medium". Only override when the user asks (e.g. "xhigh" for deep review tasks, "off" for cheap lookups). Ignored by non-reasoning models.` })),
|
|
1970
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 1200s (20 minutes). For longer tasks, explicitly set a higher value, e.g. 3600." }))
|
|
2404
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 1200s (20 minutes). For longer tasks, explicitly set a higher value, e.g. 3600." })),
|
|
2405
|
+
sessionId: Type.Optional(Type.String({ description: "Resume a previous subagent session by id (from an earlier result's Session line). The session's accumulated transcript is reused \u2014 cheaper for iterative work on the same area. Optional mode/model/thinkingLevel on a recall RECONFIGURE the session with freshly resolved tools and credentials (read sessions may be upgraded to edit). Recalls never replay on failure and must target an idle session." }))
|
|
1971
2406
|
},
|
|
1972
|
-
{ additionalProperties: false, description: "Objective goes in 'task' (or an accepted alias); plus optional name/priority/model/thinkingLevel/timeout." }
|
|
2407
|
+
{ additionalProperties: false, description: "Objective goes in 'task' (or an accepted alias); plus optional name/priority/model/thinkingLevel/timeout/sessionId." }
|
|
1973
2408
|
),
|
|
1974
2409
|
{ description: "Array of tasks to run in parallel; at least one of task/description/prompt/objective must be a non-empty string per item.", minItems: 1, maxItems: 10 }
|
|
1975
2410
|
)
|
|
@@ -1996,7 +2431,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
1996
2431
|
};
|
|
1997
2432
|
}
|
|
1998
2433
|
const tasks = normalized.map((def, index) => ({
|
|
1999
|
-
|
|
2434
|
+
// Date.now()+index alone collides when two spawn calls land in the
|
|
2435
|
+
// same millisecond (ids double as session ids and store filenames).
|
|
2436
|
+
id: `subagent-${Date.now().toString(36)}-${(++taskIdCounter).toString(36)}${Math.random().toString(36).slice(2, 6)}-${index}`,
|
|
2000
2437
|
name: def.name || `Task-${index + 1}`,
|
|
2001
2438
|
status: "pending",
|
|
2002
2439
|
task: def.task,
|
|
@@ -2005,6 +2442,10 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
2005
2442
|
timeoutSeconds: def.timeoutSeconds,
|
|
2006
2443
|
modelOverride: def.modelOverride,
|
|
2007
2444
|
thinkingLevel: def.thinkingLevel,
|
|
2445
|
+
sessionId: def.sessionId,
|
|
2446
|
+
requestedMode: def.requestedMode,
|
|
2447
|
+
requestedTimeoutSeconds: def.requestedTimeoutSeconds,
|
|
2448
|
+
batchSize: normalized.length,
|
|
2008
2449
|
evidence: [],
|
|
2009
2450
|
confidenceSignals: [],
|
|
2010
2451
|
uncertainty: "medium",
|
|
@@ -2018,24 +2459,26 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
2018
2459
|
toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
|
|
2019
2460
|
const startTime = Date.now();
|
|
2020
2461
|
const runTask = async (task) => {
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2462
|
+
await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal, liveSessions);
|
|
2463
|
+
if (task.wasRecall || task.mode !== "read" || task.status !== "failed" || !task.error) return;
|
|
2464
|
+
if (signal?.aborted) return;
|
|
2465
|
+
if (!isRetryableSubagentFailure(task.error)) return;
|
|
2466
|
+
const settleMs = Math.max(0, (deps.transientRetryBackoffMs ?? 2e3) * 10);
|
|
2467
|
+
toolContext.reportProgress?.({ message: `[${task.name}] Provider/transport failure \u2014 replaying the read task once after ${Math.round(settleMs / 1e3)}s`, increment: 0 });
|
|
2468
|
+
await cancellableBackoff(settleMs, signal);
|
|
2469
|
+
if (signal?.aborted) return;
|
|
2470
|
+
task.evidence = [];
|
|
2471
|
+
task.confidenceSignals = [];
|
|
2472
|
+
task.runtimeEvidence = [];
|
|
2473
|
+
task.reportedEvidence = [];
|
|
2474
|
+
task.error = void 0;
|
|
2475
|
+
task.result = void 0;
|
|
2476
|
+
task.details = void 0;
|
|
2477
|
+
task.parentHandoffRequired = false;
|
|
2478
|
+
task.sessionId = void 0;
|
|
2479
|
+
task.requestedMode = void 0;
|
|
2480
|
+
task.wasRecall = void 0;
|
|
2481
|
+
await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal, liveSessions);
|
|
2039
2482
|
};
|
|
2040
2483
|
await runWithConcurrencyLimit(
|
|
2041
2484
|
tasks.map((task) => () => runTask(task)),
|
|
@@ -2053,6 +2496,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
|
|
|
2053
2496
|
name: task.name,
|
|
2054
2497
|
status: task.status,
|
|
2055
2498
|
mode: task.mode,
|
|
2499
|
+
sessionId: task.sessionId,
|
|
2500
|
+
sessionRunCount: task.sessionRunCount,
|
|
2501
|
+
sessionPersisted: task.sessionPersisted,
|
|
2056
2502
|
task: task.task,
|
|
2057
2503
|
timeoutSeconds: task.timeoutSeconds,
|
|
2058
2504
|
plan: task.plan,
|
|
@@ -4987,6 +5433,163 @@ function createSessionManager(options) {
|
|
|
4987
5433
|
return new SessionManager(options);
|
|
4988
5434
|
}
|
|
4989
5435
|
|
|
5436
|
+
// ../../packages/agent-framework/src/observer.ts
|
|
5437
|
+
function notifyObserver(listener, event) {
|
|
5438
|
+
try {
|
|
5439
|
+
void Promise.resolve(listener(event)).catch(() => {
|
|
5440
|
+
});
|
|
5441
|
+
} catch {
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
// ../../packages/agent-framework/src/session/controller/end-hook.ts
|
|
5446
|
+
async function runEndHook(hook, context, isCurrent, emit) {
|
|
5447
|
+
if (!hook) return;
|
|
5448
|
+
emit({ type: "agent_end_hook_start", hasQueuedMessages: context.hasQueuedMessages });
|
|
5449
|
+
try {
|
|
5450
|
+
await hook(context);
|
|
5451
|
+
if (isCurrent()) emit({ type: "agent_end_hook_end" });
|
|
5452
|
+
} catch (error) {
|
|
5453
|
+
if (isCurrent()) emit({ type: "agent_end_hook_end", errorMessage: error instanceof Error ? error.message : String(error) });
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
// ../../packages/agent-framework/src/session/controller/convergence.ts
|
|
5458
|
+
var SessionConvergence = class {
|
|
5459
|
+
constructor(options) {
|
|
5460
|
+
this.options = options;
|
|
5461
|
+
}
|
|
5462
|
+
options;
|
|
5463
|
+
generation = 0;
|
|
5464
|
+
active = false;
|
|
5465
|
+
disposed = false;
|
|
5466
|
+
operations = /* @__PURE__ */ new Set();
|
|
5467
|
+
outcome;
|
|
5468
|
+
monitoring = /* @__PURE__ */ new Set();
|
|
5469
|
+
invalidated;
|
|
5470
|
+
invalidation = new Promise((resolve3) => {
|
|
5471
|
+
this.invalidated = resolve3;
|
|
5472
|
+
});
|
|
5473
|
+
get identity() {
|
|
5474
|
+
return { sessionId: this.options.sessionId(), generation: this.generation };
|
|
5475
|
+
}
|
|
5476
|
+
begin() {
|
|
5477
|
+
if (this.disposed || this.active) return;
|
|
5478
|
+
this.active = true;
|
|
5479
|
+
this.outcome = void 0;
|
|
5480
|
+
this.generation++;
|
|
5481
|
+
notifyObserver(this.options.emit, { type: "session_work_started", ...this.identity });
|
|
5482
|
+
}
|
|
5483
|
+
invalidate(reason) {
|
|
5484
|
+
if (this.disposed) return;
|
|
5485
|
+
if (reason === "dispose") this.disposed = true;
|
|
5486
|
+
this.invalidated();
|
|
5487
|
+
this.invalidation = new Promise((resolve3) => {
|
|
5488
|
+
this.invalidated = resolve3;
|
|
5489
|
+
});
|
|
5490
|
+
this.generation++;
|
|
5491
|
+
this.active = false;
|
|
5492
|
+
this.operations.clear();
|
|
5493
|
+
notifyObserver(this.options.emit, { type: "session_invalidated", ...this.identity, reason });
|
|
5494
|
+
}
|
|
5495
|
+
run(action) {
|
|
5496
|
+
if (this.disposed) return Promise.reject(new Error("Controller disposed"));
|
|
5497
|
+
const wasActive = this.active;
|
|
5498
|
+
const preBeginGeneration = this.generation;
|
|
5499
|
+
this.begin();
|
|
5500
|
+
if (this.disposed) return Promise.reject(new Error("Controller disposed"));
|
|
5501
|
+
if (!wasActive && (!this.active || this.generation !== preBeginGeneration + 1)) {
|
|
5502
|
+
return Promise.resolve();
|
|
5503
|
+
}
|
|
5504
|
+
const generation = this.generation;
|
|
5505
|
+
let resolve3;
|
|
5506
|
+
let reject;
|
|
5507
|
+
const operation = new Promise((yes, no) => {
|
|
5508
|
+
resolve3 = yes;
|
|
5509
|
+
reject = no;
|
|
5510
|
+
});
|
|
5511
|
+
this.operations.add(operation);
|
|
5512
|
+
try {
|
|
5513
|
+
resolve3(action());
|
|
5514
|
+
} catch (error) {
|
|
5515
|
+
reject(error);
|
|
5516
|
+
}
|
|
5517
|
+
void operation.catch(() => {
|
|
5518
|
+
if (generation === this.generation) this.outcome = "failed";
|
|
5519
|
+
}).finally(() => {
|
|
5520
|
+
this.operations.delete(operation);
|
|
5521
|
+
this.check();
|
|
5522
|
+
});
|
|
5523
|
+
this.check();
|
|
5524
|
+
return operation;
|
|
5525
|
+
}
|
|
5526
|
+
observe(event) {
|
|
5527
|
+
if (event.type === "agent_event" && event.event.type === "agent_start") this.begin();
|
|
5528
|
+
if (event.type === "turn_failed_terminal" || event.type === "turn_policy_error" || event.type === "agent_end_hook_end" && event.errorMessage) this.outcome = "failed";
|
|
5529
|
+
if (event.type === "turn_cancelled") this.outcome = "cancelled";
|
|
5530
|
+
this.check();
|
|
5531
|
+
}
|
|
5532
|
+
async wait() {
|
|
5533
|
+
const generation = this.generation;
|
|
5534
|
+
const invalidation = this.invalidation;
|
|
5535
|
+
while (generation === this.generation && !this.disposed) {
|
|
5536
|
+
await Promise.race([Promise.allSettled([...this.operations]), invalidation]);
|
|
5537
|
+
if (generation !== this.generation || this.disposed) return;
|
|
5538
|
+
await Promise.race([this.options.wait(), invalidation]);
|
|
5539
|
+
if (generation !== this.generation || this.disposed) return;
|
|
5540
|
+
if (!this.operations.size && !this.options.busy()) return;
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
check() {
|
|
5544
|
+
const generation = this.generation;
|
|
5545
|
+
if (!this.active || this.disposed || this.monitoring.has(generation)) return;
|
|
5546
|
+
this.monitoring.add(generation);
|
|
5547
|
+
void this.wait().then(() => {
|
|
5548
|
+
if (generation !== this.generation || !this.active || this.disposed) return;
|
|
5549
|
+
this.active = false;
|
|
5550
|
+
notifyObserver(this.options.emit, {
|
|
5551
|
+
type: "session_quiescent",
|
|
5552
|
+
...this.identity,
|
|
5553
|
+
outcome: this.outcome ?? (this.options.queued() ? "blocked" : "success")
|
|
5554
|
+
});
|
|
5555
|
+
}).catch(() => {
|
|
5556
|
+
if (generation !== this.generation || !this.active || this.disposed) return;
|
|
5557
|
+
this.active = false;
|
|
5558
|
+
notifyObserver(this.options.emit, { type: "session_quiescent", ...this.identity, outcome: "failed" });
|
|
5559
|
+
}).finally(() => this.monitoring.delete(generation));
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
|
|
5563
|
+
// ../../packages/agent-framework/src/session/controller/pending-operations.ts
|
|
5564
|
+
var PendingEndOperations = class {
|
|
5565
|
+
operations = /* @__PURE__ */ new Set();
|
|
5566
|
+
get pending() {
|
|
5567
|
+
return this.operations.size > 0;
|
|
5568
|
+
}
|
|
5569
|
+
clear() {
|
|
5570
|
+
this.operations.clear();
|
|
5571
|
+
}
|
|
5572
|
+
async wait() {
|
|
5573
|
+
await Promise.allSettled([...this.operations]);
|
|
5574
|
+
}
|
|
5575
|
+
track(action) {
|
|
5576
|
+
let resolve3;
|
|
5577
|
+
let reject;
|
|
5578
|
+
const operation = new Promise((yes, no) => {
|
|
5579
|
+
resolve3 = yes;
|
|
5580
|
+
reject = no;
|
|
5581
|
+
});
|
|
5582
|
+
this.operations.add(operation);
|
|
5583
|
+
void operation.finally(() => this.operations.delete(operation)).catch(() => {
|
|
5584
|
+
});
|
|
5585
|
+
try {
|
|
5586
|
+
resolve3(action());
|
|
5587
|
+
} catch (error) {
|
|
5588
|
+
reject(error);
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5592
|
+
|
|
4990
5593
|
// ../../packages/agent-framework/src/session/controller/auto-compact-policy.ts
|
|
4991
5594
|
function defaultSetTimeout(callback, delay) {
|
|
4992
5595
|
return setTimeout(callback, delay);
|
|
@@ -5086,6 +5689,8 @@ var AgentSessionAutoContinueController = class {
|
|
|
5086
5689
|
}
|
|
5087
5690
|
options;
|
|
5088
5691
|
autoContinueTimer;
|
|
5692
|
+
generation = 0;
|
|
5693
|
+
cancelExecutionWait;
|
|
5089
5694
|
pendingAutoContinue;
|
|
5090
5695
|
resolvePendingAutoContinue;
|
|
5091
5696
|
get hasPendingAutoContinue() {
|
|
@@ -5101,21 +5706,30 @@ var AgentSessionAutoContinueController = class {
|
|
|
5101
5706
|
this.ensurePending();
|
|
5102
5707
|
const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
|
|
5103
5708
|
const snapshot = messages.slice();
|
|
5709
|
+
const generation = this.generation;
|
|
5104
5710
|
this.autoContinueTimer = setTimer(() => {
|
|
5105
5711
|
this.autoContinueTimer = void 0;
|
|
5106
|
-
void this.maybeScheduleAutoContinue(snapshot).
|
|
5712
|
+
void this.maybeScheduleAutoContinue(snapshot, generation).catch((error) => {
|
|
5713
|
+
if (generation === this.generation && !this.options.getDisposed()) {
|
|
5714
|
+
this.options.emit({ type: "turn_policy_error", message: error instanceof Error ? error.message : String(error) });
|
|
5715
|
+
}
|
|
5716
|
+
}).finally(() => {
|
|
5717
|
+
if (generation === this.generation && this.autoContinueTimer === void 0) this.resolvePending();
|
|
5718
|
+
});
|
|
5107
5719
|
}, 0);
|
|
5108
5720
|
}
|
|
5109
5721
|
cancel() {
|
|
5110
|
-
|
|
5111
|
-
|
|
5722
|
+
this.generation++;
|
|
5723
|
+
if (this.autoContinueTimer !== void 0) {
|
|
5724
|
+
const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout2;
|
|
5725
|
+
clearTimer(this.autoContinueTimer);
|
|
5726
|
+
this.autoContinueTimer = void 0;
|
|
5112
5727
|
}
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
this.autoContinueTimer = void 0;
|
|
5728
|
+
this.cancelExecutionWait?.();
|
|
5729
|
+
this.cancelExecutionWait = void 0;
|
|
5116
5730
|
this.resolvePending();
|
|
5117
5731
|
}
|
|
5118
|
-
async maybeScheduleAutoContinue(messages) {
|
|
5732
|
+
async maybeScheduleAutoContinue(messages, generation) {
|
|
5119
5733
|
if (!this.options.getAutoContinueMessage || this.options.getDisposed() || this.options.getRuntimeGuardTriggered() || this.options.isBlockedByRuntimePolicy() || this.options.hasQueuedMessages() || this.options.isStreaming()) {
|
|
5120
5734
|
if (this.options.hasQueuedMessages()) {
|
|
5121
5735
|
this.options.emit({ type: "auto_continue_skipped", reason: "queued_messages" });
|
|
@@ -5134,6 +5748,7 @@ var AgentSessionAutoContinueController = class {
|
|
|
5134
5748
|
messages,
|
|
5135
5749
|
hasQueuedMessages: this.options.hasQueuedMessages()
|
|
5136
5750
|
});
|
|
5751
|
+
if (generation !== this.generation || this.options.getDisposed() || this.options.getRuntimeGuardTriggered()) return;
|
|
5137
5752
|
const message = result && "role" in result ? result : result?.message;
|
|
5138
5753
|
const reason = result && "role" in result ? void 0 : result?.reason;
|
|
5139
5754
|
if (!message) {
|
|
@@ -5148,9 +5763,11 @@ var AgentSessionAutoContinueController = class {
|
|
|
5148
5763
|
this.options.agent.followUp(message);
|
|
5149
5764
|
const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
|
|
5150
5765
|
await new Promise((resolve3) => {
|
|
5766
|
+
this.cancelExecutionWait = resolve3;
|
|
5151
5767
|
this.autoContinueTimer = setTimer(() => {
|
|
5152
5768
|
this.autoContinueTimer = void 0;
|
|
5153
|
-
|
|
5769
|
+
this.cancelExecutionWait = void 0;
|
|
5770
|
+
if (generation !== this.generation || this.options.getDisposed()) {
|
|
5154
5771
|
resolve3();
|
|
5155
5772
|
return;
|
|
5156
5773
|
}
|
|
@@ -5203,7 +5820,7 @@ async function runAgentSessionCompaction(params) {
|
|
|
5203
5820
|
throw new Error("No compaction handler configured");
|
|
5204
5821
|
}
|
|
5205
5822
|
const abortedResult = () => ({ aborted: true });
|
|
5206
|
-
const isAborted = () => params.signal?.aborted === true;
|
|
5823
|
+
const isAborted = () => params.signal?.aborted === true || params.isCurrent?.() === false;
|
|
5207
5824
|
params.emit({ type: "auto_compaction_start", reason: params.reason, willRetry: params.willRetry });
|
|
5208
5825
|
try {
|
|
5209
5826
|
if (isAborted()) {
|
|
@@ -5344,7 +5961,7 @@ async function runPreRequestAutoCompact(params) {
|
|
|
5344
5961
|
try {
|
|
5345
5962
|
await params.runAutoCompact();
|
|
5346
5963
|
} catch {
|
|
5347
|
-
params.setRecoveryState("none");
|
|
5964
|
+
if (params.isCurrent?.() !== false) params.setRecoveryState("none");
|
|
5348
5965
|
}
|
|
5349
5966
|
}
|
|
5350
5967
|
|
|
@@ -5518,6 +6135,7 @@ var AgentSessionQueueDispatcher = class {
|
|
|
5518
6135
|
}
|
|
5519
6136
|
options;
|
|
5520
6137
|
dispatchTimer;
|
|
6138
|
+
generation = 0;
|
|
5521
6139
|
pendingDispatch;
|
|
5522
6140
|
resolvePendingDispatch;
|
|
5523
6141
|
get hasPendingDispatch() {
|
|
@@ -5532,25 +6150,32 @@ var AgentSessionQueueDispatcher = class {
|
|
|
5532
6150
|
}
|
|
5533
6151
|
this.ensurePending();
|
|
5534
6152
|
const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout3;
|
|
6153
|
+
const generation = this.generation;
|
|
5535
6154
|
this.dispatchTimer = setTimer(() => {
|
|
5536
6155
|
this.dispatchTimer = void 0;
|
|
5537
|
-
void this.dispatchNext().
|
|
6156
|
+
void this.dispatchNext().catch((error) => {
|
|
6157
|
+
if (generation === this.generation) this.options.emit({ type: "turn_policy_error", message: String(error) });
|
|
6158
|
+
}).finally(() => {
|
|
6159
|
+
if (generation === this.generation) this.resolvePending();
|
|
6160
|
+
});
|
|
5538
6161
|
}, delayMs);
|
|
5539
6162
|
}
|
|
5540
6163
|
cancel() {
|
|
5541
|
-
|
|
5542
|
-
|
|
6164
|
+
this.generation++;
|
|
6165
|
+
if (this.dispatchTimer !== void 0) {
|
|
6166
|
+
const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout3;
|
|
6167
|
+
clearTimer(this.dispatchTimer);
|
|
6168
|
+
this.dispatchTimer = void 0;
|
|
5543
6169
|
}
|
|
5544
|
-
const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout3;
|
|
5545
|
-
clearTimer(this.dispatchTimer);
|
|
5546
|
-
this.dispatchTimer = void 0;
|
|
5547
6170
|
this.resolvePending();
|
|
5548
6171
|
}
|
|
5549
6172
|
async dispatchNext() {
|
|
6173
|
+
const generation = this.generation;
|
|
5550
6174
|
if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
|
|
5551
6175
|
return false;
|
|
5552
6176
|
}
|
|
5553
6177
|
await this.options.agent.waitForIdle();
|
|
6178
|
+
if (generation !== this.generation) return false;
|
|
5554
6179
|
if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
|
|
5555
6180
|
this.schedule(100);
|
|
5556
6181
|
return false;
|
|
@@ -5565,6 +6190,7 @@ var AgentSessionQueueDispatcher = class {
|
|
|
5565
6190
|
cwd: this.options.cwd,
|
|
5566
6191
|
queuedInput: cloneQueuedInput(queuedInput)
|
|
5567
6192
|
});
|
|
6193
|
+
if (generation !== this.generation) return false;
|
|
5568
6194
|
const dispatchDecision = typeof decision === "boolean" ? { allowed: decision } : decision ?? { allowed: true };
|
|
5569
6195
|
if (!dispatchDecision.allowed) {
|
|
5570
6196
|
this.options.emit({
|
|
@@ -5578,6 +6204,7 @@ var AgentSessionQueueDispatcher = class {
|
|
|
5578
6204
|
return false;
|
|
5579
6205
|
}
|
|
5580
6206
|
await this.options.runPreRequestAutoCompact();
|
|
6207
|
+
if (generation !== this.generation) return false;
|
|
5581
6208
|
if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
|
|
5582
6209
|
this.schedule(100);
|
|
5583
6210
|
return false;
|
|
@@ -5601,6 +6228,7 @@ var AgentSessionQueueDispatcher = class {
|
|
|
5601
6228
|
clearEditor: dispatched.clearEditor,
|
|
5602
6229
|
visibility: dispatched.visibility
|
|
5603
6230
|
});
|
|
6231
|
+
if (generation !== this.generation) return false;
|
|
5604
6232
|
if (!prepared.handled) {
|
|
5605
6233
|
if (prepared.systemPrompt !== void 0) {
|
|
5606
6234
|
this.options.agent.setSystemPrompt(prepared.systemPrompt);
|
|
@@ -5716,6 +6344,7 @@ var AgentSessionRetryController = class {
|
|
|
5716
6344
|
/** Total attempts across kinds in this recovery episode, for reporting. */
|
|
5717
6345
|
totalRetryAttempts = 0;
|
|
5718
6346
|
retryTimer;
|
|
6347
|
+
retryEpoch = 0;
|
|
5719
6348
|
pendingRetry;
|
|
5720
6349
|
resolvePendingRetry;
|
|
5721
6350
|
get policy() {
|
|
@@ -5771,18 +6400,26 @@ var AgentSessionRetryController = class {
|
|
|
5771
6400
|
const rawDelayMs = (this.retry.baseDelayMs ?? 2e3) * 2 ** (attempt - 1);
|
|
5772
6401
|
const delayMs = this.retry.maxDelayMs !== void 0 ? Math.min(rawDelayMs, this.retry.maxDelayMs) : rawDelayMs;
|
|
5773
6402
|
this.ensurePending();
|
|
6403
|
+
const epoch = this.retryEpoch;
|
|
6404
|
+
const pending = this.pendingRetry;
|
|
5774
6405
|
this.setRecoveryState("retry_pending");
|
|
5775
6406
|
this.emit({ type: "turn_recovery_pending", recovery: "retry", failure, attempt, maxRetries, delayMs });
|
|
6407
|
+
if (epoch !== this.retryEpoch) return;
|
|
5776
6408
|
this.emit({ type: "retry_scheduled", errorMessage: failure.message, attempt, maxRetries, delayMs });
|
|
6409
|
+
if (epoch !== this.retryEpoch) return;
|
|
5777
6410
|
this.cancelTimerOnly();
|
|
5778
6411
|
const setTimer = this.retry.setTimeout ?? defaultSetTimeout4;
|
|
5779
6412
|
this.retryTimer = setTimer(() => {
|
|
6413
|
+
if (epoch !== this.retryEpoch) return;
|
|
5780
6414
|
this.retryTimer = void 0;
|
|
5781
6415
|
this.setRecoveryState("retrying");
|
|
5782
6416
|
this.emit({ type: "retry_start", attempt, maxRetries });
|
|
6417
|
+
if (epoch !== this.retryEpoch) return;
|
|
5783
6418
|
this.emit({ type: "turn_retry_started", attempt, maxRetries, failure });
|
|
6419
|
+
if (epoch !== this.retryEpoch) return;
|
|
5784
6420
|
this.removeTrailingAssistantError();
|
|
5785
6421
|
void this.continueTurn().catch((error) => {
|
|
6422
|
+
if (epoch !== this.retryEpoch || pending !== this.pendingRetry) return;
|
|
5786
6423
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5787
6424
|
const terminalFailure = asTerminalFailure(failure, errorMessage);
|
|
5788
6425
|
this.emit({
|
|
@@ -5794,7 +6431,7 @@ var AgentSessionRetryController = class {
|
|
|
5794
6431
|
this.emit({ type: "turn_settled" });
|
|
5795
6432
|
this.resetAttempts();
|
|
5796
6433
|
this.setRecoveryState("terminal");
|
|
5797
|
-
this.resolvePending();
|
|
6434
|
+
this.resolvePending(pending);
|
|
5798
6435
|
});
|
|
5799
6436
|
}, delayMs);
|
|
5800
6437
|
}
|
|
@@ -5826,6 +6463,7 @@ var AgentSessionRetryController = class {
|
|
|
5826
6463
|
this.resolvePending();
|
|
5827
6464
|
}
|
|
5828
6465
|
cancel() {
|
|
6466
|
+
this.retryEpoch++;
|
|
5829
6467
|
this.cancelTimerOnly();
|
|
5830
6468
|
this.resetAttempts();
|
|
5831
6469
|
this.resolvePending();
|
|
@@ -5850,7 +6488,8 @@ var AgentSessionRetryController = class {
|
|
|
5850
6488
|
this.resolvePendingRetry = resolve3;
|
|
5851
6489
|
});
|
|
5852
6490
|
}
|
|
5853
|
-
resolvePending() {
|
|
6491
|
+
resolvePending(expected = this.pendingRetry) {
|
|
6492
|
+
if (expected !== this.pendingRetry) return;
|
|
5854
6493
|
const resolve3 = this.resolvePendingRetry;
|
|
5855
6494
|
this.pendingRetry = void 0;
|
|
5856
6495
|
this.resolvePendingRetry = void 0;
|
|
@@ -6015,9 +6654,14 @@ function classifyControllerRuntimeError(params) {
|
|
|
6015
6654
|
return createFailureClassification({ errorMessage, phase: params.phase }, params.retry, params.classifyFailure);
|
|
6016
6655
|
}
|
|
6017
6656
|
async function runAgentSessionRuntimePhase(params) {
|
|
6657
|
+
const startedGeneration = params.getCurrentGeneration?.();
|
|
6658
|
+
const isStale = () => params.getCurrentGeneration !== void 0 && startedGeneration !== void 0 && params.getCurrentGeneration() !== startedGeneration;
|
|
6018
6659
|
try {
|
|
6019
6660
|
return await params.action();
|
|
6020
6661
|
} catch (error) {
|
|
6662
|
+
if (isStale()) {
|
|
6663
|
+
throw error;
|
|
6664
|
+
}
|
|
6021
6665
|
const failure = classifyControllerRuntimeError({
|
|
6022
6666
|
error,
|
|
6023
6667
|
phase: params.phase,
|
|
@@ -6179,6 +6823,8 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
|
|
|
6179
6823
|
if (params.disposed || params.runtimeGuardTriggered) {
|
|
6180
6824
|
return;
|
|
6181
6825
|
}
|
|
6826
|
+
const startedGeneration = params.getCurrentGeneration?.();
|
|
6827
|
+
const isStale = () => params.getCurrentGeneration !== void 0 && params.getCurrentGeneration() !== startedGeneration;
|
|
6182
6828
|
const failure = classifyControllerTurnFailure({
|
|
6183
6829
|
message: params.message,
|
|
6184
6830
|
retry: params.retry,
|
|
@@ -6198,6 +6844,7 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
|
|
|
6198
6844
|
message: params.message,
|
|
6199
6845
|
messages: params.agent.state.messages.slice()
|
|
6200
6846
|
});
|
|
6847
|
+
if (isStale()) return;
|
|
6201
6848
|
if (failure?.compactable) {
|
|
6202
6849
|
params.scheduleAutoCompact(compactDecision ?? { reason: "overflow", willRetry: true }, failure);
|
|
6203
6850
|
return;
|
|
@@ -6216,14 +6863,18 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
|
|
|
6216
6863
|
return;
|
|
6217
6864
|
}
|
|
6218
6865
|
await params.handleRetryTurnEnd(params.message, failure);
|
|
6866
|
+
if (isStale()) return;
|
|
6219
6867
|
params.setRecoveryState("terminal");
|
|
6220
6868
|
params.emit({ type: "turn_failed_terminal", failure: asTerminalFailure(failure), message: params.message });
|
|
6869
|
+
if (isStale()) return;
|
|
6221
6870
|
params.emit({ type: "turn_settled" });
|
|
6222
6871
|
return;
|
|
6223
6872
|
}
|
|
6224
6873
|
await params.handleRetryTurnEnd(params.message);
|
|
6874
|
+
if (isStale()) return;
|
|
6225
6875
|
if (isAssistantTerminalCompletion(params.message)) {
|
|
6226
6876
|
params.emit({ type: "turn_completed", message: params.message, toolResults: params.toolResults });
|
|
6877
|
+
if (isStale()) return;
|
|
6227
6878
|
const postTurnCompactDecision = compactDecision ?? params.getAutoCompactDecision?.({
|
|
6228
6879
|
agent: params.agent,
|
|
6229
6880
|
sessionManager: params.sessionManager,
|
|
@@ -6231,6 +6882,7 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
|
|
|
6231
6882
|
message: params.message,
|
|
6232
6883
|
messages: params.agent.state.messages.slice()
|
|
6233
6884
|
});
|
|
6885
|
+
if (isStale()) return;
|
|
6234
6886
|
if (postTurnCompactDecision?.reason === "threshold") {
|
|
6235
6887
|
params.scheduleAutoCompact(postTurnCompactDecision);
|
|
6236
6888
|
return;
|
|
@@ -6250,6 +6902,7 @@ async function runAgentSessionControllerAutoCompact(params) {
|
|
|
6250
6902
|
reason: params.reason,
|
|
6251
6903
|
willRetry: params.willRetry,
|
|
6252
6904
|
signal: params.signal,
|
|
6905
|
+
isCurrent: params.isCurrent,
|
|
6253
6906
|
compaction: params.compaction,
|
|
6254
6907
|
agent: params.agent,
|
|
6255
6908
|
sessionManager: params.sessionManager,
|
|
@@ -6260,6 +6913,7 @@ async function runAgentSessionControllerAutoCompact(params) {
|
|
|
6260
6913
|
markSyncedToAgentState: params.markSyncedToAgentState,
|
|
6261
6914
|
continueTurn: params.continueTurn
|
|
6262
6915
|
});
|
|
6916
|
+
if (params.isCurrent?.() === false) return result;
|
|
6263
6917
|
if (!params.willRetry || result?.aborted) {
|
|
6264
6918
|
const alreadyCancelled = params.getRecoveryState?.() === "cancelled" || params.getDisposed?.() === true;
|
|
6265
6919
|
params.setRecoveryState(result?.aborted && cancelOnAbort ? "cancelled" : "none");
|
|
@@ -6273,11 +6927,12 @@ async function runAgentSessionControllerAutoCompact(params) {
|
|
|
6273
6927
|
params.emit({ type: "turn_settled" });
|
|
6274
6928
|
}
|
|
6275
6929
|
if (!result?.aborted && runAfterAgentEndHook) {
|
|
6276
|
-
|
|
6930
|
+
await params.handleAgentEndHook(params.agent.state.messages.slice());
|
|
6277
6931
|
}
|
|
6278
6932
|
}
|
|
6279
6933
|
return result;
|
|
6280
6934
|
} catch (error) {
|
|
6935
|
+
if (params.isCurrent?.() === false) return { aborted: true };
|
|
6281
6936
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6282
6937
|
if (!params.willRetry && params.reason !== "overflow") {
|
|
6283
6938
|
params.setRecoveryState("none");
|
|
@@ -6285,7 +6940,7 @@ async function runAgentSessionControllerAutoCompact(params) {
|
|
|
6285
6940
|
params.emit({ type: "turn_settled" });
|
|
6286
6941
|
}
|
|
6287
6942
|
if (runAfterAgentEndHook) {
|
|
6288
|
-
|
|
6943
|
+
await params.handleAgentEndHook(params.agent.state.messages.slice());
|
|
6289
6944
|
}
|
|
6290
6945
|
throw error;
|
|
6291
6946
|
}
|
|
@@ -6303,7 +6958,7 @@ async function runAgentSessionControllerAutoCompact(params) {
|
|
|
6303
6958
|
params.emit({ type: "turn_settled" });
|
|
6304
6959
|
throw error;
|
|
6305
6960
|
} finally {
|
|
6306
|
-
params.clearPendingFailure();
|
|
6961
|
+
if (params.isCurrent?.() !== false) params.clearPendingFailure();
|
|
6307
6962
|
}
|
|
6308
6963
|
}
|
|
6309
6964
|
|
|
@@ -6324,8 +6979,22 @@ var AgentSessionController = class {
|
|
|
6324
6979
|
unsubscribeSemantic;
|
|
6325
6980
|
lastSyncedMessageCount = 0;
|
|
6326
6981
|
disposed = false;
|
|
6982
|
+
transitioning = false;
|
|
6983
|
+
generation = 0;
|
|
6984
|
+
convergence = new SessionConvergence({
|
|
6985
|
+
sessionId: () => this.sessionManager.getActiveSession()?.id,
|
|
6986
|
+
wait: () => this.waitForRuntimePolicies(),
|
|
6987
|
+
busy: () => this.isStreaming || this.hasPendingRuntimePolicy(),
|
|
6988
|
+
queued: () => this.hasQueuedMessages,
|
|
6989
|
+
emit: (event) => this.emit(event)
|
|
6990
|
+
});
|
|
6991
|
+
get workIdentity() {
|
|
6992
|
+
return this.convergence.identity;
|
|
6993
|
+
}
|
|
6994
|
+
pendingEndOperations = new PendingEndOperations();
|
|
6327
6995
|
baseSystemPrompt;
|
|
6328
6996
|
prepareTurn;
|
|
6997
|
+
onExecutionEnd;
|
|
6329
6998
|
afterAgentEnd;
|
|
6330
6999
|
getAutoCompactDecision;
|
|
6331
7000
|
classifyFailure;
|
|
@@ -6340,6 +7009,7 @@ var AgentSessionController = class {
|
|
|
6340
7009
|
this.baseSystemPrompt = options.baseSystemPrompt ?? this.agent.state.systemPrompt;
|
|
6341
7010
|
this.prepareTurn = options.prepareTurn;
|
|
6342
7011
|
this.afterAgentEnd = options.afterAgentEnd;
|
|
7012
|
+
this.onExecutionEnd = options.onExecutionEnd;
|
|
6343
7013
|
this.getAutoCompactDecision = options.getAutoCompactDecision;
|
|
6344
7014
|
this.classifyFailure = options.classifyFailure;
|
|
6345
7015
|
if (options.onEvent) {
|
|
@@ -6417,7 +7087,7 @@ var AgentSessionController = class {
|
|
|
6417
7087
|
return this.agent.state.isStreaming;
|
|
6418
7088
|
}
|
|
6419
7089
|
get hasQueuedMessages() {
|
|
6420
|
-
return this.queue.hasQueuedMessages;
|
|
7090
|
+
return this.queue.hasQueuedMessages || this.agent.hasQueuedMessages();
|
|
6421
7091
|
}
|
|
6422
7092
|
get pendingMessageCount() {
|
|
6423
7093
|
return this.queue.length;
|
|
@@ -6449,18 +7119,32 @@ var AgentSessionController = class {
|
|
|
6449
7119
|
};
|
|
6450
7120
|
}
|
|
6451
7121
|
subscribe(listener) {
|
|
7122
|
+
if (this.disposed) return () => {
|
|
7123
|
+
};
|
|
6452
7124
|
this.listeners.add(listener);
|
|
6453
7125
|
return () => this.listeners.delete(listener);
|
|
6454
7126
|
}
|
|
6455
7127
|
syncFromActiveSession(messages) {
|
|
6456
|
-
|
|
6457
|
-
this.
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
7128
|
+
if (this.disposed || this.transitioning) return;
|
|
7129
|
+
this.transitioning = true;
|
|
7130
|
+
try {
|
|
7131
|
+
this.generation++;
|
|
7132
|
+
this.cancelScheduledWork();
|
|
7133
|
+
this.recoveryState = "none";
|
|
7134
|
+
this.pendingCompactionFailure = void 0;
|
|
7135
|
+
this.pendingEndOperations.clear();
|
|
7136
|
+
const activeSession = this.sessionManager.getActiveSession();
|
|
7137
|
+
this.agent.sessionId = activeSession?.id;
|
|
7138
|
+
const sessionMessages = messages ?? this.sessionManager.getMessages();
|
|
7139
|
+
const conversationMessages = getConversationMessages(sessionMessages);
|
|
7140
|
+
this.agent.replaceMessages(conversationMessages);
|
|
7141
|
+
this.lastSyncedMessageCount = conversationMessages.length;
|
|
7142
|
+
this.queue.clear();
|
|
7143
|
+
this.recordLifecycle({ type: "session_synced" });
|
|
7144
|
+
this.convergence.invalidate("sync");
|
|
7145
|
+
} finally {
|
|
7146
|
+
this.transitioning = false;
|
|
7147
|
+
}
|
|
6464
7148
|
}
|
|
6465
7149
|
markSyncedToAgentState() {
|
|
6466
7150
|
this.lastSyncedMessageCount = this.agent.state.messages.length;
|
|
@@ -6478,7 +7162,12 @@ var AgentSessionController = class {
|
|
|
6478
7162
|
this.agent.setTools(context.tools);
|
|
6479
7163
|
}
|
|
6480
7164
|
}
|
|
6481
|
-
|
|
7165
|
+
prompt(input, options) {
|
|
7166
|
+
if (this.disposed || this.transitioning) return Promise.reject(new Error("Controller disposed or transitioning"));
|
|
7167
|
+
return this.convergence.run(() => this.promptInternal(input, options));
|
|
7168
|
+
}
|
|
7169
|
+
async promptInternal(input, options) {
|
|
7170
|
+
const generation = this.generation;
|
|
6482
7171
|
const messages = normalizePromptInput(input);
|
|
6483
7172
|
if (this.isRecoveryActive()) {
|
|
6484
7173
|
this.queue.enqueue(messages, options?.streamingBehavior ?? "followUp", {
|
|
@@ -6503,8 +7192,9 @@ var AgentSessionController = class {
|
|
|
6503
7192
|
throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
|
|
6504
7193
|
}
|
|
6505
7194
|
await this.runPreRequestAutoCompactIfNeeded();
|
|
7195
|
+
if (generation !== this.generation || this.disposed) return;
|
|
6506
7196
|
const prepared = await this.runRuntimePhase("prepare", () => this.preparePromptMessages(messages, options));
|
|
6507
|
-
if (prepared.handled) {
|
|
7197
|
+
if (generation !== this.generation || this.disposed || prepared.handled) {
|
|
6508
7198
|
return;
|
|
6509
7199
|
}
|
|
6510
7200
|
this.recordLifecycle({ type: "prompt_started" });
|
|
@@ -6512,45 +7202,66 @@ var AgentSessionController = class {
|
|
|
6512
7202
|
this.agent.setSystemPrompt(prepared.systemPrompt);
|
|
6513
7203
|
}
|
|
6514
7204
|
await this.runRuntimePhase("request", () => this.agent.prompt(prepared.messages));
|
|
7205
|
+
if (generation !== this.generation || this.disposed) return;
|
|
6515
7206
|
await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
|
|
6516
7207
|
}
|
|
6517
|
-
|
|
7208
|
+
continue() {
|
|
7209
|
+
return this.disposed || this.transitioning ? Promise.reject(new Error("Controller disposed or transitioning")) : this.convergence.run(() => this.continueInternal());
|
|
7210
|
+
}
|
|
7211
|
+
async continueInternal() {
|
|
7212
|
+
const generation = this.generation;
|
|
6518
7213
|
if (this.isStreaming) {
|
|
6519
7214
|
throw new Error("Agent is already processing. Wait for completion before continuing.");
|
|
6520
7215
|
}
|
|
6521
7216
|
await this.runPreRequestAutoCompactIfNeeded();
|
|
7217
|
+
if (generation !== this.generation || this.disposed) return;
|
|
6522
7218
|
this.recordLifecycle({ type: "prompt_started" });
|
|
6523
7219
|
await this.runRuntimePhase("request", () => this.agent.continue());
|
|
7220
|
+
if (generation !== this.generation || this.disposed) return;
|
|
6524
7221
|
await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
|
|
6525
7222
|
}
|
|
6526
7223
|
abort() {
|
|
6527
|
-
this.
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
this.
|
|
6535
|
-
|
|
6536
|
-
|
|
7224
|
+
if (this.disposed || this.transitioning) return;
|
|
7225
|
+
this.transitioning = true;
|
|
7226
|
+
try {
|
|
7227
|
+
this.generation++;
|
|
7228
|
+
this.pendingEndOperations.clear();
|
|
7229
|
+
this.recordLifecycle({ type: "abort" });
|
|
7230
|
+
const cancelPendingRecovery = this.isRecoveryActive() && !this.isStreaming;
|
|
7231
|
+
this.cancelScheduledWork();
|
|
7232
|
+
if (cancelPendingRecovery) {
|
|
7233
|
+
this.recoveryState = "cancelled";
|
|
7234
|
+
this.emit({ type: "turn_cancelled", reason: "user_abort" });
|
|
7235
|
+
this.emit({ type: "turn_settled" });
|
|
7236
|
+
}
|
|
7237
|
+
this.agent.abort();
|
|
7238
|
+
this.convergence.invalidate("abort");
|
|
7239
|
+
} finally {
|
|
7240
|
+
this.transitioning = false;
|
|
6537
7241
|
}
|
|
6538
|
-
this.agent.abort();
|
|
6539
7242
|
}
|
|
6540
7243
|
waitForIdle() {
|
|
6541
7244
|
return this.agent.waitForIdle();
|
|
6542
7245
|
}
|
|
6543
|
-
|
|
6544
|
-
|
|
7246
|
+
waitForSettled() {
|
|
7247
|
+
return this.convergence.wait();
|
|
7248
|
+
}
|
|
7249
|
+
async waitForRuntimePolicies() {
|
|
7250
|
+
const generation = this.generation;
|
|
7251
|
+
while (!this.disposed && generation === this.generation) {
|
|
6545
7252
|
await this.agent.waitForIdle();
|
|
6546
|
-
if (!this.hasPendingRuntimePolicy()) {
|
|
7253
|
+
if (this.disposed || generation !== this.generation || !this.hasPendingRuntimePolicy()) {
|
|
6547
7254
|
return;
|
|
6548
7255
|
}
|
|
6549
|
-
await Promise.all([this.retryController.waitForRetry(), this.autoCompact.waitForAutoCompact(), this.autoContinue.waitForAutoContinue(), this.queueDispatcher.waitForDispatch()]);
|
|
7256
|
+
await Promise.all([this.pendingEndOperations.wait(), this.retryController.waitForRetry(), this.autoCompact.waitForAutoCompact(), this.autoContinue.waitForAutoContinue(), this.queueDispatcher.waitForDispatch()]);
|
|
6550
7257
|
}
|
|
6551
7258
|
}
|
|
6552
7259
|
async runAutoCompact(reason, willRetry = false, options) {
|
|
6553
|
-
|
|
7260
|
+
const generation = this.generation;
|
|
7261
|
+
if (this.disposed || this.transitioning) throw new Error("Controller disposed or transitioning");
|
|
7262
|
+
const isCurrent = () => generation === this.generation && !this.disposed;
|
|
7263
|
+
return this.convergence.run(() => runAgentSessionControllerAutoCompact({
|
|
7264
|
+
isCurrent,
|
|
6554
7265
|
reason,
|
|
6555
7266
|
willRetry,
|
|
6556
7267
|
signal: options?.signal,
|
|
@@ -6559,7 +7270,9 @@ var AgentSessionController = class {
|
|
|
6559
7270
|
sessionManager: this.sessionManager,
|
|
6560
7271
|
cwd: this.cwd,
|
|
6561
7272
|
pendingFailure: this.pendingCompactionFailure,
|
|
6562
|
-
emit: (event) =>
|
|
7273
|
+
emit: (event) => {
|
|
7274
|
+
if (isCurrent()) this.emit(event);
|
|
7275
|
+
},
|
|
6563
7276
|
setRecoveryState: (state) => {
|
|
6564
7277
|
this.recoveryState = state;
|
|
6565
7278
|
},
|
|
@@ -6577,19 +7290,19 @@ var AgentSessionController = class {
|
|
|
6577
7290
|
emitSettled: options?.emitSettled,
|
|
6578
7291
|
runAfterAgentEndHook: options?.runAfterAgentEndHook,
|
|
6579
7292
|
cancelOnAbort: options?.cancelOnAbort
|
|
6580
|
-
});
|
|
7293
|
+
}));
|
|
6581
7294
|
}
|
|
6582
7295
|
dispose() {
|
|
6583
7296
|
if (this.disposed) return;
|
|
6584
7297
|
this.disposed = true;
|
|
6585
|
-
this.
|
|
6586
|
-
this.
|
|
6587
|
-
this.
|
|
6588
|
-
this.autoContinue.cancel();
|
|
7298
|
+
this.generation++;
|
|
7299
|
+
this.pendingEndOperations.clear();
|
|
7300
|
+
this.cancelScheduledWork();
|
|
6589
7301
|
this.unsubscribeAgent?.();
|
|
6590
7302
|
this.unsubscribeSemantic?.();
|
|
6591
|
-
this.listeners.clear();
|
|
6592
7303
|
this.queue.clear();
|
|
7304
|
+
this.convergence.invalidate("dispose");
|
|
7305
|
+
this.listeners.clear();
|
|
6593
7306
|
}
|
|
6594
7307
|
drainQueuedInputsForEditor() {
|
|
6595
7308
|
return this.queue.drainForEditor();
|
|
@@ -6604,10 +7317,16 @@ var AgentSessionController = class {
|
|
|
6604
7317
|
this.queue.replaceText(search, replacement);
|
|
6605
7318
|
}
|
|
6606
7319
|
requestQueueDispatch() {
|
|
6607
|
-
if (this.queue.length > 0)
|
|
7320
|
+
if (!this.disposed && !this.transitioning && this.queue.length > 0) {
|
|
7321
|
+
void this.convergence.run(async () => {
|
|
7322
|
+
this.queueDispatcher.schedule();
|
|
7323
|
+
});
|
|
7324
|
+
}
|
|
6608
7325
|
}
|
|
6609
7326
|
async runPreRequestAutoCompactIfNeeded() {
|
|
7327
|
+
const generation = this.generation;
|
|
6610
7328
|
return runPreRequestAutoCompact({
|
|
7329
|
+
isCurrent: () => generation === this.generation && !this.disposed,
|
|
6611
7330
|
disposed: this.disposed,
|
|
6612
7331
|
runtimeGuardTriggered: this.runtimeGuard.triggered,
|
|
6613
7332
|
isStreaming: this.isStreaming,
|
|
@@ -6639,24 +7358,32 @@ var AgentSessionController = class {
|
|
|
6639
7358
|
this.queue.markMessageDelivered(event.message);
|
|
6640
7359
|
}
|
|
6641
7360
|
if (event.type === "turn_end") {
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
});
|
|
7361
|
+
this.pendingEndOperations.track(() => this.syncNewAgentMessages().catch((error) => {
|
|
7362
|
+
this.emit({ type: "turn_policy_error", message: String(error) });
|
|
7363
|
+
}));
|
|
6645
7364
|
this.recordLifecycle({ type: "turn_end_success" });
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
this.
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
7365
|
+
const generation = this.generation;
|
|
7366
|
+
this.pendingEndOperations.track(() => this.handleTurnEndRuntimePolicies(event.message, event.toolResults).catch((error) => {
|
|
7367
|
+
if (generation !== this.generation || this.disposed) return;
|
|
7368
|
+
handleTurnEndPolicyThrow((event2) => this.emit(event2), error, {
|
|
7369
|
+
recoveryWasActive: this.isRecoveryActive(),
|
|
7370
|
+
cancelPendingRecovery: () => {
|
|
7371
|
+
this.retryController.cancel();
|
|
7372
|
+
this.autoCompact.cancel();
|
|
7373
|
+
},
|
|
7374
|
+
markRecoveryTerminal: () => {
|
|
7375
|
+
this.recoveryState = "terminal";
|
|
7376
|
+
}
|
|
7377
|
+
});
|
|
6655
7378
|
}));
|
|
6656
7379
|
}
|
|
6657
7380
|
if (event.type === "agent_end") {
|
|
6658
7381
|
this.recordLifecycle({ type: "agent_end" });
|
|
6659
|
-
|
|
7382
|
+
const generation = this.generation;
|
|
7383
|
+
this.pendingEndOperations.track(async () => {
|
|
7384
|
+
await runEndHook(this.onExecutionEnd, { messages: event.messages, agent: this.agent, sessionManager: this.sessionManager, cwd: this.cwd, hasQueuedMessages: this.hasQueuedMessages }, () => generation === this.generation && !this.disposed, (e) => this.emit(e));
|
|
7385
|
+
if (generation === this.generation && !this.disposed) await this.handleAgentEndHook(event.messages);
|
|
7386
|
+
});
|
|
6660
7387
|
this.emit({ type: "streaming_changed", isStreaming: false });
|
|
6661
7388
|
}
|
|
6662
7389
|
}
|
|
@@ -6702,6 +7429,7 @@ var AgentSessionController = class {
|
|
|
6702
7429
|
getAutoCompactDecision: this.getAutoCompactDecision,
|
|
6703
7430
|
classifyFailure: this.classifyFailure,
|
|
6704
7431
|
retry: this.retryController.policy,
|
|
7432
|
+
getCurrentGeneration: () => this.generation,
|
|
6705
7433
|
emit: (event) => this.emit(event),
|
|
6706
7434
|
setRecoveryState: (state) => {
|
|
6707
7435
|
this.recoveryState = state;
|
|
@@ -6712,27 +7440,12 @@ var AgentSessionController = class {
|
|
|
6712
7440
|
});
|
|
6713
7441
|
}
|
|
6714
7442
|
async handleAgentEndHook(messages) {
|
|
7443
|
+
const generation = this.generation;
|
|
6715
7444
|
if (this.disposed || this.runtimeGuard.triggered || this.isBlockedByRuntimePolicy()) {
|
|
6716
7445
|
return;
|
|
6717
7446
|
}
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
try {
|
|
6721
|
-
await this.afterAgentEnd({
|
|
6722
|
-
messages,
|
|
6723
|
-
agent: this.agent,
|
|
6724
|
-
sessionManager: this.sessionManager,
|
|
6725
|
-
cwd: this.cwd,
|
|
6726
|
-
hasQueuedMessages: this.hasQueuedMessages
|
|
6727
|
-
});
|
|
6728
|
-
this.emit({ type: "agent_end_hook_end" });
|
|
6729
|
-
} catch (error) {
|
|
6730
|
-
this.emit({
|
|
6731
|
-
type: "agent_end_hook_end",
|
|
6732
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
6733
|
-
});
|
|
6734
|
-
}
|
|
6735
|
-
}
|
|
7447
|
+
await runEndHook(this.afterAgentEnd, { messages, agent: this.agent, sessionManager: this.sessionManager, cwd: this.cwd, hasQueuedMessages: this.hasQueuedMessages }, () => generation === this.generation && !this.disposed, (event) => this.emit(event));
|
|
7448
|
+
if (generation !== this.generation || this.disposed) return;
|
|
6736
7449
|
if (this.queue.length > 0) {
|
|
6737
7450
|
this.queueDispatcher.schedule();
|
|
6738
7451
|
}
|
|
@@ -6753,6 +7466,7 @@ var AgentSessionController = class {
|
|
|
6753
7466
|
action,
|
|
6754
7467
|
retry: this.retryController.policy,
|
|
6755
7468
|
classifyFailure: this.classifyFailure,
|
|
7469
|
+
getCurrentGeneration: () => this.generation,
|
|
6756
7470
|
emit: (event) => this.emit(event),
|
|
6757
7471
|
setRecoveryState: (state) => {
|
|
6758
7472
|
this.recoveryState = state;
|
|
@@ -6765,8 +7479,14 @@ var AgentSessionController = class {
|
|
|
6765
7479
|
isBlockedByRuntimePolicy() {
|
|
6766
7480
|
return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
|
|
6767
7481
|
}
|
|
7482
|
+
cancelScheduledWork() {
|
|
7483
|
+
this.retryController.cancel();
|
|
7484
|
+
this.autoCompact.cancel();
|
|
7485
|
+
this.queueDispatcher.cancel();
|
|
7486
|
+
this.autoContinue.cancel();
|
|
7487
|
+
}
|
|
6768
7488
|
hasPendingRuntimePolicy() {
|
|
6769
|
-
return this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact || this.autoContinue.hasPendingAutoContinue || this.queueDispatcher.hasPendingDispatch;
|
|
7489
|
+
return this.pendingEndOperations.pending || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact || this.autoContinue.hasPendingAutoContinue || this.queueDispatcher.hasPendingDispatch;
|
|
6770
7490
|
}
|
|
6771
7491
|
scheduleAutoCompact(decision, failure) {
|
|
6772
7492
|
this.recordLifecycle({ type: "auto_compact_scheduled" });
|
|
@@ -6784,10 +7504,7 @@ var AgentSessionController = class {
|
|
|
6784
7504
|
if (this.runtimeGuard.triggered) {
|
|
6785
7505
|
this.runtimeGuard.markTriggered();
|
|
6786
7506
|
}
|
|
6787
|
-
this.
|
|
6788
|
-
this.autoCompact.cancel();
|
|
6789
|
-
this.queueDispatcher.cancel();
|
|
6790
|
-
this.autoContinue.cancel();
|
|
7507
|
+
this.cancelScheduledWork();
|
|
6791
7508
|
this.recoveryState = "terminal";
|
|
6792
7509
|
this.emit({ type: "runtime_guard_triggered", toolName, repeatCount, reason });
|
|
6793
7510
|
this.emit({
|
|
@@ -6798,13 +7515,13 @@ var AgentSessionController = class {
|
|
|
6798
7515
|
this.agent.abort();
|
|
6799
7516
|
}
|
|
6800
7517
|
emit(event) {
|
|
7518
|
+
if (!event.type.startsWith("session_work_") && event.type !== "session_quiescent" && event.type !== "session_invalidated") this.convergence.observe(event);
|
|
6801
7519
|
for (const listener of this.listeners) {
|
|
6802
|
-
listener
|
|
7520
|
+
notifyObserver(listener, event);
|
|
6803
7521
|
}
|
|
6804
7522
|
}
|
|
6805
7523
|
recordLifecycle(input) {
|
|
6806
7524
|
this.lifecycleState = reduceAgentSessionLifecycle(this.lifecycleState, input);
|
|
6807
|
-
void this.lifecycleState.phase;
|
|
6808
7525
|
}
|
|
6809
7526
|
};
|
|
6810
7527
|
|
|
@@ -6987,6 +7704,109 @@ async function requestInteraction(request) {
|
|
|
6987
7704
|
return await currentInteractionHandler(request);
|
|
6988
7705
|
}
|
|
6989
7706
|
|
|
7707
|
+
// ../../packages/agent-framework/src/attention.ts
|
|
7708
|
+
function same(a, b) {
|
|
7709
|
+
return a?.sessionId === b.sessionId && a?.generation === b.generation;
|
|
7710
|
+
}
|
|
7711
|
+
var AttentionCoordinator = class {
|
|
7712
|
+
constructor(policy = {}) {
|
|
7713
|
+
this.policy = policy;
|
|
7714
|
+
}
|
|
7715
|
+
policy;
|
|
7716
|
+
current;
|
|
7717
|
+
requests = /* @__PURE__ */ new Map();
|
|
7718
|
+
listeners = /* @__PURE__ */ new Set();
|
|
7719
|
+
sequence = 0;
|
|
7720
|
+
userWork = false;
|
|
7721
|
+
consumed = false;
|
|
7722
|
+
disposed = false;
|
|
7723
|
+
get activeRequests() {
|
|
7724
|
+
return [...this.requests.values()].map((r) => ({ ...r }));
|
|
7725
|
+
}
|
|
7726
|
+
subscribe(listener) {
|
|
7727
|
+
if (this.disposed) return () => {
|
|
7728
|
+
};
|
|
7729
|
+
this.listeners.add(listener);
|
|
7730
|
+
return () => this.listeners.delete(listener);
|
|
7731
|
+
}
|
|
7732
|
+
/** Feed controller work events only. Old turn_settled/skipped events are not outcomes. */
|
|
7733
|
+
handle(event) {
|
|
7734
|
+
if (this.disposed || this.current && event.generation < this.current.generation) return;
|
|
7735
|
+
if (event.type === "session_invalidated") {
|
|
7736
|
+
this.current = { sessionId: event.sessionId, generation: event.generation };
|
|
7737
|
+
this.consumed = true;
|
|
7738
|
+
this.clear();
|
|
7739
|
+
return;
|
|
7740
|
+
}
|
|
7741
|
+
if (event.type === "session_work_started") {
|
|
7742
|
+
if (same(this.current, event)) return;
|
|
7743
|
+
this.current = { sessionId: event.sessionId, generation: event.generation };
|
|
7744
|
+
this.consumed = false;
|
|
7745
|
+
this.clear();
|
|
7746
|
+
return;
|
|
7747
|
+
}
|
|
7748
|
+
if (!same(this.current, event) || this.consumed) return;
|
|
7749
|
+
this.consumed = true;
|
|
7750
|
+
const userWork = this.userWork;
|
|
7751
|
+
this.userWork = false;
|
|
7752
|
+
if (event.outcome === "cancelled") this.clear();
|
|
7753
|
+
else if (event.outcome === "failed" || event.outcome === "blocked") this.request(event, event.outcome);
|
|
7754
|
+
else if (this.policy.completion === "accepted-user-work" && userWork) this.request(event, "completed");
|
|
7755
|
+
}
|
|
7756
|
+
/** Call at actual accepted human delivery, including steer/retry; never infer from message role. */
|
|
7757
|
+
acceptUserWork(identity) {
|
|
7758
|
+
if (this.disposed || !same(this.current, identity) || this.consumed) return false;
|
|
7759
|
+
this.userWork = true;
|
|
7760
|
+
return true;
|
|
7761
|
+
}
|
|
7762
|
+
/** Producers retain the returned ID until answered/cancelled. Duplicate IDs do not broadcast twice. */
|
|
7763
|
+
request(identity, reason, id = `attention-${++this.sequence}`) {
|
|
7764
|
+
if (this.disposed || !same(this.current, identity)) return void 0;
|
|
7765
|
+
const existing = this.requests.get(id);
|
|
7766
|
+
if (existing) return { ...existing };
|
|
7767
|
+
const request = { sessionId: identity.sessionId, generation: identity.generation, id, reason };
|
|
7768
|
+
this.requests.set(id, request);
|
|
7769
|
+
this.emit({ type: "attention_requested", ...request });
|
|
7770
|
+
return { ...request };
|
|
7771
|
+
}
|
|
7772
|
+
/** Only the interaction owner resolves; suppressing/defering host presentation is not resolution. */
|
|
7773
|
+
resolve(id, identity, resolution = "answered") {
|
|
7774
|
+
const request = this.requests.get(id);
|
|
7775
|
+
if (!request || !same(request, identity)) return false;
|
|
7776
|
+
this.requests.delete(id);
|
|
7777
|
+
this.emit({ type: "attention_resolved", ...request, resolution });
|
|
7778
|
+
return true;
|
|
7779
|
+
}
|
|
7780
|
+
dispose() {
|
|
7781
|
+
if (this.disposed) return;
|
|
7782
|
+
this.disposed = true;
|
|
7783
|
+
this.current = void 0;
|
|
7784
|
+
this.consumed = true;
|
|
7785
|
+
this.clear();
|
|
7786
|
+
this.listeners.clear();
|
|
7787
|
+
}
|
|
7788
|
+
clear() {
|
|
7789
|
+
const requests = [...this.requests.values()];
|
|
7790
|
+
this.requests.clear();
|
|
7791
|
+
this.userWork = false;
|
|
7792
|
+
for (const request of requests) this.emit({ type: "attention_resolved", ...request, resolution: "invalidated" });
|
|
7793
|
+
}
|
|
7794
|
+
emit(event) {
|
|
7795
|
+
for (const listener of this.listeners) notifyObserver(listener, event);
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
|
|
7799
|
+
// ../../packages/agent-framework/src/interaction-attention.ts
|
|
7800
|
+
function interactionAttentionReason(request) {
|
|
7801
|
+
return request.type === "notify" ? void 0 : "input-required";
|
|
7802
|
+
}
|
|
7803
|
+
function interactionAttentionResolution(response) {
|
|
7804
|
+
if (!response || typeof response !== "object") return "cancelled";
|
|
7805
|
+
const result = response;
|
|
7806
|
+
if (result.timedOut || result.skipped || result.unavailable) return "cancelled";
|
|
7807
|
+
return ["confirmed", "selection", "value", "answers"].some((key) => result[key] !== void 0) ? "answered" : "cancelled";
|
|
7808
|
+
}
|
|
7809
|
+
|
|
6990
7810
|
// ../../packages/agent-framework/src/context-files.ts
|
|
6991
7811
|
var AGENTS_CONTEXT_FILE_NAME = "AGENTS.md";
|
|
6992
7812
|
function normalizeContextPath(path4) {
|
|
@@ -8849,14 +9669,23 @@ function createFileObservationState(options = {}) {
|
|
|
8849
9669
|
function coverageSpansFile(coveredRanges, totalLines) {
|
|
8850
9670
|
if (totalLines <= 0) return true;
|
|
8851
9671
|
const merged = mergeLineRanges(coveredRanges);
|
|
8852
|
-
return merged.length
|
|
9672
|
+
return merged.length === 1 && merged[0].startLine <= 1 && merged[0].endLine >= totalLines;
|
|
8853
9673
|
}
|
|
8854
9674
|
function summarize(observed) {
|
|
8855
9675
|
const merged = mergeLineRanges(observed.coveredRanges);
|
|
8856
9676
|
const coveredLines = merged.reduce((total, range2) => total + (range2.endLine - range2.startLine + 1), 0);
|
|
8857
9677
|
let firstUnseenLine;
|
|
8858
9678
|
if (observed.coveredTotalLines > 0 && coveredLines < observed.coveredTotalLines) {
|
|
8859
|
-
firstUnseenLine =
|
|
9679
|
+
firstUnseenLine = 1;
|
|
9680
|
+
if (merged.length > 0 && merged[0].startLine <= 1) {
|
|
9681
|
+
firstUnseenLine = merged[merged.length - 1].endLine + 1;
|
|
9682
|
+
for (let index = 0; index + 1 < merged.length; index++) {
|
|
9683
|
+
if (merged[index + 1].startLine > merged[index].endLine + 1) {
|
|
9684
|
+
firstUnseenLine = merged[index].endLine + 1;
|
|
9685
|
+
break;
|
|
9686
|
+
}
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
8860
9689
|
}
|
|
8861
9690
|
let lastRange;
|
|
8862
9691
|
for (const range2 of observed.readRanges.values()) {
|
|
@@ -8972,22 +9801,32 @@ function createReadTool(cwd, options) {
|
|
|
8972
9801
|
...rootSchema ? { root: rootSchema } : {},
|
|
8973
9802
|
offset: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Line number to start reading from, 1-indexed. Example: 201." })),
|
|
8974
9803
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Maximum number of lines to read. Example: 120." })),
|
|
9804
|
+
tail: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Read the last N lines of the file, without needing to know the total line count. Useful for logs and files that grow. Mutually exclusive with offset/limit. Example: tail=50." })),
|
|
8975
9805
|
question: Type.Optional(Type.String({ description: "Optional focused question for image, video, audio, or document understanding. Use this when asking what a media/document file contains; omit for ordinary text reads." }))
|
|
8976
9806
|
});
|
|
8977
9807
|
const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
|
|
8978
9808
|
return {
|
|
8979
9809
|
name: "read",
|
|
8980
9810
|
label: "read",
|
|
8981
|
-
description: `Read a file's contents. Use this before editing, and use offset/limit for large files. Images are attached for the current model when it supports vision; otherwise a configured file-understanding service analyzes them. Video, audio, and documents require a configured understanding service. Pass question to specify what to focus on; it does not force a separate model request. Example: read_file path='Scripts/PlayerController.cs' or read_file path='Data/config.json'. ${pathResolutionDescription} For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). When you need the full file, continue with a larger offset until complete.`,
|
|
9811
|
+
description: `Read a file's contents. Use this before editing, and use offset/limit for large files, or tail=N to read the last N lines when the line count is unknown. Images are attached for the current model when it supports vision; otherwise a configured file-understanding service analyzes them. Video, audio, and documents require a configured understanding service. Pass question to specify what to focus on; it does not force a separate model request. Example: read_file path='Scripts/PlayerController.cs' or read_file path='Data/config.json'. ${pathResolutionDescription} For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). When you need the full file, continue with a larger offset until complete.`,
|
|
8982
9812
|
parameters: readSchema,
|
|
8983
9813
|
execute: async (_toolCallId, params, signal) => {
|
|
8984
|
-
const { path: path4, root,
|
|
9814
|
+
const { path: path4, root, question } = params;
|
|
9815
|
+
const tail = params.tail;
|
|
9816
|
+
let offset = params.offset;
|
|
9817
|
+
let limit = params.limit;
|
|
8985
9818
|
if (offset !== void 0 && (!Number.isSafeInteger(offset) || offset < 1 || offset > MAX_CROSS_HOST_LINE_NUMBER)) {
|
|
8986
9819
|
return createReadErrorResult(`Invalid arguments: offset must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
|
|
8987
9820
|
}
|
|
8988
9821
|
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CROSS_HOST_LINE_NUMBER)) {
|
|
8989
9822
|
return createReadErrorResult(`Invalid arguments: limit must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
|
|
8990
9823
|
}
|
|
9824
|
+
if (tail !== void 0 && (!Number.isSafeInteger(tail) || tail < 1 || tail > MAX_CROSS_HOST_LINE_NUMBER)) {
|
|
9825
|
+
return createReadErrorResult(`Invalid arguments: tail must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
|
|
9826
|
+
}
|
|
9827
|
+
if (tail !== void 0 && (offset !== void 0 || limit !== void 0)) {
|
|
9828
|
+
return createReadErrorResult("Invalid arguments: tail cannot be combined with offset or limit. Use tail=N to read the last N lines, or offset/limit for a window.");
|
|
9829
|
+
}
|
|
8991
9830
|
if (offset !== void 0 && limit !== void 0 && offset - 1 + limit > MAX_CROSS_HOST_LINE_NUMBER) {
|
|
8992
9831
|
return createReadErrorResult(`Invalid arguments: offset and limit exceed the ${MAX_CROSS_HOST_LINE_NUMBER}-line cross-host range.`);
|
|
8993
9832
|
}
|
|
@@ -9151,14 +9990,38 @@ Focus: ${question.trim()}` : ""}` },
|
|
|
9151
9990
|
});
|
|
9152
9991
|
return;
|
|
9153
9992
|
}
|
|
9154
|
-
|
|
9993
|
+
let translatedFromTail = false;
|
|
9994
|
+
let tailWindowIncludesPhantom = false;
|
|
9995
|
+
let tailEffectiveLines;
|
|
9996
|
+
let startLine;
|
|
9997
|
+
let range2;
|
|
9998
|
+
if (tail !== void 0) {
|
|
9999
|
+
const effectiveTail = Math.max(1, Math.min(tail, DEFAULT_MAX_LINES - 1));
|
|
10000
|
+
translatedFromTail = true;
|
|
10001
|
+
tailEffectiveLines = effectiveTail;
|
|
10002
|
+
range2 = await fs2.readTextRange(absolutePath, {
|
|
10003
|
+
tailLines: effectiveTail,
|
|
10004
|
+
maxOutputLines: DEFAULT_MAX_LINES,
|
|
10005
|
+
maxOutputBytes: DEFAULT_MAX_BYTES
|
|
10006
|
+
}, signal);
|
|
10007
|
+
tailWindowIncludesPhantom = range2.endsWithNewline === true;
|
|
10008
|
+
if (range2.selectedLines > 0) {
|
|
10009
|
+
const derivedHead = range2.totalLines - range2.selectedLines + 1;
|
|
10010
|
+
startLine = range2.outputStartLine !== void 0 && !range2.firstLineExceedsLimit ? range2.outputStartLine - 1 : derivedHead - 1;
|
|
10011
|
+
limit = range2.selectedLines;
|
|
10012
|
+
} else {
|
|
10013
|
+
startLine = 0;
|
|
10014
|
+
}
|
|
10015
|
+
} else {
|
|
10016
|
+
startLine = offset === void 0 ? 0 : offset - 1;
|
|
10017
|
+
range2 = await fs2.readTextRange(absolutePath, {
|
|
10018
|
+
offsetLine: startLine,
|
|
10019
|
+
limitLines: limit,
|
|
10020
|
+
maxOutputLines: DEFAULT_MAX_LINES,
|
|
10021
|
+
maxOutputBytes: DEFAULT_MAX_BYTES
|
|
10022
|
+
}, signal);
|
|
10023
|
+
}
|
|
9155
10024
|
const startLineDisplay = startLine + 1;
|
|
9156
|
-
const range2 = await fs2.readTextRange(absolutePath, {
|
|
9157
|
-
offsetLine: startLine,
|
|
9158
|
-
limitLines: limit,
|
|
9159
|
-
maxOutputLines: DEFAULT_MAX_LINES,
|
|
9160
|
-
maxOutputBytes: DEFAULT_MAX_BYTES
|
|
9161
|
-
}, signal);
|
|
9162
10025
|
if (options?.allowBinary === false) {
|
|
9163
10026
|
const selectedLooksBinary = range2.selectedBytesLookBinary ?? (looksLikeBinaryText(range2.content) || range2.content.includes("\uFFFD"));
|
|
9164
10027
|
const selectedBinary = selectedLooksBinary ? { kind: "document", mimeType: "application/octet-stream" } : void 0;
|
|
@@ -9177,7 +10040,7 @@ Focus: ${question.trim()}` : ""}` },
|
|
|
9177
10040
|
contentHash: range2.contentHash
|
|
9178
10041
|
};
|
|
9179
10042
|
const totalFileLines = range2.totalLines;
|
|
9180
|
-
if (startLine >= range2.totalLines) {
|
|
10043
|
+
if (startLine >= range2.totalLines && !translatedFromTail) {
|
|
9181
10044
|
throw new Error(`Offset ${offset} is beyond end of file (${range2.totalLines} lines total)`);
|
|
9182
10045
|
}
|
|
9183
10046
|
let userLimitedLines;
|
|
@@ -9198,7 +10061,7 @@ Focus: ${question.trim()}` : ""}` },
|
|
|
9198
10061
|
maxLines: DEFAULT_MAX_LINES,
|
|
9199
10062
|
maxBytes: DEFAULT_MAX_BYTES
|
|
9200
10063
|
};
|
|
9201
|
-
const readStartsAtBeginning =
|
|
10064
|
+
const readStartsAtBeginning = startLine === 0;
|
|
9202
10065
|
const readReachedEndOfFile = startLine + range2.selectedLines >= range2.totalLines;
|
|
9203
10066
|
const isCompleteTextKnown = readStartsAtBeginning && readReachedEndOfFile && !truncation.truncated && !truncation.firstLineExceedsLimit;
|
|
9204
10067
|
let callerObservation = fileObservationState;
|
|
@@ -9220,13 +10083,14 @@ Focus: ${question.trim()}` : ""}` },
|
|
|
9220
10083
|
resolve3({
|
|
9221
10084
|
content: [{
|
|
9222
10085
|
type: "text",
|
|
9223
|
-
text: `File unchanged since previous read of this range (offset=${startLineDisplay}${limit === void 0 ? ", no limit" : `, limit=${limit}`}). Earlier reads still count toward write_file coverage of this file. Use a different offset/limit if you need another section.`
|
|
10086
|
+
text: translatedFromTail ? `File unchanged since previous tail=${tailEffectiveLines} read (last ${tailWindowIncludesPhantom ? range2.selectedLines - 1 : range2.selectedLines} lines, ${range2.totalLines} lines total). Earlier reads still count toward write_file coverage of this file.` : `File unchanged since previous read of this range (offset=${startLineDisplay}${limit === void 0 ? ", no limit" : `, limit=${limit}`}). Earlier reads still count toward write_file coverage of this file. Use a different offset/limit if you need another section.`
|
|
9224
10087
|
}],
|
|
9225
10088
|
details: {
|
|
9226
10089
|
unchangedSincePreviousRead: true,
|
|
9227
10090
|
path: absolutePath,
|
|
9228
10091
|
offset: startLineDisplay,
|
|
9229
|
-
limit
|
|
10092
|
+
limit,
|
|
10093
|
+
...translatedFromTail ? { totalLines: range2.totalLines } : {}
|
|
9230
10094
|
}
|
|
9231
10095
|
});
|
|
9232
10096
|
return;
|
|
@@ -9245,7 +10109,12 @@ Base directory: ${nodePath.dirname(absolutePath)}
|
|
|
9245
10109
|
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
9246
10110
|
const nextOffset = endLineDisplay + 1;
|
|
9247
10111
|
outputText = truncation.content;
|
|
9248
|
-
if (
|
|
10112
|
+
if (translatedFromTail) {
|
|
10113
|
+
const endsAtLastLine = truncation.content.length > 0 ? ", and the content above ends at the file's last line" : "";
|
|
10114
|
+
outputText += `
|
|
10115
|
+
|
|
10116
|
+
[Tail window hit the output cap: the earliest lines of the requested tail were dropped${endsAtLastLine}. Read earlier lines with offset, or request a smaller tail.]`;
|
|
10117
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
9249
10118
|
outputText += `
|
|
9250
10119
|
|
|
9251
10120
|
[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
|
@@ -9271,18 +10140,38 @@ Base directory: ${nodePath.dirname(absolutePath)}
|
|
|
9271
10140
|
snapshot,
|
|
9272
10141
|
range2.selectedContentHash,
|
|
9273
10142
|
isCompleteTextKnown,
|
|
9274
|
-
{
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
10143
|
+
(() => {
|
|
10144
|
+
if (translatedFromTail && truncation.truncated && !truncation.firstLineExceedsLimit) {
|
|
10145
|
+
const outputStart = range2.outputStartLine ?? startLineDisplay + Math.max(0, range2.selectedLines - range2.outputLines);
|
|
10146
|
+
const clampedStart = Math.min(outputStart, range2.totalLines + 1);
|
|
10147
|
+
const clampedEnd = Math.min(range2.totalLines, clampedStart + range2.outputLines - 1);
|
|
10148
|
+
return {
|
|
10149
|
+
startLine: clampedStart,
|
|
10150
|
+
endLine: clampedStart > range2.totalLines ? clampedStart - 1 : clampedEnd,
|
|
10151
|
+
totalLines: range2.totalLines
|
|
10152
|
+
};
|
|
10153
|
+
}
|
|
10154
|
+
return {
|
|
10155
|
+
// firstLineExceedsLimit yields an intentionally empty range
|
|
10156
|
+
// (start - 1): zero lines were actually shown, so it must
|
|
10157
|
+
// contribute nothing to write coverage.
|
|
10158
|
+
startLine: startLineDisplay,
|
|
10159
|
+
endLine: startLineDisplay + (truncation.firstLineExceedsLimit ? 0 : truncation.truncated ? truncation.outputLines : range2.selectedLines) - 1,
|
|
10160
|
+
totalLines: range2.totalLines
|
|
10161
|
+
};
|
|
10162
|
+
})()
|
|
9282
10163
|
);
|
|
9283
10164
|
if (callerObservation !== fileObservationState) {
|
|
9284
10165
|
callerObservation.recordTextRead(absolutePath, rangeKey, snapshot, range2.selectedContentHash, isCompleteTextKnown);
|
|
9285
10166
|
}
|
|
10167
|
+
if (translatedFromTail && tail !== void 0 && tailEffectiveLines !== void 0 && tailEffectiveLines < tail && startLineDisplay > 1) {
|
|
10168
|
+
outputText += `
|
|
10169
|
+
|
|
10170
|
+
[Tail clamped to ${tailEffectiveLines} lines: the per-read line cap is ${DEFAULT_MAX_LINES}, so the ${startLineDisplay - 1} earliest lines of the file are not shown. Use offset reads to walk earlier lines, or a smaller tail.]`;
|
|
10171
|
+
}
|
|
10172
|
+
if (translatedFromTail) {
|
|
10173
|
+
details = { ...details ?? {}, totalLines: range2.totalLines };
|
|
10174
|
+
}
|
|
9286
10175
|
outputText = sourceHeader + outputText;
|
|
9287
10176
|
const textContent = [{ type: "text", text: outputText }];
|
|
9288
10177
|
if (aborted) return;
|
|
@@ -9661,13 +10550,15 @@ function createWriteTool(cwd, options) {
|
|
|
9661
10550
|
const observedSnapshot = fileObservationState.getCompleteTextSnapshot(absolutePath);
|
|
9662
10551
|
if (!observedSnapshot) {
|
|
9663
10552
|
const summary = fileObservationState.describeObservation(absolutePath);
|
|
9664
|
-
const coverageHint = summary && summary.totalLines > 0 ? ` Previous reads cover ${summary.coveredLines}/${summary.totalLines} lines${summary.lastRange ? ` (last read: lines ${summary.lastRange.startLine}-${summary.lastRange.endLine})` : ""}${summary.firstUnseenLine ? `; read the rest with read_file offset=${summary.firstUnseenLine}` : "; read the full file with read_file (no offset/limit)"}.` : " Read the current full file with read_file (no offset/limit) first.";
|
|
10553
|
+
const coverageHint = summary && summary.totalLines > 0 ? ` Previous reads cover ${summary.coveredLines}/${summary.totalLines} lines${summary.lastRange ? ` (last read: lines ${summary.lastRange.startLine}-${summary.lastRange.endLine})` : ""}${summary.firstUnseenLine ? `; read the rest with read_file offset=${summary.firstUnseenLine}, or a tail read spanning the whole file` : "; read the full file with read_file (no offset/limit, or a tail read spanning the whole file)"}.` : " Read the current full file with read_file (no offset/limit, or a tail read spanning the whole file) first.";
|
|
9665
10554
|
throw new Error(
|
|
9666
10555
|
usesExternalFileObservationState ? `write_file blocked: overwriting requires the file to be fully observed by read_file.${coverageHint} Alternatively, use edit_file for targeted changes.` : "Existing-file overwrite requires write_file to share a fileObservationState with read_file; use createSharedFileSystemTools or pass the same fileObservationState to both tools."
|
|
9667
10556
|
);
|
|
9668
10557
|
}
|
|
9669
10558
|
if (!snapshotsHaveSameContent(observedSnapshot, currentSnapshot)) {
|
|
9670
|
-
throw new Error(
|
|
10559
|
+
throw new Error(
|
|
10560
|
+
`File changed since it was read (${observedSnapshot.size} \u2192 ${currentSnapshot.size} bytes; out-of-band edits such as bash sed/redirects cause this). Re-read with the read_file tool \u2014 offset/limit/tail reads accumulate coverage; bash head/cat does not count \u2014 or use edit_file for targeted changes.`
|
|
10561
|
+
);
|
|
9671
10562
|
}
|
|
9672
10563
|
}
|
|
9673
10564
|
await fs2.writeFile(absolutePath, content, "utf-8");
|
|
@@ -9681,7 +10572,7 @@ function createWriteTool(cwd, options) {
|
|
|
9681
10572
|
signal.removeEventListener("abort", onAbort);
|
|
9682
10573
|
}
|
|
9683
10574
|
resolve3({
|
|
9684
|
-
content: [{ type: "text", text: `Successfully wrote ${
|
|
10575
|
+
content: [{ type: "text", text: `Successfully wrote ${writtenStat.size} bytes to ${filePath}` }],
|
|
9685
10576
|
details: void 0
|
|
9686
10577
|
});
|
|
9687
10578
|
} catch (error) {
|
|
@@ -9714,6 +10605,8 @@ function restoreLineEndings(text, ending) {
|
|
|
9714
10605
|
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
9715
10606
|
}
|
|
9716
10607
|
var MAX_FUZZY_DIAGNOSTIC_CHARS = 256 * 1024;
|
|
10608
|
+
var MAX_CLOSEST_DIFF_REGION_LINES = 80;
|
|
10609
|
+
var MAX_CLOSEST_DIFF_LINES = 30;
|
|
9717
10610
|
var RELAXED_MATCH_WORK_BUDGET = 1e6;
|
|
9718
10611
|
function normalizeForFuzzyMatch(text) {
|
|
9719
10612
|
return buildFuzzyIndexMap(text).normalized;
|
|
@@ -9862,6 +10755,9 @@ function leadingWhitespace(line) {
|
|
|
9862
10755
|
const match2 = line.match(/^[ \t]*/);
|
|
9863
10756
|
return match2 ? match2[0] : "";
|
|
9864
10757
|
}
|
|
10758
|
+
function stripLeadingWhitespace(line) {
|
|
10759
|
+
return line.replace(/^[ \t]*/, "");
|
|
10760
|
+
}
|
|
9865
10761
|
var RelaxedWindow;
|
|
9866
10762
|
((RelaxedWindow2) => {
|
|
9867
10763
|
RelaxedWindow2.EMPTY_MAP = /* @__PURE__ */ new Map();
|
|
@@ -9944,6 +10840,9 @@ function reindentNewText(newText, qLines, fileLines, window) {
|
|
|
9944
10840
|
nIdx.push(i);
|
|
9945
10841
|
}
|
|
9946
10842
|
if (nIdx.length === 0) return newText;
|
|
10843
|
+
if (qIdx.length === nIdx.length && qIdx.every((q, pos) => qLines[q].trim() === newLines[nIdx[pos]].trim()) && qIdx.some((q, pos) => leadingWhitespace(qLines[q]) !== leadingWhitespace(newLines[nIdx[pos]]))) {
|
|
10844
|
+
return newText;
|
|
10845
|
+
}
|
|
9947
10846
|
const counterpart = /* @__PURE__ */ new Map();
|
|
9948
10847
|
let p = 0;
|
|
9949
10848
|
while (p < qIdx.length && p < nIdx.length && qLines[qIdx[p]].trim() === newLines[nIdx[p]].trim()) {
|
|
@@ -10002,7 +10901,7 @@ function reindentNewText(newText, qLines, fileLines, window) {
|
|
|
10002
10901
|
}
|
|
10003
10902
|
const nPos = nPosOfLine.get(i) ?? 0;
|
|
10004
10903
|
const qPos = counterpart.get(nPos) ?? 0;
|
|
10005
|
-
result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + newLines[i]
|
|
10904
|
+
result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + stripLeadingWhitespace(newLines[i]));
|
|
10006
10905
|
}
|
|
10007
10906
|
return result.join("\n");
|
|
10008
10907
|
}
|
|
@@ -10061,7 +10960,8 @@ function describeClosestRegion(content, oldText) {
|
|
|
10061
10960
|
let matched = 0;
|
|
10062
10961
|
let fi = i;
|
|
10063
10962
|
let firstDiff;
|
|
10064
|
-
|
|
10963
|
+
let qi = firstNonBlank;
|
|
10964
|
+
for (; qi < qLines.length; qi++) {
|
|
10065
10965
|
budget -= 1;
|
|
10066
10966
|
if (budget < 0) {
|
|
10067
10967
|
return `Closest-region diagnostics were skipped because the file and the quoted block are too repetitive to compare within the relaxed-match budget \u2014 quote a larger region with distinguishing context lines and retry.`;
|
|
@@ -10070,24 +10970,102 @@ function describeClosestRegion(content, oldText) {
|
|
|
10070
10970
|
if (!q) continue;
|
|
10071
10971
|
while (fi < fileLines.length && !fileLines[fi].trim()) fi++;
|
|
10072
10972
|
if (fi >= fileLines.length || fileLines[fi].trim() !== q) {
|
|
10073
|
-
firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi] };
|
|
10973
|
+
firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi], qIdx: qi };
|
|
10074
10974
|
break;
|
|
10075
10975
|
}
|
|
10076
10976
|
matched++;
|
|
10077
10977
|
fi++;
|
|
10078
10978
|
}
|
|
10079
|
-
if (!best || matched > best.matched) best = { start: i, matched, firstDiff };
|
|
10979
|
+
if (!best || matched > best.matched) best = { start: i, walkEnd: fi, matched, firstDiff };
|
|
10080
10980
|
}
|
|
10081
10981
|
const total = qLines.filter((line) => line.trim() !== "").length;
|
|
10082
10982
|
if (!best) {
|
|
10083
10983
|
return `No line matching ${JSON.stringify(clip(anchor))} exists in this file \u2014 the text may live in a different file, or the file changed since it was read.`;
|
|
10084
10984
|
}
|
|
10085
|
-
const
|
|
10985
|
+
const regionStart = best.start + 1;
|
|
10986
|
+
let regionEnd = Math.max(regionStart, best.walkEnd + (best.firstDiff ? 1 : 0));
|
|
10987
|
+
const parts = [];
|
|
10988
|
+
let alignment = "unknown";
|
|
10989
|
+
let shiftDelta = 0;
|
|
10086
10990
|
if (best.firstDiff) {
|
|
10087
10991
|
parts.push(`First difference at line ${best.firstDiff.fileNo}: file has ${JSON.stringify(clip(best.firstDiff.fileLine.trim()))}, edit expected ${JSON.stringify(clip(best.firstDiff.qLine.trim()))}.`);
|
|
10992
|
+
const suffixIdx = [];
|
|
10993
|
+
for (let s = best.firstDiff.qIdx + 1; s < qLines.length && suffixIdx.length < 3; s++) {
|
|
10994
|
+
if (qLines[s].trim()) suffixIdx.push(s);
|
|
10995
|
+
}
|
|
10996
|
+
if (suffixIdx.length === 0) {
|
|
10997
|
+
alignment = "aligned";
|
|
10998
|
+
} else {
|
|
10999
|
+
budget -= 20;
|
|
11000
|
+
const nextNonBlank = (from) => {
|
|
11001
|
+
let i = from;
|
|
11002
|
+
while (i < fileLines.length && !fileLines[i].trim()) i++;
|
|
11003
|
+
return i;
|
|
11004
|
+
};
|
|
11005
|
+
const tryAlign = (fileStart) => {
|
|
11006
|
+
let score = 0;
|
|
11007
|
+
let fi = fileStart;
|
|
11008
|
+
for (const s of suffixIdx) {
|
|
11009
|
+
fi = nextNonBlank(fi);
|
|
11010
|
+
if (fi >= fileLines.length || fileLines[fi].trim() !== qLines[s].trim()) break;
|
|
11011
|
+
score++;
|
|
11012
|
+
fi++;
|
|
11013
|
+
}
|
|
11014
|
+
return { score, endFileIdx: fi };
|
|
11015
|
+
};
|
|
11016
|
+
const atSameLength = tryAlign(best.walkEnd + 1);
|
|
11017
|
+
if (atSameLength.score === suffixIdx.length) {
|
|
11018
|
+
alignment = "aligned";
|
|
11019
|
+
regionEnd = Math.max(regionEnd, Math.min(atSameLength.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
|
|
11020
|
+
} else {
|
|
11021
|
+
const diffQuoteLine = best.firstDiff.qLine.trim();
|
|
11022
|
+
const nextFile = nextNonBlank(best.walkEnd + 1);
|
|
11023
|
+
const quoteSkipsAFileLine = nextFile < fileLines.length && fileLines[nextFile].trim() === diffQuoteLine;
|
|
11024
|
+
const shiftedCandidates = [best.walkEnd - 1, best.walkEnd, best.walkEnd + 2, best.walkEnd + 3];
|
|
11025
|
+
const shiftedResults = shiftedCandidates.map((candidate) => tryAlign(candidate));
|
|
11026
|
+
let bestShiftedIndex = 0;
|
|
11027
|
+
for (let index = 1; index < shiftedResults.length; index++) {
|
|
11028
|
+
if (shiftedResults[index].score > shiftedResults[bestShiftedIndex].score) bestShiftedIndex = index;
|
|
11029
|
+
}
|
|
11030
|
+
const bestShifted = shiftedResults[bestShiftedIndex];
|
|
11031
|
+
if (quoteSkipsAFileLine) {
|
|
11032
|
+
alignment = "shifted";
|
|
11033
|
+
shiftDelta = nextFile - best.walkEnd;
|
|
11034
|
+
const throughSuffix = tryAlign(nextFile + 1);
|
|
11035
|
+
const end = throughSuffix.score > 0 ? throughSuffix.endFileIdx : nextFile + 1;
|
|
11036
|
+
regionEnd = Math.max(regionEnd, Math.min(end, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
|
|
11037
|
+
} else if (bestShifted.score > atSameLength.score && bestShifted.score === suffixIdx.length) {
|
|
11038
|
+
alignment = "shifted";
|
|
11039
|
+
shiftDelta = shiftedCandidates[bestShiftedIndex] - (best.walkEnd + 1);
|
|
11040
|
+
regionEnd = Math.max(regionEnd, Math.min(bestShifted.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
|
|
11041
|
+
} else if (atSameLength.score > 0) {
|
|
11042
|
+
alignment = "aligned";
|
|
11043
|
+
regionEnd = Math.max(regionEnd, Math.min(atSameLength.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
11046
|
+
}
|
|
11047
|
+
if (alignment === "shifted") {
|
|
11048
|
+
parts.push("The remainder of the quote re-aligns only at a SHIFTED position \u2014 an extra, missing, or duplicated line changed the line count; fixing the quoted difference line alone cannot repair the quote.");
|
|
11049
|
+
} else if (alignment === "unknown") {
|
|
11050
|
+
parts.push("The remainder of the quote does not re-align within a few lines of the difference \u2014 the quote is likely stale; re-read the region and re-quote it.");
|
|
11051
|
+
}
|
|
11052
|
+
const quoteSpanLines = qLines.length - firstNonBlank + (alignment === "shifted" ? shiftDelta : 0);
|
|
11053
|
+
regionEnd = Math.max(regionEnd, Math.min(best.start + quoteSpanLines, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
|
|
10088
11054
|
} else {
|
|
10089
11055
|
parts.push("All non-empty lines match by trimmed content \u2014 the mismatch is leading whitespace or blank-line placement.");
|
|
10090
11056
|
}
|
|
11057
|
+
parts.unshift(`Closest match: file lines ${regionStart}\u2013${regionEnd} (${best.matched}/${total} lines matched by trimmed content).`);
|
|
11058
|
+
if (best.firstDiff && alignment !== "unknown" && regionEnd - regionStart + 1 <= MAX_CLOSEST_DIFF_REGION_LINES) {
|
|
11059
|
+
const regionText = fileLines.slice(best.start, Math.min(regionEnd, fileLines.length)).join("\n");
|
|
11060
|
+
const { diff } = generateDiffString(regionText, oldText);
|
|
11061
|
+
const body = diff.split("\n").slice(3);
|
|
11062
|
+
if (body.length > 0) {
|
|
11063
|
+
const shown = body.slice(0, MAX_CLOSEST_DIFF_LINES);
|
|
11064
|
+
const suffix = body.length > shown.length ? ["\u2026"] : [];
|
|
11065
|
+
parts.push(`Diff of the region against the quote (- file, + quote):
|
|
11066
|
+
${[...shown, ...suffix].join("\n")}`);
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
10091
11069
|
return parts.join(" ");
|
|
10092
11070
|
}
|
|
10093
11071
|
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
@@ -16743,6 +17721,9 @@ export {
|
|
|
16743
17721
|
registerInteractionHandler,
|
|
16744
17722
|
hasInteractionHandler,
|
|
16745
17723
|
requestInteraction,
|
|
17724
|
+
AttentionCoordinator,
|
|
17725
|
+
interactionAttentionReason,
|
|
17726
|
+
interactionAttentionResolution,
|
|
16746
17727
|
AGENTS_CONTEXT_FILE_NAME,
|
|
16747
17728
|
buildProjectContextSection,
|
|
16748
17729
|
attachAgentEventsToSessionTrace,
|
|
@@ -16772,6 +17753,7 @@ export {
|
|
|
16772
17753
|
modalityForReadFileUnderstandingKind,
|
|
16773
17754
|
createReadFileUnderstandingHandler,
|
|
16774
17755
|
formatSkillSummariesForPrompt,
|
|
17756
|
+
SUBAGENT_SESSION_RECORD_VERSION,
|
|
16775
17757
|
classifyShellCommand,
|
|
16776
17758
|
interpretShellExit,
|
|
16777
17759
|
createSubagentCapability,
|