Skip to content

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-core must 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 to agent-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, webhook

Design Patterns

  • Facade: Robota is the single entry point, hiding manager/service/plugin complexity.
  • Template Method: AbstractAgent defines lifecycle hooks (beforeRun, afterRun, onError).
  • Strategy: Event services, storage strategies, error handling strategies are interchangeable.
  • Registry: ToolRegistry and ModuleRegistry for central resource management.
  • Null Object: SilentLogger and DefaultEventService provide safe no-op defaults.
  • Factory: AgentFactory for agent creation with lifecycle hooks.
  • Observer: EventEmitterPlugin for 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:

  • SilentLogger for logging (no side effects)
  • DEFAULT_ABSTRACT_EVENT_SERVICE for events (no-op)

Type Ownership

This package is the single source of truth (SSOT) for the following types:

TypeLocationPurpose
TUniversalMessageinterfaces/messages.tsCanonical message union (User, Assistant, System, Tool)
TUniversalMessageMetadatainterfaces/messages.tsMessage metadata record. Values: string | number | boolean | Date | string[] | number[] | Record<string, number> (includes token usage objects)
TUniversalValueinterfaces/types.tsRecursive value type without any
TMetadatainterfaces/types.tsMetadata record type
IAgentConfiginterfaces/agent.tsAgent configuration contract
IAIProviderinterfaces/provider.tsProvider integration contract
IProviderCapabilitiesinterfaces/provider.tsProvider-neutral capability report for function calling and provider-native web search/fetch support.
IProviderNativeWebToolRequestinterfaces/provider.tsProvider-neutral request shape for native web search/fetch enablement.
IProviderNativeRawPayloadEventinterfaces/provider.tsProvider-owned native SDK request/response/stream payload envelope emitted through IChatOptions.onProviderNativeRawPayload for replay-grade session logs without leaking provider SDK types into core.
TProviderNativeRawPayloadCallbackinterfaces/provider.tsPer-call callback type used by provider packages to report exact provider-native SDK payloads.
TProviderNativeRawPayloadKindinterfaces/provider.tsNative payload phase union: request, response, or stream_event.
IProviderDefinitioninterfaces/provider-definition.tsProvider 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.
IProviderSetupHelpLinkinterfaces/provider-definition.tsProvider-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.
TProviderSetupHelpLinkKindinterfaces/provider-definition.tsSetup link kind union: api-key, console, or official, matching the preferred fallback order for provider setup guidance.
IProviderModelCataloginterfaces/provider-definition.tsProvider-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.
IProviderModelCatalogEntryinterfaces/provider-definition.tsMinimal provider model metadata for display and selection. Provider packages own entries; generic layers must not hardcode provider-specific model lists.
IProviderModelCatalogRefreshOptionsinterfaces/provider-definition.tsProvider-neutral refresh input. Generic layers pass the effective provider profile to provider-owned refresh hooks without interpreting provider-specific model semantics.
TProviderModelCatalogRefreshinterfaces/provider-definition.tsAsync provider-owned catalog refresh hook type. Generic layers may invoke it through IProviderDefinition but must not implement provider-specific discovery.
TProviderModelCatalogStatusinterfaces/provider-definition.tsCatalog freshness status union: live, generated, fallback, or unavailable.
TProviderModelLifecycleinterfaces/provider-definition.tsProvider model lifecycle union used by command UX to avoid presenting unavailable models as selectable subcommands.
TProviderModelCapabilityinterfaces/provider-definition.tsMinimal provider-owned model capability labels for display and filtering.
IProviderConfiginterfaces/provider-definition.tsNormalized provider configuration consumed by provider definitions, including apiKey and a provider-owned options bag that generic layers pass through without interpreting.
IProviderProbeResultinterfaces/provider-definition.tsGeneric provider profile probe result used by CLI and setup flows without provider-specific branching.
TMessageFormatConverterutils/message-converter.tsOptional injected provider message conversion function. Concrete message conversion belongs to provider packages, not core.
TMessageConverterRegistryutils/message-converter.tsOptional converter registry keyed by caller-owned identifiers. Core treats all keys uniformly and never recognizes provider names internally.
IToolSchemainterfaces/provider.tsTool schema contract, including root object additionalProperties for tools that intentionally tolerate unknown parameters
TToolParametersinterfaces/types.tsTool parameter type (re-exported via interfaces/tool.ts)
IEventServiceevent-service/interfaces.tsEvent emission contract
IOwnerPathSegmentevent-service/interfaces.tsExecution path tracking
RobotaErrorutils/errors.tsBase error hierarchy
TTextDeltaCallbackinterfaces/provider.tsStreaming text delta callback (delta: string) => void
TPermissionModepermissions/types.tsPermission modes: plan, default, acceptEdits, bypassPermissions
TTrustLevelpermissions/types.tsFriendly trust aliases: safe, moderate, full
TPermissionDecisionpermissions/types.tsEvaluation outcome: auto, approve, deny
TToolArgspermissions/permission-gate.tsTool arguments record for permission matching
IPermissionListspermissions/permission-gate.tsAllow/deny pattern lists for permission config
TKnownToolNamepermissions/permission-mode.tsKnown tool names in the permission system
THookEventhooks/types.tsHook lifecycle events (9 events): PreToolUse, PostToolUse, PreCompact, PostCompact, SessionStart, Stop, UserPromptSubmit, WorktreeCreate, WorktreeRemove
THooksConfighooks/types.tsComplete hooks configuration: event to hook groups
IHookGrouphooks/types.tsHook group: matcher pattern + hook definitions
IHookDefinitionhooks/types.tsDiscriminated union hook definition (type: command, http, prompt, agent)
IHookTypeExecutorhooks/types.tsStrategy interface for hook type execution
IHookInputhooks/types.tsInput passed to hook commands via stdin
IHookResulthooks/types.tsHook execution result (exitCode, stdout, stderr)
IContextTokenUsagecontext/types.tsToken usage from a single API call (input, output, cache tokens)
IContextWindowStatecontext/types.tsContext window state snapshot (maxTokens, usedTokens, percentage)
IContextTokenEstimatecontext/estimation.tsEffective context token estimate used by status display, session compaction policy, and execution safety guards
IMessageTokenUsagecontext/token-usage.tsNormalized token usage read from message metadata or provider usage payloads
IHistoryEntryinterfaces/history.tsRich 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

