@codeany/open-agent-sdk 0.1.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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +388 -0
  4. package/examples/01-simple-query.ts +43 -0
  5. package/examples/02-multi-tool.ts +44 -0
  6. package/examples/03-multi-turn.ts +39 -0
  7. package/examples/04-prompt-api.ts +29 -0
  8. package/examples/05-custom-system-prompt.ts +26 -0
  9. package/examples/06-mcp-server.ts +49 -0
  10. package/examples/07-custom-tools.ts +87 -0
  11. package/examples/08-official-api-compat.ts +38 -0
  12. package/examples/09-subagents.ts +48 -0
  13. package/examples/10-permissions.ts +40 -0
  14. package/examples/11-custom-mcp-tools.ts +101 -0
  15. package/examples/web/index.html +365 -0
  16. package/examples/web/server.ts +157 -0
  17. package/package.json +60 -0
  18. package/src/agent.ts +425 -0
  19. package/src/engine.ts +520 -0
  20. package/src/hooks.ts +261 -0
  21. package/src/index.ts +376 -0
  22. package/src/mcp/client.ts +150 -0
  23. package/src/sdk-mcp-server.ts +78 -0
  24. package/src/session.ts +227 -0
  25. package/src/tool-helper.ts +127 -0
  26. package/src/tools/agent-tool.ts +153 -0
  27. package/src/tools/ask-user.ts +79 -0
  28. package/src/tools/bash.ts +75 -0
  29. package/src/tools/config-tool.ts +89 -0
  30. package/src/tools/cron-tools.ts +153 -0
  31. package/src/tools/edit.ts +74 -0
  32. package/src/tools/glob.ts +77 -0
  33. package/src/tools/grep.ts +168 -0
  34. package/src/tools/index.ts +232 -0
  35. package/src/tools/lsp-tool.ts +163 -0
  36. package/src/tools/mcp-resource-tools.ts +125 -0
  37. package/src/tools/notebook-edit.ts +93 -0
  38. package/src/tools/plan-tools.ts +88 -0
  39. package/src/tools/read.ts +73 -0
  40. package/src/tools/send-message.ts +96 -0
  41. package/src/tools/task-tools.ts +290 -0
  42. package/src/tools/team-tools.ts +128 -0
  43. package/src/tools/todo-tool.ts +112 -0
  44. package/src/tools/tool-search.ts +87 -0
  45. package/src/tools/types.ts +62 -0
  46. package/src/tools/web-fetch.ts +66 -0
  47. package/src/tools/web-search.ts +86 -0
  48. package/src/tools/worktree-tools.ts +140 -0
  49. package/src/tools/write.ts +42 -0
  50. package/src/types.ts +459 -0
  51. package/src/utils/compact.ts +206 -0
  52. package/src/utils/context.ts +191 -0
  53. package/src/utils/fileCache.ts +148 -0
  54. package/src/utils/messages.ts +196 -0
  55. package/src/utils/retry.ts +140 -0
  56. package/src/utils/tokens.ts +122 -0
  57. package/tsconfig.json +19 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * FileWriteTool - Write/create files
