@oh-my-pi/pi-coding-agent 3.34.0 → 3.36.0
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/CHANGELOG.md +43 -1
- package/README.md +7 -2
- package/package.json +5 -5
- package/src/core/agent-session.ts +202 -33
- package/src/core/auth-storage.ts +293 -28
- package/src/core/model-registry.ts +7 -8
- package/src/core/sdk.ts +12 -6
- package/src/core/session-manager.ts +15 -0
- package/src/core/settings-manager.ts +20 -6
- package/src/core/system-prompt.ts +1 -0
- package/src/core/title-generator.ts +3 -1
- package/src/core/tools/bash.ts +11 -3
- package/src/core/tools/calculator.ts +500 -0
- package/src/core/tools/edit.ts +1 -0
- package/src/core/tools/grep.ts +1 -1
- package/src/core/tools/index.test.ts +2 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/renderers.ts +3 -0
- package/src/core/tools/task/index.ts +10 -1
- package/src/core/tools/task/model-resolver.ts +5 -4
- package/src/core/tools/web-search/auth.ts +13 -5
- package/src/core/tools/web-search/types.ts +11 -7
- package/src/main.ts +3 -0
- package/src/modes/interactive/components/oauth-selector.ts +1 -2
- package/src/modes/interactive/components/tool-execution.ts +15 -12
- package/src/modes/interactive/interactive-mode.ts +49 -13
- package/src/prompts/tools/ask.md +11 -5
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/calculator.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.36.0] - 2026-01-10
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `calc` tool for basic mathematical calculations with support for arithmetic operators, parentheses, and hex/binary/octal literals
|
|
9
|
+
- Added support for multiple API credentials per provider with round-robin distribution across sessions
|
|
10
|
+
- Added file locking for auth.json to prevent concurrent write corruption
|
|
11
|
+
- Added clickable OAuth login URL display in terminal
|
|
12
|
+
- Added `workdir` parameter to bash tool to execute commands in a specific directory without requiring `cd` commands
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Updated bash tool rendering to display working directory context when `workdir` parameter is used
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed completion notification to only send when interactive mode is in foreground
|
|
21
|
+
- Improved completion notification message to include session title when available
|
|
22
|
+
|
|
23
|
+
## [3.35.0] - 2026-01-09
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added retry logic with exponential backoff for auto-compaction failures
|
|
27
|
+
- Added fallback to alternative models when auto-compaction fails with the primary model
|
|
28
|
+
- Added support for `pi/<role>` model aliases in task tool (e.g., `pi/slow`, `pi/default`)
|
|
29
|
+
- Added visual cycle indicator when switching between role models showing available roles
|
|
30
|
+
- Added automatic model inheritance for subtasks when parent uses default model
|
|
31
|
+
- Added `--` separator in grep tool to prevent pattern interpretation as flags
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Changed role model cycling to remember last selected role instead of matching current model
|
|
36
|
+
- Changed edit tool to merge call and result displays into single block
|
|
37
|
+
- Changed model override behavior to persist in settings when explicitly set via CLI
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Fixed retry-after parsing from error messages supporting multiple header formats (retry-after, retry-after-ms, x-ratelimit-reset)
|
|
42
|
+
- Fixed image attachments being dropped when steering/follow-up messages are queued during streaming
|
|
43
|
+
- Fixed image auto-resize not applying to clipboard images before sending
|
|
44
|
+
- Fixed clipboard image attachments being dropped when steering/follow-up messages are queued while streaming
|
|
45
|
+
- Fixed clipboard image attachments ignoring the auto-resize setting before sending
|
|
46
|
+
|
|
5
47
|
## [3.34.0] - 2026-01-09
|
|
6
48
|
|
|
7
49
|
### Added
|
|
@@ -1954,4 +1996,4 @@ Initial public release.
|
|
|
1954
1996
|
- Git branch display in footer
|
|
1955
1997
|
- Message queueing during streaming responses
|
|
1956
1998
|
- OAuth integration for Gmail and Google Calendar access
|
|
1957
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
1999
|
+
- HTML export with syntax highlighting and collapsible sections
|
package/README.md
CHANGED
|
@@ -114,10 +114,15 @@ Add API keys to `~/.omp/agent/auth.json`:
|
|
|
114
114
|
|
|
115
115
|
```json
|
|
116
116
|
{
|
|
117
|
-
"anthropic":
|
|
117
|
+
"anthropic": [
|
|
118
|
+
{ "type": "api_key", "key": "sk-ant-..." },
|
|
119
|
+
{ "type": "api_key", "key": "sk-ant-..." }
|
|
120
|
+
],
|
|
118
121
|
"openai": { "type": "api_key", "key": "sk-..." },
|
|
119
122
|
"google": { "type": "api_key", "key": "..." }
|
|
120
123
|
}
|
|
124
|
+
|
|
125
|
+
If a provider has multiple credentials, new sessions round robin across them and stay sticky per session.
|
|
121
126
|
```
|
|
122
127
|
|
|
123
128
|
**Option 2: Environment variables**
|
|
@@ -152,7 +157,7 @@ omp
|
|
|
152
157
|
/login # Select provider, authorize in browser
|
|
153
158
|
```
|
|
154
159
|
|
|
155
|
-
**Note:** `/login` replaces any existing API
|
|
160
|
+
**Note:** `/login` replaces any existing API keys for that provider with OAuth credentials in `auth.json`. If OAuth credentials already exist, `/login` appends another entry.
|
|
156
161
|
|
|
157
162
|
**GitHub Copilot notes:**
|
|
158
163
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.36.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "3.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-ai": "3.36.0",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "3.36.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "3.36.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "3.36.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -459,7 +459,10 @@ export class AgentSession {
|
|
|
459
459
|
const content = message.content;
|
|
460
460
|
if (typeof content === "string") return content;
|
|
461
461
|
const textBlocks = content.filter((c) => c.type === "text");
|
|
462
|
-
|
|
462
|
+
const text = textBlocks.map((c) => (c as TextContent).text).join("");
|
|
463
|
+
if (text.length > 0) return text;
|
|
464
|
+
const hasImages = content.some((c) => c.type === "image");
|
|
465
|
+
return hasImages ? "[Image]" : "";
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
@@ -722,9 +725,9 @@ export class AgentSession {
|
|
|
722
725
|
);
|
|
723
726
|
}
|
|
724
727
|
if (options.streamingBehavior === "followUp") {
|
|
725
|
-
await this._queueFollowUp(expandedText);
|
|
728
|
+
await this._queueFollowUp(expandedText, options?.images);
|
|
726
729
|
} else {
|
|
727
|
-
await this._queueSteer(expandedText);
|
|
730
|
+
await this._queueSteer(expandedText, options?.images);
|
|
728
731
|
}
|
|
729
732
|
return;
|
|
730
733
|
}
|
|
@@ -742,7 +745,7 @@ export class AgentSession {
|
|
|
742
745
|
}
|
|
743
746
|
|
|
744
747
|
// Validate API key
|
|
745
|
-
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
748
|
+
const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
|
|
746
749
|
if (!apiKey) {
|
|
747
750
|
throw new Error(
|
|
748
751
|
`No API key found for ${this.model.provider}.\n\n` +
|
|
@@ -953,11 +956,16 @@ export class AgentSession {
|
|
|
953
956
|
/**
|
|
954
957
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
955
958
|
*/
|
|
956
|
-
private async _queueSteer(text: string): Promise<void> {
|
|
957
|
-
|
|
959
|
+
private async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
960
|
+
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
961
|
+
this._steeringMessages.push(displayText);
|
|
962
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
963
|
+
if (images && images.length > 0) {
|
|
964
|
+
content.push(...images);
|
|
965
|
+
}
|
|
958
966
|
this.agent.steer({
|
|
959
967
|
role: "user",
|
|
960
|
-
content
|
|
968
|
+
content,
|
|
961
969
|
timestamp: Date.now(),
|
|
962
970
|
});
|
|
963
971
|
}
|
|
@@ -965,11 +973,16 @@ export class AgentSession {
|
|
|
965
973
|
/**
|
|
966
974
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
967
975
|
*/
|
|
968
|
-
private async _queueFollowUp(text: string): Promise<void> {
|
|
969
|
-
|
|
976
|
+
private async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
977
|
+
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
978
|
+
this._followUpMessages.push(displayText);
|
|
979
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
980
|
+
if (images && images.length > 0) {
|
|
981
|
+
content.push(...images);
|
|
982
|
+
}
|
|
970
983
|
this.agent.followUp({
|
|
971
984
|
role: "user",
|
|
972
|
-
content
|
|
985
|
+
content,
|
|
973
986
|
timestamp: Date.now(),
|
|
974
987
|
});
|
|
975
988
|
}
|
|
@@ -1129,7 +1142,7 @@ export class AgentSession {
|
|
|
1129
1142
|
* @throws Error if no API key available for the model
|
|
1130
1143
|
*/
|
|
1131
1144
|
async setModel(model: Model<any>, role: string = "default"): Promise<void> {
|
|
1132
|
-
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
1145
|
+
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
1133
1146
|
if (!apiKey) {
|
|
1134
1147
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1135
1148
|
}
|
|
@@ -1148,7 +1161,7 @@ export class AgentSession {
|
|
|
1148
1161
|
* @throws Error if no API key available for the model
|
|
1149
1162
|
*/
|
|
1150
1163
|
async setModelTemporary(model: Model<any>): Promise<void> {
|
|
1151
|
-
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
1164
|
+
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
1152
1165
|
if (!apiKey) {
|
|
1153
1166
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1154
1167
|
}
|
|
@@ -1175,7 +1188,7 @@ export class AgentSession {
|
|
|
1175
1188
|
|
|
1176
1189
|
/**
|
|
1177
1190
|
* Cycle through configured role models in a fixed order.
|
|
1178
|
-
* Skips missing roles
|
|
1191
|
+
* Skips missing roles.
|
|
1179
1192
|
* @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
|
|
1180
1193
|
* @param options - Optional settings: `temporary` to not persist to settings
|
|
1181
1194
|
*/
|
|
@@ -1189,7 +1202,6 @@ export class AgentSession {
|
|
|
1189
1202
|
const currentModel = this.model;
|
|
1190
1203
|
if (!currentModel) return undefined;
|
|
1191
1204
|
const roleModels: Array<{ role: string; model: Model<any> }> = [];
|
|
1192
|
-
const seen = new Set<string>();
|
|
1193
1205
|
|
|
1194
1206
|
for (const role of roleOrder) {
|
|
1195
1207
|
const roleModelStr =
|
|
@@ -1208,15 +1220,15 @@ export class AgentSession {
|
|
|
1208
1220
|
}
|
|
1209
1221
|
if (!match) continue;
|
|
1210
1222
|
|
|
1211
|
-
const key = `${match.provider}/${match.id}`;
|
|
1212
|
-
if (seen.has(key)) continue;
|
|
1213
|
-
seen.add(key);
|
|
1214
1223
|
roleModels.push({ role, model: match });
|
|
1215
1224
|
}
|
|
1216
1225
|
|
|
1217
1226
|
if (roleModels.length <= 1) return undefined;
|
|
1218
1227
|
|
|
1219
|
-
|
|
1228
|
+
const lastRole = this.sessionManager.getLastModelChangeRole();
|
|
1229
|
+
let currentIndex = lastRole
|
|
1230
|
+
? roleModels.findIndex((entry) => entry.role === lastRole)
|
|
1231
|
+
: roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
|
|
1220
1232
|
if (currentIndex === -1) currentIndex = 0;
|
|
1221
1233
|
|
|
1222
1234
|
const nextIndex = (currentIndex + 1) % roleModels.length;
|
|
@@ -1243,7 +1255,7 @@ export class AgentSession {
|
|
|
1243
1255
|
const next = this._scopedModels[nextIndex];
|
|
1244
1256
|
|
|
1245
1257
|
// Validate API key
|
|
1246
|
-
const apiKey = await this._modelRegistry.getApiKey(next.model);
|
|
1258
|
+
const apiKey = await this._modelRegistry.getApiKey(next.model, this.sessionId);
|
|
1247
1259
|
if (!apiKey) {
|
|
1248
1260
|
throw new Error(`No API key for ${next.model.provider}/${next.model.id}`);
|
|
1249
1261
|
}
|
|
@@ -1271,7 +1283,7 @@ export class AgentSession {
|
|
|
1271
1283
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1272
1284
|
const nextModel = availableModels[nextIndex];
|
|
1273
1285
|
|
|
1274
|
-
const apiKey = await this._modelRegistry.getApiKey(nextModel);
|
|
1286
|
+
const apiKey = await this._modelRegistry.getApiKey(nextModel, this.sessionId);
|
|
1275
1287
|
if (!apiKey) {
|
|
1276
1288
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
1277
1289
|
}
|
|
@@ -1401,7 +1413,7 @@ export class AgentSession {
|
|
|
1401
1413
|
throw new Error("No model selected");
|
|
1402
1414
|
}
|
|
1403
1415
|
|
|
1404
|
-
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
1416
|
+
const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
|
|
1405
1417
|
if (!apiKey) {
|
|
1406
1418
|
throw new Error(`No API key for ${this.model.provider}`);
|
|
1407
1419
|
}
|
|
@@ -1558,6 +1570,60 @@ export class AgentSession {
|
|
|
1558
1570
|
}
|
|
1559
1571
|
}
|
|
1560
1572
|
|
|
1573
|
+
private _getModelKey(model: Model<any>): string {
|
|
1574
|
+
return `${model.provider}/${model.id}`;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
private _resolveRoleModel(
|
|
1578
|
+
role: string,
|
|
1579
|
+
availableModels: Model<any>[],
|
|
1580
|
+
currentModel: Model<any> | undefined,
|
|
1581
|
+
): Model<any> | undefined {
|
|
1582
|
+
const roleModelStr =
|
|
1583
|
+
role === "default"
|
|
1584
|
+
? (this.settingsManager.getModelRole("default") ??
|
|
1585
|
+
(currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
|
|
1586
|
+
: this.settingsManager.getModelRole(role);
|
|
1587
|
+
|
|
1588
|
+
if (!roleModelStr) return undefined;
|
|
1589
|
+
|
|
1590
|
+
const parsed = parseModelString(roleModelStr);
|
|
1591
|
+
if (parsed) {
|
|
1592
|
+
return availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
1593
|
+
}
|
|
1594
|
+
const roleLower = roleModelStr.toLowerCase();
|
|
1595
|
+
return availableModels.find((m) => m.id.toLowerCase() === roleLower);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private _getCompactionModelCandidates(availableModels: Model<any>[]): Model<any>[] {
|
|
1599
|
+
const candidates: Model<any>[] = [];
|
|
1600
|
+
const seen = new Set<string>();
|
|
1601
|
+
|
|
1602
|
+
const addCandidate = (model: Model<any> | undefined): void => {
|
|
1603
|
+
if (!model) return;
|
|
1604
|
+
const key = this._getModelKey(model);
|
|
1605
|
+
if (seen.has(key)) return;
|
|
1606
|
+
seen.add(key);
|
|
1607
|
+
candidates.push(model);
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
const currentModel = this.model;
|
|
1611
|
+
addCandidate(this._resolveRoleModel("default", availableModels, currentModel));
|
|
1612
|
+
addCandidate(this._resolveRoleModel("slow", availableModels, currentModel));
|
|
1613
|
+
addCandidate(this._resolveRoleModel("small", availableModels, currentModel));
|
|
1614
|
+
addCandidate(this._resolveRoleModel("smol", availableModels, currentModel));
|
|
1615
|
+
|
|
1616
|
+
const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
|
|
1617
|
+
for (const model of sortedByContext) {
|
|
1618
|
+
if (!seen.has(this._getModelKey(model))) {
|
|
1619
|
+
addCandidate(model);
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return candidates;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1561
1627
|
/**
|
|
1562
1628
|
* Internal: Run auto-compaction with events.
|
|
1563
1629
|
*/
|
|
@@ -1577,8 +1643,8 @@ export class AgentSession {
|
|
|
1577
1643
|
return;
|
|
1578
1644
|
}
|
|
1579
1645
|
|
|
1580
|
-
const
|
|
1581
|
-
if (
|
|
1646
|
+
const availableModels = this._modelRegistry.getAvailable();
|
|
1647
|
+
if (availableModels.length === 0) {
|
|
1582
1648
|
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
1583
1649
|
return;
|
|
1584
1650
|
}
|
|
@@ -1626,14 +1692,68 @@ export class AgentSession {
|
|
|
1626
1692
|
tokensBefore = hookCompaction.tokensBefore;
|
|
1627
1693
|
details = hookCompaction.details;
|
|
1628
1694
|
} else {
|
|
1629
|
-
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
this.
|
|
1636
|
-
|
|
1695
|
+
const candidates = this._getCompactionModelCandidates(availableModels);
|
|
1696
|
+
const retrySettings = this.settingsManager.getRetrySettings();
|
|
1697
|
+
let compactResult: CompactionResult | undefined;
|
|
1698
|
+
let lastError: unknown;
|
|
1699
|
+
|
|
1700
|
+
for (const candidate of candidates) {
|
|
1701
|
+
const apiKey = await this._modelRegistry.getApiKey(candidate, this.sessionId);
|
|
1702
|
+
if (!apiKey) continue;
|
|
1703
|
+
|
|
1704
|
+
let attempt = 0;
|
|
1705
|
+
while (true) {
|
|
1706
|
+
try {
|
|
1707
|
+
compactResult = await compact(
|
|
1708
|
+
preparation,
|
|
1709
|
+
candidate,
|
|
1710
|
+
apiKey,
|
|
1711
|
+
undefined,
|
|
1712
|
+
this._autoCompactionAbortController.signal,
|
|
1713
|
+
);
|
|
1714
|
+
break;
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
if (this._autoCompactionAbortController.signal.aborted) {
|
|
1717
|
+
throw error;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1721
|
+
const retryAfterMs = this._parseRetryAfterMsFromError(message);
|
|
1722
|
+
const shouldRetry =
|
|
1723
|
+
retrySettings.enabled &&
|
|
1724
|
+
attempt < retrySettings.maxRetries &&
|
|
1725
|
+
(retryAfterMs !== undefined || this._isRetryableErrorMessage(message));
|
|
1726
|
+
if (!shouldRetry) {
|
|
1727
|
+
lastError = error;
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
|
|
1732
|
+
const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
|
|
1733
|
+
attempt++;
|
|
1734
|
+
logger.warn("Auto-compaction failed, retrying", {
|
|
1735
|
+
attempt,
|
|
1736
|
+
maxRetries: retrySettings.maxRetries,
|
|
1737
|
+
delayMs,
|
|
1738
|
+
retryAfterMs,
|
|
1739
|
+
error: message,
|
|
1740
|
+
});
|
|
1741
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (compactResult) {
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (!compactResult) {
|
|
1751
|
+
if (lastError) {
|
|
1752
|
+
throw lastError;
|
|
1753
|
+
}
|
|
1754
|
+
throw new Error("Compaction failed: no available model");
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1637
1757
|
summary = compactResult.summary;
|
|
1638
1758
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1639
1759
|
tokensBefore = compactResult.tokensBefore;
|
|
@@ -1725,12 +1845,61 @@ export class AgentSession {
|
|
|
1725
1845
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
1726
1846
|
|
|
1727
1847
|
const err = message.errorMessage;
|
|
1848
|
+
return this._isRetryableErrorMessage(err);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private _isRetryableErrorMessage(errorMessage: string): boolean {
|
|
1728
1852
|
// Match: overloaded_error, rate limit, 429, 500, 502, 503, 504, service unavailable, connection error
|
|
1729
1853
|
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error/i.test(
|
|
1730
|
-
|
|
1854
|
+
errorMessage,
|
|
1731
1855
|
);
|
|
1732
1856
|
}
|
|
1733
1857
|
|
|
1858
|
+
private _parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
1859
|
+
const now = Date.now();
|
|
1860
|
+
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
1861
|
+
if (retryAfterMsMatch) {
|
|
1862
|
+
return Math.max(0, Number(retryAfterMsMatch[1]));
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const retryAfterMatch = /retry-after\s*[:=]\s*([^\s,;]+)/i.exec(errorMessage);
|
|
1866
|
+
if (retryAfterMatch) {
|
|
1867
|
+
const value = retryAfterMatch[1];
|
|
1868
|
+
const seconds = Number(value);
|
|
1869
|
+
if (!Number.isNaN(seconds)) {
|
|
1870
|
+
return Math.max(0, seconds * 1000);
|
|
1871
|
+
}
|
|
1872
|
+
const dateMs = Date.parse(value);
|
|
1873
|
+
if (!Number.isNaN(dateMs)) {
|
|
1874
|
+
return Math.max(0, dateMs - now);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
1879
|
+
if (resetMsMatch) {
|
|
1880
|
+
const resetMs = Number(resetMsMatch[1]);
|
|
1881
|
+
if (!Number.isNaN(resetMs)) {
|
|
1882
|
+
if (resetMs > 1_000_000_000_000) {
|
|
1883
|
+
return Math.max(0, resetMs - now);
|
|
1884
|
+
}
|
|
1885
|
+
return Math.max(0, resetMs);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const resetMatch = /x-ratelimit-reset\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
1890
|
+
if (resetMatch) {
|
|
1891
|
+
const resetSeconds = Number(resetMatch[1]);
|
|
1892
|
+
if (!Number.isNaN(resetSeconds)) {
|
|
1893
|
+
if (resetSeconds > 1_000_000_000) {
|
|
1894
|
+
return Math.max(0, resetSeconds * 1000 - now);
|
|
1895
|
+
}
|
|
1896
|
+
return Math.max(0, resetSeconds * 1000);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1734
1903
|
/**
|
|
1735
1904
|
* Handle retryable errors with exponential backoff.
|
|
1736
1905
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
@@ -2175,7 +2344,7 @@ export class AgentSession {
|
|
|
2175
2344
|
let summaryDetails: unknown;
|
|
2176
2345
|
if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) {
|
|
2177
2346
|
const model = this.model!;
|
|
2178
|
-
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
2347
|
+
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
2179
2348
|
if (!apiKey) {
|
|
2180
2349
|
throw new Error(`No API key for ${model.provider}`);
|
|
2181
2350
|
}
|