Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/core/src/interfaces/agent.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }
| { type: 'error'; error: Error }
| { type: 'activity'; kind: string; detail: Record<string, unknown> } // 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<string, unknown>;
/** 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<TInput = unknown, TOutput = unknown> {
/** 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<AgentEvent, TOutput>;

/**
* Executes the agent's logic statelessly for a single turn.
*/
runEphemeral(
input: TInput,
options?: AgentRunOptions,
): AsyncGenerator<AgentEvent, TOutput>;
}
78 changes: 78 additions & 0 deletions packages/core/src/interfaces/model.ts
Original file line number Diff line number Diff line change
@@ -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<GenerateContentResponse>;

/**
* 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<ModelEvent, void, void>;
}
155 changes: 155 additions & 0 deletions packages/core/src/interfaces/verification.test.ts
Original file line number Diff line number Diff line change
@@ -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<GenerateContentResponse> {
return {
candidates: [{ content: { parts: [{ text: 'Mock response' }] } }],
} as GenerateContentResponse;
}

async *generateStream(
_input: PartListUnion,
_options?: ModelGenerationOptions,
): AsyncGenerator<ModelEvent, void, void> {
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<string, string> {
name = 'MockAgent';
description = 'A test agent';

constructor(private model: Model) {}

async *runAsync(
input: string,
_options?: AgentRunOptions,
): AsyncGenerator<AgentEvent, string> {
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<AgentEvent, string> {
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);
});
});
Loading