ExportKindDescription
IModelDefinitionInterfaceModel metadata: name, id, contextWindow, maxOutput
CLAUDE_MODELSRecordAll known Claude models (4.5+) keyed by API ID
DEFAULT_CONTEXT_WINDOWConstant200,000 tokens fallback
DEFAULT_MAX_OUTPUTConstant16,384 tokens fallback for max output
getModelContextWindow(id)FunctionGet context window size for a model ID
getModelMaxOutput(id)FunctionGet max output tokens for a model ID
getModelName(id)FunctionGet human-readable name (e.g., "Claude Sonnet 4.6")
formatTokenCount(tokens)FunctionFormat tokens as human-readable (e.g., "200K", "1M")

Public API Surface

Core

ExportKindDescription
RobotaclassMain agent facade
AbstractAgentabstract classBase agent lifecycle
AbstractAIProviderabstract classBase for provider implementations
AbstractPluginabstract classBase for plugin extensions
AbstractToolabstract classBase for tool implementations
AbstractExecutorabstract classBase for execution strategies
LocalExecutorclassLocal provider execution
IProviderDefinitioninterfaceProvider 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
IProviderSetupHelpLinkinterfaceProvider-owned official setup link metadata rendered by generic setup flows
TProviderSetupHelpLinkKindtypeProvider setup link kind union: api-key, console, or official
IProviderModelCataloginterfaceProvider-owned model catalog source, freshness status, fallback/live entries, source URL, and lastVerifiedAt metadata
IProviderModelCatalogEntryinterfaceMinimal model display metadata for provider-aware command UX
IProviderModelCatalogRefreshOptionsinterfaceProvider-neutral refresh input containing the effective provider profile
TProviderModelCatalogRefreshtypeAsync provider-owned model catalog refresh hook
IProviderCredentialRequirementinterfaceProvider-owned credential requirement over the generic API-key credential field
TProviderModelCatalogStatustypeCatalog freshness status union
TProviderModelLifecycletypeModel lifecycle union for provider-owned catalog entries
TProviderModelCapabilitytypeProvider-owned model capability labels
IProviderCapabilitiesinterfaceProvider-neutral capability report, including native web search/fetch support and enabled state
IProviderNativeWebToolRequestinterfaceProvider-neutral requested native web search/fetch flags
IProviderNativeRawPayloadEventinterfaceProvider-owned native request/response/stream payload envelope routed through core execution events for replay logs
TProviderNativeRawPayloadCallbacktypePer-call callback providers use to emit native payload events without mutating provider instances
TProviderNativeRawPayloadKindtyperequest, response, or stream_event payload phase label
getProviderCapabilitiesfunctionReturn provider capabilities with safe defaults when a provider does not implement a capability hook
assertProviderNativeWebToolsAvailablefunctionFail before provider transport execution when requested native web search/fetch is unsupported or disabled
findProviderDefinitionfunctionResolve an injected provider definition by canonical type or alias
formatSupportedProviderTypesfunctionFormat 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

