@compilr-dev/agents 0.5.1 → 0.5.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent - The main class for running AI agents with tool use
3
3
  */
4
- import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
4
+ import type { LLMProvider, Message, ChatOptions, StreamChunk, ContentBlock } from './providers/types.js';
5
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';
@@ -638,6 +638,18 @@ export interface AgentConfig {
638
638
  /** Max total tokens for inline file content after compaction (default: 4000) */
639
639
  maxInlineTokens?: number;
640
640
  };
641
+ /**
642
+ * Logger instance for structured diagnostic logging.
643
+ * When omitted, no logging is performed.
644
+ *
645
+ * @example
646
+ * ```typescript
647
+ * import { createLogger } from '@compilr-dev/logger';
648
+ * const log = createLogger({ package: 'my-app' });
649
+ * const agent = new Agent({ provider, logger: log });
650
+ * ```
651
+ */
652
+ logger?: import('@compilr-dev/logger').Logger;
641
653
  }
642
654
  /**
643
655
  * Options for a single run
@@ -1638,7 +1650,7 @@ export declare class Agent {
1638
1650
  /**
1639
1651
  * Run the agent with a user message
1640
1652
  */
1641
- run(userMessage: string, options?: RunOptions): Promise<AgentRunResult>;
1653
+ run(userMessage: string | ContentBlock[], options?: RunOptions): Promise<AgentRunResult>;
1642
1654
  /**
1643
1655
  * Stream the agent's response with full tool use support
1644
1656
  *
package/dist/agent.js CHANGED
@@ -2091,18 +2091,33 @@ export class Agent {
2091
2091
  const finalTokens = this.contextManager.estimateTokens(toolResultContent);
2092
2092
  this.contextManager.addToCategory('toolResults', finalTokens);
2093
2093
  }
2094
+ // Build content blocks: tool_result + optional image blocks
2095
+ const contentBlocks = [
2096
+ {
2097
+ type: 'tool_result',
2098
+ toolUseId: toolUse.id,
2099
+ content: toolResultContent,
2100
+ isError: !result.success,
2101
+ },
2102
+ ];
2103
+ // Inject image blocks from tool result (e.g., view_image tool)
2104
+ if (result.imageBlocks?.length) {
2105
+ for (const img of result.imageBlocks) {
2106
+ contentBlocks.push({
2107
+ type: 'image',
2108
+ data: img.data,
2109
+ mediaType: img.mediaType,
2110
+ filename: img.filename,
2111
+ width: img.width,
2112
+ height: img.height,
2113
+ });
2114
+ }
2115
+ }
2094
2116
  return {
2095
2117
  result,
2096
2118
  toolResultMsg: {
2097
2119
  role: 'user',
2098
- content: [
2099
- {
2100
- type: 'tool_result',
2101
- toolUseId: toolUse.id,
2102
- content: toolResultContent,
2103
- isError: !result.success,
2104
- },
2105
- ],
2120
+ content: contentBlocks,
2106
2121
  },
2107
2122
  skipped: false,
2108
2123
  aborted: false,
@@ -2352,8 +2367,9 @@ export class Agent {
2352
2367
  // Context management: increment turn count and update token count
2353
2368
  if (this.contextManager) {
2354
2369
  this.contextManager.incrementTurn();
2355
- // Observation masking: mask old tool results in-place before token update
2370
+ // Observation masking: stamp new images, then mask old results + images
2356
2371
  if (this.observationMasker) {
2372
+ this.observationMasker.stampImages(messages, this.contextManager.getTurnCount());
2357
2373
  this.observationMasker.maskHistory(messages, this.contextManager.getTurnCount());
2358
2374
  }
2359
2375
  // Dead message pruning: prune superseded errors and permission exchanges
@@ -21,7 +21,7 @@ export type { ToolResultDelegatorOptions } from './tool-result-delegator.js';
21
21
  export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
22
22
  export type { DelegationConfig, StoredResult, DelegationEvent } from './delegation-types.js';
23
23
  export { compactToolResult } from './result-compactor.js';
24
- export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
24
+ export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, maskImageBlock, } from './observation-masker.js';
25
25
  export type { InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, } from './observation-masker.js';
26
26
  export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
27
27
  export type { PruneConfig, PruneResult, PruneStats } from './dead-message-pruner.js';
@@ -18,7 +18,7 @@ export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
18
18
  // Compact Tool Result Formatting (Phase 2 Token Optimization)
19
19
  export { compactToolResult } from './result-compactor.js';
20
20
  // Observation Masking (Phase 1 Token Optimization) + Tool Input Compaction (Phase 1b)
21
- export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
21
+ export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, maskImageBlock, } from './observation-masker.js';
22
22
  // Dead Message Pruning (Phase 4 Token Optimization)
23
23
  export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
24
24
  // Smart Windowing (Programmatic Context Compaction)
@@ -8,7 +8,7 @@
8
8
  * Strategy: In-place masking of conversationHistory after N turns.
9
9
  * The agent can re-read from the environment if needed (files, git, etc.).
10
10
  */
