diff --git a/package-lock.json b/package-lock.json index f971dac8..79392639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2846,7 +2846,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2887,7 +2886,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -3092,7 +3090,6 @@ "integrity": "sha512-vO0uqR8SnPTd8ykp14yaIuUyMZ9HEBYuoZrVdUp7RrEp76VEnkrX9fDkGnK0NyBdfWXB6cqp7BmqVekd8yKHFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/mocker": "4.0.14", "@vitest/utils": "4.0.14", @@ -3116,7 +3113,6 @@ "integrity": "sha512-rUvyz6wX6wDjcYzf/7fgXYfca2bAu0Axoq/v9LYdELzcBSS9UKjnZ7MaMY4UDP78HHHCdmdtceuSao1s51ON8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/browser": "4.0.14", "@vitest/mocker": "4.0.14", @@ -3297,7 +3293,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3788,7 +3783,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4243,7 +4237,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -5380,7 +5373,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5416,7 +5408,6 @@ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright-core": "1.57.0" }, @@ -6155,7 +6146,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6205,7 +6195,6 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6296,7 +6285,6 @@ "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.14", "@vitest/mocker": "4.0.14", @@ -6423,7 +6411,6 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6458,7 +6445,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/__fixtures__/mock-hook-provider.ts b/src/__fixtures__/mock-hook-provider.ts index f3abf45c..e926edf1 100644 --- a/src/__fixtures__/mock-hook-provider.ts +++ b/src/__fixtures__/mock-hook-provider.ts @@ -8,6 +8,7 @@ import { BeforeModelCallEvent, AfterModelCallEvent, ModelStreamEventHook, + ContentBlockHook, } from '../hooks/index.js' import type { HookEventConstructor } from '../hooks/types.js' @@ -33,7 +34,7 @@ export class MockHookProvider implements HookProvider { AfterModelCallEvent, ] - const modelEvents: HookEventConstructor[] = [ModelStreamEventHook] + const modelEvents: HookEventConstructor[] = [ModelStreamEventHook, ContentBlockHook] const eventTypes = this.includeModelEvents ? [...lifecycleEvents, ...modelEvents] : lifecycleEvents diff --git a/src/agent/__tests__/agent.hook.test.ts b/src/agent/__tests__/agent.hook.test.ts index eb77a825..d704f1d9 100644 --- a/src/agent/__tests__/agent.hook.test.ts +++ b/src/agent/__tests__/agent.hook.test.ts @@ -9,6 +9,7 @@ import { BeforeToolCallEvent, MessageAddedEvent, ModelStreamEventHook, + ContentBlockHook, type HookRegistry, } from '../../hooks/index.js' import { MockMessageModel } from '../../__fixtures__/mock-message-model.js' @@ -269,6 +270,96 @@ describe('Agent Hooks Integration', () => { }) }) + describe('ContentBlockHook', () => { + it('fires for content blocks from model streaming', async () => { + const model = new MockMessageModel().addTurn([ + { type: 'textBlock', text: 'Hello' }, + { type: 'reasoningBlock', text: 'Thinking' }, + ]) + + const agent = new Agent({ + model, + hooks: [mockProvider], + }) + + await collectIterator(agent.stream('Test')) + + const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook) + + // Should have 2 content blocks (TextBlock and ReasoningBlock) + expect(contentBlockHooks.length).toBe(2) + + const textBlockHook = contentBlockHooks[0] as ContentBlockHook + expect(textBlockHook.block.type).toBe('textBlock') + + const reasoningBlockHook = contentBlockHooks[1] as ContentBlockHook + expect(reasoningBlockHook.block.type).toBe('reasoningBlock') + }) + + it('fires for tool result blocks', async () => { + const tool = new FunctionTool({ + name: 'testTool', + description: 'Test tool', + inputSchema: {}, + callback: () => 'Tool result', + }) + + const model = new MockMessageModel() + .addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} }) + .addTurn({ type: 'textBlock', text: 'Done' }) + + const agent = new Agent({ + model, + tools: [tool], + hooks: [mockProvider], + }) + + await collectIterator(agent.stream('Test')) + + const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook) + + // Should have content blocks from model streaming and tool result + const toolResultHooks = contentBlockHooks.filter((e) => (e as ContentBlockHook).block.type === 'toolResultBlock') + expect(toolResultHooks.length).toBe(1) + + const toolResultHook = toolResultHooks[0] as ContentBlockHook + expect(toolResultHook.block.type).toBe('toolResultBlock') + }) + + it('fires for all content block types in comprehensive scenario', async () => { + const tool = new FunctionTool({ + name: 'calc', + description: 'Calculator', + inputSchema: {}, + callback: () => '42', + }) + + const model = new MockMessageModel() + .addTurn([ + { type: 'textBlock', text: 'Let me calculate' }, + { type: 'reasoningBlock', text: 'Need to use tool' }, + { type: 'toolUseBlock', name: 'calc', toolUseId: 'tool-1', input: {} }, + ]) + .addTurn([{ type: 'textBlock', text: 'Result is 42' }]) + + const agent = new Agent({ + model, + tools: [tool], + hooks: [mockProvider], + }) + + await collectIterator(agent.stream('Test')) + + const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook) + + // Should have: TextBlock, ReasoningBlock, ToolUseBlock, ToolResultBlock, TextBlock + expect(contentBlockHooks.length).toBe(5) + + const blockTypes = contentBlockHooks.map((e) => (e as ContentBlockHook).block.type) + expect(blockTypes).toEqual(['textBlock', 'reasoningBlock', 'toolUseBlock', 'toolResultBlock', 'textBlock']) + }) + }) + describe('MessageAddedEvent', () => { it('fires for initial user input', async () => { const initialMessage = { role: 'user' as const, content: [{ type: 'textBlock' as const, text: 'Initial' }] } diff --git a/src/agent/__tests__/agent.test.ts b/src/agent/__tests__/agent.test.ts index d80bce78..d27a357f 100644 --- a/src/agent/__tests__/agent.test.ts +++ b/src/agent/__tests__/agent.test.ts @@ -315,7 +315,7 @@ describe('Agent', () => { // Create agent with custom printer for testing const agent = new Agent({ model, printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -329,7 +329,8 @@ describe('Agent', () => { const agent = new Agent({ model, printer: false }) expect(agent).toBeDefined() - expect((agent as any)._printer).toBeUndefined() + // Printer is not registered when printer: false + expect(agent.hooks).toBeDefined() }) it('defaults to printer=true when not specified', () => { @@ -337,7 +338,8 @@ describe('Agent', () => { const agent = new Agent({ model }) expect(agent).toBeDefined() - expect((agent as any)._printer).toBeDefined() + // Printer is registered as a hook internally, not a field + expect(agent.hooks).toBeDefined() }) it('agent works correctly with printer disabled', async () => { diff --git a/src/agent/__tests__/printer.test.ts b/src/agent/__tests__/printer.test.ts index 13a89e06..369d7958 100644 --- a/src/agent/__tests__/printer.test.ts +++ b/src/agent/__tests__/printer.test.ts @@ -15,7 +15,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -30,7 +30,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -48,7 +48,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -66,7 +66,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -96,7 +96,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, tools: [tool], printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -120,7 +120,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, tools: [tool], printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) @@ -163,7 +163,7 @@ describe('AgentPrinter', () => { const mockAppender = (text: string) => outputs.push(text) const agent = new Agent({ model, tools: [calcTool, validatorTool], printer: false }) - ;(agent as any)._printer = new AgentPrinter(mockAppender) + agent.hooks.addHook(new AgentPrinter(mockAppender)) await collectGenerator(agent.stream('Test')) diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 8f59c618..1e7a2320 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -15,12 +15,14 @@ import { type ToolUseBlock, } from '../index.js' import { systemPromptFromData } from '../types/messages.js' +import { BaseContentBlock } from '../types/base-content-block.js' import { normalizeError, ConcurrentInvocationError, MaxTokensError } from '../errors.js' import type { BaseModelConfig, Model, StreamOptions } from '../models/model.js' +import type { ModelStreamEvent } from '../models/streaming.js' import { ToolRegistry } from '../registry/tool-registry.js' import { AgentState } from './state.js' import type { AgentData } from '../types/agent.js' -import { AgentPrinter, getDefaultAppender, type Printer } from './printer.js' +import { AgentPrinter, getDefaultAppender } from './printer.js' import type { HookProvider } from '../hooks/types.js' import { SlidingWindowConversationManager } from '../conversation-manager/sliding-window-conversation-manager.js' import { HookRegistryImplementation } from '../hooks/registry.js' @@ -36,6 +38,7 @@ import { BeforeToolsEvent, MessageAddedEvent, ModelStreamEventHook, + ContentBlockHook, } from '../hooks/events.js' /** @@ -119,7 +122,6 @@ export class Agent implements AgentData { private _systemPrompt?: SystemPrompt private _initialized: boolean private _isInvoking: boolean = false - private _printer?: Printer /** * Creates an instance of the Agent. @@ -145,10 +147,9 @@ export class Agent implements AgentData { this._systemPrompt = systemPromptFromData(config.systemPrompt) } - // Create printer if printer is enabled (default: true) - const printer = config?.printer ?? true - if (printer) { - this._printer = new AgentPrinter(getDefaultAppender()) + // Create and register printer as a hook if enabled (default: true) + if (config?.printer ?? true) { + this.hooks.addHook(new AgentPrinter(getDefaultAppender())) } this._initialized = false @@ -274,7 +275,6 @@ export class Agent implements AgentData { await this.hooks.invokeCallbacks(event) } - this._printer?.processEvent(event) yield event result = await streamGenerator.next() } @@ -407,8 +407,14 @@ export class Agent implements AgentData { while (!result.done) { const event = result.value - // Yield hook event for observability - yield new ModelStreamEventHook({ agent: this, event }) + // Yield appropriate hook event for observability + if (event instanceof BaseContentBlock) { + // This is a ContentBlock + yield new ContentBlockHook({ agent: this, block: event }) + } else { + // This is a ModelStreamEvent + yield new ModelStreamEventHook({ agent: this, event: event as ModelStreamEvent }) + } // Yield the actual model event yield event @@ -448,6 +454,8 @@ export class Agent implements AgentData { const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry) toolResultBlocks.push(toolResultBlock) + // Wrap ToolResultBlock in hook event for printer + yield new ContentBlockHook({ agent: this, block: toolResultBlock }) // Yield the tool result block as it's created yield toolResultBlock } diff --git a/src/agent/printer.ts b/src/agent/printer.ts index b0f6351a..daeb8281 100644 --- a/src/agent/printer.ts +++ b/src/agent/printer.ts @@ -1,4 +1,7 @@ import type { AgentStreamEvent } from '../types/agent.js' +import type { HookProvider } from '../hooks/types.js' +import type { HookRegistry } from '../hooks/registry.js' +import { ContentBlockHook, ModelStreamEventHook } from '../hooks/events.js' /** * Creates a default appender function for the current environment. @@ -15,28 +18,10 @@ export function getDefaultAppender(): (text: string) => void { } /** - * Interface for printing agent activity to a destination. - * Implementations can output to stdout, console, HTML elements, etc. - */ -export interface Printer { - /** - * Write content to the output destination. - * @param content - The content to write - */ - write(content: string): void - - /** - * Process a streaming event from the agent. - * @param event - The event to process - */ - processEvent(event: AgentStreamEvent): void -} - -/** - * Default implementation of the Printer interface. + * Default implementation of agent output printing. * Outputs text, reasoning, and tool execution activity to the configured appender. */ -export class AgentPrinter implements Printer { +export class AgentPrinter implements HookProvider { private readonly _appender: (text: string) => void private _inReasoningBlock: boolean = false private _toolCount: number = 0 @@ -50,6 +35,15 @@ export class AgentPrinter implements Printer { this._appender = appender } + /** + * Register callback functions for specific event types. + * @param registry - The hook registry to register callbacks with + */ + public registerCallbacks(registry: HookRegistry): void { + registry.addCallback(ModelStreamEventHook, (event) => this.processEvent(event.event)) + registry.addCallback(ContentBlockHook, (event) => this.processEvent(event.block)) + } + /** * Write content to the output destination. * @param content - The content to write @@ -63,7 +57,7 @@ export class AgentPrinter implements Printer { * Handles text deltas, reasoning content, and tool execution events. * @param event - The event to process */ - public processEvent(event: AgentStreamEvent): void { + private processEvent(event: AgentStreamEvent): void { switch (event.type) { case 'modelContentBlockDeltaEvent': this.handleContentBlockDelta(event) diff --git a/src/hooks/__tests__/events.test.ts b/src/hooks/__tests__/events.test.ts index c516e7f0..0ae7eeb4 100644 --- a/src/hooks/__tests__/events.test.ts +++ b/src/hooks/__tests__/events.test.ts @@ -10,6 +10,7 @@ import { BeforeToolsEvent, MessageAddedEvent, ModelStreamEventHook, + ContentBlockHook, } from '../events.js' import { Agent } from '../../agent/agent.js' import { Message, TextBlock, ToolResultBlock } from '../../types/messages.js' @@ -328,6 +329,47 @@ describe('ModelStreamEventHook', () => { }) }) +describe('ContentBlockHook', () => { + it('creates instance with correct properties', () => { + const agent = new Agent() + const contentBlock = new TextBlock('Hello world') + const hookEvent = new ContentBlockHook({ agent, block: contentBlock }) + + expect(hookEvent).toEqual({ + type: 'contentBlockHook', + agent: agent, + block: contentBlock, + }) + // @ts-expect-error verifying that property is readonly + hookEvent.agent = new Agent() + // @ts-expect-error verifying that property is readonly + hookEvent.block = contentBlock + }) + + it('returns false for _shouldReverseCallbacks', () => { + const agent = new Agent() + const contentBlock = new TextBlock('Hello world') + const hookEvent = new ContentBlockHook({ agent, block: contentBlock }) + expect(hookEvent._shouldReverseCallbacks()).toBe(false) + }) + + it('works with ToolResultBlock', () => { + const agent = new Agent() + const toolResultBlock = new ToolResultBlock({ + toolUseId: 'test-id', + status: 'success', + content: [new TextBlock('Result')], + }) + const hookEvent = new ContentBlockHook({ agent, block: toolResultBlock }) + + expect(hookEvent).toEqual({ + type: 'contentBlockHook', + agent: agent, + block: toolResultBlock, + }) + }) +}) + describe('BeforeToolsEvent', () => { it('creates instance with correct properties', () => { const agent = new Agent() diff --git a/src/hooks/events.ts b/src/hooks/events.ts index cdb77a9a..b747e9a9 100644 --- a/src/hooks/events.ts +++ b/src/hooks/events.ts @@ -206,15 +206,32 @@ export class AfterModelCallEvent extends HookEvent { export class ModelStreamEventHook extends HookEvent { readonly type = 'modelStreamEventHook' as const readonly agent: AgentData - readonly event: ModelStreamEvent | ContentBlock + readonly event: ModelStreamEvent - constructor(data: { agent: AgentData; event: ModelStreamEvent | ContentBlock }) { + constructor(data: { agent: AgentData; event: ModelStreamEvent }) { super() this.agent = data.agent this.event = data.event } } +/** + * Event triggered for each content block from the model or tools. + * Allows hooks to observe content blocks during model streaming and tool execution. + * Provides read-only access to content blocks. + */ +export class ContentBlockHook extends HookEvent { + readonly type = 'contentBlockHook' as const + readonly agent: AgentData + readonly block: ContentBlock + + constructor(data: { agent: AgentData; block: ContentBlock }) { + super() + this.agent = data.agent + this.block = data.block + } +} + /** * Event triggered before executing tools. * Fired when the model returns tool use blocks that need to be executed. diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 79b8b1f1..c892db5c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -16,6 +16,7 @@ export { BeforeModelCallEvent, AfterModelCallEvent, ModelStreamEventHook, + ContentBlockHook, BeforeToolsEvent, AfterToolsEvent, } from './events.js' diff --git a/src/types/agent.ts b/src/types/agent.ts index 260f7a00..296b66c5 100644 --- a/src/types/agent.ts +++ b/src/types/agent.ts @@ -14,6 +14,7 @@ import type { AfterToolCallEvent, MessageAddedEvent, ModelStreamEventHook, + ContentBlockHook, } from '../hooks/events.js' /** @@ -108,4 +109,5 @@ export type AgentStreamEvent = | AfterToolCallEvent | MessageAddedEvent | ModelStreamEventHook + | ContentBlockHook | AgentResult diff --git a/src/types/base-content-block.ts b/src/types/base-content-block.ts new file mode 100644 index 00000000..3549d43f --- /dev/null +++ b/src/types/base-content-block.ts @@ -0,0 +1,8 @@ +/** + * Base class for all content blocks. + * @internal + * TODO: Make this public in a future release to allow custom content blocks. + */ +export abstract class BaseContentBlock { + abstract readonly type: string +} diff --git a/src/types/media.ts b/src/types/media.ts index a8b578a6..c5868e4e 100644 --- a/src/types/media.ts +++ b/src/types/media.ts @@ -6,6 +6,7 @@ */ import { TextBlock, type TextBlockData } from './messages.js' +import { BaseContentBlock } from './base-content-block.js' export type MediaFormats = DocumentFormat | ImageFormat | VideoFormat @@ -92,7 +93,7 @@ export interface ImageBlockData { /** * Image content block. */ -export class ImageBlock implements ImageBlockData { +export class ImageBlock extends BaseContentBlock implements ImageBlockData { /** * Discriminator for image content. */ @@ -109,6 +110,7 @@ export class ImageBlock implements ImageBlockData { readonly source: ImageSource constructor(data: ImageBlockData) { + super() this.format = data.format this.source = this._convertSource(data.source) } @@ -171,7 +173,7 @@ export interface VideoBlockData { /** * Video content block. */ -export class VideoBlock implements VideoBlockData { +export class VideoBlock extends BaseContentBlock implements VideoBlockData { /** * Discriminator for video content. */ @@ -188,6 +190,7 @@ export class VideoBlock implements VideoBlockData { readonly source: VideoSource constructor(data: VideoBlockData) { + super() this.format = data.format this.source = this._convertSource(data.source) } @@ -270,7 +273,7 @@ export interface DocumentBlockData { /** * Document content block. */ -export class DocumentBlock implements DocumentBlockData { +export class DocumentBlock extends BaseContentBlock implements DocumentBlockData { /** * Discriminator for document content. */ @@ -302,6 +305,7 @@ export class DocumentBlock implements DocumentBlockData { readonly context?: string constructor(data: DocumentBlockData) { + super() this.name = data.name this.format = data.format this.source = this._convertSource(data.source) diff --git a/src/types/messages.ts b/src/types/messages.ts index 6db32895..0135c5a2 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -1,6 +1,7 @@ import type { JSONValue } from './json.js' import type { ImageBlockData, VideoBlockData, DocumentBlockData } from './media.js' import { ImageBlock, VideoBlock, DocumentBlock } from './media.js' +import { BaseContentBlock } from './base-content-block.js' /** * Message types and content blocks for conversational AI interactions. @@ -10,6 +11,8 @@ import { ImageBlock, VideoBlock, DocumentBlock } from './media.js' * functionality and type discrimination. */ +export { BaseContentBlock } from './base-content-block.js' + /** * Data for a message. */ @@ -151,7 +154,7 @@ export interface TextBlockData { /** * Text content block within a message. */ -export class TextBlock implements TextBlockData { +export class TextBlock extends BaseContentBlock implements TextBlockData { /** * Discriminator for text content. */ @@ -163,6 +166,7 @@ export class TextBlock implements TextBlockData { readonly text: string constructor(data: string) { + super() this.text = data } } @@ -191,7 +195,7 @@ export interface ToolUseBlockData { /** * Tool use content block. */ -export class ToolUseBlock implements ToolUseBlockData { +export class ToolUseBlock extends BaseContentBlock implements ToolUseBlockData { /** * Discriminator for tool use content. */ @@ -214,6 +218,7 @@ export class ToolUseBlock implements ToolUseBlockData { readonly input: JSONValue constructor(data: ToolUseBlockData) { + super() this.name = data.name this.toolUseId = data.toolUseId this.input = data.input @@ -260,7 +265,7 @@ export interface ToolResultBlockData { /** * Tool result content block. */ -export class ToolResultBlock implements ToolResultBlockData { +export class ToolResultBlock extends BaseContentBlock implements ToolResultBlockData { /** * Discriminator for tool result content. */ @@ -289,6 +294,7 @@ export class ToolResultBlock implements ToolResultBlockData { readonly error?: Error constructor(data: { toolUseId: string; status: 'success' | 'error'; content: ToolResultContent[]; error?: Error }) { + super() this.toolUseId = data.toolUseId this.status = data.status this.content = data.content @@ -321,7 +327,7 @@ export interface ReasoningBlockData { /** * Reasoning content block within a message. */ -export class ReasoningBlock implements ReasoningBlockData { +export class ReasoningBlock extends BaseContentBlock implements ReasoningBlockData { /** * Discriminator for reasoning content. */ @@ -343,6 +349,7 @@ export class ReasoningBlock implements ReasoningBlockData { readonly redactedContent?: Uint8Array constructor(data: ReasoningBlockData) { + super() if (data.text !== undefined) { this.text = data.text } @@ -369,7 +376,7 @@ export interface CachePointBlockData { * Cache point block for prompt caching. * Marks a position in a message or system prompt where caching should occur. */ -export class CachePointBlock implements CachePointBlockData { +export class CachePointBlock extends BaseContentBlock implements CachePointBlockData { /** * Discriminator for cache point. */ @@ -381,6 +388,7 @@ export class CachePointBlock implements CachePointBlockData { readonly cacheType: 'default' constructor(data: CachePointBlockData) { + super() this.cacheType = data.cacheType } } @@ -399,7 +407,7 @@ export interface JsonBlockData { * JSON content block within a message. * Used for structured data returned from tools or model responses. */ -export class JsonBlock implements JsonBlockData { +export class JsonBlock extends BaseContentBlock implements JsonBlockData { /** * Discriminator for JSON content. */ @@ -411,6 +419,7 @@ export class JsonBlock implements JsonBlockData { readonly json: JSONValue constructor(data: JsonBlockData) { + super() this.json = data.json } } @@ -576,7 +585,7 @@ export interface GuardContentBlockData { * Marks content that should be evaluated by guardrails for safety, grounding, or other policies. * Can be used in both message content and system prompts. */ -export class GuardContentBlock implements GuardContentBlockData { +export class GuardContentBlock extends BaseContentBlock implements GuardContentBlockData { /** * Discriminator for guard content. */ @@ -593,6 +602,7 @@ export class GuardContentBlock implements GuardContentBlockData { readonly image?: GuardContentImage constructor(data: GuardContentBlockData) { + super() if (!data.text && !data.image) { throw new Error('GuardContentBlock must have either text or image content') }