ExportKindDescription
evaluatePermissionfunction3-step deterministic policy: deny list, allow list, mode
MODE_POLICYconstPermission mode to tool decision matrix
TRUST_TO_MODEconstMaps TTrustLevel to TPermissionMode
UNKNOWN_TOOL_FALLBACKconstFallback decisions for unknown tools per mode
TPermissionModetype'plan' | 'default' | 'acceptEdits' | 'bypassPermissions'
TTrustLeveltype'safe' | 'moderate' | 'full'
TPermissionDecisiontype'auto' | 'approve' | 'deny'
TToolArgstypeTool arguments record for permission matching
IPermissionListstypeAllow/deny pattern lists
TKnownToolNametypeKnown 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.

ExportKindDescription
ENV_REFERENCE_PREFIXconst'$ENV:' — the canonical prefix for environment variable references
isEnvReferencefunctionReturn true when a string starts with $ENV:
formatEnvReferencefunctionReturn the $ENV:<name> formatted string for the given variable name
resolveEnvReferencefunctionResolve $ENV:<name>process.env[name]; return value or undefined
hasUsableSecretReferencefunctionReturn true when the value is a non-empty string that resolves to a value

Hooks

ExportKindDescription
runHooksfunctionExecute hooks for lifecycle events using pluggable type executors
THookEventtype9 events: PreToolUse, PostToolUse, SessionStart, Stop, PreCompact, PostCompact, UserPromptSubmit, WorktreeCreate, WorktreeRemove
THooksConfigtypeEvent to hook group array mapping
IHookGrouptypeMatcher pattern + hook definitions
IHookDefinitiontypeDiscriminated union: command, http, prompt, agent hook types
IHookTypeExecutorinterfaceStrategy interface for executing a specific hook type
CommandExecutorclassBuilt-in executor for command type hooks (shell execution)
HttpExecutorclassBuilt-in executor for http type hooks (HTTP request)
IHookInputtypeJSON input passed to hooks via stdin
IHookResulttypeHook result: exitCode (0=allow, 2=block), stdout, stderr

Streaming

