@compilr-dev/agents 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +107 -2
- package/dist/agent.js +151 -22
- package/dist/context/manager.d.ts +8 -0
- package/dist/context/manager.js +25 -2
- package/dist/errors.d.ts +20 -1
- package/dist/errors.js +44 -2
- package/dist/index.d.ts +16 -1
- package/dist/index.js +13 -1
- package/dist/messages/index.d.ts +12 -5
- package/dist/messages/index.js +53 -15
- package/dist/providers/claude.js +7 -0
- package/dist/providers/fireworks.d.ts +86 -0
- package/dist/providers/fireworks.js +123 -0
- package/dist/providers/gemini-native.d.ts +86 -0
- package/dist/providers/gemini-native.js +374 -0
- package/dist/providers/groq.d.ts +86 -0
- package/dist/providers/groq.js +123 -0
- package/dist/providers/index.d.ts +17 -2
- package/dist/providers/index.js +13 -2
- package/dist/providers/openai-compatible.js +12 -1
- package/dist/providers/openrouter.d.ts +95 -0
- package/dist/providers/openrouter.js +138 -0
- package/dist/providers/perplexity.d.ts +86 -0
- package/dist/providers/perplexity.js +123 -0
- package/dist/providers/together.d.ts +86 -0
- package/dist/providers/together.js +123 -0
- package/dist/providers/types.d.ts +19 -0
- package/dist/state/agent-state.d.ts +1 -0
- package/dist/state/agent-state.js +2 -0
- package/dist/state/serializer.js +20 -2
- package/dist/state/types.d.ts +5 -0
- package/dist/tools/builtin/ask-user-simple.js +1 -0
- package/dist/tools/builtin/ask-user.js +1 -0
- package/dist/tools/builtin/bash.js +123 -2
- package/dist/tools/builtin/shell-manager.d.ts +15 -0
- package/dist/tools/builtin/shell-manager.js +51 -0
- package/dist/tools/builtin/suggest.js +1 -0
- package/dist/tools/builtin/todo.js +2 -0
- package/dist/tools/define.d.ts +6 -0
- package/dist/tools/define.js +1 -0
- package/dist/tools/types.d.ts +19 -0
- package/dist/utils/index.d.ts +119 -4
- package/dist/utils/index.js +164 -13
- package/package.json +7 -1
package/dist/agent.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Agent - The main class for running AI agents with tool use
|
|
3
3
|
*/
|
|
4
4
|
import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
|
|
5
|
-
import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult } from './tools/types.js';
|
|
5
|
+
import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult, ToolExecutionContext } from './tools/types.js';
|
|
6
6
|
import type { ContextStats, VerbosityLevel, SmartCompactionResult } from './context/types.js';
|
|
7
7
|
import type { AgentState, Checkpointer, SessionMetadata } from './state/types.js';
|
|
8
8
|
import type { Anchor, AnchorInput, AnchorQueryOptions, AnchorClearOptions, AnchorManagerOptions } from './anchors/types.js';
|
|
@@ -16,6 +16,7 @@ import { ContextManager } from './context/manager.js';
|
|
|
16
16
|
import { FileAccessTracker } from './context/file-tracker.js';
|
|
17
17
|
import { AnchorManager } from './anchors/manager.js';
|
|
18
18
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
19
|
+
import { type RetryConfig } from './utils/index.js';
|
|
19
20
|
/**
|
|
20
21
|
* Event types emitted during agent execution
|
|
21
22
|
*/
|
|
@@ -146,6 +147,18 @@ export type AgentEvent = {
|
|
|
146
147
|
type: 'iteration_limit_extended';
|
|
147
148
|
newMaxIterations: number;
|
|
148
149
|
addedIterations: number;
|
|
150
|
+
} | {
|
|
151
|
+
type: 'llm_retry';
|
|
152
|
+
attempt: number;
|
|
153
|
+
maxAttempts: number;
|
|
154
|
+
error: string;
|
|
155
|
+
delayMs: number;
|
|
156
|
+
provider: string;
|
|
157
|
+
} | {
|
|
158
|
+
type: 'llm_retry_exhausted';
|
|
159
|
+
attempts: number;
|
|
160
|
+
error: string;
|
|
161
|
+
provider: string;
|
|
149
162
|
};
|
|
150
163
|
/**
|
|
151
164
|
* Event handler function type
|
|
@@ -268,6 +281,29 @@ export interface AgentConfig {
|
|
|
268
281
|
* Event handler for monitoring agent execution
|
|
269
282
|
*/
|
|
270
283
|
onEvent?: AgentEventHandler;
|
|
284
|
+
/**
|
|
285
|
+
* Configuration for automatic retry on transient LLM errors.
|
|
286
|
+
* Enabled by default with sensible defaults (10 attempts, exponential backoff).
|
|
287
|
+
*
|
|
288
|
+
* Retryable errors include:
|
|
289
|
+
* - Rate limit (429)
|
|
290
|
+
* - Server errors (5xx)
|
|
291
|
+
* - Connection/network errors
|
|
292
|
+
* - Anthropic overloaded (529)
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* const agent = new Agent({
|
|
297
|
+
* provider,
|
|
298
|
+
* retry: {
|
|
299
|
+
* maxAttempts: 5, // Max 5 retries
|
|
300
|
+
* baseDelayMs: 2000, // Start with 2s delay
|
|
301
|
+
* maxDelayMs: 60000, // Cap at 60s
|
|
302
|
+
* }
|
|
303
|
+
* });
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
retry?: RetryConfig;
|
|
271
307
|
/**
|
|
272
308
|
* Checkpointer for persisting agent state.
|
|
273
309
|
* If provided, enables checkpoint() and resume() functionality.
|
|
@@ -377,6 +413,15 @@ export interface AgentConfig {
|
|
|
377
413
|
* ```
|
|
378
414
|
*/
|
|
379
415
|
permissions?: PermissionManagerOptions;
|
|
416
|
+
/**
|
|
417
|
+
* Pre-existing PermissionManager instance.
|
|
418
|
+
*
|
|
419
|
+
* Use this to share a PermissionManager between agents (e.g., parent and sub-agents).
|
|
420
|
+
* When provided, takes precedence over `permissions` options.
|
|
421
|
+
*
|
|
422
|
+
* This is primarily used internally for sub-agent permission inheritance.
|
|
423
|
+
*/
|
|
424
|
+
permissionManager?: PermissionManager;
|
|
380
425
|
/**
|
|
381
426
|
* Pre-loaded project memory to prepend to system prompt.
|
|
382
427
|
*
|
|
@@ -544,6 +589,29 @@ export interface RunOptions {
|
|
|
544
589
|
* ```
|
|
545
590
|
*/
|
|
546
591
|
toolFilter?: string[];
|
|
592
|
+
/**
|
|
593
|
+
* Callback to provide additional tool execution context.
|
|
594
|
+
* Called before each tool execution, allowing the caller to inject
|
|
595
|
+
* abort signals or other context-specific options.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* // Provide abort signal for bash commands (for Ctrl+B backgrounding)
|
|
600
|
+
* const bashAbortController = new AbortController();
|
|
601
|
+
* await agent.stream(message, {
|
|
602
|
+
* getToolContext: (toolName, toolUseId) => {
|
|
603
|
+
* if (toolName === 'bash') {
|
|
604
|
+
* return {
|
|
605
|
+
* abortSignal: bashAbortController.signal,
|
|
606
|
+
* onBackground: (shellId, output) => { ... },
|
|
607
|
+
* };
|
|
608
|
+
* }
|
|
609
|
+
* return {};
|
|
610
|
+
* },
|
|
611
|
+
* });
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
getToolContext?: (toolName: string, toolUseId: string) => Partial<Omit<ToolExecutionContext, 'toolUseId' | 'onOutput'>>;
|
|
547
615
|
}
|
|
548
616
|
/**
|
|
549
617
|
* Agent run result
|
|
@@ -649,6 +717,20 @@ export interface SubAgentConfig {
|
|
|
649
717
|
* Default: false (uses shared store)
|
|
650
718
|
*/
|
|
651
719
|
stateIsolation?: boolean;
|
|
720
|
+
/**
|
|
721
|
+
* Inherit parent's permission manager.
|
|
722
|
+
*
|
|
723
|
+
* When true (default), the sub-agent uses the parent's PermissionManager,
|
|
724
|
+
* sharing session grants and permission rules. This ensures:
|
|
725
|
+
* - Sub-agents follow the same permission rules as parent
|
|
726
|
+
* - Session grants from parent are available to sub-agents
|
|
727
|
+
* - User sees permission prompts for sub-agent tool usage
|
|
728
|
+
*
|
|
729
|
+
* Set to false to allow sub-agents to bypass permissions (use with caution).
|
|
730
|
+
*
|
|
731
|
+
* Default: true
|
|
732
|
+
*/
|
|
733
|
+
inheritPermissions?: boolean;
|
|
652
734
|
}
|
|
653
735
|
/**
|
|
654
736
|
* Result from a sub-agent execution
|
|
@@ -712,6 +794,7 @@ export declare class Agent {
|
|
|
712
794
|
private readonly autoContextManagement;
|
|
713
795
|
private readonly onEvent?;
|
|
714
796
|
private readonly onIterationLimitReached?;
|
|
797
|
+
private readonly retryConfig;
|
|
715
798
|
private readonly checkpointer?;
|
|
716
799
|
private readonly _sessionId;
|
|
717
800
|
private readonly autoCheckpoint;
|
|
@@ -1136,8 +1219,14 @@ export declare class Agent {
|
|
|
1136
1219
|
/**
|
|
1137
1220
|
* Set the conversation history (for manual compaction/restoration)
|
|
1138
1221
|
* Also updates the context manager's token count if configured.
|
|
1222
|
+
*
|
|
1223
|
+
* @param messages - The message history to restore
|
|
1224
|
+
* @param options - Optional restore options
|
|
1225
|
+
* @param options.turnCount - The turn count to restore (important for compaction)
|
|
1139
1226
|
*/
|
|
1140
|
-
setHistory(messages: Message[]
|
|
1227
|
+
setHistory(messages: Message[], options?: {
|
|
1228
|
+
turnCount?: number;
|
|
1229
|
+
}): Promise<this>;
|
|
1141
1230
|
/**
|
|
1142
1231
|
* Get the context manager (if configured)
|
|
1143
1232
|
*/
|
|
@@ -1439,6 +1528,10 @@ export declare class Agent {
|
|
|
1439
1528
|
* Get all registered tool definitions
|
|
1440
1529
|
*/
|
|
1441
1530
|
getToolDefinitions(): ToolDefinition[];
|
|
1531
|
+
/**
|
|
1532
|
+
* Check if a tool is marked as silent (no spinner or result output)
|
|
1533
|
+
*/
|
|
1534
|
+
isToolSilent(name: string): boolean;
|
|
1442
1535
|
/**
|
|
1443
1536
|
* Run the agent with a user message
|
|
1444
1537
|
*/
|
|
@@ -1454,6 +1547,18 @@ export declare class Agent {
|
|
|
1454
1547
|
* Process stream chunks into text, tool uses, and usage data
|
|
1455
1548
|
*/
|
|
1456
1549
|
private processChunks;
|
|
1550
|
+
/**
|
|
1551
|
+
* Wrap provider.chat() with automatic retry on transient errors.
|
|
1552
|
+
*
|
|
1553
|
+
* Retries the entire stream on failure with exponential backoff.
|
|
1554
|
+
* Emits llm_retry events before each retry attempt.
|
|
1555
|
+
*
|
|
1556
|
+
* @param messages - Messages to send
|
|
1557
|
+
* @param options - Chat options
|
|
1558
|
+
* @param emit - Event emitter function
|
|
1559
|
+
* @param signal - Optional abort signal
|
|
1560
|
+
*/
|
|
1561
|
+
private chatWithRetry;
|
|
1457
1562
|
/**
|
|
1458
1563
|
* Generate a summary of messages using the LLM provider.
|
|
1459
1564
|
* Used for context summarization when approaching limits.
|
package/dist/agent.js
CHANGED
|
@@ -11,10 +11,11 @@ import { FileAccessTracker } from './context/file-tracker.js';
|
|
|
11
11
|
import { createFileTrackingHook } from './context/file-tracking-hook.js';
|
|
12
12
|
import { AnchorManager } from './anchors/manager.js';
|
|
13
13
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
14
|
-
import { MaxIterationsError, ToolLoopError } from './errors.js';
|
|
14
|
+
import { MaxIterationsError, ToolLoopError, ProviderError } from './errors.js';
|
|
15
15
|
import { generateSessionId, createAgentState, deserializeTodos } from './state/agent-state.js';
|
|
16
16
|
import { getDefaultTodoStore, createIsolatedTodoStore } from './tools/builtin/todo.js';
|
|
17
17
|
import { repairToolPairing } from './messages/index.js';
|
|
18
|
+
import { DEFAULT_RETRY_CONFIG, withRetryGenerator } from './utils/index.js';
|
|
18
19
|
/**
|
|
19
20
|
* Agent class - orchestrates LLM interactions with tool use
|
|
20
21
|
*
|
|
@@ -44,6 +45,8 @@ export class Agent {
|
|
|
44
45
|
autoContextManagement;
|
|
45
46
|
onEvent;
|
|
46
47
|
onIterationLimitReached;
|
|
48
|
+
// Retry configuration
|
|
49
|
+
retryConfig;
|
|
47
50
|
// State management
|
|
48
51
|
checkpointer;
|
|
49
52
|
_sessionId;
|
|
@@ -120,7 +123,12 @@ export class Agent {
|
|
|
120
123
|
this.guardrailManager = new GuardrailManager(config.guardrails);
|
|
121
124
|
}
|
|
122
125
|
// Permission management
|
|
123
|
-
if (
|
|
126
|
+
// Use pre-existing permissionManager if provided (for sub-agent inheritance)
|
|
127
|
+
// Otherwise create one from options
|
|
128
|
+
if (config.permissionManager !== undefined) {
|
|
129
|
+
this.permissionManager = config.permissionManager;
|
|
130
|
+
}
|
|
131
|
+
else if (config.permissions !== undefined) {
|
|
124
132
|
this.permissionManager = new PermissionManager(config.permissions);
|
|
125
133
|
}
|
|
126
134
|
// Project memory - store and prepend to system prompt
|
|
@@ -183,6 +191,13 @@ export class Agent {
|
|
|
183
191
|
// Hooks manager without file tracking
|
|
184
192
|
this.hooksManager = new HooksManager({ hooks: config.hooks });
|
|
185
193
|
}
|
|
194
|
+
// Retry configuration with defaults
|
|
195
|
+
this.retryConfig = {
|
|
196
|
+
enabled: config.retry?.enabled ?? DEFAULT_RETRY_CONFIG.enabled,
|
|
197
|
+
maxAttempts: config.retry?.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts,
|
|
198
|
+
baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
|
|
199
|
+
maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,
|
|
200
|
+
};
|
|
186
201
|
}
|
|
187
202
|
// ==========================================================================
|
|
188
203
|
// Static Factory Methods
|
|
@@ -710,11 +725,28 @@ export class Agent {
|
|
|
710
725
|
/**
|
|
711
726
|
* Set the conversation history (for manual compaction/restoration)
|
|
712
727
|
* Also updates the context manager's token count if configured.
|
|
728
|
+
*
|
|
729
|
+
* @param messages - The message history to restore
|
|
730
|
+
* @param options - Optional restore options
|
|
731
|
+
* @param options.turnCount - The turn count to restore (important for compaction)
|
|
713
732
|
*/
|
|
714
|
-
async setHistory(messages) {
|
|
715
|
-
|
|
733
|
+
async setHistory(messages, options) {
|
|
734
|
+
// Repair tool_use/tool_result pairing to fix any corrupted messages
|
|
735
|
+
// This is especially important after resuming from a saved session
|
|
736
|
+
const repairedMessages = repairToolPairing(messages);
|
|
737
|
+
this.conversationHistory = [...repairedMessages];
|
|
716
738
|
if (this.contextManager) {
|
|
717
|
-
await this.contextManager.updateTokenCount(
|
|
739
|
+
await this.contextManager.updateTokenCount(repairedMessages);
|
|
740
|
+
// Restore turn count if provided
|
|
741
|
+
if (options?.turnCount !== undefined) {
|
|
742
|
+
this.contextManager.setTurnCount(options.turnCount);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
// Infer turn count from messages as a fallback
|
|
746
|
+
// Count user messages as a proxy for turns
|
|
747
|
+
const inferredTurnCount = repairedMessages.filter((m) => m.role === 'user').length;
|
|
748
|
+
this.contextManager.setTurnCount(inferredTurnCount);
|
|
749
|
+
}
|
|
718
750
|
}
|
|
719
751
|
return this;
|
|
720
752
|
}
|
|
@@ -926,6 +958,7 @@ export class Agent {
|
|
|
926
958
|
messages: this.conversationHistory,
|
|
927
959
|
todos: todoStore.getAll(),
|
|
928
960
|
currentIteration: this._currentIteration,
|
|
961
|
+
turnCount: this.contextManager?.getTurnCount() ?? 0,
|
|
929
962
|
totalTokensUsed: this._totalTokensUsed,
|
|
930
963
|
createdAt: this._createdAt,
|
|
931
964
|
});
|
|
@@ -1054,6 +1087,11 @@ export class Agent {
|
|
|
1054
1087
|
agent._createdAt = state.createdAt;
|
|
1055
1088
|
agent._totalTokensUsed = state.totalTokensUsed;
|
|
1056
1089
|
agent._currentIteration = state.currentIteration;
|
|
1090
|
+
// Restore turn count to context manager if configured
|
|
1091
|
+
// Note: turnCount is guaranteed to exist - deserializer ensures backward compatibility
|
|
1092
|
+
if (agent.contextManager) {
|
|
1093
|
+
agent.contextManager.setTurnCount(state.turnCount);
|
|
1094
|
+
}
|
|
1057
1095
|
// Restore todos
|
|
1058
1096
|
if (state.todos.length > 0) {
|
|
1059
1097
|
const todoStore = getDefaultTodoStore();
|
|
@@ -1118,6 +1156,9 @@ export class Agent {
|
|
|
1118
1156
|
for (const tool of toolsToRegister) {
|
|
1119
1157
|
subAgentToolRegistry.register(tool);
|
|
1120
1158
|
}
|
|
1159
|
+
// Determine if sub-agent should inherit parent's permissions
|
|
1160
|
+
// Default: true (sub-agents follow same permission rules as parent)
|
|
1161
|
+
const inheritPermissions = config.inheritPermissions ?? true;
|
|
1121
1162
|
// Create the sub-agent
|
|
1122
1163
|
const subAgent = new Agent({
|
|
1123
1164
|
provider: this.provider,
|
|
@@ -1128,6 +1169,12 @@ export class Agent {
|
|
|
1128
1169
|
contextManager: subAgentContextManager,
|
|
1129
1170
|
autoContextManagement: true,
|
|
1130
1171
|
onEvent: this.onEvent, // Forward events to parent
|
|
1172
|
+
// Sub-agents should summarize their findings when hitting iteration limit
|
|
1173
|
+
// rather than throwing an error with no response
|
|
1174
|
+
iterationLimitBehavior: 'summarize',
|
|
1175
|
+
// Inherit parent's permission manager if enabled
|
|
1176
|
+
// This shares session grants and permission rules with sub-agents
|
|
1177
|
+
permissionManager: inheritPermissions ? this.permissionManager : undefined,
|
|
1131
1178
|
});
|
|
1132
1179
|
// Store the sub-agent
|
|
1133
1180
|
this.subAgents.set(config.name, { config, agent: subAgent });
|
|
@@ -1375,6 +1422,13 @@ export class Agent {
|
|
|
1375
1422
|
getToolDefinitions() {
|
|
1376
1423
|
return this.toolRegistry.getDefinitions();
|
|
1377
1424
|
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Check if a tool is marked as silent (no spinner or result output)
|
|
1427
|
+
*/
|
|
1428
|
+
isToolSilent(name) {
|
|
1429
|
+
const tool = this.toolRegistry.get(name);
|
|
1430
|
+
return tool?.silent === true;
|
|
1431
|
+
}
|
|
1378
1432
|
/**
|
|
1379
1433
|
* Run the agent with a user message
|
|
1380
1434
|
*/
|
|
@@ -1382,6 +1436,7 @@ export class Agent {
|
|
|
1382
1436
|
let maxIterations = options?.maxIterations ?? this.maxIterations;
|
|
1383
1437
|
const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
|
|
1384
1438
|
const signal = options?.signal;
|
|
1439
|
+
const getToolContext = options?.getToolContext;
|
|
1385
1440
|
// Combined event emitter
|
|
1386
1441
|
const emit = (event) => {
|
|
1387
1442
|
this.onEvent?.(event);
|
|
@@ -1390,22 +1445,34 @@ export class Agent {
|
|
|
1390
1445
|
// Build messages: system prompt + history + new user message
|
|
1391
1446
|
let messages = [];
|
|
1392
1447
|
// Add system message if present
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
// Inject anchors (critical information that survives context compaction)
|
|
1448
|
+
// NOTE: Anchors are now appended to the system prompt (not a separate message)
|
|
1449
|
+
// to avoid multiple system messages which can confuse Claude's role identity
|
|
1450
|
+
let systemContent = this.systemPrompt;
|
|
1451
|
+
// Inject anchors into the system prompt (not as separate message)
|
|
1400
1452
|
if (this.anchorManager && this.anchorManager.size > 0) {
|
|
1401
1453
|
const anchorsContent = this.anchorManager.format();
|
|
1402
1454
|
if (anchorsContent) {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1455
|
+
// Insert anchors BEFORE the role ending (if present) so role reinforcement stays at the end
|
|
1456
|
+
const roleEndingMarker = '## YOUR ASSIGNED ROLE:';
|
|
1457
|
+
const roleEndingIndex = systemContent.indexOf(roleEndingMarker);
|
|
1458
|
+
if (roleEndingIndex > 0) {
|
|
1459
|
+
// Insert anchors before the role ending
|
|
1460
|
+
const beforeRole = systemContent.substring(0, roleEndingIndex);
|
|
1461
|
+
const roleEnding = systemContent.substring(roleEndingIndex);
|
|
1462
|
+
systemContent = `${beforeRole}\n\n---\n\n## Active Anchors (Critical Information)\n\n${anchorsContent}\n\n---\n\n${roleEnding}`;
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
// No role ending, just append anchors
|
|
1466
|
+
systemContent = `${systemContent}\n\n---\n\n## Active Anchors (Critical Information)\n\n${anchorsContent}`;
|
|
1467
|
+
}
|
|
1407
1468
|
}
|
|
1408
1469
|
}
|
|
1470
|
+
if (systemContent) {
|
|
1471
|
+
messages.push({
|
|
1472
|
+
role: 'system',
|
|
1473
|
+
content: systemContent,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1409
1476
|
// Add conversation history
|
|
1410
1477
|
messages.push(...this.conversationHistory);
|
|
1411
1478
|
// Add new user message
|
|
@@ -1514,10 +1581,10 @@ export class Agent {
|
|
|
1514
1581
|
const llmStartTime = Date.now();
|
|
1515
1582
|
const chunks = [];
|
|
1516
1583
|
try {
|
|
1517
|
-
for await (const chunk of this.
|
|
1584
|
+
for await (const chunk of this.chatWithRetry(messages, {
|
|
1518
1585
|
...chatOptions,
|
|
1519
1586
|
tools: tools.length > 0 ? tools : undefined,
|
|
1520
|
-
})) {
|
|
1587
|
+
}, emit, signal)) {
|
|
1521
1588
|
// Check for abort during streaming
|
|
1522
1589
|
if (signal?.aborted) {
|
|
1523
1590
|
aborted = true;
|
|
@@ -1615,6 +1682,7 @@ export class Agent {
|
|
|
1615
1682
|
id: tu.id,
|
|
1616
1683
|
name: tu.name,
|
|
1617
1684
|
input: tu.input,
|
|
1685
|
+
signature: tu.signature, // Gemini 3 thought signature (required for multi-turn)
|
|
1618
1686
|
})),
|
|
1619
1687
|
],
|
|
1620
1688
|
};
|
|
@@ -1757,6 +1825,8 @@ export class Agent {
|
|
|
1757
1825
|
}
|
|
1758
1826
|
const toolStartTime = Date.now();
|
|
1759
1827
|
try {
|
|
1828
|
+
// Get additional context from caller (e.g., for bash backgrounding)
|
|
1829
|
+
const additionalContext = getToolContext?.(toolUse.name, toolUse.id) ?? {};
|
|
1760
1830
|
const toolContext = {
|
|
1761
1831
|
toolUseId: toolUse.id,
|
|
1762
1832
|
onOutput: (output, stream) => {
|
|
@@ -1768,6 +1838,8 @@ export class Agent {
|
|
|
1768
1838
|
stream,
|
|
1769
1839
|
});
|
|
1770
1840
|
},
|
|
1841
|
+
// Merge in additional context (abortSignal, onBackground, etc.)
|
|
1842
|
+
...additionalContext,
|
|
1771
1843
|
};
|
|
1772
1844
|
result = await this.toolRegistry.execute(toolUse.name, toolInput, toolContext);
|
|
1773
1845
|
}
|
|
@@ -2111,6 +2183,7 @@ export class Agent {
|
|
|
2111
2183
|
id: chunk.toolUse.id,
|
|
2112
2184
|
name: chunk.toolUse.name,
|
|
2113
2185
|
inputJson: '',
|
|
2186
|
+
signature: chunk.toolUse.signature, // Capture Gemini 3 thought signature
|
|
2114
2187
|
};
|
|
2115
2188
|
}
|
|
2116
2189
|
break;
|
|
@@ -2129,6 +2202,7 @@ export class Agent {
|
|
|
2129
2202
|
id: currentToolUse.id,
|
|
2130
2203
|
name: currentToolUse.name,
|
|
2131
2204
|
input,
|
|
2205
|
+
signature: currentToolUse.signature, // Pass through the signature
|
|
2132
2206
|
});
|
|
2133
2207
|
}
|
|
2134
2208
|
catch {
|
|
@@ -2146,6 +2220,53 @@ export class Agent {
|
|
|
2146
2220
|
}
|
|
2147
2221
|
return { text, toolUses, usage, model };
|
|
2148
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Wrap provider.chat() with automatic retry on transient errors.
|
|
2225
|
+
*
|
|
2226
|
+
* Retries the entire stream on failure with exponential backoff.
|
|
2227
|
+
* Emits llm_retry events before each retry attempt.
|
|
2228
|
+
*
|
|
2229
|
+
* @param messages - Messages to send
|
|
2230
|
+
* @param options - Chat options
|
|
2231
|
+
* @param emit - Event emitter function
|
|
2232
|
+
* @param signal - Optional abort signal
|
|
2233
|
+
*/
|
|
2234
|
+
chatWithRetry(messages, options, emit, signal) {
|
|
2235
|
+
// If retry is disabled, return the raw provider stream
|
|
2236
|
+
if (!this.retryConfig.enabled) {
|
|
2237
|
+
return this.provider.chat(messages, options);
|
|
2238
|
+
}
|
|
2239
|
+
const providerName = this.provider.name;
|
|
2240
|
+
const { maxAttempts, baseDelayMs, maxDelayMs } = this.retryConfig;
|
|
2241
|
+
return withRetryGenerator(() => this.provider.chat(messages, options), {
|
|
2242
|
+
maxAttempts,
|
|
2243
|
+
baseDelayMs,
|
|
2244
|
+
maxDelayMs,
|
|
2245
|
+
isRetryable: (error) => {
|
|
2246
|
+
// Use the ProviderError's built-in retryable check
|
|
2247
|
+
return error instanceof ProviderError && error.isRetryable();
|
|
2248
|
+
},
|
|
2249
|
+
onRetry: (attempt, max, error, delayMs) => {
|
|
2250
|
+
emit({
|
|
2251
|
+
type: 'llm_retry',
|
|
2252
|
+
attempt,
|
|
2253
|
+
maxAttempts: max,
|
|
2254
|
+
error: error.message,
|
|
2255
|
+
delayMs,
|
|
2256
|
+
provider: providerName,
|
|
2257
|
+
});
|
|
2258
|
+
},
|
|
2259
|
+
onExhausted: (attempts, error) => {
|
|
2260
|
+
emit({
|
|
2261
|
+
type: 'llm_retry_exhausted',
|
|
2262
|
+
attempts,
|
|
2263
|
+
error: error.message,
|
|
2264
|
+
provider: providerName,
|
|
2265
|
+
});
|
|
2266
|
+
},
|
|
2267
|
+
signal,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2149
2270
|
/**
|
|
2150
2271
|
* Generate a summary of messages using the LLM provider.
|
|
2151
2272
|
* Used for context summarization when approaching limits.
|
|
@@ -2184,11 +2305,15 @@ Keep the summary under 500 words.
|
|
|
2184
2305
|
Conversation to summarize:
|
|
2185
2306
|
${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n\n')}`;
|
|
2186
2307
|
const summaryMessages = [{ role: 'user', content: summaryPrompt }];
|
|
2308
|
+
// Create emit function for retry events (uses main event handler)
|
|
2309
|
+
const emit = (event) => {
|
|
2310
|
+
this.onEvent?.(event);
|
|
2311
|
+
};
|
|
2187
2312
|
let summary = '';
|
|
2188
|
-
for await (const chunk of this.
|
|
2313
|
+
for await (const chunk of this.chatWithRetry(summaryMessages, {
|
|
2189
2314
|
...this.chatOptions,
|
|
2190
2315
|
maxTokens: this.contextManager?.getConfig().summarization.summaryMaxTokens ?? 2000,
|
|
2191
|
-
})) {
|
|
2316
|
+
}, emit)) {
|
|
2192
2317
|
if (chunk.type === 'text' && chunk.text) {
|
|
2193
2318
|
summary += chunk.text;
|
|
2194
2319
|
}
|
|
@@ -2227,11 +2352,15 @@ ${messages
|
|
|
2227
2352
|
.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 500) : '[complex content]'}`)
|
|
2228
2353
|
.join('\n\n')}`;
|
|
2229
2354
|
const summaryMessages = [{ role: 'user', content: summaryPrompt }];
|
|
2355
|
+
// Create emit function for retry events (uses main event handler)
|
|
2356
|
+
const emit = (event) => {
|
|
2357
|
+
this.onEvent?.(event);
|
|
2358
|
+
};
|
|
2230
2359
|
let summary = '';
|
|
2231
|
-
for await (const chunk of this.
|
|
2360
|
+
for await (const chunk of this.chatWithRetry(summaryMessages, {
|
|
2232
2361
|
...this.chatOptions,
|
|
2233
2362
|
maxTokens: 1000, // Keep summary concise
|
|
2234
|
-
})) {
|
|
2363
|
+
}, emit)) {
|
|
2235
2364
|
if (chunk.type === 'text' && chunk.text) {
|
|
2236
2365
|
summary += chunk.text;
|
|
2237
2366
|
}
|
|
@@ -98,6 +98,14 @@ export declare class ContextManager {
|
|
|
98
98
|
* Increment turn count (call after each assistant response)
|
|
99
99
|
*/
|
|
100
100
|
incrementTurn(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Get the current turn count
|
|
103
|
+
*/
|
|
104
|
+
getTurnCount(): number;
|
|
105
|
+
/**
|
|
106
|
+
* Set the turn count (for restoring from saved state)
|
|
107
|
+
*/
|
|
108
|
+
setTurnCount(count: number): void;
|
|
101
109
|
/**
|
|
102
110
|
* Check if compaction is needed
|
|
103
111
|
*/
|
package/dist/context/manager.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
+
import { repairToolPairing } from '../messages/index.js';
|
|
20
21
|
/**
|
|
21
22
|
* Default budget allocation
|
|
22
23
|
*/
|
|
@@ -176,6 +177,23 @@ export class ContextManager {
|
|
|
176
177
|
incrementTurn() {
|
|
177
178
|
this.turnCount++;
|
|
178
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the current turn count
|
|
182
|
+
*/
|
|
183
|
+
getTurnCount() {
|
|
184
|
+
return this.turnCount;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Set the turn count (for restoring from saved state)
|
|
188
|
+
*/
|
|
189
|
+
setTurnCount(count) {
|
|
190
|
+
this.turnCount = count;
|
|
191
|
+
// If we're restoring state, assume last compaction was at turn 0
|
|
192
|
+
// to prevent immediate compaction after restore
|
|
193
|
+
if (this.lastCompactionTurn === 0 && count > 0) {
|
|
194
|
+
this.lastCompactionTurn = count;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
179
197
|
/**
|
|
180
198
|
* Check if compaction is needed
|
|
181
199
|
*/
|
|
@@ -588,12 +606,15 @@ export class ContextManager {
|
|
|
588
606
|
}
|
|
589
607
|
// Reconstruct messages in original order
|
|
590
608
|
// System messages come first, then summary (if any), then tool results, then recent
|
|
591
|
-
|
|
609
|
+
let finalMessages = [
|
|
592
610
|
...categorized.system,
|
|
593
611
|
...compactedHistory,
|
|
594
612
|
...compactedToolResults,
|
|
595
613
|
...categorized.recentMessages,
|
|
596
614
|
];
|
|
615
|
+
// Repair tool_use/tool_result pairing after compaction
|
|
616
|
+
// This fixes orphaned blocks that cause API errors
|
|
617
|
+
finalMessages = repairToolPairing(finalMessages);
|
|
597
618
|
// If history was summarized, add assistant acknowledgment after summary
|
|
598
619
|
if (categoryStats.history.action === 'summarized' && compactedHistory.length > 0) {
|
|
599
620
|
// Insert assistant acknowledgment after the summary message
|
|
@@ -894,7 +915,9 @@ export class ContextManager {
|
|
|
894
915
|
compactedOld.push({ ...msg, content: compactedBlocks });
|
|
895
916
|
}
|
|
896
917
|
}
|
|
897
|
-
|
|
918
|
+
// Repair tool_use/tool_result pairing after compaction
|
|
919
|
+
// This fixes orphaned blocks that cause API errors
|
|
920
|
+
const compactedMessages = repairToolPairing([...compactedOld, ...recentMessages]);
|
|
898
921
|
const tokensAfter = await this.countTokens(compactedMessages);
|
|
899
922
|
this.compactionCount++;
|
|
900
923
|
this.lastCompactionTurn = this.turnCount;
|
package/dist/errors.d.ts
CHANGED
|
@@ -43,7 +43,26 @@ export declare class ProviderError extends AgentError {
|
|
|
43
43
|
*/
|
|
44
44
|
isServerError(): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Check if this
|
|
46
|
+
* Check if this is a connection/network error.
|
|
47
|
+
* These errors typically have no status code but are retryable.
|
|
48
|
+
*/
|
|
49
|
+
isConnectionError(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Check if this is an Anthropic overloaded error (529)
|
|
52
|
+
*/
|
|
53
|
+
isOverloadedError(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Check if this error is retryable.
|
|
56
|
+
* Retryable errors include:
|
|
57
|
+
* - Rate limit (429)
|
|
58
|
+
* - Server errors (5xx)
|
|
59
|
+
* - Connection/network errors
|
|
60
|
+
* - Anthropic overloaded (529)
|
|
61
|
+
*
|
|
62
|
+
* Non-retryable errors include:
|
|
63
|
+
* - Authentication errors (401, 403)
|
|
64
|
+
* - Bad request (400)
|
|
65
|
+
* - Not found (404)
|
|
47
66
|
*/
|
|
48
67
|
isRetryable(): boolean;
|
|
49
68
|
}
|
package/dist/errors.js
CHANGED
|
@@ -62,10 +62,52 @@ export class ProviderError extends AgentError {
|
|
|
62
62
|
return this.statusCode !== undefined && this.statusCode >= 500 && this.statusCode < 600;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Check if this
|
|
65
|
+
* Check if this is a connection/network error.
|
|
66
|
+
* These errors typically have no status code but are retryable.
|
|
67
|
+
*/
|
|
68
|
+
isConnectionError() {
|
|
69
|
+
if (this.statusCode !== undefined) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const msg = this.message.toLowerCase();
|
|
73
|
+
return (msg.includes('connection') ||
|
|
74
|
+
msg.includes('econnrefused') ||
|
|
75
|
+
msg.includes('econnreset') ||
|
|
76
|
+
msg.includes('etimedout') ||
|
|
77
|
+
msg.includes('enotfound') ||
|
|
78
|
+
msg.includes('network') ||
|
|
79
|
+
msg.includes('socket') ||
|
|
80
|
+
msg.includes('dns'));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if this is an Anthropic overloaded error (529)
|
|
84
|
+
*/
|
|
85
|
+
isOverloadedError() {
|
|
86
|
+
return this.statusCode === 529;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if this error is retryable.
|
|
90
|
+
* Retryable errors include:
|
|
91
|
+
* - Rate limit (429)
|
|
92
|
+
* - Server errors (5xx)
|
|
93
|
+
* - Connection/network errors
|
|
94
|
+
* - Anthropic overloaded (529)
|
|
95
|
+
*
|
|
96
|
+
* Non-retryable errors include:
|
|
97
|
+
* - Authentication errors (401, 403)
|
|
98
|
+
* - Bad request (400)
|
|
99
|
+
* - Not found (404)
|
|
66
100
|
*/
|
|
67
101
|
isRetryable() {
|
|
68
|
-
|
|
102
|
+
// Never retry auth errors
|
|
103
|
+
if (this.isAuthError()) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Retry transient errors
|
|
107
|
+
return (this.isRateLimitError() ||
|
|
108
|
+
this.isServerError() ||
|
|
109
|
+
this.isConnectionError() ||
|
|
110
|
+
this.isOverloadedError());
|
|
69
111
|
}
|
|
70
112
|
}
|
|
71
113
|
/**
|