import { VoleNetConfig } from '@openvole/volenet'; export { VoleNetConfig, VoleNetManager, generateKeyPair, loadAuthorizedVoles, loadKeyPair, parsePublicKey, revokePeer, trustPeer } from '@openvole/volenet'; import { ZodSchema } from 'zod'; import { Emitter } from 'mitt'; /** Summary of a tool available to the Brain */ interface ToolSummary { name: string; description: string; pawName: string; /** Parameter schema (Zod) — passed to Brain Paw for function calling */ parameters?: unknown; } /** A Skill whose required tools are all satisfied (compact — Brain reads full instructions on demand) */ interface ActiveSkill { name: string; description: string; satisfiedBy: string[]; } /** A single message in the agent's reasoning history */ interface AgentMessage { role: 'user' | 'brain' | 'tool_result' | 'error'; content: string; toolCall?: { name: string; params: unknown; }; timestamp: number; /** Set by the loop after the Brain has seen this tool result. Used by ContextBudgetManager for lifecycle trimming. */ seenAtIteration?: number; /** Base64 image data extracted from tool result. Brain paws should send this as a proper image content block instead of text. */ imageBase64?: string; /** MIME type of the image (e.g. "image/png") */ imageMimeType?: string; } /** The shared data structure that flows through the agent loop */ interface AgentContext { taskId: string; messages: AgentMessage[]; availableTools: ToolSummary[]; activeSkills: ActiveSkill[]; metadata: Record; /** System prompt built by core — brain paws use this directly */ systemPrompt?: string; iteration: number; maxIterations: number; } /** Create an empty AgentContext for a new task */ declare function createAgentContext(taskId: string, maxIterations: number): AgentContext; /** Error codes for categorizing failures in the agent loop */ type ActionErrorCode = 'TOOL_TIMEOUT' | 'TOOL_EXCEPTION' | 'TOOL_NOT_FOUND' | 'PERMISSION_DENIED' | 'PAW_CRASHED' | 'BRAIN_ERROR' | 'INVALID_PLAN'; /** Structured error attached to a failed action */ interface ActionError { code: ActionErrorCode; message: string; toolName?: string; pawName?: string; details?: unknown; } /** Result of a single tool execution during the Act phase */ interface ActionResult { /** * The conversation this result belongs to, when the run is a turn in one. * * Observe hooks receive results with no other way to tell which run produced them, so a paw * recording them had to consult its own "current session" — module state that answers for * whichever task bootstrapped last. Absent means the run has no conversation (a heartbeat, a * board task), and a recorder should skip it rather than pick one. */ sessionId?: string; toolName: string; pawName: string; success: boolean; output?: unknown; error?: ActionError; durationMs: number; } /** Create a structured ActionError */ declare function createActionError(code: ActionErrorCode, message: string, opts?: { toolName?: string; pawName?: string; details?: unknown; }): ActionError; /** Create a successful ActionResult */ declare function successResult(toolName: string, pawName: string, output: unknown, durationMs: number): ActionResult; /** Create a failed ActionResult */ declare function failureResult(toolName: string, pawName: string, error: ActionError, durationMs: number): ActionResult; /** Pluggable I/O interface for user-facing communication */ interface VoleIO { /** Ask the user for a yes/no confirmation */ confirm(message: string): Promise; /** Ask the user for free-form input */ prompt(message: string): Promise; /** Send a notification to the user (fire-and-forget) */ notify(message: string): void; } /** * Projects and their work items. * * A **project** is one body of work living in the agent's workspace: a directory under * `.openvole/workspace/` containing a `.project.json` manifest. The filesystem is the index — * there is no separate registry to drift out of sync. * * A **ProjectTask** is a durable unit of work inside a project — a goal with done-criteria that * may span many agent runs over hours or days. Deliberately NOT called `Task`: core already has * `AgentTask`/`TaskQueue` (core/task.ts), which is a single in-memory loop *execution*. One * ProjectTask ("port paw-database off better-sqlite3") is worked on across many AgentTasks. */ /** What kind of work this is — drives scan heuristics and, later, kind-specific paws. */ type ProjectKind = 'code' | 'writing' | 'media' | 'research' | 'general'; type ProjectStatus = 'active' | 'paused' | 'archived'; interface ProjectManifest { /** Also the directory name. Lowercase, filesystem- and URL-safe. */ id: string; name: string; kind: ProjectKind; /** * Absolute path to files the project operates on, when they live outside the workspace * (a git repo, a footage folder). Absent means the project *is* its workspace folder. * * A privilege boundary, not a convenience field — see `validateProjectRoot`. */ root?: string; status: ProjectStatus; /** Inferred by project_scan (phase 2). Informational — never used for permissions. */ stack?: string[]; /** Narrows the agent's tool profile for this project. Can never widen it. */ toolProfile?: { allow?: string[]; deny?: string[]; }; /** * Which markdown files in the project folder are inlined into the prompt, in order. * * Absent means all of them, CONTEXT.md first. Set it when a project accumulates docs that are * worth keeping but not worth spending prompt on every task. */ contextFiles?: string[]; tags?: string[]; createdAt: number; updatedAt: number; } type TaskState = 'queued' | 'running' /** Checking output against doneCriteria. The phase that stops "the agent said it finished". */ | 'verifying' /** Reserved for the phase-3 approval gate; nothing transitions here yet. */ | 'waiting_approval' /** Recoverable stop — criteria unmet, budget spent, or a human is needed. Carries `note`. */ | 'blocked' | 'done' | 'failed' | 'cancelled'; /** Legal transitions. Anything not listed is rejected by `TaskStore.update`. */ declare const TASK_TRANSITIONS: Record; declare const TERMINAL_TASK_STATES: readonly TaskState[]; interface TaskBudget { /** Loop iterations across all runs of this task. Exhaustion blocks, never silently truncates. */ maxIterations?: number; /** Epoch ms. Past deadline blocks. */ deadline?: number | null; } interface ProjectTask { id: string; projectId: string; goal: string; /** Checked in the `verifying` state. Empty means the agent's own judgement is the bar. */ doneCriteria: string[]; budget?: TaskBudget; state: TaskState; /** Higher runs first. Ties break oldest-first so a queue drains in order. */ priority: number; /** Why it is blocked / what happened. Surfaced to the human. */ note?: string | null; /** Paths (workspace-relative or absolute) this task produced. */ artifacts?: string[]; /** * The agent doing this work, when it is not the one that owns the project. * * A project belongs to whoever owns the *outcome*, not whoever does the labor — a channel * project sits with the coordinator while editing, thumbnails and publishing land on * different siblings. Without this a delegated task reads as abandoned: nobody is visibly * holding it while the work actually happens somewhere else. */ assignee?: string; /** The run id in the assignee's queue, so its status can be polled and recorded back. */ delegatedTaskId?: string; /** Iterations consumed so far, against `budget.maxIterations`. */ iterationsUsed?: number; createdAt: number; updatedAt: number; } /** * One step in a task's life — a state it entered and when. * * Not stored: `tasks.jsonl` already appends a full record per change, so the trail is reconstructed * from the file. Every task carries `createdAt`/`updatedAt`, but two timestamps cannot answer "how * long did this sit in verifying" or "how many times was it unblocked". */ interface TaskEvent { state: TaskState; at: number; /** The note recorded with this move, when there was one — usually why it blocked. */ note?: string; } /** What the loop puts on `context.metadata.project` for the system prompt. */ interface ProjectContextInfo { id: string; name: string; kind: ProjectKind; /** Absolute external root, or undefined for a self-contained project. */ root?: string; /** Absolute path of the project's own folder in the workspace. */ dir: string; /** Narrows tool access for the duration of this task. Never rendered into the prompt. */ toolProfile?: { allow?: string[]; deny?: string[]; }; /** The project's markdown docs, inlined into the prompt (already capped by the store). */ contextFiles?: Array<{ name: string; body: string; }>; /** Docs that exist but did not fit the budget — named so the agent can read them on purpose. */ otherFiles?: string[]; task?: { id: string; goal: string; doneCriteria: string[]; }; } /** Thrown when a project root is missing, not a directory, or outside the sandbox. */ declare class ProjectRootError extends Error { constructor(message: string); } /** Thrown for illegal state transitions and malformed input. */ declare class ProjectError extends Error { constructor(message: string); } /** * What core knows about the call a tool is serving. * * Passed per call rather than captured when the tool is built, because task concurrency is * configurable: a tool that read "the current project" off shared mutable state would answer for * whichever task last started, which is the wrong answer roughly half the time at concurrency 2. * * Optional everywhere — a tool that ignores it behaves exactly as before, and tools that run in a * Paw subprocess never receive one (the IPC boundary carries params only). */ interface ToolContext { /** The project this task is scoped to, when it has one. */ project?: ProjectContextInfo; /** * Where this run reports back to — the chat session that gets its result. Derived per run by * `replyAddressFor`, so a tool that talks to the human lands where the work came from instead * of defaulting to the general chat. */ replyTo?: string; /** * How many agent-to-agent hops this run is already deep, so a message it sends carries the * count onward. Without it every reply looks like a fresh conversation and nothing ever stops. */ hops?: number; /** * The conversation this run is ultimately answering, when it is not this one — a person who * asked a question that has since been passed to a colleague. Carried so their answer can find * its way back to whoever is actually waiting. */ relayTo?: string; } /** A tool definition as provided by a Paw */ interface ToolDefinition { name: string; description: string; parameters: ZodSchema; execute: (params: unknown, ctx?: ToolContext) => Promise; } /** An entry in the tool registry — includes ownership metadata */ interface ToolRegistryEntry { name: string; description: string; parameters: ZodSchema; pawName: string; inProcess: boolean; execute: (params: unknown, ctx?: ToolContext) => Promise; } /** The plan returned by a Brain Paw during the Think phase */ interface AgentPlan { actions: PlannedAction[]; execution?: 'parallel' | 'sequential'; response?: string; done?: boolean; /** Token usage reported by the Brain Paw (for cost tracking) */ usage?: { inputTokens?: number; outputTokens?: number; model?: string; provider?: string; }; } /** A single tool call the Brain wants to execute */ interface PlannedAction { tool: string; params: unknown; } /** Hook called once when a task starts — initialize Paw state for this task */ type BootstrapHook = (context: AgentContext) => Promise; /** Hook called during the Perceive phase — enrich context before Think */ type PerceiveHook = (context: AgentContext) => Promise; /** Hook called during the Observe phase — fire-and-forget side effect */ type ObserveHook = (result: ActionResult) => Promise; /** Hook called when context exceeds size threshold — compress/summarize */ type CompactHook = (context: AgentContext) => Promise; /** A cron-triggered schedule hook */ interface ScheduleHook { cron: string; handler: () => Promise; } /** Input for creating a new agent task */ interface AgentTaskInput { input: string; source?: 'user' | 'schedule' | 'paw'; } /** The full Paw definition — the contract every Paw must implement */ interface PawDefinition { name: string; version: string; description: string; brain?: boolean; inProcess?: boolean; config?: ZodSchema; hooks?: { onBootstrap?: BootstrapHook; onPerceive?: PerceiveHook; onObserve?: ObserveHook; onCompact?: CompactHook; onSchedule?: ScheduleHook[]; }; tools?: ToolDefinition[]; think?: (context: AgentContext) => Promise; io?: VoleIO; onLoad?: (config: unknown) => Promise; onUnload?: () => Promise; } /** Transport type for IPC communication */ type TransportType = 'ipc' | 'stdio'; /** Paw manifest as read from vole-paw.json */ interface PawManifest { name: string; version: string; description: string; entry: string; brain: boolean; /** Paw category (required) */ category: 'brain' | 'channel' | 'tool' | 'infrastructure'; inProcess?: boolean; transport?: TransportType; tools: Array<{ name: string; description: string; }>; permissions?: { network?: string[]; listen?: number[]; filesystem?: string[]; env?: string[]; /** If true, the Paw needs to spawn child processes */ childProcess?: boolean; }; /** Embedded dashboard panel: a static HTML file served by the control plane. */ panel?: { title?: string; html: string; }; } /** Paw configuration in vole.config.ts */ interface PawConfig { name: string; hooks?: { perceive?: { order?: number; pipeline?: boolean; }; }; allow?: { network?: string[]; listen?: number[]; filesystem?: string[]; env?: string[]; /** If true, allow this Paw to spawn child processes */ childProcess?: boolean; }; } /** Runtime state of a loaded Paw */ interface PawInstance { name: string; manifest: PawManifest; config: PawConfig; healthy: boolean; transport: TransportType; inProcess: boolean; definition?: PawDefinition; process?: { kill: () => void; pid?: number; }; sendRequest?: (method: string, params?: unknown) => Promise; /** Bus events this Paw has subscribed to */ subscriptions?: string[]; } /** Effective permissions = intersection of manifest requests and config grants */ interface EffectivePermissions { network: string[]; listen: number[]; filesystem: string[]; env: string[]; childProcess: boolean; } /** Rate limit configuration */ interface RateLimits { /** Max LLM (Brain) calls per minute */ llmCallsPerMinute?: number; /** Max LLM (Brain) calls per hour */ llmCallsPerHour?: number; /** Max tool executions per single task */ toolExecutionsPerTask?: number; /** Max tasks per hour, keyed by source */ tasksPerHour?: Record; } /** Loop configuration */ interface LoopConfig { maxIterations: number; confirmBeforeAct: boolean; taskConcurrency: number; /** Max messages before triggering compact hooks (0 = disabled) */ compactThreshold: number; /** Rate limits (undefined = no limits) */ rateLimits?: RateLimits; /** Enable Tool Horizon — Brain starts with core tools only, discovers others on demand */ toolHorizon?: boolean; /** Max context size in tokens (approximate). Brain paws use this to trim messages before API calls. 0 = use brain paw's default for the model. */ maxContextTokens?: number; /** Tokens reserved for the Brain's response. Default: 4000. */ responseReserve?: number; /** Cost alert threshold in USD — warn when a single task exceeds this amount */ costAlertThreshold?: number; /** * Cost tracking mode: * - "auto" (default): track for cloud providers, show "free" for local Ollama * - "enabled": track costs for all providers including Ollama cloud * - "disabled": no cost tracking */ costTracking?: 'auto' | 'enabled' | 'disabled'; } /** Tool profile — restricts which tools a task source can use */ interface ToolProfile { /** Tools allowed (if set, only these tools can be used) */ allow?: string[]; /** Tools denied (if set, these tools are blocked) */ deny?: string[]; } /** Heartbeat configuration */ interface HeartbeatConfig { enabled: boolean; intervalMinutes: number; /** * Cron expression for the wake-up, e.g. "0 12 * * *" (daily at noon UTC). Takes * precedence over intervalMinutes — the only way to express schedules cron can do * but a plain interval cannot (a specific hour, weekdays, ...). */ cron?: string; /** If true, run heartbeat immediately on startup (default: false) */ runOnStart?: boolean; } /** Docker sandbox configuration */ interface DockerSandboxConfig$1 { /** Enable Docker sandboxing (default: false) */ enabled?: boolean; /** Docker image to use (default: node:20-slim) */ image?: string; /** Memory limit per container (default: 512m) */ memory?: string; /** CPU limit per container (default: 1.0) */ cpus?: string; /** Container scope: per-session or shared (default: session) */ scope?: 'session' | 'shared'; /** Network mode: none, bridge, or host (default: none) */ network?: 'none' | 'bridge' | 'host'; /** Allowed outbound domains when network=bridge */ allowedDomains?: string[]; } /** Security configuration */ interface SecurityConfig { /** If false, disables filesystem sandboxing for paw subprocesses. Default: true (sandboxed) */ sandboxFilesystem?: boolean; /** Additional paths paws are allowed to access outside .openvole/ */ allowedPaths?: string[]; /** Docker container sandbox (optional, stronger isolation) */ docker?: DockerSandboxConfig$1; } /** Agent profile — named agent with role, tool restrictions, and resource limits */ interface AgentProfile { /** Human-readable role description */ role?: string; /** Instructions injected into the sub-agent's context */ instructions?: string; /** Tools this agent is allowed to use (allowlist) */ allowTools?: string[]; /** Tools this agent is denied (denylist — takes precedence over allow) */ denyTools?: string[]; /** Max iterations for this agent (default: 10) */ maxIterations?: number; } /** The full OpenVole configuration */ interface VoleConfig { brain?: string; paws: Array; skills: string[]; loop: LoopConfig; heartbeat: HeartbeatConfig; /** Tool profiles per task source — restrict which tools can be used */ toolProfiles?: Record; /** Security settings */ security?: SecurityConfig; /** Named agent profiles for sub-agent spawning */ agents?: Record; /** VoleNet distributed networking */ net?: VoleNetConfig; /** Demo lockdown. When true, the dashboard cannot edit config or identity files * (they become read-only). Settable only by editing vole.config.json on the * server — there is no UI toggle, so it cannot be turned off from the dashboard. */ demo?: boolean; } /** Create a VoleConfig with defaults applied */ declare function defineConfig(config: Partial): VoleConfig; /** Load configuration from vole.config.json */ declare function loadConfig(configPath: string): Promise; /** Events emitted on the message bus */ type BusEvents = { 'tool:registered': { toolName: string; pawName: string; }; 'tool:unregistered': { toolName: string; pawName: string; }; 'paw:registered': { pawName: string; }; 'paw:unregistered': { pawName: string; }; 'paw:crashed': { pawName: string; error?: unknown; }; 'task:queued': { taskId: string; }; 'task:started': { taskId: string; }; /** `replyTo` is where the report is delivered — see core/reply-address.ts. Always set. */ 'task:completed': { taskId: string; result?: string; sessionId?: string; replyTo?: string; source?: string; }; 'task:failed': { taskId: string; error?: unknown; result?: string; sessionId?: string; replyTo?: string; source?: string; }; 'task:cancelled': { taskId: string; }; 'agent:completed': { taskId: string; parentTaskId: string; status: string; result?: string; error?: string; }; 'volenet:tool:executed': { toolName: string; fromInstance: string; success: boolean; durationMs: number; error?: string; }; 'volenet:chat': { from: string; fromName: string; text: string; messageId: string; timestamp: number; /** True when the message arrived through a relay hub as a sealed envelope. */ relayed?: boolean; }; 'volenet:relay:error': { /** The relay hub that reported the failure. */ via: string; to?: string; reason?: string; }; /** A chat message could not be delivered — the member is away — and now waits in MY outbox. */ 'volenet:chat:queued': { to: string; toName: string; ref: string; text: string; sentAt: number; via: string; }; /** A message that had been waiting in my outbox has now reached its recipient. */ 'volenet:chat:flushed': { to: string; toName: string; ref: string; sentAt: number; }; /** On reconnecting to a hub: who tried to reach me while I was away. Their messages follow. */ 'volenet:chat:pending': { via: string; from: Array<{ from: string; fromName: string; count: number; first: number; last: number; }>; }; /** A relay member asked to connect — awaiting this agent's approval. */ 'volenet:relay:request': { from: string; fromName: string; note?: string; }; /** A relay member accepted this agent's connect-request. */ 'volenet:relay:accepted': { from: string; fromName: string; }; /** A relay member denied this agent's connect-request. */ 'volenet:relay:denied': { from: string; fromName: string; }; /** An unknown node asked to pair (vole net pair) — awaiting this operator's accept. */ 'volenet:pair:request': { from: string; fromName: string; note?: string; }; /** VoleDrop: a peer offered a file (auto=true when acceptFrom auto-accepted it). */ 'volenet:file:offer': { transferId: string; from: string; fromName: string; name: string; size: number; note?: string; auto: boolean; }; /** VoleDrop: transfer progress (throttled to ~2/sec per transfer). */ 'volenet:file:progress': { transferId: string; dir: 'send' | 'recv'; bytes: number; totalBytes: number; pct: number; }; /** VoleDrop: a file landed in the inbox (sha256-verified). */ 'volenet:file:received': { transferId: string; from: string; fromName: string; name: string; path: string; size: number; sha256: string; }; /** VoleDrop: the peer verified and stored a file this agent sent. */ 'volenet:file:sent': { transferId: string; to: string; toName: string; name: string; size: number; }; 'volenet:file:failed': { transferId: string; dir: 'send' | 'recv'; code: string; detail?: string; }; 'volenet:file:rejected': { transferId: string; by: string; reason: string; }; 'rate:limited': { bucket: string; source?: string; }; 'engine:restart': Record; /** * A human-facing message crossed a channel — emitted by channel Paws (paw-chat for the * dashboard chat, paw-telegram, paw-slack, …), not by core. `dir: 'out'` is the agent * reaching its human on its own initiative; `dir: 'in'` is an inbound message a channel * recorded before turning it into a task. paw-session files these into the transcript and * the dashboard raises them as chat + unread. `pawName` is stamped by core from the * emitting Paw, so provenance never comes from the payload. */ 'channel:message': { /** Channel id — the paw name minus the `@openvole/paw-` prefix (`chat`, `telegram`). */ channel: string; dir: 'in' | 'out'; /** Transcript this belongs to (`dashboard` for the dashboard chat). */ sessionId: string; text: string; ts: number; /** Channel-specific sender handle, for inbound messages. */ from?: string; /** Stamped by core — the Paw that emitted the event. */ pawName?: string; /** True when the emitter already wrote this to the session transcript — do not store it again. */ stored?: boolean; }; }; type MessageBus = Emitter; /** Create a new message bus instance */ declare function createMessageBus(): MessageBus; declare class ToolRegistry { private bus; private tools; private horizonEnabled; private horizonTools; /** Paw names whose tools are always visible in horizon mode */ private alwaysVisiblePaws; constructor(bus: MessageBus); /** Enable/disable Tool Horizon mode */ setHorizon(enabled: boolean): void; /** Mark a paw's tools as always visible in horizon mode */ addAlwaysVisiblePaw(pawName: string): void; /** Add tools to the horizon (make them visible to the Brain) */ addToHorizon(toolNames: string[]): void; /** Reset horizon for a new task */ resetHorizon(): void; /** Search all tools by intent using BM25 over descriptions */ searchTools(query: string, limit?: number): Array<{ name: string; description: string; pawName: string; score: number; }>; /** Register tools from a Paw. Auto-prefixes with paw name on conflict. */ register(pawName: string, tools: ToolDefinition[], inProcess: boolean): void; /** Remove all tools owned by a specific Paw */ unregister(pawName: string): void; /** Get a tool entry by name */ get(toolName: string): ToolRegistryEntry | undefined; /** List all registered tools */ list(): ToolRegistryEntry[]; /** Check if a tool exists */ has(toolName: string): boolean; /** Get tool summaries for AgentContext (respects horizon if enabled) */ summaries(): ToolSummary[]; /** Every tool with its JSON-schema parameters, ignoring the horizon (e.g. for the MCP bridge). */ allSummaries(): ToolSummary[]; /** Get all tool names owned by a specific Paw */ toolsForPaw(pawName: string): string[]; /** Clear all tools (for shutdown) */ clear(): void; } /** Interface for queryable registries (avoids circular imports) */ interface QueryableSkillRegistry { list(): Array<{ name: string; active: boolean; missingTools: string[]; definition: { description: string; }; }>; } interface QueryableTaskQueue { list(): Array<{ id: string; source: string; input: string; status: string; createdAt: number; }>; enqueue(input: string, source?: 'user' | 'schedule' | 'paw', options?: { sessionId?: string; metadata?: Record; }): { id: string; }; } interface QueryableScheduler { list(): Array<{ id: string; input: string; cron: string; nextRun?: string; createdAt: number; }>; } /** Manages loaded Paws and their lifecycle */ declare class PawRegistry { private bus; private toolRegistry; private projectRoot; private paws; private transports; private perceiveHooks; private observeHookPaws; private bootstrapPaws; private compactPaws; private brainPawName; /** Maps config path → manifest name (e.g. "./paws/paw-ollama" → "@openvole/paw-ollama") */ private configToManifest; private skillRegistry?; private taskQueue?; private scheduler?; private security?; constructor(bus: MessageBus, toolRegistry: ToolRegistry, projectRoot: string); /** Inject queryable registries (called after construction to avoid circular deps) */ setQuerySources(skills: QueryableSkillRegistry, tasks: QueryableTaskQueue, scheduler?: QueryableScheduler): void; /** Set security config for filesystem sandboxing */ setSecurity(security?: SecurityConfig): void; /** Load and register a Paw */ load(config: PawConfig): Promise; /** Unload a Paw (accepts config path or manifest name) */ unload(name: string): Promise; /** Resolve a config name to its manifest name */ resolveManifestName(configName: string): string; /** Set the Brain Paw name (accepts config path, manifest name, or package name) */ setBrain(name: string): void; /** Get the Brain Paw name */ getBrainName(): string | undefined; /** Get a Paw instance */ get(name: string): PawInstance | undefined; /** List all loaded Paws */ list(): PawInstance[]; /** Check if a Paw is healthy */ isHealthy(name: string): boolean; /** * Run GLOBAL perceive hooks — only Paws without tools. * Paws with tools use lazy perceive (called just before their tool executes). */ runGlobalPerceiveHooks(context: AgentContext): Promise; /** * Run LAZY perceive for a specific Paw — called just before its tool executes. * Only runs if the Paw has an onPerceive hook registered. */ runLazyPerceive(pawName: string, context: AgentContext): Promise; /** Run all Observe hooks concurrently (fire-and-forget) */ runObserveHooks(result: ActionResult): void; /** Run bootstrap hooks — called once at the start of a task */ runBootstrapHooks(context: AgentContext): Promise; /** * Run compact hooks — called when context exceeds size threshold. * Paws can compress/summarize messages to free up context window space. */ runCompactHooks(context: AgentContext): Promise; /** Call the Brain Paw's think function */ think(context: AgentContext): Promise; /** Execute a tool on a subprocess Paw */ executeRemoteTool(pawName: string, toolName: string, params: unknown): Promise; /** Read a Paw's embedded panel HTML (static file declared in its manifest), or null. */ getPanelHtml(pawName: string): Promise; private callPerceive; private callObserve; private registerInProcessTools; private setupTransportHandlers; /** Forward bus events to a Paw that subscribed */ private setupBusForwarding; /** Handle state queries from Paws */ private handleQuery; private waitForRegistration; } /** A Skill definition — parsed from SKILL.md */ interface SkillDefinition { name: string; description: string; version?: string; requiredTools: string[]; optionalTools: string[]; instructions: string; tags: string[]; /** OpenClaw compatibility — runtime requirements */ requires?: SkillRequirements; } /** Runtime requirements (OpenClaw-compatible) */ interface SkillRequirements { /** Environment variables the skill expects */ env: string[]; /** CLI binaries that must all be installed */ bins: string[]; /** CLI binaries where at least one must exist */ anyBins: string[]; } /** Runtime state of a loaded Skill */ interface SkillInstance { name: string; definition: SkillDefinition; /** Path to the skill directory */ path: string; active: boolean; missingTools: string[]; } /** Manages loaded Skills and their activation state */ declare class SkillRegistry { private bus; private toolRegistry; private projectRoot; private skills; constructor(bus: MessageBus, toolRegistry: ToolRegistry, projectRoot: string); /** Load a Skill from a directory containing SKILL.md */ load(nameOrPath: string): Promise; /** Unload a Skill */ unload(name: string): boolean; /** Re-run the resolver against the current tool registry */ resolve(): void; /** Get all Skill instances */ list(): SkillInstance[]; /** Get active Skills only */ active(): SkillInstance[]; /** Get a Skill by name */ get(name: string): SkillInstance | undefined; } /** * Sliding window counter rate limiter. * Tracks timestamps per bucket and checks against limits. */ declare class RateLimiter { private buckets; /** * Try to consume one token from the bucket. * Returns true if the request is under the limit, false if rate-limited. */ tryConsume(bucket: string, limit: number, windowMs: number): boolean; /** * Returns the number of remaining tokens in the bucket for the current window. */ remaining(bucket: string, limit: number, windowMs: number): number; /** * Remove expired timestamps from a bucket. */ private cleanup; } /** Loaded prompt and identity content — cached on engine start */ interface SystemPromptContent { brainPrompt: string; identityContext: string; /** Absolute path of this agent's scratch/project area (.openvole/workspace). */ workspaceDir?: string; } /** * Load BRAIN.md and identity files from the project directory. * Called once on engine start. Content is cached and reused for every task. * * BRAIN.md resolution: * 1. .openvole/paws//BRAIN.md (user customized) * 2. Falls back to default prompt if not found * * Identity files: .openvole/SOUL.md, .openvole/USER.md, .openvole/AGENT.md */ declare function loadSystemPromptContent(projectRoot: string, brainPawName?: string): Promise; /** * Build the complete system prompt from cached content + dynamic context. * * Ordering: static content first (for provider prompt caching), dynamic last. * 1. BRAIN.md (static) * 2. Identity files (static) * 3. Skills list (semi-static) * 4. Tool descriptions (static per session) * 5. Runtime context (dynamic) * 6. Memory (dynamic) */ declare function buildSystemPrompt(content: SystemPromptContent, activeSkills: ActiveSkill[], availableTools: ToolSummary[], metadata?: Record): string; /** Task states */ type TaskStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'; /** Task priority — urgent tasks are processed before normal and low */ type TaskPriority = 'urgent' | 'normal' | 'low'; /** A discrete unit of work for the agent loop */ interface AgentTask { id: string; source: 'user' | 'schedule' | 'heartbeat' | 'paw' | 'agent'; input: string; status: TaskStatus; createdAt: number; startedAt?: number; completedAt?: number; result?: string; error?: string; sessionId?: string; metadata?: Record; /** ID of the parent task that spawned this sub-agent task */ parentTaskId?: string; /** Task priority — default: normal */ priority?: TaskPriority; /** Task IDs this task depends on — waits until all are completed */ dependsOn?: string[]; } type TaskRunner = (task: AgentTask) => Promise; /** FIFO task queue with configurable concurrency */ declare class TaskQueue { private bus; private concurrency; private rateLimiter?; private rateLimits?; private queue; private running; private completed; private runner; private draining; constructor(bus: MessageBus, concurrency?: number, rateLimiter?: RateLimiter | undefined, rateLimits?: RateLimits | undefined); /** Set the task runner function (called by the agent loop) */ setRunner(runner: TaskRunner): void; /** Enqueue a new task */ enqueue(input: string, source?: 'user' | 'schedule' | 'heartbeat' | 'paw' | 'agent', options?: { sessionId?: string; metadata?: Record; parentTaskId?: string; priority?: TaskPriority; dependsOn?: string[]; }): AgentTask; /** Cancel a task by ID */ cancel(taskId: string): boolean; /** Cancel all queued tasks (for shutdown) */ cancelAll(): void; /** Get all tasks (queued + running + completed) */ list(): AgentTask[]; /** Get a task by ID */ get(taskId: string): AgentTask | undefined; /** Get all currently running tasks */ getRunning(): AgentTask[]; /** Check if a task has been cancelled */ isCancelled(taskId: string): boolean; /** Check if a task's dependencies are all satisfied (completed) */ private areDependenciesMet; /** Pick the next ready task from the queue (priority-aware, dependency-aware) */ private pickNextTask; private drain; private runTask; } interface LoopDependencies { bus: MessageBus; toolRegistry: ToolRegistry; pawRegistry: PawRegistry; skillRegistry: SkillRegistry; io: VoleIO; config: LoopConfig; toolProfiles?: Record; rateLimiter?: RateLimiter; /** Cached system prompt content (loaded on engine start) */ systemPromptContent?: SystemPromptContent; /** * Resolve the project this task belongs to, if any. Called once per task — project context is * constant for a task's lifetime, unlike the per-iteration prompt rebuild. * * Injected rather than imported so the loop stays free of project storage, and so scope always * arrives *with the task* instead of from ambient state: tasks interleave (a heartbeat can * start mid-chat), and a global "current project" would hand one task another's context — the * same bug shape that filed brain replies under the wrong session. */ resolveProject?: (task: AgentTask) => Promise; /** Every active project, so the agent can see and switch between them from any run. */ listProjects?: () => Promise>; } /** * Run the agent loop for a single task. * Perceive → Think → Act → Observe → loop */ declare function runAgentLoop(task: AgentTask, deps: LoopDependencies): Promise; /** Persistent store for recurring schedules using cron expressions */ declare class SchedulerStore { private schedules; private savePath; private tickHandler; private writeChain; private restoring; /** Set the file path for persistence */ setPersistence(filePath: string): void; /** Set the handler called when a schedule ticks */ setTickHandler(handler: (input: string, opts?: { projectId?: string; }) => void): void; /** Load schedule data from disk without starting cron jobs (for read-only access). Never persists. */ loadFromDisk(): Promise; /** Load persisted schedules from disk and restart their jobs */ restore(): Promise; /** Create or replace a recurring schedule */ add(id: string, input: string, cron: string, onTick: () => void, createdAt?: number, immediate?: boolean, projectId?: string): void; /** * Fire a schedule now without waiting for its next cron tick, leaving the schedule itself * untouched. Runs the entry's own callback, so a project-scoped schedule triggers scoped. */ trigger(id: string): boolean; /** Cancel a schedule by ID */ cancel(id: string, skipPersist?: boolean): boolean; /** List all active schedules */ list(): Array<{ id: string; input: string; cron: string; nextRun?: string; createdAt: number; projectId?: string; }>; /** Clear all schedules (for shutdown). Disables persistence so the file is never overwritten. */ clearAll(): void; /** Save schedules to disk (serialized — only one write at a time) */ private persist; } interface TokenBudget { systemPrompt: number; tools: number; sessionHistory: number; taskMessages: number; responseReserve: number; total: number; maxTokens: number; free: number; } /** * Manages context token budgets — estimation, budget calculation, * priority-based trimming, and compaction triggers. * * Lives in core. The loop uses it before calling think() so brain paws * receive pre-trimmed context and just call the API. */ declare class ContextBudgetManager { private maxTokens; private responseReserve; constructor(maxTokens: number, responseReserve?: number); /** * Estimate tokens for a string. * JSON/code uses ~2 chars per token (denser). * Natural text uses ~4 chars per token. * Matches OpenClaw's heuristic (~90% accuracy). */ estimateTokens(text: string): number; /** * Estimate tokens for an array of messages. * Each message has ~4 tokens overhead (role, formatting). */ estimateMessagesTokens(messages: AgentMessage[]): number; /** * Calculate full budget breakdown. */ calculateBudget(systemPromptTokens: number, toolTokens: number, sessionHistoryTokens: number, messagesTokens: number): TokenBudget; /** * Should compaction trigger? Returns true when >75% of budget is used. */ shouldCompact(budget: TokenBudget): boolean; /** * Priority-based message trimming to fit within available token budget. * * Trimming order (lowest priority first): * 1. Old tool results (seenAtIteration set and >2 iterations ago) → summarize * 2. Old error messages (>5 messages back) → remove * 3. Old brain messages (>8 messages back, not last 2) → truncate * 4. Old user messages (>8 messages back, not first or last) → truncate * 5. Session history messages → remove oldest * * Never trimmed: first user message, last 2 brain messages, last user message */ trimMessages(messages: AgentMessage[], availableTokens: number, currentIteration: number): AgentMessage[]; /** * Format budget breakdown for logging. */ formatBudget(budget: TokenBudget): string; /** * Break down message tokens by role. */ messageBreakdown(messages: AgentMessage[]): { user: number; brain: number; toolResult: number; error: number; total: number; }; private findLastIndex; private findLastNIndices; } interface CostEntry { inputTokens: number; outputTokens: number; inputCost: number; outputCost: number; totalCost: number; model: string; } interface TaskCostSummary { llmCalls: number; totalInputTokens: number; totalOutputTokens: number; totalCost: number; entries: CostEntry[]; } type CostTrackingMode = 'auto' | 'enabled' | 'disabled'; declare class CostTracker { private entries; private alertThreshold; private mode; constructor(alertThreshold?: number, mode?: CostTrackingMode); /** * Check if a provider is local (free) based on mode and provider/model info. * In "auto" mode, Ollama is free unless the model name contains ":cloud" (Ollama cloud). * In "enabled" mode, everything is tracked. */ private isLocalFree; /** * Record an LLM call's cost. * Accepts token counts and model name, estimates USD cost. */ record(inputTokens: number | string | undefined, outputTokens: number | string | undefined, model: string, provider?: string): CostEntry; getTotalCost(): number; getSummary(): TaskCostSummary; private getPricing; } /** * Docker sandbox for Paw subprocess isolation. * Optional alternative to Node.js --permission model. * * Provides stronger isolation (container-level) at the cost of startup time. * Uses dockerode for container lifecycle management. * * Config: security.docker.enabled = true in vole.config.json */ interface DockerSandboxConfig { /** Enable Docker sandboxing (default: false) */ enabled?: boolean; /** Docker image to use (default: node:20-slim) */ image?: string; /** Memory limit (default: 512m) */ memory?: string; /** CPU limit (default: 1.0) */ cpus?: string; /** Container scope: per-session or shared (default: session) */ scope?: 'session' | 'shared'; /** Network mode: none, bridge, or host (default: none) */ network?: 'none' | 'bridge' | 'host'; /** Allowed outbound domains (only when network=bridge) */ allowedDomains?: string[]; } /** * Docker sandbox manager. * Handles container lifecycle for paw subprocesses. */ declare class DockerSandboxManager { private docker; private containers; private config; private projectRoot; private initialized; constructor(config: DockerSandboxConfig, projectRoot: string); init(): Promise; /** * Run a paw inside a Docker container. * Returns a handle with stdio streams for IPC transport. */ spawnInContainer(pawPath: string, pawName: string, entryPath: string, _permissions: Record, env: Record): Promise<{ containerId: string; exec: any; kill: () => Promise; }>; /** Stop and remove all managed containers */ cleanup(): Promise; /** Check if Docker is available */ static isAvailable(): Promise; /** List running containers */ listContainers(): Array<{ pawName: string; containerId: string; uptime: number; }>; private parseMemoryLimit; } /** * VoleHub registry client. * * Phase 1: GitHub-based registry at openvole/volehub. * Skills stored as directories with SKILL.md + manifest.json. * INDEX.json provides searchable skill metadata. * * Installation flow: * 1. Fetch INDEX.json from GitHub * 2. Find skill by name * 3. Download every bundled file (SKILL.md + scripts/references/assets) from its `files` * manifest, or discover them from the registry when the entry predates manifests * 4. Verify each file's SHA-256 hash * 5. Write to .openvole/skills/volehub//, preserving directory structure */ /** One bundled file in a skill, with its integrity hash. */ interface SkillFile { path: string; sha256: string; } interface VoleHubSkill { name: string; version: string; description: string; publisher: string; tags: string[]; requiredTools: string[]; optionalTools?: string[]; contentHash: string; publishedAt: string; downloadUrl?: string; repository?: string; /** All bundled files (SKILL.md + scripts/references/assets) with per-file hashes. */ files?: SkillFile[]; } interface VoleHubIndex { updatedAt: string; skills: VoleHubSkill[]; } declare class VoleHubClient { private registryUrl; constructor(registryUrl?: string); /** Fetch the skill index from the registry */ fetchIndex(): Promise; /** Search skills by query (text match on name, description, tags) */ search(query: string): Promise; /** Get a specific skill by name */ getSkill(name: string): Promise; /** Download and install a skill (all bundled files) to the project */ install(skillName: string, projectRoot: string): Promise<{ installed: boolean; path: string; skill: VoleHubSkill; files: string[]; }>; /** * Discover a skill's files from the registry when the index has no `files` manifest. * Works for GitHub-hosted registries via the git-trees API; otherwise SKILL.md only. */ private discoverFiles; /** Uninstall a VoleHub skill */ uninstall(skillName: string, projectRoot: string): Promise; /** List installed VoleHub skills */ listInstalled(projectRoot: string): Promise>; /** * Prepare a skill for publishing. * Reads SKILL.md, generates hash, builds manifest. * Returns the data needed to create a PR against openvole/volehub. */ preparePublish(skillPath: string): Promise<{ name: string; version: string; description: string; content: string; contentHash: string; requiredTools: string[]; tags: string[]; files: SkillFile[]; }>; } /** A registered agent (an isolated agent container = a normal OpenVole project dir). */ interface AgentEntry { /** Stable id (slug of the name). */ id: string; /** Human-friendly name. */ name: string; /** Absolute path to the agent's project directory. */ path: string; /** ISO timestamp of creation. */ createdAt: string; /** * This agent may supervise its siblings via the control plane's reverse-RPC (agent_* tools). * Parent-owned: lives in the registry, outside every agent's sandbox — an agent cannot * grant itself orchestrator. */ orchestrator?: boolean; } /** The global agents registry — persisted at ~/.openvole/agents.json. */ interface AgentRegistry { /** Currently active agent id (for CLI targeting). */ activeId?: string; agents: AgentEntry[]; } type AgentRunState = 'running' | 'stopped'; /** An agent entry plus its derived (live-checked) runtime status. */ interface AgentStatus extends AgentEntry { state: AgentRunState; pid?: number; } /** * Supervisor for agents. Manages the global registry (~/.openvole/agents.json) and * one engine subprocess per active agent. Agents MUST run as separate processes — the * paw-sdk IPC transport singleton and VoleNet globals make in-process multi-engine unsafe. */ declare class AgentManager { private readonly home; private readonly registryPath; constructor(opts?: { home?: string; }); /** The resolved vole home — where the registry, templates, and server-wide logs live. */ get homeDir(): string; readRegistry(): Promise; private writeRegistry; private slug; private getEntry; private runtimePath; private readRuntime; private writeRuntime; private clearRuntime; private isAlive; /** Live pid for an agent, or undefined if not running. Clears stale runtime hints. */ private livePid; /** Path to the optional agent template at /agent-template. */ get templatePath(): string; private pathExists; /** Existing template dir: /agent-template, else the legacy /space-template. */ private resolveTemplate; /** Scaffold the agent template if absent. Returns its path and whether it was just created. */ ensureTemplate(): Promise<{ path: string; created: boolean; }>; /** Recursively copy the template into a new agent dir, skipping volatile/installed files. */ private copyTemplate; create(name: string, opts?: { path?: string; orchestrator?: boolean; }): Promise; /** Seed the orchestrator AGENT.md brief — but never clobber a customized identity. */ private seedOrchestratorIdentity; list(): Promise; status(idOrName?: string): Promise; /** * Lazily start an agent's engine subprocess (no-op if already running). * `cliPath` is the absolute path to the running dist/cli.js (the `__run-agent` daemon entry). */ start(idOrName: string, opts: { cliPath: string; }): Promise<{ pid: number; reused: boolean; }>; stop(idOrName: string): Promise; stopAll(): Promise; private stopEntry; /** Grant or revoke an agent's orchestrator authority (persisted in the registry). */ setOrchestrator(idOrName: string, value: boolean): Promise; /** * Rename an agent — its **display name only**. * * The id stays put on purpose. It is the directory name, `VOLE_AGENT_ID` in the running * engine, the MCP endpoint path, and the key the dashboard files chat history and unread * counts under. Renaming it would orphan all of that and break any orchestrator brief * addressing the agent mid-flight, to spare the operator one cosmetic detail. So the name * moves and the identity does not — no restart needed, and a running agent is unaffected. * * The new name must not collide with another agent's id *or* name: both are accepted when * targeting an agent (`agent_submit`, the CLI, the control plane), so a duplicate would make * "which agent did you mean" unanswerable. */ rename(idOrName: string, newName: string): Promise; switchTo(idOrName: string): Promise; remove(idOrName: string, opts?: { purge?: boolean; }): Promise; } /** * Channels — how an agent reaches its human. * * A channel is not a core abstraction with its own tool: it is a Paw that declares * `"category": "channel"` in its manifest and exposes its own send tool (`chat_send`, * `telegram_send`, `slack_send`, …). Core's only job is to *know which Paws are channels* so it * can tell the agent they exist (system prompt), keep them reachable under tool horizon, and * skip them when there is no human attached (headless). * * This is deliberately thin. There is no `message_user` wrapper, no channel config, no routing * layer: the agent calls the channel's own tool, exactly as it would any other tool. The naming * convention is what makes a channel legible — `_send`, where the id is the Paw name minus * the `@openvole/paw-` prefix. */ /** A channel available to the agent — a channel Paw, or one core provides itself. */ interface ChannelInfo { /** Channel id — the Paw name minus `@openvole/paw-` (`chat`, `telegram`, `slack`). */ id: string; pawName: string; /** The tool that sends a message out on this channel, when one is discoverable. */ sendTool?: string; /** Every tool this channel registered. */ tools: string[]; description: string; } /** Strip the package scope and `paw-` prefix: `@openvole/paw-telegram` → `telegram`. */ declare function channelIdFor(pawName: string): string; /** * Pick the send tool for a channel. * * Prefers the convention (`_send`) so a Paw with several tools — `telegram_send`, * `telegram_reply`, `telegram_get_chat` — advertises the right one. Falls back to any `*_send`, * then to a lone single tool. Returns undefined rather than guessing wrong: the prompt then * lists the Paw's tools and lets the agent choose. */ declare function pickSendTool(id: string, tools: string[]): string | undefined; /** * List every channel this agent can reach a human through: core's built-ins plus loaded, * healthy channel Paws. * * Unhealthy Paws are left out on purpose: a crashed channel is worse than no channel, because * the agent would report a message as delivered when nothing was sent. */ declare function listChannels(paws: PawInstance[], toolRegistry: ToolRegistry): ChannelInfo[]; /** * Project storage — one directory per project under `.openvole/workspace/`. * * A directory is a project exactly when it holds a readable `.project.json`. There is no index * file: the filesystem is the truth, so a project stays portable (VoleDrop the folder to another * agent, sync it over VoleNet) and nothing can desync. */ declare const MANIFEST_NAME = ".project.json"; /** The name this used to have. Still read, so projects that have one keep working. */ declare const CONTEXT_NAME = "CONTEXT.md"; declare const TASKS_NAME = "tasks.jsonl"; /** Files the project tools own. The workspace_* scratch tools refuse to write these. */ declare const RESERVED_BASENAMES: readonly string[]; interface CreateProjectInput { id: string; name?: string; kind?: ProjectKind; root?: string; stack?: string[]; toolProfile?: { allow?: string[]; deny?: string[]; }; tags?: string[]; /** Initial CONTEXT.md body. */ context?: string; } interface ProjectStoreOptions { /** * Roots the agent may point a project at, beyond its own agent directory — this is * `security.allowedPaths` from vole.config.json. Empty means external projects are refused * until a human grants a path. */ allowedPaths?: string[]; /** The agent root. Always allowed (the workspace lives inside it). */ agentRoot?: string; } declare function isValidProjectId(id: string): boolean; /** * Resolve and authorize a project root. * * This is a privilege boundary: an external root hands the agent files outside its workspace, so a * confused or compromised agent must not be able to widen its own reach by creating a project * rooted at `/` or `~`. The root must already resolve inside an allowed path — we never edit * vole.config.json here, and the error names the exact path a human would have to grant. * * Symlinks are resolved before the containment check, so a link inside the workspace pointing at * /etc does not smuggle access. */ declare function validateProjectRoot(root: string, allowed: string[]): Promise; declare class ProjectStore { private readonly workspaceDir; private readonly allowed; constructor(workspaceDir: string, opts?: ProjectStoreOptions); init(): Promise; /** * Roots a project may point at: the agent directory plus `security.allowedPaths`. * * Exposed so everything that resolves an external path — creating a project, scanning a * candidate directory — authorizes against the same set. Passing the list separately let the * two drift, which meant a directory you could turn into a project might refuse to be scanned. */ get allowedRoots(): string[]; /** Absolute path of a project's own folder. */ dirFor(id: string): string; manifestPath(id: string): string; create(input: CreateProjectInput): Promise; exists(id: string): Promise; get(id: string): Promise; /** * Every project in the workspace. A directory without a readable manifest is simply not a * project — a corrupt or half-written manifest must never take down the listing. */ list(filter?: { status?: ProjectStatus | 'all'; }): Promise; update(id: string, patch: Partial>): Promise; /** Archive keeps every file — it only drops the project out of the default listing. */ archive(id: string): Promise; /** * The project's context documents — every markdown file at the top of its folder. * * `CONTEXT.md` used to be the only file with any standing, which made it arbitrary: a project * that wants CONVENTIONS.md and GLOSSARY.md alongside it had nowhere to put them. The folder * is the context now, and CONTEXT.md is merely the conventional first file rather than a * hardcoded one. * * They are *inlined into the system prompt*, which is the whole reason this is a push rather * than leaving the agent to read them: an overnight run that never got round to opening the * docs is the failure mode projects exist to prevent. So there is a total budget, and anything * past it comes back under `listed` — named in the prompt, for the agent to read on purpose. */ readContextFiles(id: string, only?: string[]): Promise<{ inlined: Array<{ name: string; body: string; }>; listed: string[]; }>; writeContext(id: string, body: string): Promise; private writeManifest; } /** * Task storage — `tasks.jsonl` inside each project directory. * * Append-only with last-line-wins per id, matching the house idiom (agent/event-log.ts, * paw-session, paw-recall). Every state change is a new line, so the file *is* the audit trail: * how long a task sat queued, what blocked it, how many times it was retried. No separate history * store, and a partially-written trailing line costs one update rather than the file. */ interface CreateTaskInput { projectId: string; goal: string; doneCriteria?: string[]; budget?: TaskBudget; priority?: number; } interface TaskFilter { projectId?: string; state?: TaskState | 'open' | 'all'; } declare class TaskStore { private readonly projects; constructor(projects: ProjectStore); private fileFor; create(input: CreateTaskInput): Promise; get(projectId: string, taskId: string): Promise; list(filter?: TaskFilter): Promise; /** * The scheduler's entry point: the highest-priority queued task, oldest first within a * priority. Only `active` projects are considered — pausing a project stops its work being * picked up without touching its tasks. */ next(projectId?: string): Promise; /** * Apply a patch. State changes are validated against TASK_TRANSITIONS — an illegal move * throws rather than silently landing, because the states carry meaning the review queue * depends on (a task must not reach `done` without passing through `verifying`). */ update(projectId: string, taskId: string, patch: Partial>): Promise; /** * Charge iterations against the task's budget, blocking when it runs out. * * Exhaustion is a *recoverable stop*, never a silent truncation: the human sees a blocked task * with the reason and can raise the budget and requeue it. */ chargeIterations(projectId: string, taskId: string, used: number): Promise<{ task: ProjectTask; exhausted: boolean; }>; /** * The lifecycle of every task in a project: which states it passed through and when. * * The data was always there — `append` writes a full record per change and `readAll` throws all * but the last away — so this is a second pass over the same file, not new bookkeeping. * * Only *state changes* become events. A record that merely charged iterations moved nothing a * reader cares about, and emitting one per write would bury the six moves that matter under * dozens of identical lines. * * Newest first, matching how the board reads: what happened most recently is what you came to * find out. */ history(projectId: string): Promise>; private append; /** Last line wins per id. A malformed line is skipped, not fatal. */ private readAll; /** Every record per id, in the order they were written. A malformed line is skipped, not fatal. */ private readRecords; } /** * Browsing and editing the files a project owns. * * A project has at most two places its files live: its own folder in the agent workspace * (`.openvole/workspace//` — CONTEXT.md, notes, anything the agent writes for itself) and its * `root`, when it is attached to files elsewhere such as a repo or a footage folder. This module is * the only path the dashboard uses to read or change either, so the boundary is enforced in one * place rather than at each call site. * * The boundary is *the project's own roots*, which is tighter than `security.allowedPaths`: a file * manager writes and deletes, so it should not be able to wander the whole grant. The root is * re-authorized against `allowedPaths` on every request rather than trusted from the manifest — * revoking a grant has to actually revoke it, even for a project created while it was still held. */ type FileRootKey = 'workspace' | 'root'; interface FileRoot { key: FileRootKey; /** What to call it in a UI — "Project folder" or the basename of the attached root. */ label: string; path: string; } interface FileEntry { name: string; kind: 'file' | 'dir'; size: number; modified: string; /** A symlink, resolved for its kind but flagged: following it can leave the project. */ link?: boolean; /** Readable but not writable — see RESERVED. */ reserved?: boolean; } interface FileListing { root: FileRootKey; /** Path relative to the root, `''` at the top. Always POSIX-separated for the UI. */ rel: string; /** Absolute path, so the UI can show where it actually is on disk. */ path: string; parent: string | null; entries: FileEntry[]; truncated: boolean; } interface FileContent { root: FileRootKey; rel: string; path: string; size: number; modified: string; /** Absent when `binary` or `tooLarge` — there is nothing sensible to put in an editor. */ content?: string; binary?: boolean; tooLarge?: boolean; } declare class ProjectFileError extends ProjectError { } declare class ProjectFiles { private readonly projects; constructor(projects: ProjectStore); /** * Where this project's files live — one entry, or two when it is attached to an external root. * * An external root that no longer resolves inside `allowedPaths` is simply absent, so a revoked * grant closes the browser rather than erroring on every click. */ roots(projectId: string): Promise; private rootPath; /** Absolute path for a project-relative path, authorized. Public so callers can display it. */ resolve(projectId: string, key: FileRootKey, rel: string): Promise; list(projectId: string, key: FileRootKey, rel?: string): Promise; read(projectId: string, key: FileRootKey, rel: string): Promise; write(projectId: string, key: FileRootKey, rel: string, content: string): Promise<{ path: string; rel: string; size: number; }>; /** * Create a new empty file, refusing one that is already there. * * Separate from `write` because "New file" with the name of an existing file would otherwise * truncate it — silently destroying content is the one thing a create button must never do. * `wx` does the check and the create in one syscall, so there is no window between them. */ create(projectId: string, key: FileRootKey, rel: string): Promise<{ path: string; rel: string; }>; /** * Pick a free path for an uploaded file, authorized and collision-free. * * An upload must never fail for a name clash and never overwrite what is already there, so a * taken name gets a " (n)" suffix — the same thing every file manager does when you drop a * second copy in. The ledger names are treated as taken for the same reason they are read-only. */ uploadTarget(projectId: string, key: FileRootKey, relDir: string, name: string): Promise<{ path: string; rel: string; }>; mkdir(projectId: string, key: FileRootKey, rel: string): Promise<{ path: string; }>; remove(projectId: string, key: FileRootKey, rel: string): Promise<{ path: string; kind: 'file' | 'dir'; }>; /** * Rename within the same root. `to` is a full project-relative path, so this also moves. */ rename(projectId: string, key: FileRootKey, rel: string, to: string): Promise<{ path: string; rel: string; }>; /** Resolve for a mutating call, refusing the ledger files. */ private assertWritable; } /** * Resolving a task's project scope into what the system prompt needs. * * Scope arrives *with the task* — set by the scheduler for a scoped schedule, by the dashboard for * a scoped chat, or by the agent when it picks up work — and never from ambient state. Tasks * interleave (a heartbeat can fire mid-chat), so a global "current project" would hand one task * another's context; that is the bug shape that once filed brain replies under the wrong session. */ /** Task metadata keys that carry project scope. */ interface ProjectScope { projectId?: unknown; projectTaskId?: unknown; } /** * Build the prompt-facing view of a task's project, or null when the task has no project — in * which case the prompt is byte-identical to the pre-projects behaviour. * * Never throws for missing data: an unknown project id or a deleted work item degrades to running * unscoped, because losing project context must not lose the task. */ /** One line per project for the prompt's roster. */ interface ProjectRosterEntry { id: string; name: string; kind: string; openTasks: number; } /** * Every active project with its open-task count. * * Without this an agent only learns its projects exist by calling project_list, which it has no * reason to do mid-conversation — so "carry on with the openvole work" would find an agent that * cannot see the openvole project. Cheap enough to build per task: a directory read plus one file * per project. */ declare function listProjectRoster(projects: ProjectStore, tasks: TaskStore): Promise; interface ResolveOptions { /** * When the scope names a project but no specific work item, pull the project's highest-priority * queued task. This is what closes the scheduler loop — a scoped schedule fires and the agent * arrives already knowing the goal and its done-criteria, instead of waking with no objective. * * Only for self-initiated runs (schedule, heartbeat). A chat turn must NOT pull work: the * human's message is the instruction, and attaching an unrelated task's done-criteria to it * would have the agent answer against the wrong bar. * * Selection does not claim the task — no state transition happens from a timer, so a crashed * run can never strand a task in `running`. The agent moves it with the task tools. */ autoSelectTask?: boolean; } declare function resolveProjectContext(projects: ProjectStore, tasks: TaskStore, scope: ProjectScope | undefined, opts?: ResolveOptions): Promise; /** * Combine a project's tool profile with whatever restriction the task already carries. * * Narrowing only, in both directions: denies union (anything either side forbids stays * forbidden) and allows intersect (a tool must clear both lists). A project therefore cannot * hand its agent a capability the agent did not already have — which is what makes it safe for * the *agent* to write project manifests. Sub-agent profiles keep their restrictions too. */ declare function narrowToolAccess(current: { allow?: string[]; deny?: string[]; }, project: { allow?: string[]; deny?: string[]; } | undefined): { allow?: string[]; deny?: string[]; }; interface VaultEntry { value: string; source: 'user' | 'tool' | 'brain'; createdAt: number; /** Optional metadata — context about the stored value (service, handle, url, etc.) */ meta?: Record; } declare class Vault { private entries; private vaultPath; private encryptionKey?; constructor(vaultPath: string, encryptionKey?: string); init(): Promise; store(key: string, value: string, source?: string, meta?: Record): Promise; get(key: string): Promise; list(): Promise; }>>; delete(key: string): Promise; private save; private encrypt; private decrypt; } /** * Hook phase definitions. * * The actual hook execution logic lives in PawRegistry (perceive/observe hooks) * and the agent loop (think/act orchestration). This module defines the hook * lifecycle constants and types used across the system. */ /** The four phases of the agent loop */ type LoopPhase = 'perceive' | 'think' | 'act' | 'observe'; /** Phase ordering for logging and tracing */ declare const PHASE_ORDER: readonly LoopPhase[]; /** Resolve a Paw package path from its name */ declare function resolvePawPath(name: string, projectRoot: string): string; /** Read and validate a vole-paw.json manifest */ declare function readPawManifest(pawPath: string): Promise; /** * Compute effective permissions as the intersection of * what the manifest requests and what the config grants. */ declare function computeEffectivePermissions(manifest: PawManifest, config: PawConfig): EffectivePermissions; /** * Validate that a Paw's manifest permissions are reasonable. * Returns warnings (non-blocking) for review. */ declare function validatePermissions(manifest: PawManifest, config: PawConfig): string[]; /** * Resolve Skill activation based on the current tool registry * and runtime requirements (env vars, binaries). * * A Skill is active if: * - All requiredTools are registered in the tool registry * - All requires.env vars are set in the environment * - All requires.bins are available on PATH * - At least one of requires.anyBins is available (if specified) */ declare function resolveSkills(skills: SkillInstance[], toolRegistry: ToolRegistry): void; /** Build ActiveSkill entries for the AgentContext */ declare function buildActiveSkills(skills: SkillInstance[], toolRegistry: ToolRegistry): ActiveSkill[]; /** Default TTY I/O implementation using stdin/stdout */ declare function createTtyIO(): VoleIO; interface VoleEngine { bus: ReturnType; toolRegistry: ToolRegistry; pawRegistry: PawRegistry; skillRegistry: SkillRegistry; taskQueue: TaskQueue; scheduler: SchedulerStore; io: VoleIO; config: VoleConfig; /** Projects in this agent's workspace. */ projects: ProjectStore; /** Durable work items inside those projects (distinct from the in-memory TaskQueue). */ projectTasks: TaskStore; /** Start the engine — load Paws and Skills */ start(): Promise; /** Submit a task for execution. Returns the task id. */ run(input: string, source?: 'user' | 'schedule' | 'heartbeat' | 'paw' | 'agent', sessionId?: string, /** Task metadata — notably `projectId`/`projectTaskId` to scope the run to a project. */ metadata?: Record): string; /** Graceful shutdown */ shutdown(): Promise; } /** Create and initialize the OpenVole engine */ /** * Turn `heartbeat.intervalMinutes` into a valid cron expression. * * A naive minutes-step pattern breaks the moment the interval reaches 60: the minutes field * only accepts steps up to 59, croner throws, and the engine dies at startup — so an hourly * (60) or daily (1440) heartbeat used to brick the agent. Map onto the right field instead: * under an hour steps minutes; an hour or more steps hours; a day or more runs once daily * (cron cannot express "every n days"). */ declare function heartbeatCronFor(intervalMinutes: number): string; declare function createEngine(projectRoot: string, options?: { io?: VoleIO; configPath?: string; headless?: boolean; }): Promise; export { type ActionError, type ActionErrorCode, type ActionResult, type ActiveSkill, type AgentContext, type AgentEntry, AgentManager, type AgentMessage, type AgentPlan, type AgentProfile, type AgentRegistry, type AgentRunState, type AgentStatus, type AgentTask, type BootstrapHook, type BusEvents, CONTEXT_NAME, type ChannelInfo, type CompactHook, ContextBudgetManager, type CostEntry, CostTracker, type DockerSandboxConfig$1 as DockerSandboxConfig, DockerSandboxManager, type EffectivePermissions, type FileContent, type FileEntry, type FileListing, type FileRoot, type FileRootKey, type HeartbeatConfig, type LoopConfig, type LoopDependencies, type LoopPhase, MANIFEST_NAME, type MessageBus, type ObserveHook, PHASE_ORDER, type PawConfig, type PawDefinition, type PawInstance, type PawManifest, PawRegistry, type PerceiveHook, type PlannedAction, type ProjectContextInfo, ProjectError, ProjectFileError, ProjectFiles, type ProjectKind, type ProjectManifest, ProjectRootError, type ProjectStatus, ProjectStore, type ProjectTask, RESERVED_BASENAMES, RateLimiter, type RateLimits, type ScheduleHook, SchedulerStore, type SkillDefinition, type SkillInstance, SkillRegistry, type AgentEntry as SpaceEntry, AgentManager as SpaceManager, type AgentRegistry as SpaceRegistry, type AgentRunState as SpaceRunState, type AgentStatus as SpaceStatus, type SystemPromptContent, TASKS_NAME, TASK_TRANSITIONS, TERMINAL_TASK_STATES, type TaskBudget, type TaskCostSummary, type TaskPriority, TaskQueue, type TaskState, type TaskStatus, TaskStore, type TokenBudget, type ToolDefinition, ToolRegistry, type ToolRegistryEntry, type ToolSummary, type TransportType, Vault, type VaultEntry, type VoleConfig, type VoleEngine, VoleHubClient, type VoleHubIndex, type VoleHubSkill, type VoleIO, buildActiveSkills, buildSystemPrompt, channelIdFor, computeEffectivePermissions, createActionError, createAgentContext, createEngine, createMessageBus, createTtyIO, defineConfig, failureResult, heartbeatCronFor, isValidProjectId, listChannels, listProjectRoster, loadConfig, loadSystemPromptContent, narrowToolAccess, pickSendTool, readPawManifest, resolvePawPath, resolveProjectContext, resolveSkills, runAgentLoop, successResult, validatePermissions, validateProjectRoot };