11
- import type { Message } from '../providers/types.js';
11
+ import type { Message, ImageBlock, TextBlock } from '../providers/types.js';
12
12
  /**
13
13
  * Defines which input fields to keep when compacting a tool_use input.
14
14
  * All other fields are removed.
@@ -62,6 +62,8 @@ export declare class ObservationMasker {
62
62
  private readonly stamps;
63
63
  private readonly config;
64
64
  private stats;
65
+ /** Tracks which image blocks have been stamped (by identity) to avoid re-stamping */
66
+ private readonly stampedImages;
65
67
  constructor(config?: Partial<ObservationMaskConfig>);
66
68
  /**
67
69
  * Register a tool result with its turn number and input context.
@@ -69,8 +71,16 @@ export declare class ObservationMasker {
69
71
  */
70
72
  stamp(toolUseId: string, toolName: string, input: Record<string, unknown>, contentLength: number, turn: number): void;
71
73
  /**
72
- * Mask old tool results and compact old tool_use inputs in-place.
74
+ * Stamp all image blocks in a message array with the current turn.
75
+ * Call this after adding user messages that may contain images.
76
+ */
77
+ stampImages(messages: Message[], turn: number): void;
78
+ /** Turn at which each image block was first seen */
79
+ private readonly imageStamps;
80
+ /**
81
+ * Mask old tool results, images, and compact old tool_use inputs in-place.
73
82
  * - tool_result: replaces content with compact mask text (Phase 1)
83
+ * - image: replaces with text placeholder after maskAfterTurns (Phase 2)
74
84
  * - tool_use input: strips large fields, keeping only identifying fields (Phase 1b)
75
85
  */
76
86
  maskHistory(messages: Message[], currentTurn: number): MaskResult;
@@ -101,4 +111,9 @@ export declare function buildMaskText(stamp: TurnStamp): string;
101
111
  * Check if a tool result content string is already masked.
102
112
  */
103
113
  export declare function isMasked(content: string): boolean;
114
+ /**
115
+ * Replace an image content block with a text placeholder.
116
+ * Preserves filename and dimensions for context.
117
+ */
118
+ export declare function maskImageBlock(block: ImageBlock, turn: number): TextBlock;
104
119
  export {};
@@ -26,6 +26,8 @@ export class ObservationMasker {
26
26
  stamps = new Map();
27
27
  config;
28
28
  stats = { maskedCount: 0, tokensSaved: 0, inputsCompacted: 0 };
29
+ /** Tracks which image blocks have been stamped (by identity) to avoid re-stamping */
30
+ stampedImages = new WeakSet();
29
31
  constructor(config) {
30
32
  this.config = { ...DEFAULT_MASK_CONFIG, ...config };
31
33
  }
@@ -45,11 +47,33 @@ export class ObservationMasker {
45
47
  });
46
48
  }
47
49
  // ----------------------------------------------------------