ExportKindDescription
TTextDeltaCallbacktype(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 as responses, chat-completions, anthropic-messages, or gemini-generate-content.
  • payloadKind: request, response, or stream_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.supported means the provider package has a documented hosted/server-side search path.
  • nativeWebTools.webSearch.enabled means that hosted path is active for the current provider instance.
  • nativeWebTools.webFetch.supported and enabled follow the same semantics for provider-side page extraction/fetch behavior.
  • IChatOptions.nativeWebTools requests provider-native hosted web behavior for one call. Providers must call assertProviderNativeWebToolsAvailable() 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

ExportKindDescription
IContextTokenUsageinterfaceToken usage from a single API call (inputTokens, outputTokens, cache)
IContextWindowStateinterfaceContext window state snapshot (maxTokens, usedTokens, usedPercentage)
IContextTokenEstimateinterfaceEffective estimate with serialized, provider, and caller floor token candidates
IMessageTokenUsageinterfaceNormalized message token usage from metadata or provider usage payloads
estimateContextTokensFromMessagesfunctionReturns the maximum effective token estimate from serialized messages and latest provider metadata
estimateSerializedContextTokensfunctionDeterministic serialized-history fallback estimate
readTokenUsageFromMessagefunctionReads 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/outputTokens metadata is the canonical history form for context accounting.
  • Provider-normalized promptTokens/completionTokens/totalTokens metadata and assistant usage payloads 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

ExportKindDescription
IHistoryEntryinterfaceRich history entry: id, timestamp, category ('chat' | 'event'), type, data
isChatEntryfunctionType guard: (entry: IHistoryEntry) => entry is IChatHistoryEntry — narrows to chat category entries
chatEntryToMessagefunctionConverts an IChatHistoryEntry to a TUniversalMessage for API use
messageToHistoryEntryfunctionConverts a TUniversalMessage to an IHistoryEntry with category: 'chat'
getMessagesForAPIfunctionExtracts TUniversalMessage[] from IHistoryEntry[] for provider API calls (filters to chat entries only)

Managers

ExportKindDescription
AgentFactoryclassAgent creation and lifecycle
AgentTemplatesclassTemplate-based agent creation
ConversationHistoryclassHistory management
ConversationStoreclassSession management

Services

ExportKindDescription
EventHistoryModuleclassEvent recording

Note: AbstractEventService, DefaultEventService, StructuredEventService, and ObservableEventService are internal implementation details and are not exported from src/index.ts.

Plugins (1 built-in)

PluginCategoryDescription
EventEmitterPluginevent_processingEvent 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:

HookTimingPurpose
beforeRunBefore LLM callInput transformation, validation
afterRunAfter LLM responseOutput processing, recording
onErrorOn execution errorError handling, recovery
onStreamChunkDuring streamingChunk processing
beforeToolExecutionBefore tool callTool input validation
afterToolExecutionAfter tool resultTool output processing

Plugins declare category (PluginCategory) and priority (PluginPriority) for execution ordering.

Event Architecture

Event Naming

Full event names follow the pattern ownerType.localName:

PrefixOwnerExamples
execution.*ExecutionServiceexecution.start, execution.complete
tool.*ToolExecutionServicetool.execute_start, tool.execute_success
agent.*Robotaagent.completion, agent.created
task.*Task systemtask.started, task.completed
user.*User actionsuser.input

Owner Path Tracking

Each event carries an ownerPath array of IOwnerPathSegment objects that traces the execution hierarchy:

typescript
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)

  1. Deny list match -- If any deny pattern matches the tool invocation, return 'deny'.
  2. Allow list match -- If any allow pattern matches, return 'auto' (proceed without prompting).
  3. Mode policy lookup -- Look up the tool in MODE_POLICY[mode]. If found, return the mapped decision. Otherwise, return UNKNOWN_TOOL_FALLBACK[mode].

Permission Modes

ModeRead toolsWrite toolsBash
planautodenydeny
defaultautoapprove (prompt)approve (prompt)
acceptEditsautoautoapprove (prompt)
bypassPermissionsautoautoauto

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 argument
  • ToolName -- 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

