import type { AssistantMessage, AssistantMessageEvent, ImageContent, Message, Model, SimpleStreamOptions, streamSimple, TextContent, Tool, ToolResultMessage } from "@dreb/ai"; import type { Static, TSchema } from "@sinclair/typebox"; /** * Stream function used by the agent loop. * * Contract: * - Must not throw or return a rejected promise for request/model/runtime failures. * - Must return an AssistantMessageEventStream. * - Failures must be encoded in the returned stream via protocol events and a * final AssistantMessage with stopReason "error" or "aborted" and errorMessage. */ export type StreamFn = (...args: Parameters) => ReturnType | Promise>; /** * Configuration for how tool calls from a single assistant message are executed. * * - "sequential": each tool call is prepared, executed, and finalized before the next one starts. * - "parallel": tool calls are prepared sequentially, then allowed tools execute concurrently. * Final tool results are still emitted in assistant source order. */ export type ToolExecutionMode = "sequential" | "parallel"; /** A single tool call content block emitted by an assistant message. */ export type AgentToolCall = Extract; /** * Result returned from `beforeToolCall`. * * Returning `{ block: true }` prevents the tool from executing. The loop emits an error tool result instead. * `reason` becomes the text shown in that error result. If omitted, a default blocked message is used. */ export interface BeforeToolCallResult { block?: boolean; reason?: string; } /** * Partial override returned from `afterToolCall`. * * Merge semantics are field-by-field: * - `content`: if provided, replaces the tool result content array in full * - `details`: if provided, replaces the tool result details value in full * - `isError`: if provided, replaces the tool result error flag * * Omitted fields keep the original executed tool result values. * There is no deep merge for `content` or `details`. */ export interface AfterToolCallResult { content?: (TextContent | ImageContent)[]; details?: unknown; isError?: boolean; } /** Context passed to `beforeToolCall`. */ export interface BeforeToolCallContext { /** The assistant message that requested the tool call. */ assistantMessage: AssistantMessage; /** The raw tool call block from `assistantMessage.content`. */ toolCall: AgentToolCall; /** Validated tool arguments for the target tool schema. */ args: unknown; /** Current agent context at the time the tool call is prepared. */ context: AgentContext; } /** Context passed to `afterToolCall`. */ export interface AfterToolCallContext { /** The assistant message that requested the tool call. */ assistantMessage: AssistantMessage; /** The raw tool call block from `assistantMessage.content`. */ toolCall: AgentToolCall; /** Validated tool arguments for the target tool schema. */ args: unknown; /** The executed tool result before any `afterToolCall` overrides are applied. */ result: AgentToolResult; /** Whether the executed tool result is currently treated as an error. */ isError: boolean; /** Current agent context at the time the tool call is finalized. */ context: AgentContext; } /** Atomic context/model replacement returned by `beforeLlmCall`. */ export interface BeforeLlmCallResult { /** Replacement messages for the upcoming request and subsequent loop iterations. */ messages?: AgentMessage[]; /** Replacement model for the upcoming request and subsequent loop iterations. */ model?: Model; } export interface AgentLoopConfig extends SimpleStreamOptions { model: Model; /** * Called after loop guardrails pass and immediately before each LLM call. * * The callback sees a settled context. It may atomically replace the messages * and/or model used by the upcoming request and subsequent loop iterations. * Contract: low-level callers must not throw or reject; surface failures and * return `undefined` to keep the existing context. `Agent` converts a thrown * hook error into its normal visible assistant-error termination. */ beforeLlmCall?: (context: AgentContext, signal?: AbortSignal) => Promise; /** * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. * * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications, * status messages) should be filtered out. * * Contract: must not throw or reject. Return a safe fallback value instead. * Throwing interrupts the low-level agent loop without producing a normal event sequence. * * @example * ```typescript * convertToLlm: (messages) => messages.flatMap(m => { * if (m.role === "custom") { * // Convert custom message to user message * return [{ role: "user", content: m.content, timestamp: m.timestamp }]; * } * if (m.role === "notification") { * // Filter out UI-only messages * return []; * } * // Pass through standard LLM messages * return [m]; * }) * ``` */ convertToLlm: (messages: AgentMessage[]) => Message[] | Promise; /** * Optional transform applied to the context before `convertToLlm`. * * Use this for operations that work at the AgentMessage level: * - Context window management (pruning old messages) * - Injecting context from external sources * * Contract: must not throw or reject. Return the original messages or another * safe fallback value instead. * * @example * ```typescript * transformContext: async (messages) => { * if (estimateTokens(messages) > MAX_TOKENS) { * return pruneOldMessages(messages); * } * return messages; * } * ``` */ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise; /** * Resolves an API key dynamically for each LLM call. * * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire * during long-running tool execution phases. * * Contract: must not throw or reject. Return undefined when no key is available. */ getApiKey?: (provider: string) => Promise | string | undefined; /** * Returns steering messages to inject into the conversation mid-run. * * Called after the current assistant turn finishes executing its tool calls. * If messages are returned, they are added to the context before the next LLM call. * Tool calls from the current assistant message are not skipped. * * Use this for "steering" the agent while it's working. * * Contract: must not throw or reject. Return [] when no steering messages are available. */ getSteeringMessages?: () => Promise; /** * Returns follow-up messages to process after the agent would otherwise stop. * * Called when the agent has no more tool calls and no steering messages. * If messages are returned, they're added to the context and the agent * continues with another turn. * * Use this for follow-up messages that should wait until the agent finishes. * * Contract: must not throw or reject. Return [] when no follow-up messages are available. */ getFollowUpMessages?: () => Promise; /** * Tool execution mode. * - "sequential": execute tool calls one by one * - "parallel": preflight tool calls sequentially, then execute allowed tools concurrently * * Default: "parallel" */ toolExecution?: ToolExecutionMode; /** * Called before each LLM call in the inner loop. If it returns false, * the loop exits cleanly as if no more tool calls were pending. * * Use this to enforce turn limits or other dynamic stopping conditions. * The callback is NOT called before the very first LLM call of a run — * only before subsequent calls triggered by tool results or steering. */ shouldContinue?: () => boolean; /** * Maximum number of times to retry when a stream drops mid-response. * Retries only trigger on detected stream-drop errors (connection dropped before * the provider sent its terminal completion event). Other errors are not retried. * * Default: 3 */ streamRetries?: number; /** * Base delay in milliseconds for exponential backoff between stream retries. * Actual delay is `baseDelay * 2^attempt` (e.g., 1000, 2000, 4000). * * Default: 1000 */ streamRetryBaseDelayMs?: number; /** * Maximum number of times to retry when a turn ends with stopReason "length" * (the model reached its configured output limit mid-response). Each retry * discards the truncated partial and re-issues the request at the same * `maxTokens` limit. This is SEPARATE from `streamRetries`. * * Default: 2 */ lengthRetries?: number; /** * Called before a tool is executed, after arguments have been validated. * * Return `{ block: true }` to prevent execution. The loop emits an error tool result instead. * The hook receives the agent abort signal and is responsible for honoring it. */ beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise; /** * Called after a tool finishes executing, before final tool events are emitted. * * Return an `AfterToolCallResult` to override parts of the executed tool result: * - `content` replaces the full content array * - `details` replaces the full details payload * - `isError` replaces the error flag * * Any omitted fields keep their original values. No deep merge is performed. * The hook receives the agent abort signal and is responsible for honoring it. */ afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise; } /** * Thinking/reasoning level for models that support it. * Note: "xhigh" is only supported by OpenAI reasoning models that explicitly advertise extended reasoning (for example GPT-5.2+ and Codex GPT-5.3+). */ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** * Extensible interface for custom app messages. * Apps can extend via declaration merging: * * @example * ```typescript * declare module "@dreb/agent-core" { * interface CustomAgentMessages { * artifact: ArtifactMessage; * notification: NotificationMessage; * } * } * ``` */ export interface CustomAgentMessages { } /** * AgentMessage: Union of LLM messages + custom messages. * This abstraction allows apps to add custom message types while maintaining * type safety and compatibility with the base LLM messages. */ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages]; /** * Agent state containing all configuration and conversation data. */ export interface AgentState { systemPrompt: string; model: Model; thinkingLevel: ThinkingLevel; tools: AgentTool[]; messages: AgentMessage[]; isStreaming: boolean; streamMessage: AgentMessage | null; pendingToolCalls: Set; error?: string; } export interface AgentToolResult { content: (TextContent | ImageContent)[]; details: T; /** * When true, the agent loop will stop after processing all tool results * from the current assistant message. No further LLM call is made. * * Use this when a tool's result means the agent should yield control * (e.g., after launching background agents that will deliver results later). */ endTurn?: boolean; } export type AgentToolUpdateCallback = (partialResult: AgentToolResult) => void; export interface AgentTool extends Tool { label: string; execute: (toolCallId: string, params: Static, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback) => Promise>; } export interface AgentContext { systemPrompt: string; messages: AgentMessage[]; tools?: AgentTool[]; } /** * Events emitted by the Agent for UI updates. * These events provide fine-grained lifecycle information for messages, turns, and tool executions. */ export type AgentEvent = { type: "agent_start"; model?: { provider: string; id: string; }; /** Effective thinking level sent to the provider for this run. */ thinkingLevel?: ThinkingLevel; } | { type: "agent_end"; messages: AgentMessage[]; } | { type: "turn_start"; } | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[]; } | { type: "message_start"; message: AgentMessage; } | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent; } | { type: "message_end"; message: AgentMessage; } | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; } | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any; } | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean; } | { type: "stream_retry"; attempt: number; maxAttempts: number; error: string; /** Partial assistant message discarded before retry, for debugging/instrumentation only. */ discardedPartial?: AssistantMessage; } | { type: "length_retry"; attempt: number; maxAttempts: number; /** The maxTokens limit used for both the truncated attempt and retry. */ maxTokens: number; /** Partial assistant message discarded before retry, for debugging/instrumentation only. */ discardedPartial?: AssistantMessage; }; //# sourceMappingURL=types.d.ts.map