From 9da2e15a229c79e5ee6b1755f756221386e5ecf9 Mon Sep 17 00:00:00 2001 From: DeWitt Clinton Date: Wed, 11 Mar 2026 16:33:32 -0700 Subject: [PATCH] Add Core Agent and Model Interfaces --- packages/core/src/interfaces/agent.ts | 79 +++++++++ packages/core/src/interfaces/model.ts | 78 +++++++++ .../core/src/interfaces/verification.test.ts | 155 ++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 packages/core/src/interfaces/agent.ts create mode 100644 packages/core/src/interfaces/model.ts create mode 100644 packages/core/src/interfaces/verification.test.ts diff --git a/packages/core/src/interfaces/agent.ts b/packages/core/src/interfaces/agent.ts new file mode 100644 index 00000000000..e45885cb92b --- /dev/null +++ b/packages/core/src/interfaces/agent.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + ToolCallRequestInfo, + ToolCallResponseInfo, +} from '../scheduler/types.js'; + +/** + * Events emitted by an Agent during its execution. + * These provide visibility into the agent's thought process and actions. + */ +export type AgentEvent = + | { type: 'thought'; content: string } + | { type: 'content'; content: string } // Text output meant for the user + | { type: 'tool_call'; call: ToolCallRequestInfo } + | { type: 'tool_result'; result: ToolCallResponseInfo } + | { type: 'call_code'; code: { code: string; language: string } } + | { type: 'code_result'; result: { outcome: string; output: string } } + | { type: 'tool_confirmation'; confirmations: Record } + | { type: 'error'; error: Error } + | { type: 'activity'; kind: string; detail: Record } // Generic activity hook + | { type: 'finished'; output?: unknown }; + +/** + * Options to control a specific run of an agent. + */ +export interface AgentRunOptions { + /** Signal to abort the execution */ + signal?: AbortSignal; + /** Override the configured maximum number of turns */ + maxTurns?: number; + /** Override the configured maximum execution time */ + maxTime?: number; + /** Optional session ID for stateful conversations */ + sessionId?: string; + /** Optional state delta to initialize the session with */ + stateDelta?: Record; + /** Optional prompt ID for tracing */ + prompt_id?: string; +} + +/** + * The core Agent interface. + * An Agent is an entity that takes an input and executes a loop (Model -> Tools -> Model) + * until a termination condition is met, yielding events along the way. + * + * @template TInput The type of input the agent expects (e.g., string, object). + * @template TOutput The type of the final result the agent returns. + */ +export interface Agent { + /** The unique name of the agent */ + readonly name: string; + /** A human-readable description of what the agent does */ + readonly description: string; + + /** + * Executes the agent's logic sequentially with persisted state. + * + * @param input The input task or data. + * @param options Execution options. + * @returns An async generator that yields events and returns the final result. + */ + runAsync( + input: TInput, + options?: AgentRunOptions, + ): AsyncGenerator; + + /** + * Executes the agent's logic statelessly for a single turn. + */ + runEphemeral( + input: TInput, + options?: AgentRunOptions, + ): AsyncGenerator; +} diff --git a/packages/core/src/interfaces/model.ts b/packages/core/src/interfaces/model.ts new file mode 100644 index 00000000000..0a4eff2db23 --- /dev/null +++ b/packages/core/src/interfaces/model.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + type PartListUnion, + type GenerateContentResponse, + type Tool, +} from '@google/genai'; +import type { ModelConfigKey } from '../services/modelConfigService.js'; +import type { ToolCallRequestInfo } from '../scheduler/types.js'; + +/** + * Events emitted by a Model during generation (usually streaming). + */ +export type ModelEvent = + | { type: 'chunk'; content: GenerateContentResponse } + | { type: 'thought'; content: string } + | { type: 'tool_call'; call: ToolCallRequestInfo } + | { type: 'finished'; reason: string } + | { type: 'error'; error: Error }; + +/** + * Options for generating content with a Model. + */ +export interface ModelGenerationOptions { + /** The model configuration to use (e.g., model name, provider) */ + model?: ModelConfigKey; + /** Tools available to the model for this generation */ + tools?: Tool[]; + /** System instruction or preamble */ + systemInstruction?: string; + /** The maximum number of tokens to generate */ + maxOutputTokens?: number; + /** Sampling temperature */ + temperature?: number; + /** Top-p sampling */ + topP?: number; + /** Top-k sampling */ + topK?: number; + /** Stop sequences */ + stopSequences?: string[]; + /** Signal to abort the request */ + signal?: AbortSignal; +} + +/** + * The core Model interface. + * A Model abstracts the underlying LLM provider (e.g., Gemini, OpenAI, Anthropic). + * It takes input messages and returns generated content or events. + */ +export interface Model { + /** + * Generates a complete response (non-streaming). + * + * @param input The messages or content to generate from. + * @param options Generation options. + * @returns The complete generated response. + */ + generate( + input: PartListUnion, + options?: ModelGenerationOptions, + ): Promise; + + /** + * Generates a streaming response. + * + * @param input The messages or content to generate from. + * @param options Generation options. + * @returns An async generator yielding model events. + */ + generateStream( + input: PartListUnion, + options?: ModelGenerationOptions, + ): AsyncGenerator; +} diff --git a/packages/core/src/interfaces/verification.test.ts b/packages/core/src/interfaces/verification.test.ts new file mode 100644 index 00000000000..d1ebdd7ec52 --- /dev/null +++ b/packages/core/src/interfaces/verification.test.ts @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import type { Agent, AgentEvent, AgentRunOptions } from './agent.js'; +import type { Model, ModelEvent, ModelGenerationOptions } from './model.js'; +import type { GenerateContentResponse, PartListUnion } from '@google/genai'; + +// --- Mock Implementations --- + +class MockModel implements Model { + async generate( + _input: PartListUnion, + _options?: ModelGenerationOptions, + ): Promise { + return { + candidates: [{ content: { parts: [{ text: 'Mock response' }] } }], + } as GenerateContentResponse; + } + + async *generateStream( + _input: PartListUnion, + _options?: ModelGenerationOptions, + ): AsyncGenerator { + yield { type: 'thought', content: 'Thinking...' }; + yield { + type: 'chunk', + content: { + candidates: [{ content: { parts: [{ text: 'Mock' }] } }], + } as GenerateContentResponse, + }; + yield { + type: 'chunk', + content: { + candidates: [{ content: { parts: [{ text: ' Response' }] } }], + } as GenerateContentResponse, + }; + yield { type: 'finished', reason: 'mock_finish' }; + } +} + +class MockAgent implements Agent { + name = 'MockAgent'; + description = 'A test agent'; + + constructor(private model: Model) {} + + async *runAsync( + input: string, + _options?: AgentRunOptions, + ): AsyncGenerator { + const stream = this.model.generateStream(input); + for await (const event of stream) { + if (event.type === 'thought') { + yield { type: 'thought', content: event.content }; + } else if (event.type === 'chunk') { + const text = + event.content.candidates?.[0]?.content?.parts?.[0]?.text || ''; + yield { type: 'content', content: text }; + } + } + + yield { + type: 'tool_call', + call: { + callId: '1', + name: 'test_tool', + args: {}, + prompt_id: 'test', + isClientInitiated: false, + }, + }; + + yield { + type: 'tool_result', + result: { + callId: '1', + responseParts: [{ text: 'Tool Result' }], + error: undefined, + errorType: undefined, + resultDisplay: undefined, + }, + }; + + return 'Final Result'; + } + + async *runEphemeral( + input: string, + options?: AgentRunOptions, + ): AsyncGenerator { + return yield* this.runAsync(input, options); + } +} + +// --- Verification Tests --- + +describe('Interface Verification', () => { + it('should allow implementing a Model', async () => { + const model = new MockModel(); + const result = await model.generate('test'); + expect(result.candidates?.[0]?.content?.parts?.[0]?.text).toBe( + 'Mock response', + ); + }); + + it('should allow implementing an Agent that uses a Model', async () => { + const model = new MockModel(); + const agent = new MockAgent(model); + const events: AgentEvent[] = []; + + const iterator = agent.runAsync('start'); + let result = await iterator.next(); + + while (!result.done) { + events.push(result.value); + result = await iterator.next(); + } + + const finalOutput = result.value; + + expect(finalOutput).toBe('Final Result'); + expect(events).toHaveLength(5); + expect(events[0]).toEqual({ type: 'thought', content: 'Thinking...' }); + expect(events[1]).toEqual({ type: 'content', content: 'Mock' }); + expect(events[2]).toEqual({ type: 'content', content: ' Response' }); + expect(events[3].type).toBe('tool_call'); + expect(events[4].type).toBe('tool_result'); + }); + + it('should handle async iteration correctly', async () => { + const agent = new MockAgent(new MockModel()); + const events: AgentEvent[] = []; + + for await (const event of agent.runAsync('test')) { + events.push(event); + } + + expect(events.length).toBe(5); + }); + + it('should allow ephemeral runs', async () => { + const agent = new MockAgent(new MockModel()); + const events: AgentEvent[] = []; + + for await (const event of agent.runEphemeral('test')) { + events.push(event); + } + + expect(events.length).toBe(5); + }); +});