Agent Core Specification
Scope
- Owns the core Robota agent runtime, tool integration, conversation execution, and plugin-facing agent behavior.
- Defines the canonical agent orchestration surface used by provider and higher-level packages.
- Provides abstract base classes that provider packages and extensions must implement.
Boundaries
- Keeps all provider-specific transport behavior in provider packages. Core must not branch on concrete provider names or model names.
- Keeps package-specific domain contracts owned once and reused through public surfaces.
- Does not own workflow visualization or session persistence; session persistence belongs to
@robota-sdk/agent-session. - Zero dependency on other agent-* packages.
agent-coremust never import any other@robota-sdk/agent-*package as a production dependency. This is the foundation of the layered assembly architecture: other agent-* packages register with agent-core through its abstract contracts; agent-core never depends on them. Plugins were externalized toagent-plugin-*packages specifically to preserve this constraint.
Architecture Overview
Layer Structure
Robota (Facade)
├── ExecutionService (Orchestrator)
│ ├── AI Provider call (via AIProviders manager)
│ └── Tool execution (via ToolExecutionService)
├── Manager Layer
│ ├── AIProviders — provider registration and selection
│ ├── Tools — tool registry and schema lookup
│ ├── AgentFactory — agent creation and lifecycle
│ ├── ConversationHistory — session and message storage
│ └── ModuleRegistry — dynamic module loading
├── Service Layer
│ ├── ExecutionService — message handling, LLM calls, response assembly
│ ├── ToolExecutionService — schema validation, tool lookup, batch execution
│ └── EventService — unified event emission with ownerPath binding
├── Permission Layer
│ ├── permission-gate.ts — evaluatePermission(): 3-step deterministic policy
│ ├── permission-mode.ts — MODE_POLICY matrix, UNKNOWN_TOOL_FALLBACK
│ └── types.ts — TPermissionMode, TTrustLevel, TPermissionDecision
├── Hook Layer
│ ├── hook-runner.ts — runHooks(): pluggable hook execution engine (strategy pattern)
│ ├── command-executor.ts — CommandExecutor: shell command hook execution
│ ├── http-executor.ts — HttpExecutor: HTTP request hook execution
│ └── types.ts — THookEvent (9 events), IHookDefinition (discriminated union), IHookTypeExecutor
└── Plugin Layer (1 built-in + 8 external @robota-sdk/agent-plugin-* packages)
├── EventEmitterPlugin (built-in — event coordination)
└── External plugins (per @robota-sdk/agent-plugin-*):
conversation-history, logging, usage, performance,
execution-analytics, error-handling, limits, webhookDesign Patterns
- Facade:
Robotais the single entry point, hiding manager/service/plugin complexity. - Template Method:
AbstractAgentdefines lifecycle hooks (beforeRun,afterRun,onError). - Strategy: Event services, storage strategies, error handling strategies are interchangeable.
- Registry:
ToolRegistryandModuleRegistryfor central resource management. - Null Object:
SilentLoggerandDefaultEventServiceprovide safe no-op defaults. - Factory:
AgentFactoryfor agent creation with lifecycle hooks. - Observer:
EventEmitterPluginfor pub/sub event coordination.
Dependency Injection
All managers, services, and tools accept dependencies through constructor injection. No global singletons exist. Each Robota instance is completely independent.
Safe defaults use the Null Object pattern:
SilentLoggerfor logging (no side effects)DEFAULT_ABSTRACT_EVENT_SERVICEfor events (no-op)
Type Ownership
This package is the single source of truth (SSOT) for the following types:
| Type | Location | Purpose |
|---|---|---|
TUniversalMessage | interfaces/messages.ts | Canonical message union (User, Assistant, System, Tool) |
TUniversalMessageMetadata | interfaces/messages.ts | Message metadata record. Values: string | number | boolean | Date | string[] | number[] | Record<string, number> (includes token usage objects) |
TUniversalValue | interfaces/types.ts | Recursive value type without any |
TMetadata | interfaces/types.ts | Metadata record type |
IAgentConfig | interfaces/agent.ts | Agent configuration contract |
IAIProvider | interfaces/provider.ts | Provider integration contract |
IProviderCapabilities | interfaces/provider.ts | Provider-neutral capability report for function calling and provider-native web search/fetch support. |
IProviderNativeWebToolRequest | interfaces/provider.ts | Provider-neutral request shape for native web search/fetch enablement. |
IProviderNativeRawPayloadEvent | interfaces/provider.ts | Provider-owned native SDK request/response/stream payload envelope emitted through IChatOptions.onProviderNativeRawPayload for replay-grade session logs without leaking provider SDK types into core. |
TProviderNativeRawPayloadCallback | interfaces/provider.ts | Per-call callback type used by provider packages to report exact provider-native SDK payloads. |
TProviderNativeRawPayloadKind | interfaces/provider.ts | Native payload phase union: request, response, or stream_event. |
IProviderDefinition | interfaces/provider-definition.ts | Provider assembly contract. Provider packages expose definitions with display metadata, compatibility aliases, defaults, official setup help links, setup prompts, credential requirements, model catalog fallback metadata, optional provider-owned catalog refresh hooks, probe hooks, and createProvider() factories. |
IProviderSetupHelpLink | interfaces/provider-definition.ts | Provider-owned official setup link metadata. Provider packages use it to expose API key, console, or official documentation URLs to generic setup flows without CLI/TUI provider-name branches. |
TProviderSetupHelpLinkKind | interfaces/provider-definition.ts | Setup link kind union: api-key, console, or official, matching the preferred fallback order for provider setup guidance. |
IProviderModelCatalog | interfaces/provider-definition.ts | Provider-owned model catalog contract used by command UX. Static entries are staleable fallback metadata with source and verification timestamps; live/generated catalogs come only from provider-owned refresh hooks. |
IProviderModelCatalogEntry | interfaces/provider-definition.ts | Minimal provider model metadata for display and selection. Provider packages own entries; generic layers must not hardcode provider-specific model lists. |
IProviderModelCatalogRefreshOptions | interfaces/provider-definition.ts | Provider-neutral refresh input. Generic layers pass the effective provider profile to provider-owned refresh hooks without interpreting provider-specific model semantics. |
TProviderModelCatalogRefresh | interfaces/provider-definition.ts | Async provider-owned catalog refresh hook type. Generic layers may invoke it through IProviderDefinition but must not implement provider-specific discovery. |
TProviderModelCatalogStatus | interfaces/provider-definition.ts | Catalog freshness status union: live, generated, fallback, or unavailable. |
TProviderModelLifecycle | interfaces/provider-definition.ts | Provider model lifecycle union used by command UX to avoid presenting unavailable models as selectable subcommands. |
TProviderModelCapability | interfaces/provider-definition.ts | Minimal provider-owned model capability labels for display and filtering. |
IProviderConfig | interfaces/provider-definition.ts | Normalized provider configuration consumed by provider definitions, including apiKey and a provider-owned options bag that generic layers pass through without interpreting. |
IProviderProbeResult | interfaces/provider-definition.ts | Generic provider profile probe result used by CLI and setup flows without provider-specific branching. |
TMessageFormatConverter | utils/message-converter.ts | Optional injected provider message conversion function. Concrete message conversion belongs to provider packages, not core. |
TMessageConverterRegistry | utils/message-converter.ts | Optional converter registry keyed by caller-owned identifiers. Core treats all keys uniformly and never recognizes provider names internally. |
IToolSchema | interfaces/provider.ts | Tool schema contract, including root object additionalProperties for tools that intentionally tolerate unknown parameters |
TToolParameters | interfaces/types.ts | Tool parameter type (re-exported via interfaces/tool.ts) |
IEventService | event-service/interfaces.ts | Event emission contract |
IOwnerPathSegment | event-service/interfaces.ts | Execution path tracking |
RobotaError | utils/errors.ts | Base error hierarchy |
TTextDeltaCallback | interfaces/provider.ts | Streaming text delta callback (delta: string) => void |
TPermissionMode | permissions/types.ts | Permission modes: plan, default, acceptEdits, bypassPermissions |
TTrustLevel | permissions/types.ts | Friendly trust aliases: safe, moderate, full |
TPermissionDecision | permissions/types.ts | Evaluation outcome: auto, approve, deny |
TToolArgs | permissions/permission-gate.ts | Tool arguments record for permission matching |
IPermissionLists | permissions/permission-gate.ts | Allow/deny pattern lists for permission config |
TKnownToolName | permissions/permission-mode.ts | Known tool names in the permission system |
THookEvent | hooks/types.ts | Hook lifecycle events (9 events): PreToolUse, PostToolUse, PreCompact, PostCompact, SessionStart, Stop, UserPromptSubmit, WorktreeCreate, WorktreeRemove |
THooksConfig | hooks/types.ts | Complete hooks configuration: event to hook groups |
IHookGroup | hooks/types.ts | Hook group: matcher pattern + hook definitions |
IHookDefinition | hooks/types.ts | Discriminated union hook definition (type: command, http, prompt, agent) |
IHookTypeExecutor | hooks/types.ts | Strategy interface for hook type execution |
IHookInput | hooks/types.ts | Input passed to hook commands via stdin |
IHookResult | hooks/types.ts | Hook execution result (exitCode, stdout, stderr) |
IContextTokenUsage | context/types.ts | Token usage from a single API call (input, output, cache tokens) |
IContextWindowState | context/types.ts | Context window state snapshot (maxTokens, usedTokens, percentage) |
IContextTokenEstimate | context/estimation.ts | Effective context token estimate used by status display, session compaction policy, and execution safety guards |
IMessageTokenUsage | context/token-usage.ts | Normalized token usage read from message metadata or provider usage payloads |
IHistoryEntry | interfaces/history.ts | Rich history entry that wraps a message with category, type, and structured data fields. Fields: id (string), timestamp (Date), category ('chat' | 'event'), type (string), data (varies by category/type) |
Provider packages import these types. They must not re-declare them.
Model Definitions (SSOT)
context/models.ts is the single source of truth for Claude model metadata. Source: https://platform.claude.com/docs/en/about-claude/models/overview
| Export | Kind | Description |
|---|---|---|
IModelDefinition | Interface | Model metadata: name, id, contextWindow, maxOutput |
CLAUDE_MODELS | Record | All known Claude models (4.5+) keyed by API ID |
DEFAULT_CONTEXT_WINDOW | Constant | 200,000 tokens fallback |
DEFAULT_MAX_OUTPUT | Constant | 16,384 tokens fallback for max output |
getModelContextWindow(id) | Function | Get context window size for a model ID |
getModelMaxOutput(id) | Function | Get max output tokens for a model ID |
getModelName(id) | Function | Get human-readable name (e.g., "Claude Sonnet 4.6") |
formatTokenCount(tokens) | Function | Format tokens as human-readable (e.g., "200K", "1M") |
Public API Surface
Core
| Export | Kind | Description |
|---|---|---|
Robota | class | Main agent facade |
AbstractAgent | abstract class | Base agent lifecycle |
AbstractAIProvider | abstract class | Base for provider implementations |
AbstractPlugin | abstract class | Base for plugin extensions |
AbstractTool | abstract class | Base for tool implementations |
AbstractExecutor | abstract class | Base for execution strategies |
LocalExecutor | class | Local provider execution |
IProviderDefinition | interface | Provider assembly definition, including optional setup display metadata, official setup help links, compatibility aliases, defaults, credential requirements, model catalog fallback metadata, provider-owned refresh hooks, and provider-owned options |
IProviderSetupHelpLink | interface | Provider-owned official setup link metadata rendered by generic setup flows |
TProviderSetupHelpLinkKind | type | Provider setup link kind union: api-key, console, or official |
IProviderModelCatalog | interface | Provider-owned model catalog source, freshness status, fallback/live entries, source URL, and lastVerifiedAt metadata |
IProviderModelCatalogEntry | interface | Minimal model display metadata for provider-aware command UX |
IProviderModelCatalogRefreshOptions | interface | Provider-neutral refresh input containing the effective provider profile |
TProviderModelCatalogRefresh | type | Async provider-owned model catalog refresh hook |
IProviderCredentialRequirement | interface | Provider-owned credential requirement over the generic API-key credential field |
TProviderModelCatalogStatus | type | Catalog freshness status union |
TProviderModelLifecycle | type | Model lifecycle union for provider-owned catalog entries |
TProviderModelCapability | type | Provider-owned model capability labels |
IProviderCapabilities | interface | Provider-neutral capability report, including native web search/fetch support and enabled state |
IProviderNativeWebToolRequest | interface | Provider-neutral requested native web search/fetch flags |
IProviderNativeRawPayloadEvent | interface | Provider-owned native request/response/stream payload envelope routed through core execution events for replay logs |
TProviderNativeRawPayloadCallback | type | Per-call callback providers use to emit native payload events without mutating provider instances |
TProviderNativeRawPayloadKind | type | request, response, or stream_event payload phase label |
getProviderCapabilities | function | Return provider capabilities with safe defaults when a provider does not implement a capability hook |
assertProviderNativeWebToolsAvailable | function | Fail before provider transport execution when requested native web search/fetch is unsupported or disabled |
findProviderDefinition | function | Resolve an injected provider definition by canonical type or alias |
formatSupportedProviderTypes | function | Format injected provider types and aliases for generic errors |
Tools
NOTE: ToolRegistry, FunctionTool, createFunctionTool, createZodFunctionTool, and OpenAPITool have been moved to @robota-sdk/agent-tools. MCPTool and RelayMcpTool have been moved to @robota-sdk/agent-tool-mcp.
Permissions
| Export | Kind | Description |
|---|---|---|
evaluatePermission | function | 3-step deterministic policy: deny list, allow list, mode |
MODE_POLICY | const | Permission mode to tool decision matrix |
TRUST_TO_MODE | const | Maps TTrustLevel to TPermissionMode |
UNKNOWN_TOOL_FALLBACK | const | Fallback decisions for unknown tools per mode |
TPermissionMode | type | 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions' |
TTrustLevel | type | 'safe' | 'moderate' | 'full' |
TPermissionDecision | type | 'auto' | 'approve' | 'deny' |
TToolArgs | type | Tool arguments record for permission matching |
IPermissionLists | type | Allow/deny pattern lists |
TKnownToolName | type | Known tool names: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch |
Environment Reference Utilities
Zero-dependency utilities for the $ENV:<name> environment variable reference format. This is the canonical location for env-ref logic; all higher layers import from here.
| Export | Kind | Description |
|---|---|---|
ENV_REFERENCE_PREFIX | const | '$ENV:' — the canonical prefix for environment variable references |
isEnvReference | function | Return true when a string starts with $ENV: |
formatEnvReference | function | Return the $ENV:<name> formatted string for the given variable name |
resolveEnvReference | function | Resolve $ENV:<name> → process.env[name]; return value or undefined |
hasUsableSecretReference | function | Return true when the value is a non-empty string that resolves to a value |
Hooks
| Export | Kind | Description |
|---|---|---|
runHooks | function | Execute hooks for lifecycle events using pluggable type executors |
THookEvent | type | 9 events: PreToolUse, PostToolUse, SessionStart, Stop, PreCompact, PostCompact, UserPromptSubmit, WorktreeCreate, WorktreeRemove |
THooksConfig | type | Event to hook group array mapping |
IHookGroup | type | Matcher pattern + hook definitions |
IHookDefinition | type | Discriminated union: command, http, prompt, agent hook types |
IHookTypeExecutor | interface | Strategy interface for executing a specific hook type |
CommandExecutor | class | Built-in executor for command type hooks (shell execution) |
HttpExecutor | class | Built-in executor for http type hooks (HTTP request) |
IHookInput | type | JSON input passed to hooks via stdin |
IHookResult | type | Hook result: exitCode (0=allow, 2=block), stdout, stderr |
Streaming
| Export | Kind | Description |
|---|---|---|
TTextDeltaCallback | type | (delta: string) => void — streaming text callback |
This callback is declared in IChatOptions.onTextDelta and IRunOptions.onTextDelta. Provider implementations use IChatOptions.onTextDelta to emit text chunks during streaming responses. The execution engine (execution-round.ts, execution-pipeline.ts) uses only IRunOptions.onTextDelta (the run-scoped callback) — there is no fallback to a provider instance-level callback. Callers must pass the callback explicitly through the run context. Provider instance-level onTextDelta properties (if any) are a provider-internal concern and must not be relied upon by agent-core.
Provider-Native Replay Payloads
IChatOptions.onProviderNativeRawPayload is the provider-neutral callback bridge for replay-grade raw payload capture. Provider packages own the native SDK request/response/stream objects and call this callback with IProviderNativeRawPayloadEvent:
provider: concrete provider identifier as known by the provider package.apiSurface: optional provider-owned API surface label such asresponses,chat-completions,anthropic-messages, orgemini-generate-content.payloadKind:request,response, orstream_event.sequence: optional provider-owned stream/request order. Core assigns a monotonically increasing fallback when omitted.payload: the SDK-native payload object or primitive chosen by the provider package.metadata: provider-owned scalar diagnostics only.
agent-core must not import concrete provider SDK types, inspect provider names, or choose provider-specific payload fields. During a provider call, core wraps the callback and emits a provider_native_raw_payload execution event with the current executionId, conversationId, and round. The existing provider_response_raw event remains the provider-normalized Robota message snapshot and is not a substitute for provider-native payload capture.
Provider Capabilities
IAIProvider.getCapabilities() is an optional provider hook. AbstractAIProvider supplies a default implementation reporting function-calling support from supportsTools() and provider-native web search/fetch as unsupported. Generic layers must call getProviderCapabilities(provider) instead of branching on provider names.
Provider-native web tools are not the same as Robota local function tools:
nativeWebTools.webSearch.supportedmeans the provider package has a documented hosted/server-side search path.nativeWebTools.webSearch.enabledmeans that hosted path is active for the current provider instance.nativeWebTools.webFetch.supportedandenabledfollow the same semantics for provider-side page extraction/fetch behavior.IChatOptions.nativeWebToolsrequests provider-native hosted web behavior for one call. Providers must callassertProviderNativeWebToolsAvailable()before transport execution so unsupported or disabled native web requests fail before streaming starts.IAIProvider.configureNativeWebTools()is an optional provider hook for session/runtime assembly. Session layers may call it to enable provider-owned native web tools without importing concrete provider classes or checking provider names.
Robota local WebSearch and WebFetch tools remain ordinary function tools owned by @robota-sdk/agent-tools; they are advertised through tool schemas and do not make nativeWebTools supported.
Context Window Tracking
| Export | Kind | Description |
|---|---|---|
IContextTokenUsage | interface | Token usage from a single API call (inputTokens, outputTokens, cache) |
IContextWindowState | interface | Context window state snapshot (maxTokens, usedTokens, usedPercentage) |
IContextTokenEstimate | interface | Effective estimate with serialized, provider, and caller floor token candidates |
IMessageTokenUsage | interface | Normalized message token usage from metadata or provider usage payloads |
estimateContextTokensFromMessages | function | Returns the maximum effective token estimate from serialized messages and latest provider metadata |
estimateSerializedContextTokens | function | Deterministic serialized-history fallback estimate |
readTokenUsageFromMessage | function | Reads normalized token usage from a single message |
These types and helpers are consumed by @robota-sdk/agent-session to track effective token usage and context window state across conversation turns. When latest provider usage belongs to the terminal message, it is treated as the exact post-response state. When metadata-free user or tool messages follow the latest provider usage, the estimate becomes max(serialized history estimate, latest provider usage, optional caller floor). Historical full-request provider usage is not summed. This prevents previous provider metadata from hiding a large metadata-free prompt and prevents multi-turn provider input counts from being double-counted.
Provider response usage is normalized before assistant messages are committed:
inputTokens/outputTokensmetadata is the canonical history form for context accounting.- Provider-normalized
promptTokens/completionTokens/totalTokensmetadata and assistantusagepayloads are accepted and converted to the same canonical metadata. - Core must not branch on provider names to perform this conversion.
- If no exact provider usage exists, context accounting falls back to deterministic character-based estimation.
History Entry Helpers
| Export | Kind | Description |
|---|---|---|
IHistoryEntry | interface | Rich history entry: id, timestamp, category ('chat' | 'event'), type, data |
isChatEntry | function | Type guard: (entry: IHistoryEntry) => entry is IChatHistoryEntry — narrows to chat category entries |
chatEntryToMessage | function | Converts an IChatHistoryEntry to a TUniversalMessage for API use |
messageToHistoryEntry | function | Converts a TUniversalMessage to an IHistoryEntry with category: 'chat' |
getMessagesForAPI | function | Extracts TUniversalMessage[] from IHistoryEntry[] for provider API calls (filters to chat entries only) |
Managers
| Export | Kind | Description |
|---|---|---|
AgentFactory | class | Agent creation and lifecycle |
AgentTemplates | class | Template-based agent creation |
ConversationHistory | class | History management |
ConversationStore | class | Session management |
Services
| Export | Kind | Description |
|---|---|---|
EventHistoryModule | class | Event recording |
Note: AbstractEventService, DefaultEventService, StructuredEventService, and ObservableEventService are internal implementation details and are not exported from src/index.ts.
Plugins (1 built-in)
| Plugin | Category | Description |
|---|---|---|
EventEmitterPlugin | event_processing | Event coordination |
8 plugins were extracted to @robota-sdk/agent-plugin-* packages to comply with the agent-core zero-dependency rule. They extend AbstractPlugin (defined here) and are wired by the consuming layer.
Plugin Contract
Plugins extend AbstractPlugin and implement lifecycle hooks:
| Hook | Timing | Purpose |
|---|---|---|
beforeRun | Before LLM call | Input transformation, validation |
afterRun | After LLM response | Output processing, recording |
onError | On execution error | Error handling, recovery |
onStreamChunk | During streaming | Chunk processing |
beforeToolExecution | Before tool call | Tool input validation |
afterToolExecution | After tool result | Tool output processing |
Plugins declare category (PluginCategory) and priority (PluginPriority) for execution ordering.
Event Architecture
Event Naming
Full event names follow the pattern ownerType.localName:
| Prefix | Owner | Examples |
|---|---|---|
execution.* | ExecutionService | execution.start, execution.complete |
tool.* | ToolExecutionService | tool.execute_start, tool.execute_success |
agent.* | Robota | agent.completion, agent.created |
task.* | Task system | task.started, task.completed |
user.* | User actions | user.input |
Owner Path Tracking
Each event carries an ownerPath array of IOwnerPathSegment objects that traces the execution hierarchy:
interface IOwnerPathSegment {
ownerType: string; // 'agent' | 'tool' | 'execution'
ownerId: string;
}Events are bound to their owner via bindWithOwnerPath().
Permission System
The permission module (src/permissions/) provides a deterministic, three-step policy evaluation for tool calls. It is consumed by @robota-sdk/agent-session to gate tool execution before delegating to the actual tool.
Evaluation Algorithm (evaluatePermission)
- Deny list match -- If any deny pattern matches the tool invocation, return
'deny'. - Allow list match -- If any allow pattern matches, return
'auto'(proceed without prompting). - Mode policy lookup -- Look up the tool in
MODE_POLICY[mode]. If found, return the mapped decision. Otherwise, returnUNKNOWN_TOOL_FALLBACK[mode].
Permission Modes
| Mode | Read tools | Write tools | Bash |
|---|---|---|---|
plan | auto | deny | deny |
default | auto | approve (prompt) | approve (prompt) |
acceptEdits | auto | auto | approve (prompt) |
bypassPermissions | auto | auto | auto |
Pattern Syntax
Patterns follow the format ToolName(argGlob):
Bash(pnpm *)-- Bash tool whose command starts with "pnpm "Read(/src/**)-- Read tool whose filePath is under /src/Write(*)-- Write tool with any argumentToolName-- Match any invocation of that tool (no argument constraint)
Hook System
The hook module (src/hooks/) provides a pluggable lifecycle hook mechanism. Hooks support multiple execution types (command, http, prompt, agent) via the strategy pattern. Command hooks receive JSON input on stdin and communicate results via exit codes.
Hook Events
| Event | Timing | Purpose |
|---|---|---|
PreToolUse | Before tool execution | Validation, blocking, transformation |
PostToolUse | After tool execution | Logging, auditing, notification |
SessionStart | Session initialization | Setup, environment checks |
Stop | Session termination | Cleanup, reporting |
PreCompact | Before context compaction | Validation, logging (trigger: auto/manual) |
PostCompact | After context compaction | Logging, notification (includes compact_summary) |
UserPromptSubmit | After user submits prompt | Pre-processing, validation, prompt rewriting |
Hook Definition Types (Discriminated Union)
IHookDefinition is a discriminated union on the type field:
| Type | Fields | Description |
|---|---|---|
command | command: string | Shell command execution (stdin JSON, exit codes) |
http | url: string, method?, headers? | HTTP request to an external endpoint |
prompt | prompt: string | LLM prompt injection into session context |
agent | agent: string, config? | Delegate to a nested agent execution for processing |
Hook Type Executors (Strategy Pattern)
IHookTypeExecutor defines the strategy interface for executing a specific hook type:
interface IHookTypeExecutor {
readonly type: string;
execute(hook: IHookDefinition, input: IHookInput): Promise<IHookResult>;
}runHooks accepts an optional executors map to register additional hook type executors beyond the built-in ones. This enables higher-level packages to add prompt and agent executors without modifying agent-core.
Built-in executors (agent-core):
| Executor | Hook Type | Behavior |
|---|---|---|
CommandExecutor | command | Spawns shell process, passes JSON via stdin, reads exit code |
HttpExecutor | http | Sends HTTP request, maps response status to exit code |
Extended executors (agent-sdk):
| Executor | Hook Type | Behavior |
|---|---|---|
PromptExecutor | prompt | Injects prompt text into session context |
AgentExecutor | agent | Delegates to a nested agent session for complex processing |
Exit Code Protocol
| Code | Meaning |
|---|---|
| 0 | Allow / proceed |
| 2 | Block / deny (stderr = reason) |
| other | Proceed with warning |
Hook Configuration
Hooks are configured as a THooksConfig object mapping events to arrays of IHookGroup entries. Each group has a matcher regex pattern (empty = match all) and an array of IHookDefinition entries. Hooks have a 10-second timeout.
Abort Execution Support
The execution loop supports cooperative cancellation via the standard AbortSignal API. An AbortSignal can be threaded through the entire execution pipeline to allow callers to cancel in-progress runs.
Interface Changes
| Interface | Field | Description |
|---|---|---|
IRunOptions | signal?: AbortSignal | Allows callers to cancel execution of Robota.run() |
IRunOptions | onTextDelta?: TTextDeltaCallback | Per-run streaming callback forwarded through execution context |
IRunOptions | onExecutionEvent?: TExecutionEventCallback | Per-run replay event callback for provider/tool boundaries |
IRunOptions | maxExecutionRounds?: number | Maximum model/tool rounds for one run. 0 means unlimited. |
IChatOptions | signal?: AbortSignal | Passed to provider chat() / chatStream() for cancelling calls |
IAgentConfig | timeout?: number | Provider idle timeout in milliseconds for a model call |
IAgentConfig | maxExecutionRounds?: number | Default maximum model/tool rounds for each run. 0 means unlimited. |
IExecutionContext | signal?: AbortSignal | Threaded through the execution context for round-level checks |
IExecutionContext | onTextDelta?: TTextDeltaCallback | Run-scoped callback used before provider-level callback fallback |
IExecutionContext | onExecutionEvent?: TExecutionEventCallback | Internal replay event callback forwarded to provider/tool rounds |
IExecutionContext | maxExecutionRounds?: number | Run-scoped override for execution round limit |
IExecutionResult | interrupted?: boolean | Indicates the execution was aborted before natural completion |
IToolExecutionBatchContext | signal?: AbortSignal | Allows skipping queued tool executions when abort is signalled |
IToolExecutionBatchContext | maxConcurrency?: number | Bounds active tool executions when batch mode is parallel |
Replay Boundary Events
onExecutionEvent emits provider-neutral, append-only replay events. agent-core must not expose concrete provider SDK objects or branch on provider names. The required event families are:
| Event | Emitted When | Required Data |
|---|---|---|
provider_request | Immediately before a provider call | executionId, conversationId, round, provider, model, messages, tools |
provider_stream_raw_delta | A provider text delta reaches core streaming path | executionId, conversationId, round, sequence, delta |
provider_response_raw | Immediately after provider chat() returns | executionId, conversationId, round, response, responseKind |
provider_response_normalized | After provider response is accepted by core | executionId, conversationId, round, response, toolCallsCount |
assistant_message_committed | Assistant message is committed to history | executionId, conversationId, round, message |
tool_batch_started | Before a tool batch executes | executionId, conversationId, round, batchId, mode, maxConcurrency, requestCount, tools |
tool_execution_request | For each parsed tool call | executionId, conversationId, round, batchId, index, toolName, toolCallId, parameters, ownerPath |
tool_execution_result | For each terminal tool result | executionId, conversationId, round, batchId, index, toolName, toolCallId, success/result/error, metadata |
tool_message_committed | Tool result message is committed to history | executionId, conversationId, round, batchId, index, message |
history_mutation | A chat message is appended to canonical history | executionId, conversationId, mutation, index, message |
provider_response_raw.responseKind is provider-normalized-message until provider packages add provider-owned SDK-payload capture hooks. This keeps replay validation deterministic without making core depend on concrete provider SDK response types.
Signal Propagation
AbortSignal flows through: Session -> robota.run() -> ExecutionService -> callProviderWithCache -> provider.chat() -> streamWithAbort.
- ExecutionService: Checks
signal.abortedat round loop boundaries. If aborted, the loop exits early and the result includesinterrupted: true. - callProviderWithCache: Accepts
signaland passes it to the provider'schat()call, enabling mid-request cancellation. WhenIAgentConfig.timeoutis set, it also enforces a provider idle timeout that resets on eachonTextDeltacallback and aborts/reports a provider error if no activity arrives before the timeout. - executeAndRecordToolCalls: Passes
signalto the tool batch context so queued tools are skipped once abort is triggered. - streamWithAbort: Races
iterator.next()against abort, checkssignal.abortedbefore and after each yielded event, and callsiterator.return()when an abort stops the stream. - AbortError handling:
AbortErrorexceptions thrown by the fetch layer are caught by the execution loop and treated as a clean interruption (not an error).
Tool Batch Concurrency
When IToolExecutionBatchContext.mode is parallel, ToolExecutionService enforces maxConcurrency with bounded worker execution. The batch result preserves one result slot per request in request order, while errors are aggregated after all started or skipped work settles. If maxConcurrency is omitted, all requests may run concurrently; if it is less than 1, execution is clamped to one active tool.
Partial Content Preservation on Abort
When abort occurs during provider streaming, the provider uses streamWithAbort which breaks out of the iteration loop on signal.aborted. The provider then returns partial content collected so far with stopReason: 'aborted'. executeRound commits this partial response via commitAssistant('interrupted') through the standard single commit path. The execution loop then exits via the signal.aborted check in ExecutionService. robota.run() always returns normally on abort — it does not throw.
This ensures:
- The partial response is saved in conversation history for the next turn
- The model can see what it started saying before interruption
- Tool results from completed tools in earlier rounds are preserved
If the partial response includes tool_use blocks (abort during tool call streaming), the tool execution step runs but skips queued tools via signal.aborted check in IToolExecutionBatchContext. Completed tools have normal results; skipped tools have "Execution interrupted by user" error results. Both are recorded in history.
Conversation History Principles
- Append-only: Messages are only added, never edited or deleted.
- Read-only: Consumers read history but do not mutate existing messages.
- Always committed:
beginAssistant()+commitAssistant()guarantees an assistant message is always appended, even on abort with empty content. - No fallback: If a message should be in history, it IS in history. No fallback to alternative data sources.
Message Model
IBaseMessage is the foundation for all message types in the conversation history.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | UUID identifier, auto-generated via randomUUID() |
state | TMessageState | Yes | 'complete' | 'interrupted' |
role | string | Yes | Message role (user, assistant, system, tool) |
State rules:
- Non-assistant messages (user, system, tool) always have
state: 'complete'. - Only assistant messages may have
state: 'interrupted', indicating the response was aborted by the user before natural completion.
Message Factories
All message factory functions auto-generate id via randomUUID() and set state: 'complete' by default.
| Factory | Role | Notes |
|---|---|---|
createUserMessage | user | Always state: 'complete' |
createAssistantMessage | assistant | Accepts optional state parameter (default: complete) |
createSystemMessage | system | Always state: 'complete' |
createToolMessage | tool | Always state: 'complete' |
ConversationStore Streaming State
ConversationStore (renamed from ConversationSession) manages pending assistant state during streaming:
| Method | Description |
|---|---|
beginAssistant() | Initializes pending state before provider call. Guarantees commitAssistant has data. |
appendStreaming(delta) | Accumulates streaming text into pending state |
appendToolCall(toolCall) | Adds tool call to pending state (deduplicates by id) |
commitAssistant(state, metadata?) | Commits pending to history. Text is ALWAYS preserved. History is append-only. |
discardPending() | Clears pending without saving |
hasPendingAssistant() | Checks if streaming is in progress |
getPendingContent() | Returns the accumulated pending text content |
addEntry(entry: IHistoryEntry) | Appends a pre-built IHistoryEntry to history (used for event entries such as tool summaries). |
getHistory() | Returns the full history as IHistoryEntry[]. Each chat message wraps a TUniversalMessage via data. |
commitAssistant behavior:
- Text content is ALWAYS preserved — no stripping, even when tool calls are present. Context savings is compaction's job.
- The
stateparameter determines whether the committed message hasstate: 'complete'orstate: 'interrupted'. - Single commit path — no branching between normal completion and abort.
getMessagesForAPI
getMessagesForAPI() prepares the conversation history for provider API calls. For interrupted assistant messages (state: 'interrupted'), the text is annotated with [This response was interrupted by the user] suffix. This allows the model to understand that its previous response was cut short.
executeRound Streaming Flow
The executeRound function manages streaming through ConversationStore:
beginAssistant()initializes pending state before the provider call.- The run-scoped
onTextDeltacallback is preferred over provider-level callback state, then wrapped to callappendStreaming(delta)on each delta. - After the provider returns: tool calls are added via
appendToolCall(toolCall)without rewriting provider-supplied IDs. commitAssistant(state, metadata?)is called with state determined bysignal.aborted—'interrupted'if aborted,'complete'otherwise.- Single commit path — no branching between normal and abort flows.
Provider Tool Call ID Ownership
Provider adapters own the tool_call.id value. Core treats it as the provider transcript token that links an assistant tool call to the corresponding tool message.
- Core must not branch on provider names, model names, or transport packages.
- Core must preserve provider-supplied tool call IDs in committed assistant
toolCallsand recorded tool messagetoolCallId. - Conversation history must not require provider tool call IDs to be unique across the whole conversation. Some OpenAI-compatible providers reuse IDs such as
call_0in later assistant turns. - If an internal subsystem needs a globally unique execution/event identifier, it must use an internal ID or owner path and keep the provider
toolCallIdas transcript data.
Regression coverage must include a multi-round execution where the provider returns call_0 in more than one assistant response and execution preserves both provider IDs without throwing duplicate tool message errors.
Unavailable Tool Call Handling
Provider adapters must preserve provider-native tool calls and pass them to core, even when the tool name is not registered locally. Core owns the execution decision.
Rules:
ToolExecutionServicechecks the requested tool name before invokingIToolManager.executeTool().- If the tool is not registered, core must not execute anything or alias the request to another tool.
- The skipped result is recorded as
success: falsewithmetadata.errorCode: "unknown_tool",metadata.requestedTool, andmetadata.availableTools. - The corresponding tool message content must explicitly say that the tool call was not executed because the tool is not registered.
- Skipped unknown tools must not be counted as executed tools in
IExecutionResult.toolsExecuted. tool_execution_resultreplay events must include the same metadata so session logs and transports can explain the skipped call.- If unavailable tool calls repeat for consecutive model/tool rounds, the loop guard stops normal tool rounds and performs one final provider call without tools. The forced instruction tells the model which tool names were unavailable and that those calls were not executed because they are not registered.
- Provider packages must not implement ad hoc aliases such as
agent->robota_command_agent; command and tool selection must be corrected by model-visible descriptors, schemas, and the normal tool-result feedback loop.
Extension Points
| Extension | Base Class | Contract |
|---|---|---|
| AI Provider | AbstractAIProvider | Implement chat(), chatStream() |
| Tool | AbstractTool | Implement execute(), provide schema |
| Plugin | AbstractPlugin | Override lifecycle hooks |
| Module | AbstractModule | Implement execute() |
| Executor | AbstractExecutor | Implement execute(), executeStream() |
| Storage | Per-plugin interfaces | Implement storage adapter (memory, file, remote) |
Error Taxonomy
All errors extend RobotaError with code, category, and recoverable properties:
| Error Class | Code | Category | Recoverable |
|---|---|---|---|
ConfigurationError | CONFIGURATION_ERROR | user | no |
ValidationError | VALIDATION_ERROR | user | no |
ProviderError | PROVIDER_ERROR | provider | yes |
AuthenticationError | AUTHENTICATION_ERROR | user | no |
RateLimitError | RATE_LIMIT_ERROR | provider | yes |
NetworkError | NETWORK_ERROR | system | yes |
ToolExecutionError | TOOL_EXECUTION_ERROR | system | no |
ModelNotAvailableError | MODEL_NOT_AVAILABLE | user | no |
CircuitBreakerOpenError | CIRCUIT_BREAKER_OPEN | system | yes |
PluginError | PLUGIN_ERROR | system | no |
StorageError | STORAGE_ERROR | system | yes |
ErrorUtils provides isRecoverable(), getErrorCode(), fromUnknown(), and wrapProviderError().
Execution Loop Error Handling
The default core execution round limit is 10 model/tool rounds. Callers can override it with IRunOptions.maxExecutionRounds, IExecutionContext.maxExecutionRounds, or IAgentConfig.maxExecutionRounds. Run-scoped values win over config defaults. A value of 0 means the execution loop has no round cap and relies on abort, context-window checks, provider idle timeout, and runtime-level controls to stop runaway execution.
When the execution loop ends without a final assistant text message (e.g., due to max round limit or context overflow during tool execution):
- Force a final summary call — inject a synthetic user message requesting the AI to respond with what it has so far, noting what remains incomplete and that the user can follow up. Call
provider.chat()WITHOUT tools (preventing further tool calls). The system message from config must be included. Use streaming (onTextDelta) if available. - Preserve conversation history — strip the synthetic user message from history after the provider call completes so it doesn't pollute future turns.
- Fallback on empty response — if the forced call produces no text, return:
"Maximum rounds reached. Partial results available in conversation history.". - If the forced call throws — catch the error and return the fallback message without re-throwing.
Pre-Send Context Check
Before each provider.chat() call in the execution loop, token usage is checked against the model's context window limit using estimateContextTokensFromMessages() plus the current round's provider usage floor. This is a hard-capacity guard, not the automatic compaction policy. Automatic compaction remains owned by @robota-sdk/agent-session at its configured threshold. The hard guard stops only when the effective estimate exceeds 95% of the context window and emits a diagnostic assistant message with estimated tokens, max tokens, serialized estimate, provider usage floor, and threshold values so UI layers can explain why the prompt was blocked.
Provider Error Recovery
If provider.chat() throws an error (e.g., API 400 for context too large), executeRound catches it and injects an assistant message with the error. This ensures the user always sees a readable error message rather than "No response received." If the entire execution pipeline throws, ExecutionService.execute() catches it and returns a graceful error result instead of re-throwing.
AbstractAIProvider.streamWithAbort
streamWithAbort() is a protected async generator on AbstractAIProvider that wraps any async iterable with cooperative abort checking. All provider implementations MUST use this method for streaming iteration.
Mechanism:
- Races each source
iterator.next()against the suppliedAbortSignal, so a stream waiting for the next provider chunk can settle when aborted. - For each event from the source iterable, yields with a
setTimeout(0)interleave to allow the event loop to process abort signals. - Checks
signal.abortedbefore yielding and callsiterator.return()when abort ends iteration. - Providers wrap their SDK stream with
this.streamWithAbort(stream, signal)in theirchatWithStreamingimplementation.
Usage pattern (in provider):
for await (const event of this.streamWithAbort(stream, signal)) {
// process event
}
// After loop: check signal.aborted to determine stopReasonThis ensures all providers have consistent, low-latency abort responsiveness without duplicating the abort-checking logic.
Tool Result Context Budget
After the assistant message is committed to history, tool results are added to history one by one. After each addition, the estimated token count (chars/2) is checked against 80% of the model's context window.
If exceeded, remaining tool results are replaced with a short context-error message (permission-deny pattern):
Error: Context window near capacity. Tool execution result skipped.Key behavior:
- Follows the permission-deny pattern — AI receives a mix of normal results and context-error results
- The execution loop does NOT break — it continues to the next provider call so the AI can see the mixed results and respond
- AI autonomously decides how to handle: partial answer from available results, retry with fewer tools, etc.
- Skipped tool results are short error messages (~80 chars), so the next provider call succeeds
Example flow:
[assistant] text + tool_use(Read, Bash, Glob, Write)
[tool] Read result (normal, context at 75%)
[tool] Bash result (normal, context at 82% → overflow detected)
[tool] Glob: "Error: Context window near capacity. Tool execution result skipped."
[tool] Write: "Error: Context window near capacity. Tool execution result skipped."
→ next provider call succeeds
→ AI responds based on Read and Bash results, notes Glob and Write were skippedReturn value: addToolResultsToHistory returns IToolResultsOutcome with contextOverflowed, addedCount, and skippedCount.
Streaming Round Separator
When the execution loop starts round 2+ (after tool execution), execution-round.ts emits '\n\n' through the run-scoped onTextDelta callback before calling provider.chat(), falling back to provider.onTextDelta only when no run callback is present. This separates streaming text from different rounds in the CLI, which would otherwise concatenate without line breaks.
Class Contract Registry
Interface Implementations
| Interface | Implementor | Kind | Location |
|---|---|---|---|
IAgent | AbstractAgent | abstract base | src/abstracts/abstract-agent.ts |
IAgent | Robota | production | src/core/robota.ts |
IAIProvider | AbstractAIProvider | abstract base | src/abstracts/abstract-ai-provider.ts |
IExecutor | AbstractExecutor | abstract base | src/abstracts/abstract-executor.ts |
IPluginContract, IPluginHooks | AbstractPlugin | abstract base | src/abstracts/abstract-plugin.ts |
IToolWithEventService | AbstractTool | abstract base | src/abstracts/abstract-tool.ts |
IModule, IModuleHooks | AbstractModule | abstract base | src/abstracts/abstract-module.ts |
IWorkflowConverter | AbstractWorkflowConverter | abstract base | src/abstracts/abstract-workflow-converter.ts |
IWorkflowValidator | AbstractWorkflowValidator | abstract base | src/abstracts/abstract-workflow-validator.ts |
IEventService | AbstractEventService | abstract base | src/event-service/event-service.ts |
IEventService | DefaultEventService | production (null object) | src/event-service/event-service.ts |
IEventService | StructuredEventService | production | src/event-service/event-service.ts |
IEventService | ObservableEventService | production | src/event-service/event-service.ts |
IConversationHistory | ConversationHistory | production | src/managers/conversation-history-manager.ts |
IConversationHistory | ConversationStore | production | src/managers/conversation-store.ts |
IConversationService | ConversationService | production | src/services/conversation-service/index.ts |
IToolManager | Tools | production | src/managers/tool-manager.ts |
IAIProviderManager | AIProviders | production | src/managers/ai-provider-manager.ts |
IPluginsManager | Plugins | production | src/managers/plugins.ts |
ILogger | ConsoleLogger | production | src/utils/logger.ts |
IEventHistoryModule | EventHistoryModule | production | src/services/history-module.ts |
IEventHistoryModule | InMemoryHistoryStore | production | src/services/in-memory-history-store.ts |
IEventEmitterMetrics | InMemoryEventEmitterMetrics | production | src/plugins/event-emitter/metrics.ts |
ICacheStorage | MemoryCacheStorage | production | src/services/cache/memory-cache-storage.ts |
NOTE: FunctionTool, ToolRegistry, OpenAPITool moved to @robota-sdk/agent-tools. MCPTool, RelayMcpTool moved to @robota-sdk/agent-tool-mcp. Plugin storage implementations (ILogStorage, IUsageStorage, IPerformanceStorage, IHistoryStorage, etc.) moved to their respective @robota-sdk/agent-plugin-* packages.
Inheritance Chains (within agent-core)
| Base | Derived | Location | Notes |
|---|---|---|---|
AbstractAgent | Robota | src/core/robota.ts | Main facade |
AbstractEventService | DefaultEventService | src/event-service/event-service.ts | Null object |
AbstractEventService | StructuredEventService | src/event-service/event-service.ts | Owner-bound events |
AbstractEventService | ObservableEventService | src/event-service/event-service.ts | RxJS integration |
AbstractExecutor | LocalExecutor | src/executors/local-executor.ts | Local provider execution |
AbstractPlugin | EventEmitterPlugin | src/plugins/event-emitter-plugin.ts | Event coordination |
NOTE: Tool implementations (FunctionTool, OpenAPITool) in @robota-sdk/agent-tools implement IFunctionTool/ITool directly without extending AbstractTool. Plugin implementations in @robota-sdk/agent-plugin-* extend AbstractPlugin.
Cross-Package Port Consumers
| Port (Owner) | Adapter (Consumer Package) | Location |
|---|---|---|
AbstractAIProvider (agent-core) | OpenAIProvider (agent-provider-openai) | packages/agent-provider-openai/src/provider.ts |
AbstractAIProvider (agent-core) | AnthropicProvider (agent-provider-anthropic) | packages/agent-provider-anthropic/src/provider.ts |
AbstractAIProvider (agent-core) | GeminiProvider (agent-provider-gemini) | packages/agent-provider-gemini/src/provider.ts |
AbstractAIProvider (agent-core) | GoogleProvider (agent-provider-google) | packages/agent-provider-google/src/provider.ts |
AbstractAIProvider (agent-core) | MockAIProvider (agent-sessions) | packages/agent-session/examples/verify-offline.ts |
AbstractExecutor (agent-core) | SimpleRemoteExecutor (agent-remote) | packages/agent-remote/src/client/remote-executor-simple.ts |
Test Strategy
Current Coverage
| Layer | Test Files | Coverage |
|---|---|---|
| Core (Robota) | robota.test.ts | Core flow |
| Executors | local-executor.test.ts | Local execution |
| Managers | agent-factory.test.ts, tool-manager.test.ts, conversation-history-manager.test.ts | Creation, tools, history |
| Plugins | event-emitter-plugin.test.ts | Event coordination |
| Providers | provider-capabilities.test.ts | Default capabilities and native web validation |
| Services | event-service.test.ts, execution-service.test.ts | Events, execution |
Scenario Verification
- Command:
pnpm scenario:verify(runsexamples/verify-offline.tswith MockAIProvider) - Record:
examples/scenarios/offline-verify.record.json - Validates: agent creation, tool registration, conversation flow without network
Coverage Gaps (Improvement Targets)
- Service edge cases: tool-execution-service, task-events, user-events
- Utility tests: errors, validation, message-converter
- NOTE: Plugin tests belong to
@robota-sdk/agent-plugin-*packages. Tool tests belong to@robota-sdk/agent-tools.
Dependencies
Production (2)
jssha— SHA hashing for content verificationzod— Schema validation for tool parameters
Key Peer Contracts
- Provider packages implement
AbstractAIProviderandIAIProvider @robota-sdk/agent-sessionconsumesRobota,runHooks,evaluatePermission,TUniversalMessage@robota-sdk/agent-toolsconsumesAbstractTool,IFunctionTool,IToolWithEventService@robota-sdk/agent-plugin-*packages extendAbstractPlugin@robota-sdk/agent-teamconsumesRobota,IAgentConfig, event services