EventTimingPurpose
PreToolUseBefore tool executionValidation, blocking, transformation
PostToolUseAfter tool executionLogging, auditing, notification
SessionStartSession initializationSetup, environment checks
StopSession terminationCleanup, reporting
PreCompactBefore context compactionValidation, logging (trigger: auto/manual)
PostCompactAfter context compactionLogging, notification (includes compact_summary)
UserPromptSubmitAfter user submits promptPre-processing, validation, prompt rewriting

Hook Definition Types (Discriminated Union)

IHookDefinition is a discriminated union on the type field:

TypeFieldsDescription
commandcommand: stringShell command execution (stdin JSON, exit codes)
httpurl: string, method?, headers?HTTP request to an external endpoint
promptprompt: stringLLM prompt injection into session context
agentagent: 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:

typescript
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):

ExecutorHook TypeBehavior
CommandExecutorcommandSpawns shell process, passes JSON via stdin, reads exit code
HttpExecutorhttpSends HTTP request, maps response status to exit code

Extended executors (agent-sdk):

ExecutorHook TypeBehavior
PromptExecutorpromptInjects prompt text into session context
AgentExecutoragentDelegates to a nested agent session for complex processing

Exit Code Protocol

CodeMeaning
0Allow / proceed
2Block / deny (stderr = reason)
otherProceed 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

InterfaceFieldDescription
IRunOptionssignal?: AbortSignalAllows callers to cancel execution of Robota.run()
IRunOptionsonTextDelta?: TTextDeltaCallbackPer-run streaming callback forwarded through execution context
IRunOptionsonExecutionEvent?: TExecutionEventCallbackPer-run replay event callback for provider/tool boundaries
IRunOptionsmaxExecutionRounds?: numberMaximum model/tool rounds for one run. 0 means unlimited.
IChatOptionssignal?: AbortSignalPassed to provider chat() / chatStream() for cancelling calls
IAgentConfigtimeout?: numberProvider idle timeout in milliseconds for a model call
IAgentConfigmaxExecutionRounds?: numberDefault maximum model/tool rounds for each run. 0 means unlimited.
IExecutionContextsignal?: AbortSignalThreaded through the execution context for round-level checks
IExecutionContextonTextDelta?: TTextDeltaCallbackRun-scoped callback used before provider-level callback fallback
IExecutionContextonExecutionEvent?: TExecutionEventCallbackInternal replay event callback forwarded to provider/tool rounds
IExecutionContextmaxExecutionRounds?: numberRun-scoped override for execution round limit
IExecutionResultinterrupted?: booleanIndicates the execution was aborted before natural completion
IToolExecutionBatchContextsignal?: AbortSignalAllows skipping queued tool executions when abort is signalled
IToolExecutionBatchContextmaxConcurrency?: numberBounds 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:

EventEmitted WhenRequired Data
provider_requestImmediately before a provider callexecutionId, conversationId, round, provider, model, messages, tools
provider_stream_raw_deltaA provider text delta reaches core streaming pathexecutionId, conversationId, round, sequence, delta
provider_response_rawImmediately after provider chat() returnsexecutionId, conversationId, round, response, responseKind
provider_response_normalizedAfter provider response is accepted by coreexecutionId, conversationId, round, response, toolCallsCount
assistant_message_committedAssistant message is committed to historyexecutionId, conversationId, round, message
tool_batch_startedBefore a tool batch executesexecutionId, conversationId, round, batchId, mode, maxConcurrency, requestCount, tools
tool_execution_requestFor each parsed tool callexecutionId, conversationId, round, batchId, index, toolName, toolCallId, parameters, ownerPath
tool_execution_resultFor each terminal tool resultexecutionId, conversationId, round, batchId, index, toolName, toolCallId, success/result/error, metadata
tool_message_committedTool result message is committed to historyexecutionId, conversationId, round, batchId, index, message
history_mutationA chat message is appended to canonical historyexecutionId, 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.aborted at round loop boundaries. If aborted, the loop exits early and the result includes interrupted: true.
  • callProviderWithCache: Accepts signal and passes it to the provider's chat() call, enabling mid-request cancellation. When IAgentConfig.timeout is set, it also enforces a provider idle timeout that resets on each onTextDelta callback and aborts/reports a provider error if no activity arrives before the timeout.
  • executeAndRecordToolCalls: Passes signal to the tool batch context so queued tools are skipped once abort is triggered.
  • streamWithAbort: Races iterator.next() against abort, checks signal.aborted before and after each yielded event, and calls iterator.return() when an abort stops the stream.
  • AbortError handling: AbortError exceptions 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.

