Skip to content

Commit aab64fe

Browse files
committed
Call Anthropic token count endpoint via our web api every step. Use in context pruner
1 parent 7b2ecf5 commit aab64fe

File tree

19 files changed

+532
-120
lines changed

19 files changed

+532
-120
lines changed

.agents/context-pruner.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,7 @@ const definition: AgentDefinition = {
241241
}
242242

243243
// Initial check - if already under limit, return early (skip all pruning)
244-
const initialTokens = countMessagesTokens(currentMessages)
245-
if (initialTokens < maxMessageTokens) {
244+
if (agentState.contextTokenCount < maxMessageTokens) {
246245
yield {
247246
toolName: 'set_messages',
248247
input: { messages: currentMessages },

.agents/types/agent-definition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ export interface AgentState {
291291
string,
292292
{ description: string | undefined; inputSchema: {} }
293293
>
294+
295+
/**
296+
* The token count from the Anthropic API.
297+
* This is updated on every agent step via the /api/v1/token-count endpoint.
298+
*/
299+
contextTokenCount: number
294300
}
295301

296302
/**

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
CLAUDE_CODE_KEY=dummy_claude_code_key
33
OPEN_ROUTER_API_KEY=dummy_openrouter_key
44
OPENAI_API_KEY=dummy_openai_key
5+
ANTHROPIC_API_KEY=dummy_anthropic_key
56

67
# Database & Server
78
DATABASE_URL=postgresql://manicode_user_local:secretpassword_local@localhost:5432/manicode_db_local

common/src/constants/analytics-events.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ export enum AnalyticsEvent {
122122
// Web - Ads API
123123
ADS_API_AUTH_ERROR = 'api.ads_auth_error',
124124

125+
// Web - Token Count API
126+
TOKEN_COUNT_REQUEST = 'api.token_count_request',
127+
TOKEN_COUNT_AUTH_ERROR = 'api.token_count_auth_error',
128+
TOKEN_COUNT_VALIDATION_ERROR = 'api.token_count_validation_error',
129+
TOKEN_COUNT_ERROR = 'api.token_count_error',
130+
125131
// Common
126132
FLUSH_FAILED = 'common.flush_failed',
127133
}

common/src/templates/initial-agents-dir/types/agent-definition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ export interface AgentState {
291291
string,
292292
{ description: string | undefined; inputSchema: {} }
293293
>
294+
295+
/**
296+
* The token count from the Anthropic API.
297+
* This is updated on every agent step via the /api/v1/token-count endpoint.
298+
*/
299+
contextTokenCount: number
294300
}
295301

296302
/**

common/src/types/session-state.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type AgentState = {
4444
string,
4545
{ description: string | undefined; inputSchema: {} }
4646
>
47+
/**
48+
* The accurate token count from the Anthropic API.
49+
* This is updated on every agent step via the /api/v1/token-count endpoint.
50+
*/
51+
contextTokenCount: number
4752
}
4853

4954
export const AgentOutputSchema = z.discriminatedUnion('type', [
@@ -127,6 +132,7 @@ export function getInitialAgentState(): AgentState {
127132
parentId: undefined,
128133
systemPrompt: '',
129134
toolDefinitions: {},
135+
contextTokenCount: 0,
130136
}
131137
}
132138
export function getInitialSessionState(

packages/agent-runtime/src/__tests__/cost-aggregation.test.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe('Cost Aggregation System', () => {
124124
directCreditsUsed: 50,
125125
systemPrompt: 'Test system prompt',
126126
toolDefinitions: {},
127+
contextTokenCount: 0,
127128
}
128129

129130
// Mock executeAgent to return results with different credit costs
@@ -136,7 +137,10 @@ describe('Cost Aggregation System', () => {
136137
stepsRemaining: 10,
137138
creditsUsed: 75, // First subagent uses 75 credits
138139
},
139-
output: { type: 'lastMessage', value: [assistantMessage('Sub-agent 1 response')] },
140+
output: {
141+
type: 'lastMessage',
142+
value: [assistantMessage('Sub-agent 1 response')],
143+
},
140144
})
141145
.mockResolvedValueOnce({
142146
agentState: {
@@ -146,7 +150,10 @@ describe('Cost Aggregation System', () => {
146150
stepsRemaining: 10,
147151
creditsUsed: 100, // Second subagent uses 100 credits
148152
},
149-
output: { type: 'lastMessage', value: [assistantMessage('Sub-agent 2 response')] },
153+
output: {
154+
type: 'lastMessage',
155+
value: [assistantMessage('Sub-agent 2 response')],
156+
},
150157
})
151158

152159
const mockToolCall = {
@@ -200,7 +207,10 @@ describe('Cost Aggregation System', () => {
200207
stepsRemaining: 10,
201208
creditsUsed: 50, // Successful agent
202209
},
203-
output: { type: 'lastMessage', value: [assistantMessage('Successful response')] },
210+
output: {
211+
type: 'lastMessage',
212+
value: [assistantMessage('Successful response')],
213+
},
204214
})
205215
.mockRejectedValueOnce(
206216
(() => {
@@ -347,7 +357,10 @@ describe('Cost Aggregation System', () => {
347357
stepsRemaining: 10,
348358
creditsUsed: subAgent1Cost,
349359
} as AgentState,
350-
output: { type: 'lastMessage', value: [assistantMessage('Sub-agent 1 response')] },
360+
output: {
361+
type: 'lastMessage',
362+
value: [assistantMessage('Sub-agent 1 response')],
363+
},
351364
})
352365
.mockResolvedValueOnce({
353366
agentState: {
@@ -358,7 +371,10 @@ describe('Cost Aggregation System', () => {
358371
stepsRemaining: 10,
359372
creditsUsed: subAgent2Cost,
360373
} as AgentState,
361-
output: { type: 'lastMessage', value: [assistantMessage('Sub-agent 2 response')] },
374+
output: {
375+
type: 'lastMessage',
376+
value: [assistantMessage('Sub-agent 2 response')],
377+
},
362378
})
363379

364380
const mockToolCall = {

packages/agent-runtime/src/__tests__/main-prompt.test.ts

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ describe('mainPrompt', () => {
9999
onResponseChunk: () => {},
100100
localAgentTemplates: mockLocalAgentTemplates,
101101
signal: new AbortController().signal,
102+
// Mock fetch to return a token count response
103+
fetch: async () =>
104+
({
105+
ok: true,
106+
text: async () => JSON.stringify({ inputTokens: 1000 }),
107+
}) as Response,
102108
}
103109

104110
// Mock analytics and tracing
@@ -451,56 +457,4 @@ describe('mainPrompt', () => {
451457

452458
expect(output.type).toBeDefined() // Output should exist even for empty response
453459
})
454-
455-
it('should unescape ampersands in run_terminal_command tool calls', async () => {
456-
const sessionState = getInitialSessionState(mockFileContext)
457-
const userPromptText = 'Run the backend tests'
458-
const expectedCommand = 'cd backend && bun test'
459-
460-
mockAgentStream([
461-
createToolCallChunk('run_terminal_command', {
462-
command: expectedCommand,
463-
process_type: 'SYNC',
464-
}),
465-
createToolCallChunk('end_turn', {}),
466-
])
467-
468-
// Get reference to the spy so we can check if it was called
469-
const requestToolCallSpy = mainPromptBaseParams.requestToolCall
470-
471-
const action = {
472-
type: 'prompt' as const,
473-
prompt: userPromptText,
474-
sessionState,
475-
fingerprintId: 'test',
476-
costMode: 'max' as const,
477-
promptId: 'test',
478-
toolResults: [],
479-
}
480-
481-
await mainPrompt({
482-
...mainPromptBaseParams,
483-
repoId: undefined,
484-
repoUrl: undefined,
485-
action,
486-
userId: TEST_USER_ID,
487-
clientSessionId: 'test-session',
488-
onResponseChunk: () => {},
489-
localAgentTemplates: mockLocalAgentTemplates,
490-
})
491-
492-
// Assert that requestToolCall was called exactly once
493-
expect(requestToolCallSpy).toHaveBeenCalledTimes(1)
494-
495-
// Verify the run_terminal_command call was made with the correct arguments
496-
expect(requestToolCallSpy).toHaveBeenCalledWith({
497-
userInputId: expect.any(String), // userInputId
498-
toolName: 'run_terminal_command',
499-
input: expect.objectContaining({
500-
command: expectedCommand,
501-
process_type: 'SYNC',
502-
mode: 'assistant',
503-
}),
504-
})
505-
})
506460
})

packages/agent-runtime/src/__tests__/prompt-caching-subagents.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ describe('Prompt Caching for Subagents with inheritParentSystemPrompt', () => {
4444
let capturedMessages: Message[] = []
4545
let loopAgentStepsBaseParams: ParamsExcluding<
4646
typeof loopAgentSteps,
47-
'agentState' | 'userInputId' | 'prompt' | 'agentType' | 'parentSystemPrompt'
47+
| 'agentState'
48+
| 'userInputId'
49+
| 'prompt'
50+
| 'agentType'
51+
| 'parentSystemPrompt'
52+
| 'agentTemplate'
4853
>
4954

5055
beforeAll(() => {

packages/agent-runtime/src/__tests__/read-docs-tool.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import type { ParamsExcluding } from '@codebuff/common/types/function-params'
3030
let agentRuntimeImpl: AgentRuntimeDeps & AgentRuntimeScopedDeps
3131
let runAgentStepBaseParams: ParamsExcluding<
3232
typeof runAgentStep,
33-
'fileContext' | 'localAgentTemplates' | 'agentState' | 'prompt'
33+
'fileContext' | 'localAgentTemplates' | 'agentState' | 'prompt' | 'agentTemplate'
3434
>
3535

3636
import type { StreamChunk } from '@codebuff/common/types/contracts/llm'
@@ -126,6 +126,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
126126
...runAgentStepBaseParams,
127127
fileContext: mockFileContextWithAgents,
128128
localAgentTemplates: agentTemplates,
129+
agentTemplate: agentTemplates['researcher'],
129130
agentState,
130131
prompt: 'Get React documentation',
131132
})
@@ -173,6 +174,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
173174
...runAgentStepBaseParams,
174175
fileContext: mockFileContextWithAgents,
175176
localAgentTemplates: agentTemplates,
177+
agentTemplate: agentTemplates['researcher'],
176178
agentState,
177179
prompt: 'Get React hooks documentation',
178180
})
@@ -212,6 +214,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
212214
...runAgentStepBaseParams,
213215
fileContext: mockFileContextWithAgents,
214216
localAgentTemplates: agentTemplates,
217+
agentTemplate: agentTemplates['researcher'],
215218
agentState,
216219
prompt: 'Get documentation for NonExistentLibrary',
217220
})
@@ -251,6 +254,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
251254
...runAgentStepBaseParams,
252255
fileContext: mockFileContextWithAgents,
253256
localAgentTemplates: agentTemplates,
257+
agentTemplate: agentTemplates['researcher'],
254258
agentState,
255259
prompt: 'Get React documentation',
256260
})
@@ -289,6 +293,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
289293
...runAgentStepBaseParams,
290294
fileContext: mockFileContextWithAgents,
291295
localAgentTemplates: agentTemplates,
296+
agentTemplate: agentTemplates['researcher'],
292297
agentState,
293298
prompt: 'Get React server components documentation',
294299
})
@@ -329,6 +334,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
329334
...runAgentStepBaseParams,
330335
fileContext: mockFileContextWithAgents,
331336
localAgentTemplates: agentTemplates,
337+
agentTemplate: agentTemplates['researcher'],
332338
agentState,
333339
prompt: 'Get React documentation',
334340
})
@@ -374,6 +380,7 @@ describe('read_docs tool with researcher agent (via web API facade)', () => {
374380
...runAgentStepBaseParams,
375381
fileContext: mockFileContextWithAgents,
376382
localAgentTemplates: agentTemplates,
383+
agentTemplate: agentTemplates['researcher'],
377384
agentState,
378385
prompt: 'Get React documentation',
379386
})

0 commit comments

Comments
 (0)