50
+ // Image stamping — called when messages with images are added
51
+ // ----------------------------------------------------------
52
+ /**
53
+ * Stamp all image blocks in a message array with the current turn.
54
+ * Call this after adding user messages that may contain images.
55
+ */
56
+ stampImages(messages, turn) {
57
+ for (const msg of messages) {
58
+ if (typeof msg.content === 'string')
59
+ continue;
60
+ for (const block of msg.content) {
61
+ if (block.type === 'image' && !this.stampedImages.has(block)) {
62
+ this.imageStamps.set(block, turn);
63
+ this.stampedImages.add(block);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ /** Turn at which each image block was first seen */
69
+ imageStamps = new WeakMap();
70
+ // ----------------------------------------------------------
48
71
  // Masking — called after incrementTurn()
49
72
  // ----------------------------------------------------------
50
73
  /**
51
- * Mask old tool results and compact old tool_use inputs in-place.
74
+ * Mask old tool results, images, and compact old tool_use inputs in-place.
52
75
  * - tool_result: replaces content with compact mask text (Phase 1)
76
+ * - image: replaces with text placeholder after maskAfterTurns (Phase 2)
53
77
  * - tool_use input: strips large fields, keeping only identifying fields (Phase 1b)
54
78
  */
55
79
  maskHistory(messages, currentTurn) {
@@ -90,6 +114,34 @@ export class ObservationMasker {
90
114
  this.stamps.delete(block.toolUseId);
91
115
  }
92
116
  }
117
+ // Phase 2: Replace old image blocks with text placeholders
118
+ const contentArr = msg.content;
119
+ for (let i = 0; i < contentArr.length; i++) {
120
+ const block = contentArr[i];
121
+ if (block.type !== 'image')
122
+ continue;
123
+ // Stamp if not already stamped (images added before stampImages existed)
124
+ if (!this.stampedImages.has(block)) {
125
+ this.imageStamps.set(block, currentTurn);
126
+ this.stampedImages.add(block);
127
+ continue; // Don't mask on the same turn we stamp
128
+ }
129
+ const imageTurn = this.imageStamps.get(block);
130
+ if (imageTurn === undefined)
131
+ continue;
132
+ const age = currentTurn - imageTurn;
133
+ if (age < this.config.maskAfterTurns)
134
+ continue;
135
+ // Replace image block with text placeholder
136
+ const placeholder = maskImageBlock(block, imageTurn);
137
+ contentArr[i] = placeholder;
138
+ // Estimate tokens saved: base64 image data is ~4 chars per 3 bytes
139
+ // A typical image is 1000-5000 tokens; the placeholder is ~20 tokens
140
+ const imageTokens = Math.ceil(block.data.length / 4);
141
+ const savedTokens = Math.max(0, imageTokens - 20);
142
+ tokensSaved += savedTokens;
143
+ maskedCount++;
144
+ }
93
145
  // Phase 1b: Compact old tool_use inputs in assistant messages
94
146
  if (msg.role === 'assistant') {
95
147
  for (const block of msg.content) {
@@ -252,3 +304,15 @@ export function buildMaskText(stamp) {
252
304
  export function isMasked(content) {
253
305
  return content.startsWith('[') && content.endsWith(']') && content.includes('@turn:');
254
306
  }
307
+ /**
308
+ * Replace an image content block with a text placeholder.
309
+ * Preserves filename and dimensions for context.
310
+ */
311
+ export function maskImageBlock(block, turn) {
312
+ const name = block.filename ?? 'image';
313
+ const dims = block.width && block.height ? `, ${String(block.width)}x${String(block.height)}` : '';
314
+ return {
315
+ type: 'text',
316
+ text: `[Image: ${name}${dims}, sent@turn:${String(turn)}]`,
317
+ };
318
+ }
package/dist/index.d.ts CHANGED
@@ -39,7 +39,7 @@ export type { ToolPairingValidation } from './messages/index.js';
39
39
  export { generateId, sleep, retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG, countTokens, countMessageTokens, } from './utils/index.js';
40
40
  export type { RetryConfig as LLMRetryConfig, WithRetryOptions } from './utils/index.js';
41
41
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
42
- export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, compactToolResult, ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned, } from './context/index.js';
42
+ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, compactToolResult, ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, maskImageBlock, DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned, } from './context/index.js';
43
43
  export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, RestorationHintMessage, DelegatedResultStoreStats, ToolResultDelegatorOptions, DelegationConfig, StoredResult, DelegationEvent, InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, PruneConfig, PruneResult, PruneStats, WindowingConfig, WindowingResult, ImportanceLevel, } from './context/index.js';
44
44
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
45
45
  export type { Skill, SkillInvocationResult, SkillInvokeOptions } from './skills/index.js';
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DEL
51
51
  // Compact tool result formatting (Phase 2 Token Optimization)
52
52
  compactToolResult,
53
53
  // Observation masking (Phase 1 Token Optimization) + Tool Input Compaction (Phase 1b)
54
- ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked,
54
+ ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, maskImageBlock,
55
55
  // Dead message pruning (Phase 4 Token Optimization)
56
56
  DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned, } from './context/index.js';
57
57
  // Skills system
@@ -228,6 +228,15 @@ export class ClaudeProvider {
228
228
  // Thinking blocks are passed through as text for now
229
229
  // The API expects thinking in a specific format during beta
230
230
  return { type: 'text', text: `<thinking>${block.thinking}</thinking>` };
231
+ case 'image':
232
+ return {
233
+ type: 'image',
234
+ source: {
235
+ type: 'base64',
236
+ media_type: block.mediaType,
237
+ data: block.data,
238
+ },
239
+ };
231
240
  default: {
232
241
  // Exhaustive check - this should never happen
233
242
  const _exhaustive = block;
@@ -232,6 +232,15 @@ export class GeminiNativeProvider {
232
232
  // They are internal model reasoning. Only the signature on function calls matters.
233
233
  // Skip - do not add to parts.
234
234
  break;
235
+ case 'image':
236
+ // Convert to Gemini's inlineData format
237
+ parts.push({
238
+ inlineData: {
239
+ mimeType: block.mediaType,
240
+ data: block.data,
241
+ },
242
+ });
243
+ break;
235
244
  default: {
236
245
  // Exhaustive check
237
246
  const _exhaustive = block;
@@ -221,12 +221,20 @@ export class OpenAICompatibleProvider {
221
221
  else if (Array.isArray(msg.content)) {
222
222
  // Handle content blocks
223
223
  const blocks = msg.content;
224
- const textParts = [];
224
+ const contentParts = [];
225
225
  const toolCallsList = [];
226
226
  const toolResults = [];
227
+ let hasImages = false;
227
228
  for (const block of blocks) {
228
229
  if (block.type === 'text') {
229
- textParts.push(block.text);
230
+ contentParts.push({ type: 'text', text: block.text });
231
+ }
232
+ else if (block.type === 'image') {
233
+ contentParts.push({
234
+ type: 'image_url',
235
+ image_url: { url: `data:${block.mediaType};base64,${block.data}` },
236
+ });
237
+ hasImages = true;
230
238
  }
231
239
  else if (block.type === 'tool_use') {
232
240
  toolCallsList.push({
@@ -247,6 +255,7 @@ export class OpenAICompatibleProvider {
247
255
  }
248
256
  // Note: 'thinking' blocks are ignored (Claude-specific)
249
257
  }
258
+ const textParts = contentParts.filter((p) => p.type === 'text').map((p) => p.text ?? '');
250
259
  // Handle tool results - each needs its own message
251
260
  if (toolResults.length > 0) {
252
261
  for (const tr of toolResults) {
@@ -265,6 +274,13 @@ export class OpenAICompatibleProvider {
265
274
  tool_calls: toolCallsList,
266
275
  });
267
276
  }
277
+ else if (hasImages) {
278
+ // Message with images — send as content parts array
279
+ result.push({
280
+ role: this.mapRole(msg.role),
281
+ content: contentParts,
282
+ });
283
+ }
268
284
  else if (textParts.length > 0) {
269
285
  // Regular text message
270
286
  result.push({
@@ -51,10 +51,26 @@ export interface ThinkingBlock {
51
51
  */
52
52
  signature?: string;
53
53
  }
54
+ /**
55
+ * Image content block (user-attached or tool-provided image for vision)
56
+ */
57
+ export interface ImageBlock {
58
+ type: 'image';
59
+ /** Base64-encoded image data */
60
+ data: string;
61
+ /** MIME type: image/png, image/jpeg, image/webp, image/gif */
62
+ mediaType: string;
63
+ /** Original filename (for display and observation masking placeholder) */
64
+ filename?: string;
65
+ /** Image width in pixels */
66
+ width?: number;
67
+ /** Image height in pixels */
68
+ height?: number;
69
+ }
54
70
  /**
55
71
  * Union of all content block types
56
72
  */
57
- export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock;
73
+ export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock | ImageBlock;
58
74
  /**
59
75
  * A message in a conversation
60
76
  */
@@ -24,6 +24,19 @@ export interface ToolExecutionResult {
24
24
  success: boolean;
25
25
  result?: unknown;
26
26
  error?: string;
27
+ /**
28
+ * Optional image blocks to inject alongside the tool result.
29
+ * When present, these are added as sibling content blocks in the
30
+ * tool result message, enabling vision-capable LLMs to see images.
31
+ * Used by tools like view_image that return visual content.
32
+ */
33
+ imageBlocks?: Array<{
34
+ data: string;
35
+ mediaType: string;
36
+ filename?: string;
37
+ width?: number;
38
+ height?: number;
39
+ }>;
27
40
  }
28
41
  /**
29
42
  * Context passed to tool execution for streaming output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -78,6 +78,7 @@
78
78
  "vitest": "^4.0.18"
79
79
  },
80
80
  "dependencies": {
81
+ "@compilr-dev/logger": "^0.1.0",
81
82
  "@google/genai": "^1.42.0",
82
83
  "@opentelemetry/api": "^1.9.0",
83
84
  "js-tiktoken": "^1.0.21"