FieldTypeRequiredDescription
idstringYesUUID identifier, auto-generated via randomUUID()
stateTMessageStateYes'complete' | 'interrupted'
rolestringYesMessage 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.

FactoryRoleNotes
createUserMessageuserAlways state: 'complete'
createAssistantMessageassistantAccepts optional state parameter (default: complete)
createSystemMessagesystemAlways state: 'complete'
createToolMessagetoolAlways state: 'complete'

ConversationStore Streaming State

ConversationStore (renamed from ConversationSession) manages pending assistant state during streaming:

MethodDescription
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 state parameter determines whether the committed message has state: 'complete' or state: '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:

  1. beginAssistant() initializes pending state before the provider call.
  2. The run-scoped onTextDelta callback is preferred over provider-level callback state, then wrapped to call appendStreaming(delta) on each delta.
  3. After the provider returns: tool calls are added via appendToolCall(toolCall) without rewriting provider-supplied IDs.
  4. commitAssistant(state, metadata?) is called with state determined by signal.aborted'interrupted' if aborted, 'complete' otherwise.
  5. 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 toolCalls and recorded tool message toolCallId.
  • 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_0 in 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 toolCallId as 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:

  • ToolExecutionService checks the requested tool name before invoking IToolManager.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: false with metadata.errorCode: "unknown_tool", metadata.requestedTool, and metadata.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_result replay 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

ExtensionBase ClassContract
AI ProviderAbstractAIProviderImplement chat(), chatStream()
ToolAbstractToolImplement execute(), provide schema
PluginAbstractPluginOverride lifecycle hooks
ModuleAbstractModuleImplement execute()
ExecutorAbstractExecutorImplement execute(), executeStream()
StoragePer-plugin interfacesImplement storage adapter (memory, file, remote)

Error Taxonomy

All errors extend RobotaError with code, category, and recoverable properties:

Error ClassCodeCategoryRecoverable
ConfigurationErrorCONFIGURATION_ERRORuserno
ValidationErrorVALIDATION_ERRORuserno
ProviderErrorPROVIDER_ERRORprovideryes
AuthenticationErrorAUTHENTICATION_ERRORuserno
RateLimitErrorRATE_LIMIT_ERRORprovideryes
NetworkErrorNETWORK_ERRORsystemyes
ToolExecutionErrorTOOL_EXECUTION_ERRORsystemno
ModelNotAvailableErrorMODEL_NOT_AVAILABLEuserno
CircuitBreakerOpenErrorCIRCUIT_BREAKER_OPENsystemyes
PluginErrorPLUGIN_ERRORsystemno
StorageErrorSTORAGE_ERRORsystemyes

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):

  1. 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.
  2. Preserve conversation history — strip the synthetic user message from history after the provider call completes so it doesn't pollute future turns.
  3. Fallback on empty response — if the forced call produces no text, return: "Maximum rounds reached. Partial results available in conversation history.".
  4. 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:

  1. Races each source iterator.next() against the supplied AbortSignal, so a stream waiting for the next provider chunk can settle when aborted.
  2. For each event from the source iterable, yields with a setTimeout(0) interleave to allow the event loop to process abort signals.
  3. Checks signal.aborted before yielding and calls iterator.return() when abort ends iteration.
  4. Providers wrap their SDK stream with this.streamWithAbort(stream, signal) in their chatWithStreaming implementation.

Usage pattern (in provider):

typescript
for await (const event of this.streamWithAbort(stream, signal)) {
  // process event
}
// After loop: check signal.aborted to determine stopReason

