open-multi-agent/src/types.ts

470 lines
15 KiB
TypeScript

/**
* @fileoverview Core type definitions for the open-multi-agent orchestration framework.
*
* All public types are exported from this single module. Downstream modules
* import only what they need, keeping the dependency graph acyclic.
*/
import type { ZodSchema } from 'zod'
// ---------------------------------------------------------------------------
// Content blocks
// ---------------------------------------------------------------------------
/** Plain-text content produced by a model or supplied by the user. */
export interface TextBlock {
readonly type: 'text'
readonly text: string
}
/**
* A request by the model to invoke a named tool with a structured input.
* The `id` is unique per turn and is referenced by {@link ToolResultBlock}.
*/
export interface ToolUseBlock {
readonly type: 'tool_use'
readonly id: string
readonly name: string
readonly input: Record<string, unknown>
}
/**
* The result of executing a tool, keyed back to the originating
* {@link ToolUseBlock} via `tool_use_id`.
*/
export interface ToolResultBlock {
readonly type: 'tool_result'
readonly tool_use_id: string
readonly content: string
readonly is_error?: boolean
}
/** A base64-encoded image passed to or returned from a model. */
export interface ImageBlock {
readonly type: 'image'
readonly source: {
readonly type: 'base64'
readonly media_type: string
readonly data: string
}
}
/** Union of all content block variants that may appear in a message. */
export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock
// ---------------------------------------------------------------------------
// LLM messages & responses
// ---------------------------------------------------------------------------
/**
* A single message in a conversation thread.
* System messages are passed separately via {@link LLMChatOptions.systemPrompt}.
*/
export interface LLMMessage {
readonly role: 'user' | 'assistant'
readonly content: ContentBlock[]
}
/** Token accounting for a single API call. */
export interface TokenUsage {
readonly input_tokens: number
readonly output_tokens: number
}
/** Normalised response returned by every {@link LLMAdapter} implementation. */
export interface LLMResponse {
readonly id: string
readonly content: ContentBlock[]
readonly model: string
readonly stop_reason: string
readonly usage: TokenUsage
}
// ---------------------------------------------------------------------------
// Streaming
// ---------------------------------------------------------------------------
/**
* A discrete event emitted during streaming generation.
*
* - `text` — incremental text delta
* - `tool_use` — the model has begun or completed a tool-use block
* - `tool_result` — a tool result has been appended to the stream
* - `done` — the stream has ended; `data` is the final {@link LLMResponse}
* - `error` — an unrecoverable error occurred; `data` is an `Error`
*/
export interface StreamEvent {
readonly type: 'text' | 'tool_use' | 'tool_result' | 'done' | 'error'
readonly data: unknown
}
// ---------------------------------------------------------------------------
// Tool definitions
// ---------------------------------------------------------------------------
/** The serialisable tool schema sent to the LLM provider. */
export interface LLMToolDef {
readonly name: string
readonly description: string
/** JSON Schema object describing the tool's `input` parameter. */
readonly inputSchema: Record<string, unknown>
}
/**
* Context injected into every tool execution.
*
* Both `abortSignal` and `abortController` are provided so that tools and the
* executor can choose the most ergonomic API for their use-case:
*
* - Long-running shell commands that need to kill a child process can use
* `abortController.signal` directly.
* - Simple cancellation checks can read `abortSignal?.aborted`.
*
* When constructing a context, set `abortController` and derive `abortSignal`
* from it, or provide both independently.
*/
export interface ToolUseContext {
/** High-level description of the agent invoking this tool. */
readonly agent: AgentInfo
/** Team context, present when the tool runs inside a multi-agent team. */
readonly team?: TeamInfo
/**
* Convenience reference to the abort signal.
* Equivalent to `abortController?.signal` when an `abortController` is set.
*/
readonly abortSignal?: AbortSignal
/**
* Full abort controller, available when the caller needs to inspect or
* programmatically abort the signal.
* Tools should prefer `abortSignal` for simple cancellation checks.
*/
readonly abortController?: AbortController
/** Working directory hint for file-system tools. */
readonly cwd?: string
/** Arbitrary caller-supplied metadata (session ID, request ID, etc.). */
readonly metadata?: Readonly<Record<string, unknown>>
}
/** Minimal descriptor for the agent that is invoking a tool. */
export interface AgentInfo {
readonly name: string
readonly role: string
readonly model: string
}
/** Descriptor for a team of agents with shared memory. */
export interface TeamInfo {
readonly name: string
readonly agents: readonly string[]
readonly sharedMemory: MemoryStore
}
/** Value returned by a tool's `execute` function. */
export interface ToolResult {
readonly data: string
readonly isError?: boolean
}
/**
* A tool registered with the framework.
*
* `inputSchema` is a Zod schema used for validation before `execute` is called.
* At API call time it is converted to JSON Schema via {@link LLMToolDef}.
*/
export interface ToolDefinition<TInput = Record<string, unknown>> {
readonly name: string
readonly description: string
readonly inputSchema: ZodSchema<TInput>
execute(input: TInput, context: ToolUseContext): Promise<ToolResult>
}
// ---------------------------------------------------------------------------
// Agent
// ---------------------------------------------------------------------------
/** Context passed to the {@link AgentConfig.beforeRun} hook. */
export interface BeforeRunHookContext {
/** The user prompt text. */
readonly prompt: string
/** The agent's static configuration. */
readonly agent: AgentConfig
}
/** Static configuration for a single agent. */
export interface AgentConfig {
readonly name: string
readonly model: string
readonly provider?: 'anthropic' | 'copilot' | 'grok' | 'openai'
/**
* Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.).
* Note: local servers that don't require auth still need `apiKey` set to a
* non-empty placeholder (e.g. `'ollama'`) because the OpenAI SDK validates it.
*/
readonly baseURL?: string
/** API key override; falls back to the provider's standard env var. */
readonly apiKey?: string
readonly systemPrompt?: string
/** Names of tools (from the tool registry) available to this agent. */
readonly tools?: readonly string[]
readonly maxTurns?: number
readonly maxTokens?: number
readonly temperature?: number
/**
* Optional Zod schema for structured output. When set, the agent's final
* output is parsed as JSON and validated against this schema. A single
* retry with error feedback is attempted on validation failure.
*/
readonly outputSchema?: ZodSchema
/**
* Called before each agent run. Receives the prompt and agent config.
* Return a (possibly modified) context to continue, or throw to abort the run.
*/
readonly beforeRun?: (context: BeforeRunHookContext) => Promise<BeforeRunHookContext> | BeforeRunHookContext
/**
* Called after each agent run completes. Receives the run result.
* Return a (possibly modified) result, or throw to mark the run as failed.
*/
readonly afterRun?: (result: AgentRunResult) => Promise<AgentRunResult> | AgentRunResult
}
/** Lifecycle state tracked during an agent run. */
export interface AgentState {
status: 'idle' | 'running' | 'completed' | 'error'
messages: LLMMessage[]
tokenUsage: TokenUsage
error?: Error
}
/** A single recorded tool invocation within a run. */
export interface ToolCallRecord {
readonly toolName: string
readonly input: Record<string, unknown>
readonly output: string
/** Wall-clock duration in milliseconds. */
readonly duration: number
}
/** The final result produced when an agent run completes (or fails). */
export interface AgentRunResult {
readonly success: boolean
readonly output: string
readonly messages: LLMMessage[]
readonly tokenUsage: TokenUsage
readonly toolCalls: ToolCallRecord[]
/**
* Parsed and validated structured output when `outputSchema` is set on the
* agent config. `undefined` when no schema is configured or validation
* failed after retry.
*/
readonly structured?: unknown
}
// ---------------------------------------------------------------------------
// Team
// ---------------------------------------------------------------------------
/** Static configuration for a team of cooperating agents. */
export interface TeamConfig {
readonly name: string
readonly agents: readonly AgentConfig[]
readonly sharedMemory?: boolean
readonly maxConcurrency?: number
}
/** Aggregated result for a full team run. */
export interface TeamRunResult {
readonly success: boolean
/** Keyed by agent name. */
readonly agentResults: Map<string, AgentRunResult>
readonly totalTokenUsage: TokenUsage
}
// ---------------------------------------------------------------------------
// Task
// ---------------------------------------------------------------------------
/** Valid states for a {@link Task}. */
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'
/** A discrete unit of work tracked by the orchestrator. */
export interface Task {
readonly id: string
readonly title: string
readonly description: string
status: TaskStatus
/** Agent name responsible for executing this task. */
assignee?: string
/** IDs of tasks that must complete before this one can start. */
dependsOn?: readonly string[]
result?: string
readonly createdAt: Date
updatedAt: Date
/** Maximum number of retry attempts on failure (default: 0 — no retry). */
readonly maxRetries?: number
/** Base delay in ms before the first retry (default: 1000). */
readonly retryDelayMs?: number
/** Exponential backoff multiplier (default: 2). */
readonly retryBackoff?: number
}
// ---------------------------------------------------------------------------
// Orchestrator
// ---------------------------------------------------------------------------
/** Progress event emitted by the orchestrator during a run. */
export interface OrchestratorEvent {
readonly type:
| 'agent_start'
| 'agent_complete'
| 'task_start'
| 'task_complete'
| 'task_retry'
| 'message'
| 'error'
readonly agent?: string
readonly task?: string
readonly data?: unknown
}
/** Top-level configuration for the orchestrator. */
export interface OrchestratorConfig {
readonly maxConcurrency?: number
readonly defaultModel?: string
readonly defaultProvider?: 'anthropic' | 'copilot' | 'grok' | 'openai'
readonly defaultBaseURL?: string
readonly defaultApiKey?: string
readonly onProgress?: (event: OrchestratorEvent) => void
readonly onTrace?: (event: TraceEvent) => void | Promise<void>
}
// ---------------------------------------------------------------------------
// Trace events — lightweight observability spans
// ---------------------------------------------------------------------------
/** Trace event type discriminants. */
export type TraceEventType = 'llm_call' | 'tool_call' | 'task' | 'agent'
/** Shared fields present on every trace event. */
export interface TraceEventBase {
/** Unique identifier for the entire run (runTeam / runTasks / runAgent call). */
readonly runId: string
readonly type: TraceEventType
/** Unix epoch ms when the span started. */
readonly startMs: number
/** Unix epoch ms when the span ended. */
readonly endMs: number
/** Wall-clock duration in milliseconds (`endMs - startMs`). */
readonly durationMs: number
/** Agent name associated with this span. */
readonly agent: string
/** Task ID associated with this span. */
readonly taskId?: string
}
/** Emitted for each LLM API call (one per agent turn). */
export interface LLMCallTrace extends TraceEventBase {
readonly type: 'llm_call'
readonly model: string
readonly turn: number
readonly tokens: TokenUsage
}
/** Emitted for each tool execution. */
export interface ToolCallTrace extends TraceEventBase {
readonly type: 'tool_call'
readonly tool: string
readonly isError: boolean
}
/** Emitted when a task completes (wraps the full retry sequence). */
export interface TaskTrace extends TraceEventBase {
readonly type: 'task'
readonly taskId: string
readonly taskTitle: string
readonly success: boolean
readonly retries: number
}
/** Emitted when an agent run completes (wraps the full conversation loop). */
export interface AgentTrace extends TraceEventBase {
readonly type: 'agent'
readonly turns: number
readonly tokens: TokenUsage
readonly toolCalls: number
}
/** Discriminated union of all trace event types. */
export type TraceEvent = LLMCallTrace | ToolCallTrace | TaskTrace | AgentTrace
// ---------------------------------------------------------------------------
// Memory
// ---------------------------------------------------------------------------
/** A single key-value record stored in a {@link MemoryStore}. */
export interface MemoryEntry {
readonly key: string
readonly value: string
readonly metadata?: Readonly<Record<string, unknown>>
readonly createdAt: Date
}
/**
* Persistent (or in-memory) key-value store shared across agents.
* Implementations may be backed by Redis, SQLite, or plain objects.
*/
export interface MemoryStore {
get(key: string): Promise<MemoryEntry | null>
set(key: string, value: string, metadata?: Record<string, unknown>): Promise<void>
list(): Promise<MemoryEntry[]>
delete(key: string): Promise<void>
clear(): Promise<void>
}
// ---------------------------------------------------------------------------
// LLM adapter
// ---------------------------------------------------------------------------
/** Options shared by both chat and streaming calls. */
export interface LLMChatOptions {
readonly model: string
readonly tools?: readonly LLMToolDef[]
readonly maxTokens?: number
readonly temperature?: number
readonly systemPrompt?: string
readonly abortSignal?: AbortSignal
}
/**
* Options for streaming calls.
* Extends {@link LLMChatOptions} without additional fields — the separation
* exists so callers can type-narrow and implementations can diverge later.
*/
export interface LLMStreamOptions extends LLMChatOptions {}
/**
* Provider-agnostic interface that every LLM backend must implement.
*
* @example
* ```ts
* const adapter: LLMAdapter = createAdapter('anthropic')
* const response = await adapter.chat(messages, { model: 'claude-opus-4-6' })
* ```
*/
export interface LLMAdapter {
/** Human-readable provider name, e.g. `'anthropic'` or `'openai'`. */
readonly name: string
/**
* Send a chat request and return the complete response.
* Throws on non-retryable API errors.
*/
chat(messages: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse>
/**
* Send a chat request and yield {@link StreamEvent}s incrementally.
* The final event in the sequence always has `type === 'done'` on success,
* or `type === 'error'` on failure.
*/
stream(messages: LLMMessage[], options: LLMStreamOptions): AsyncIterable<StreamEvent>
}