3
+ */
4
+
5
+ import { writeFile, mkdir } from 'fs/promises'
6
+ import { resolve, dirname } from 'path'
7
+ import { defineTool } from './types.js'
8
+
9
+ export const FileWriteTool = defineTool({
10
+ name: 'Write',
11
+ description: 'Write content to a file. Creates the file if it does not exist, or overwrites if it does. Creates parent directories as needed.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ file_path: {
16
+ type: 'string',
17
+ description: 'The absolute path to the file to write',
18
+ },
19
+ content: {
20
+ type: 'string',
21
+ description: 'The content to write to the file',
22
+ },
23
+ },
24
+ required: ['file_path', 'content'],
25
+ },
26
+ isReadOnly: false,
27
+ isConcurrencySafe: false,
28
+ async call(input, context) {
29
+ const filePath = resolve(context.cwd, input.file_path)
30
+
31
+ try {
32
+ await mkdir(dirname(filePath), { recursive: true })
33
+ await writeFile(filePath, input.content, 'utf-8')
34
+
35
+ const lines = input.content.split('\n').length
36
+ const bytes = Buffer.byteLength(input.content, 'utf-8')
37
+ return `File written: ${filePath} (${lines} lines, ${bytes} bytes)`
38
+ } catch (err: any) {
39
+ return { data: `Error writing file: ${err.message}`, is_error: true }
40
+ }
41
+ },
42
+ })
package/src/types.ts ADDED
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Core type definitions for the Agent SDK
3
+ */
4
+
5
+ import type Anthropic from '@anthropic-ai/sdk'
6
+
7
+ // --------------------------------------------------------------------------
8
+ // Message Types
9
+ // --------------------------------------------------------------------------
10
+
11
+ export type MessageRole = 'user' | 'assistant'
12
+
13
+ export interface ConversationMessage {
14
+ role: MessageRole
15
+ content: string | Anthropic.ContentBlockParam[]
16
+ }
17
+
18
+ export interface UserMessage {
19
+ type: 'user'
20
+ message: ConversationMessage
21
+ uuid: string
22
+ timestamp: string
23
+ }
24
+
25
+ export interface AssistantMessage {
26
+ type: 'assistant'
27
+ message: {
28
+ role: 'assistant'
29
+ content: Anthropic.ContentBlock[]
30
+ }
31
+ uuid: string
32
+ timestamp: string
33
+ usage?: TokenUsage
34
+ cost?: number
35
+ }
36
+
37
+ export type Message = UserMessage | AssistantMessage
38
+
39
+ // --------------------------------------------------------------------------
40
+ // SDK Message Types (streaming events)
41
+ // --------------------------------------------------------------------------
42
+
43
+ export type SDKMessage =
44
+ | SDKAssistantMessage
45
+ | SDKToolResultMessage
46
+ | SDKResultMessage
47
+ | SDKPartialMessage
48
+ | SDKSystemMessage
49
+ | SDKCompactBoundaryMessage
50
+ | SDKStatusMessage
51
+ | SDKTaskNotificationMessage
52
+ | SDKRateLimitEvent
53
+
54
+ export interface SDKAssistantMessage {
55
+ type: 'assistant'
56
+ uuid?: string
57
+ session_id?: string
58
+ message: {
59
+ role: 'assistant'
60
+ content: Anthropic.ContentBlock[]
61
+ }
62
+ parent_tool_use_id?: string | null
63
+ }
64
+
65
+ export interface SDKToolResultMessage {
66
+ type: 'tool_result'
67
+ result: {
68
+ tool_use_id: string
69
+ tool_name: string
70
+ output: string
71
+ }
72
+ }
73
+
74
+ export interface SDKResultMessage {
75
+ type: 'result'
76
+ subtype: 'success' | 'error_max_turns' | 'error_during_execution' | 'error_max_budget_usd' | string
77
+ uuid?: string
78
+ session_id?: string
79
+ is_error?: boolean
80
+ num_turns?: number
81
+ result?: string
82
+ stop_reason?: string | null
83
+ total_cost_usd?: number
84
+ duration_ms?: number
85
+ duration_api_ms?: number
86
+ usage?: TokenUsage
87
+ model_usage?: Record<string, { input_tokens: number; output_tokens: number }>
88
+ permission_denials?: Array<{ tool: string; reason: string }>
89
+ structured_output?: unknown
90
+ errors?: string[]
91
+ /** @deprecated Use total_cost_usd */
92
+ cost?: number
93
+ }
94
+
95
+ export interface SDKPartialMessage {
96
+ type: 'partial_message'
97
+ partial: {
98
+ type: 'text' | 'tool_use'
99
+ text?: string
100
+ name?: string
101
+ input?: string
102
+ }
103
+ }
104
+
105
+ /** Emitted once at session start with initialization info. */
106
+ export interface SDKSystemMessage {
107
+ type: 'system'
108
+ subtype: 'init'
109
+ uuid?: string
110
+ session_id: string
111
+ tools: string[]
112
+ model: string
113
+ cwd: string
114
+ mcp_servers: Array<{ name: string; status: string }>
115
+ permission_mode: string
116
+ }
117
+
118
+ /** Marks a compaction boundary in the conversation. */
119
+ export interface SDKCompactBoundaryMessage {
120
+ type: 'system'
121
+ subtype: 'compact_boundary'
122
+ summary?: string
123
+ }
124
+
125
+ /** Status update during long operations. */
126
+ export interface SDKStatusMessage {
127
+ type: 'system'
128
+ subtype: 'status'
129
+ message: string
130
+ }
131
+
132
+ /** Task lifecycle notification. */
133
+ export interface SDKTaskNotificationMessage {
134
+ type: 'system'
135
+ subtype: 'task_notification'
136
+ task_id: string
137
+ status: string
138
+ message?: string
139
+ }
140
+
141
+ /** Rate limit event. */
142
+ export interface SDKRateLimitEvent {
143
+ type: 'system'
144
+ subtype: 'rate_limit'
145
+ retry_after_ms?: number
146
+ message: string
147
+ }
148
+
149
+ // --------------------------------------------------------------------------
150
+ // Token Usage
151
+ // --------------------------------------------------------------------------
152
+
153
+ export interface TokenUsage {
154
+ input_tokens: number
155
+ output_tokens: number
156
+ cache_creation_input_tokens?: number
157
+ cache_read_input_tokens?: number
158
+ }
159
+
160
+ // --------------------------------------------------------------------------
161
+ // Tool Types
162
+ // --------------------------------------------------------------------------
163
+
164
+ export interface ToolDefinition {
165
+ name: string
166
+ description: string
167
+ inputSchema: ToolInputSchema
168
+ call: (input: any, context: ToolContext) => Promise<ToolResult>
169
+ isReadOnly?: () => boolean
170
+ isConcurrencySafe?: () => boolean
171
+ isEnabled?: () => boolean
172
+ prompt?: (context: ToolContext) => Promise<string>
173
+ }
174
+
175
+ export interface ToolInputSchema {
176
+ type: 'object'
177
+ properties: Record<string, any>
178
+ required?: string[]
179
+ }
180
+
181
+ export interface ToolContext {
182
+ cwd: string
183
+ abortSignal?: AbortSignal
184
+ }
185
+
186
+ export interface ToolResult {
187
+ type: 'tool_result'
188
+ tool_use_id: string
189
+ content: string | Anthropic.ToolResultBlockParam['content']
190
+ is_error?: boolean
191
+ }
192
+
193
+ // --------------------------------------------------------------------------
194
+ // Permission Types
195
+ // --------------------------------------------------------------------------
196
+
197
+ export type PermissionMode =
198
+ | 'default'
199
+ | 'acceptEdits'
200
+ | 'bypassPermissions'
201
+ | 'plan'
202
+ | 'dontAsk'
203
+ | 'auto'
204
+
205
+ export type CanUseToolResult = {
206
+ behavior: 'allow' | 'deny'
207
+ updatedInput?: unknown
208
+ message?: string
209
+ }
210
+
211
+ export type CanUseToolFn = (
212
+ tool: ToolDefinition,
213
+ input: unknown,
214
+ ) => Promise<CanUseToolResult>
215
+
216
+ // --------------------------------------------------------------------------
217
+ // MCP Types
218
+ // --------------------------------------------------------------------------
219
+
220
+ export type McpServerConfig =
221
+ | McpStdioConfig
222
+ | McpSseConfig
223
+ | McpHttpConfig
224
+
225
+ export interface McpStdioConfig {
226
+ type?: 'stdio'
227
+ command: string
228
+ args?: string[]
229
+ env?: Record<string, string>
230
+ }
231
+
232
+ export interface McpSseConfig {
233
+ type: 'sse'
234
+ url: string
235
+ headers?: Record<string, string>
236
+ }
237
+
238
+ export interface McpHttpConfig {
239
+ type: 'http'
240
+ url: string
241
+ headers?: Record<string, string>
242
+ }
243
+
244
+ // --------------------------------------------------------------------------
245
+ // Agent Types
246
+ // --------------------------------------------------------------------------
247
+
248
+ export interface AgentDefinition {
249
+ description: string
250
+ prompt: string
251
+ tools?: string[]
252
+ disallowedTools?: string[]
253
+ model?: 'sonnet' | 'opus' | 'haiku' | 'inherit' | string
254
+ mcpServers?: Array<string | { name: string; tools?: string[] }>
255
+ skills?: string[]
256
+ maxTurns?: number
257
+ criticalSystemReminder_EXPERIMENTAL?: string
258
+ }
259
+
260
+ export interface ThinkingConfig {
261
+ type: 'adaptive' | 'enabled' | 'disabled'
262
+ budgetTokens?: number
263
+ }
264
+
265
+ // --------------------------------------------------------------------------
266
+ // Sandbox Types
267
+ // --------------------------------------------------------------------------
268
+
269
+ export interface SandboxSettings {
270
+ enabled?: boolean
271
+ autoAllowBashIfSandboxed?: boolean
272
+ excludedCommands?: string[]
273
+ allowUnsandboxedCommands?: boolean
274
+ network?: SandboxNetworkConfig
275
+ filesystem?: SandboxFilesystemConfig
276
+ ignoreViolations?: Record<string, string[]>
277
+ enableWeakerNestedSandbox?: boolean
278
+ ripgrep?: { command: string; args?: string[] }
279
+ }
280
+
281
+ export interface SandboxNetworkConfig {
282
+ allowedDomains?: string[]
283
+ allowManagedDomainsOnly?: boolean
284
+ allowLocalBinding?: boolean
285
+ allowUnixSockets?: string[]
286
+ allowAllUnixSockets?: boolean
287
+ httpProxyPort?: number
288
+ socksProxyPort?: number
289
+ }
290
+
291
+ export interface SandboxFilesystemConfig {
292
+ allowWrite?: string[]
293
+ denyWrite?: string[]
294
+ denyRead?: string[]
295
+ }
296
+
297
+ // --------------------------------------------------------------------------
298
+ // Output Format
299
+ // --------------------------------------------------------------------------
300
+
301
+ export interface OutputFormat {
302
+ type: 'json_schema'
303
+ schema: Record<string, unknown>
304
+ }
305
+
306
+ // --------------------------------------------------------------------------
307
+ // Setting Sources
308
+ // --------------------------------------------------------------------------
309
+
310
+ export type SettingSource = 'user' | 'project' | 'local'
311
+
312
+ // --------------------------------------------------------------------------
313
+ // Model Info
314
+ // --------------------------------------------------------------------------
315
+
316
+ export interface ModelInfo {
317
+ value: string
318
+ displayName: string
319
+ description: string
320
+ supportsEffort?: boolean
321
+ supportedEffortLevels?: ('low' | 'medium' | 'high' | 'max')[]
322
+ supportsAdaptiveThinking?: boolean
323
+ supportsFastMode?: boolean
324
+ }
325
+
326
+ export interface AgentOptions {
327
+ /** LLM model ID */
328
+ model?: string
329
+ /** API key. Falls back to CODEANY_API_KEY env var. */
330
+ apiKey?: string
331
+ /** API base URL override */
332
+ baseURL?: string
333
+ /** Working directory for file/shell tools */
334
+ cwd?: string
335
+ /** System prompt override or preset */
336
+ systemPrompt?: string | { type: 'preset'; preset: 'default'; append?: string }
337
+ /** Append to default system prompt */
338
+ appendSystemPrompt?: string
339
+ /** Available tools (ToolDefinition[] or string[] preset) */
340
+ tools?: ToolDefinition[] | string[] | { type: 'preset'; preset: 'default' }
341
+ /** Maximum number of agentic turns per query */
342
+ maxTurns?: number
343
+ /** Maximum USD budget per query */
344
+ maxBudgetUsd?: number
345
+ /** Extended thinking configuration */
346
+ thinking?: ThinkingConfig
347
+ /** Maximum thinking tokens (deprecated, use thinking.budgetTokens) */
348
+ maxThinkingTokens?: number
349
+ /** Structured output JSON schema */
350
+ jsonSchema?: Record<string, unknown>
351
+ /** Structured output format */
352
+ outputFormat?: OutputFormat
353
+ /** Permission handler callback */
354
+ canUseTool?: CanUseToolFn
355
+ /** Permission mode controlling tool approval behavior */
356
+ permissionMode?: PermissionMode
357
+ /** Abort controller for cancellation */
358
+ abortController?: AbortController
359
+ /** Abort signal for cancellation */
360
+ abortSignal?: AbortSignal
361
+ /** Whether to include partial streaming events */
362
+ includePartialMessages?: boolean
363
+ /** Environment variables */
364
+ env?: Record<string, string | undefined>
365
+ /** Tool names to pre-approve without prompting */
366
+ allowedTools?: string[]
367
+ /** Tool names to deny */
368
+ disallowedTools?: string[]
369
+ /** MCP server configurations */
370
+ mcpServers?: Record<string, McpServerConfig | any> // supports McpSdkServerConfig
371
+ /** Custom subagent definitions */
372
+ agents?: Record<string, AgentDefinition>
373
+ /** Maximum tokens for responses */
374
+ maxTokens?: number
375
+ /** Effort level for reasoning */
376
+ effort?: 'low' | 'medium' | 'high' | 'max'
377
+ /** Fallback model if primary is unavailable */
378
+ fallbackModel?: string
379
+ /** Continue the most recent session in cwd */
380
+ continue?: boolean
381
+ /** Resume a specific session by ID */
382
+ resume?: string
383
+ /** Fork a session instead of continuing it */
384
+ forkSession?: boolean
385
+ /** Persist session to disk */
386
+ persistSession?: boolean
387
+ /** Explicit session ID */
388
+ sessionId?: string
389
+ /** Enable file checkpointing (for rewindFiles) */
390
+ enableFileCheckpointing?: boolean
391
+ /** Sandbox configuration */
392
+ sandbox?: SandboxSettings
393
+ /** Load settings from filesystem */
394
+ settingSources?: SettingSource[]
395
+ /** Plugin configurations */
396
+ plugins?: Array<{ name: string; config?: Record<string, unknown> }>
397
+ /** Additional working directories */
398
+ additionalDirectories?: string[]
399
+ /** Default agent to use */
400
+ agent?: string
401
+ /** Debug mode */
402
+ debug?: boolean
403
+ /** Debug log file */
404
+ debugFile?: string
405
+ /** Tool-specific configuration */
406
+ toolConfig?: Record<string, unknown>
407
+ /** Enable prompt suggestions */
408
+ promptSuggestions?: boolean
409
+ /** Strict MCP config validation */
410
+ strictMcpConfig?: boolean
411
+ /** Extra CLI arguments */
412
+ extraArgs?: Record<string, string | null>
413
+ /** SDK betas to enable */
414
+ betas?: string[]
415
+ /** Permission prompt tool name override */
416
+ permissionPromptToolName?: string
417
+ /** Hook configurations */
418
+ hooks?: Record<string, Array<{
419
+ matcher?: string
420
+ hooks: Array<(input: any, toolUseId: string, context: { signal: AbortSignal }) => Promise<any>>
421
+ timeout?: number
422
+ }>>
423
+ }
424
+
425
+ export interface QueryResult {
426
+ /** Final text output from the assistant */
427
+ text: string
428
+ /** Token usage */
429
+ usage: TokenUsage
430
+ /** Number of agentic turns */
431
+ num_turns: number
432
+ /** Duration in milliseconds */
433
+ duration_ms: number
434
+ /** All conversation messages */
435
+ messages: Message[]
436
+ }
437
+
438
+ // --------------------------------------------------------------------------
439
+ // Query Engine Types
440
+ // --------------------------------------------------------------------------
441
+
442
+ export interface QueryEngineConfig {
443
+ cwd: string
444
+ model: string
445
+ apiKey?: string
446
+ baseURL?: string
447
+ tools: ToolDefinition[]
448
+ systemPrompt?: string
449
+ appendSystemPrompt?: string
450
+ maxTurns: number
451
+ maxBudgetUsd?: number
452
+ maxTokens: number
453
+ thinking?: ThinkingConfig
454
+ jsonSchema?: Record<string, unknown>
455
+ canUseTool: CanUseToolFn
456
+ includePartialMessages: boolean
457
+ abortSignal?: AbortSignal
458
+ agents?: Record<string, AgentDefinition>
459
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Context Compression / Auto-Compaction
3
+ *
4
+ * Summarizes long conversation histories when context window fills up.
5
+ * Three-tier system:
6
+ * 1. Auto-compact: triggered when tokens exceed threshold
7
+ * 2. Micro-compact: cache-aware per-request optimization
8
+ * 3. Session memory compaction: consolidates across sessions
9
+ */
10
+
11
+ import Anthropic from '@anthropic-ai/sdk'
12
+ import {
13
+ estimateMessagesTokens,
14
+ getAutoCompactThreshold,
15
+ } from './tokens.js'
16
+
17
+ /**
18
+ * State for tracking auto-compaction across turns.
19
+ */
20
+ export interface AutoCompactState {
21
+ compacted: boolean
22
+ turnCounter: number
23
+ consecutiveFailures: number
24
+ }
25
+
26
+ /**
27
+ * Create initial auto-compact state.
28
+ */
29
+ export function createAutoCompactState(): AutoCompactState {
30
+ return {
31
+ compacted: false,
32
+ turnCounter: 0,
33
+ consecutiveFailures: 0,
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if auto-compaction should trigger.
39
+ */
40
+ export function shouldAutoCompact(
41
+ messages: Anthropic.MessageParam[],
42
+ model: string,
43
+ state: AutoCompactState,
44
+ ): boolean {
45
+ if (state.consecutiveFailures >= 3) return false
46
+
47
+ const estimatedTokens = estimateMessagesTokens(messages)
48
+ const threshold = getAutoCompactThreshold(model)
49
+
50
+ return estimatedTokens >= threshold
51
+ }
52
+
53
+ /**
54
+ * Compact conversation by summarizing with the LLM.
55
+ *
56
+ * Sends the entire conversation to the LLM for summarization,
57
+ * then replaces the history with a compact summary.
58
+ */
59
+ export async function compactConversation(
60
+ client: Anthropic,
61
+ model: string,
62
+ messages: Anthropic.MessageParam[],
63
+ state: AutoCompactState,
64
+ ): Promise<{
65
+ compactedMessages: Anthropic.MessageParam[]
66
+ summary: string
67
+ state: AutoCompactState
68
+ }> {
69
+ try {
70
+ // Strip images before compacting to save tokens
71
+ const strippedMessages = stripImagesFromMessages(messages)
72
+
73
+ // Build compaction prompt
74
+ const compactionPrompt = buildCompactionPrompt(strippedMessages)
75
+
76
+ const response = await client.messages.create({
77
+ model,
78
+ max_tokens: 8192,
79
+ system: 'You are a conversation summarizer. Create a detailed summary of the conversation that preserves all important context, decisions made, files modified, tool outputs, and current state. The summary should allow the conversation to continue seamlessly.',
80
+ messages: [
81
+ {
82
+ role: 'user',
83
+ content: compactionPrompt,
84
+ },
85
+ ],
86
+ })
87
+
88
+ const summary = response.content
89
+ .filter((b): b is Anthropic.TextBlock => b.type === 'text')
90
+ .map((b) => b.text)
91
+ .join('\n')
92
+
93
+ // Replace messages with summary
94
+ const compactedMessages: Anthropic.MessageParam[] = [
95
+ {
96
+ role: 'user',
97
+ content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary - conversation continues below]`,
98
+ },
99
+ {
100
+ role: 'assistant',
101
+ content: 'I understand the context from the previous conversation. I\'ll continue from where we left off.',
102
+ },
103
+ ]
104
+
105
+ return {
106
+ compactedMessages,
107
+ summary,
108
+ state: {
109
+ compacted: true,
110
+ turnCounter: state.turnCounter,
111
+ consecutiveFailures: 0,
112
+ },
113
+ }
114
+ } catch (err: any) {
115
+ return {
116
+ compactedMessages: messages,
117
+ summary: '',
118
+ state: {
119
+ ...state,
120
+ consecutiveFailures: state.consecutiveFailures + 1,
121
+ },
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Strip images from messages for compaction safety.
128
+ */
129
+ function stripImagesFromMessages(
130
+ messages: Anthropic.MessageParam[],
131
+ ): Anthropic.MessageParam[] {
132
+ return messages.map((msg) => {
133
+ if (typeof msg.content === 'string') return msg
134
+
135
+ const filtered = (msg.content as any[]).filter((block: any) => {
136
+ return block.type !== 'image'
137
+ })
138
+
139
+ return { ...msg, content: filtered.length > 0 ? filtered : '[content removed for compaction]' }
140
+ })
141
+ }
142
+
143
+ /**
144
+ * Build compaction prompt from messages.
145
+ */
146
+ function buildCompactionPrompt(messages: Anthropic.MessageParam[]): string {
147
+ const parts: string[] = ['Please summarize this conversation:\n']
148
+
149
+ for (const msg of messages) {
150
+ const role = msg.role === 'user' ? 'User' : 'Assistant'
151
+
152
+ if (typeof msg.content === 'string') {
153
+ parts.push(`${role}: ${msg.content.slice(0, 5000)}`)
154
+ } else if (Array.isArray(msg.content)) {
155
+ const texts: string[] = []
156
+ for (const block of msg.content as any[]) {
157
+ if (block.type === 'text') {
158
+ texts.push(block.text.slice(0, 3000))
159
+ } else if (block.type === 'tool_use') {
160
+ texts.push(`[Tool: ${block.name}]`)
161
+ } else if (block.type === 'tool_result') {
162
+ const content = typeof block.content === 'string'
163
+ ? block.content.slice(0, 1000)
164
+ : '[tool result]'
165
+ texts.push(`[Tool Result: ${content}]`)
166
+ }
167
+ }
168
+ if (texts.length > 0) {
169
+ parts.push(`${role}: ${texts.join('\n')}`)
170
+ }
171
+ }
172
+ }
173
+
174
+ return parts.join('\n\n')
175
+ }
176
+
177
+ /**
178
+ * Micro-compact: optimize messages by truncating large tool results
179
+ * to fit within token budgets.
180
+ */
181
+ export function microCompactMessages(
182
+ messages: Anthropic.MessageParam[],
183
+ maxToolResultChars: number = 50000,
184
+ ): Anthropic.MessageParam[] {
185
+ return messages.map((msg) => {
186
+ if (typeof msg.content === 'string') return msg
187
+ if (!Array.isArray(msg.content)) return msg
188
+
189
+ const content = (msg.content as any[]).map((block: any) => {
190
+ if (block.type === 'tool_result' && typeof block.content === 'string') {
191
+ if (block.content.length > maxToolResultChars) {
192
+ return {
193
+ ...block,
194
+ content:
195
+ block.content.slice(0, maxToolResultChars / 2) +
196
+ '\n...(truncated)...\n' +
197
+ block.content.slice(-maxToolResultChars / 2),
198
+ }
199
+ }
200
+ }
201
+ return block
202
+ })
203
+
204
+ return { ...msg, content }
205
+ })
206
+ }