This 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 skipped

Return 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

InterfaceImplementorKindLocation
IAgentAbstractAgentabstract basesrc/abstracts/abstract-agent.ts
IAgentRobotaproductionsrc/core/robota.ts
IAIProviderAbstractAIProviderabstract basesrc/abstracts/abstract-ai-provider.ts
IExecutorAbstractExecutorabstract basesrc/abstracts/abstract-executor.ts
IPluginContract, IPluginHooksAbstractPluginabstract basesrc/abstracts/abstract-plugin.ts
IToolWithEventServiceAbstractToolabstract basesrc/abstracts/abstract-tool.ts
IModule, IModuleHooksAbstractModuleabstract basesrc/abstracts/abstract-module.ts
IWorkflowConverterAbstractWorkflowConverterabstract basesrc/abstracts/abstract-workflow-converter.ts
IWorkflowValidatorAbstractWorkflowValidatorabstract basesrc/abstracts/abstract-workflow-validator.ts
IEventServiceAbstractEventServiceabstract basesrc/event-service/event-service.ts
IEventServiceDefaultEventServiceproduction (null object)src/event-service/event-service.ts
IEventServiceStructuredEventServiceproductionsrc/event-service/event-service.ts
IEventServiceObservableEventServiceproductionsrc/event-service/event-service.ts
IConversationHistoryConversationHistoryproductionsrc/managers/conversation-history-manager.ts
IConversationHistoryConversationStoreproductionsrc/managers/conversation-store.ts
IConversationServiceConversationServiceproductionsrc/services/conversation-service/index.ts
IToolManagerToolsproductionsrc/managers/tool-manager.ts
IAIProviderManagerAIProvidersproductionsrc/managers/ai-provider-manager.ts
IPluginsManagerPluginsproductionsrc/managers/plugins.ts
ILoggerConsoleLoggerproductionsrc/utils/logger.ts
IEventHistoryModuleEventHistoryModuleproductionsrc/services/history-module.ts
IEventHistoryModuleInMemoryHistoryStoreproductionsrc/services/in-memory-history-store.ts
IEventEmitterMetricsInMemoryEventEmitterMetricsproductionsrc/plugins/event-emitter/metrics.ts
ICacheStorageMemoryCacheStorageproductionsrc/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)

BaseDerivedLocationNotes
AbstractAgentRobotasrc/core/robota.tsMain facade
AbstractEventServiceDefaultEventServicesrc/event-service/event-service.tsNull object
AbstractEventServiceStructuredEventServicesrc/event-service/event-service.tsOwner-bound events
AbstractEventServiceObservableEventServicesrc/event-service/event-service.tsRxJS integration
AbstractExecutorLocalExecutorsrc/executors/local-executor.tsLocal provider execution
AbstractPluginEventEmitterPluginsrc/plugins/event-emitter-plugin.tsEvent 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

LayerTest FilesCoverage
Core (Robota)robota.test.tsCore flow
Executorslocal-executor.test.tsLocal execution
Managersagent-factory.test.ts, tool-manager.test.ts, conversation-history-manager.test.tsCreation, tools, history
Pluginsevent-emitter-plugin.test.tsEvent coordination
Providersprovider-capabilities.test.tsDefault capabilities and native web validation
Servicesevent-service.test.ts, execution-service.test.tsEvents, execution

Scenario Verification

  • Command: pnpm scenario:verify (runs examples/verify-offline.ts with 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 verification
  • zod — Schema validation for tool parameters

Key Peer Contracts

  • Provider packages implement AbstractAIProvider and IAIProvider
  • @robota-sdk/agent-session consumes Robota, runHooks, evaluatePermission, TUniversalMessage
  • @robota-sdk/agent-tools consumes AbstractTool, IFunctionTool, IToolWithEventService
  • @robota-sdk/agent-plugin-* packages extend AbstractPlugin
  • @robota-sdk/agent-team consumes Robota, IAgentConfig, event services

Released under the MIT License.