UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

300 lines • 16.3 kB
import type { AgentDeps } from '../agent-deps.js'; import type { AgentDepsFactory } from '../agent-deps-factory.js'; import { type ChatQueueService } from '../chat-queue-service.js'; import type { HumanReply } from '../elicitation.js'; import type { PriceCatalogOptions } from '../pricing/boot-pricing.js'; import type { AgentStore } from '../spi/agent-store.js'; import type { AgentPricingStore } from '../spi/pricing-store.js'; import type { TokenStreamSink } from '../spi/token-stream-sink.js'; import type { AiToolCtx } from '../spi/tool.js'; import type { AgentUiComponent } from '../stream-events.js'; import type { ToolPresentation } from '../tool-presentation.js'; import type { ToolRegistry } from '../tool-registry.js'; import type { Actor, AgentRunInput, MessageUsage } from '../types.js'; import type { OpenCodeHost, OpenCodeSessionStore } from './host.js'; import { type Milestone, type OpenCodeUsage, type PendingAsk, type StepCost, type StepModel, type TurnOutcome } from './turn.js'; /** * The app's tools, served to OpenCode sessions over the engine's own MCP endpoint * (`POST <agent path>/opencode/mcp`, mounted by the engine). */ export interface OpenCodeToolsOptions { /** * The URL the OpenCode server calls that endpoint at, passed to it verbatim (no default). It is * resolved from where OpenCode runs, not from the app: e.g. `http://127.0.0.1:3333/agent/opencode/mcp` * on the same machine, the app's Compose service or Kubernetes Service name otherwise. * `/agent` is the agent's route prefix (`path`). */ url: string; /** The MCP server's name in OpenCode; its tools are `<server>.<tool>` / `<server>_<tool>`. Default `'agora'`. */ server?: string; /** * The secret the endpoint's bearer tokens are signed with. Default: derived from the app's * `APP_KEY`; without one, a random per-process secret (which only works with one process). */ secret?: string; /** * How long a token OpenCode is given stays valid. A token only ever reaches a turn of its own * actor that is running on the session that calls, and it is re-issued on the turns of a session * once it is half-way through. Default 7 days. */ ttlMs?: number; } export interface OpenCodeEngineSettings { /** The id a run gets (e.g. a tenant prefix your durable store partitions by). Default: a random UUID. */ runId?: (input: AgentRunInput) => string; /** * `openCodeDurable()` only: how each turn's workflow run starts — tags, search attributes, a * concurrency quota (`WorkflowEngine.start` options) — and what a refused start becomes (e.g. a * concurrency limit → a 429 for the person sending). */ durable?: { start?: (input: AgentRunInput, runId: string) => Record<string, unknown> | Promise<Record<string, unknown>>; startError?: (error: unknown, input: AgentRunInput) => unknown; }; /** How many earlier messages a NEW session is told about (a thread whose session was lost). Default 20. */ historyMessages?: number; /** The instructions key the agent's own prompt is put under. Default `'agora.system'`. */ systemKey?: string; /** Serve the app's tools to the sessions. Omit → the session has the host's tools only. */ tools?: OpenCodeToolsOptions; /** Where skills are written, relative to the session's directory. Default `'.opencode/skills'`. */ skillsDir?: string; /** * How long `observe` waits for a milestone before failing the turn. Default 30 minutes. A turn * parked on a person is not observing: this bounds one stretch of OpenCode working on its own. */ turnTimeoutMs?: number; /** * Whose figure a model call is recorded at. OpenCode prices each call itself, off its model * catalog (the `cost` its config declares, else models.dev's list price): * * - `'reported'` (default) — OpenCode's figure when it has one (`cost_source` `'provider'`); a call * OpenCode priced at 0 (no price for the model) is estimated from the library's price instead * (`priceCatalog`, the pricing store, the built-in GovCloud Bedrock table, models.dev; * `cost_source` `'estimate'`). * - `'estimate'` — the library's price whenever it has one, OpenCode's figure otherwise. For * models OpenCode prices at a list price that is not yours (Bedrock in GovCloud, a negotiated * rate) when you would rather not declare `cost` in OpenCode's config. */ cost?: 'reported' | 'estimate'; } /** A thread's session, as the steps of a turn pass it along (and a durable run journals it). */ export interface SessionHandle { sessionId: string; serverKey: string; bootId?: string; directory?: string; } /** What the engine's MCP endpoint tells the turns about the call it is serving. */ export interface OpenCodeToolCall { actor: Actor; /** The OpenCode server the endpoint's token was issued for. */ serverKey: string; /** The MCP request id, so two calls' unnamed components stay apart. */ requestId: string; /** The call's `_meta` — OpenCode names its session there (`ai.opencode/sessionID`). */ meta: Readonly<Record<string, unknown>> | undefined; } /** The turn a tool call over MCP belongs to — see {@link OpenCodeTurns.callContext}. */ export interface OpenCodeCallContext { input: AgentRunInput; runId: string; ctx: Pick<AiToolCtx, 'threadId' | 'runId' | 'emitUi'> & { agentName?: string; }; } /** The context the engine's runners and endpoint are built from. */ export interface OpenCodeTurnsDeps { host: OpenCodeHost; sessions: OpenCodeSessionStore; settings: OpenCodeEngineSettings; factory: AgentDepsFactory; store: AgentStore; sink: TokenStreamSink; registry: ToolRegistry; queue?: ChatQueueService; /** Mints the bearer token a session's tools endpoint is registered with (`settings.tools`). */ mintToolsToken?: (actor: Actor, serverKey: string) => string; /** Where a call OpenCode did not price gets its price from (`pricingStore`, `priceCatalog`). */ pricingStore?: AgentPricingStore; priceCatalog?: PriceCatalogOptions | false; } /** * The steps of an OpenCode turn — `begin` (thread, session, instructions), `prompt`, `observe` * (until a person is asked something or the execution ends), `reply`, `settle` — so an in-memory * runner can run them in a row and a durable one can checkpoint each. A step may run in a process * that did not run the one before (a durable run resumed elsewhere): the live turn is rebuilt from * the session, catching up on what OpenCode asked while nobody listened. */ export declare class OpenCodeTurns { private readonly events; private readonly live; /** Actions approved per run, and the approvals the MCP endpoint already spent. */ private readonly grants; private readonly spent; /** When the tools endpoint was last registered for a location (`server|boot|directory`). */ private readonly toolsIssued; readonly host: OpenCodeHost; private readonly sessions; readonly settings: OpenCodeEngineSettings; private readonly factory; private readonly store; private readonly sink; readonly registry: ToolRegistry; private readonly queue; private readonly mintToolsToken; /** Starts the next queued message of the thread; set by the runner (it owns `start`). */ startNext: ((next: AgentRunInput, runId: string) => Promise<unknown>) | undefined; constructor(deps: OpenCodeTurnsDeps); private readonly prices; /** * What a model call OpenCode made cost, by the same rule the library's loop records its own steps * at: a reported figure wins, else an estimate off the model's price — see * {@link OpenCodeEngineSettings.cost}. */ priceStep(step: { model: StepModel | undefined; usage: MessageUsage; reportedCostUsd: number | undefined; }): Promise<StepCost>; /** The tools server's name in OpenCode. */ get toolsServer(): string; /** The app tool an OpenCode permission `action` names, and its presentation — `undefined` for a built-in. */ presentationOf(action: string): { name: string; presentation?: ToolPresentation; } | undefined; /** The agent's deps (prompt, approval policy, allow-list, skills, memory) for a turn. */ depsFor(input: Pick<AgentRunInput, 'agentName'>): AgentDeps; /** The tools a turn may reach: the agent's allow-list, narrowed by its persona's. */ allowedTools(input: Pick<AgentRunInput, 'agentName' | 'persona'>): string[] | undefined; /** Whether the memory provider writes, so the endpoint serves `remember`. */ memoryWritable(input: Pick<AgentRunInput, 'agentName'>): boolean; /** The runs this process is following now (an updater drains on it). */ liveRuns(): string[]; /** Stop following every run, and close the event streams. */ close(): void; /** * The turn a tool call over the engine's MCP endpoint belongs to, or `undefined` when it belongs * to none — which the endpoint refuses: it exists only to serve turns. OpenCode names its session in * every call's `_meta` (`ai.opencode/sessionID`); the call is that session's running turn's, and * only when the endpoint's caller is the person that turn runs for, on the server the token was * issued for. A session this process is not following is found through OpenCode (`session.get` → * the thread it was created for → the thread's running turn); its components still reach the * stream, as a message of their own. */ callContext(call: OpenCodeToolCall): Promise<OpenCodeCallContext | undefined>; /** * Spend one approval of `tool` in `runId` — what the MCP endpoint asks before it runs an `action`. * OpenCode asks the person (or the policy answers) before it calls an action tool; the endpoint * runs it only against an approval this engine saw granted, so a caller that skipped OpenCode's * permission rules (anything that got hold of the token) cannot run an action unapproved. The * approval is the run's own: live in this process, else read off the run's persisted calls. */ spendApproval(runId: string, threadId: string, tool: string): Promise<boolean>; /** * Push a component into the run an OpenCode session is serving, for the host's own trusted * callers (no caller check — the MCP endpoint checks its own). `false` when no run is live on * that session in this process. */ pushToSession(sessionId: string, component: AgentUiComponent): Promise<boolean>; private uiPushed; private threadOfSession; private activeRun; /** * Open the run's row, persist the user message (or rewind the thread for a regenerate), find or * create the thread's session, and refresh what the session is told this turn. */ begin(runId: string, input: AgentRunInput, durable?: boolean): Promise<SessionHandle>; private createSession; /** The user message, as the loop persists it (or, on a regenerate, the thread rewound to it). */ private prepareThread; /** Rewind the session to before its last user message, so the regenerated answer replaces it. */ private revertLastExchange; /** The agent's prompt, the memory block and the host's entries — refreshed every turn. */ private refreshInstructions; /** * The agent's prompt as the loop resolves it: a persona's prompt stands in for the base (a * builder is handed the base as `basePrompt`), else the agent's `systemPrompt`. */ private systemPrompt; private scopeContext; /** What is on file about the actor (`memory` in the config); writable when `remember` is served. */ private memoryBlock; /** * The agent's skills (`skills` in the config) as `SKILL.md` files under the session's directory, * where OpenCode's own `skill` tool finds them. Returns the names written. */ private writeSkills; /** * Permission rules for the app's tools: the MCP server allowed as a whole (OpenCode offers a * server's tools only when the server itself is allowed), then each `action` tool asked — the last * matching rule wins, so an action still lands on an approval card. Tools outside the agent's * allow-list, and the kinds only the loop serves, are denied. */ private toolRules; /** * Register the tools endpoint at the session's location, with a bearer token for the turn's * actor. On a new session always; on a kept one when this process has not registered it yet or * its token is half-way through its life. */ private addTools; /** Start listening to the session (so nothing it says is missed), then send the user message. */ prompt(runId: string, input: AgentRunInput, handle: SessionHandle, spent?: OpenCodeUsage): Promise<void>; /** * Wait for the turn's next milestone. In a process that was not following the turn (a resumed * durable run), the turn is rebuilt and first catches up on requests OpenCode raised meanwhile; * `session.wait` is the safety net for a terminal event that was missed. * * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn * rebuilt here starts from it, so the answer it persists carries the whole run's usage. */ observe(runId: string, input: AgentRunInput, handle: SessionHandle, spent?: OpenCodeUsage): Promise<Milestone>; /** * Hand a person's answer back to OpenCode. When the server restarted while the turn waited, the * request is gone with it: a new session is opened, told what the person decided, and prompted * to go on. */ reply(runId: string, input: AgentRunInput, handle: SessionHandle, ask: PendingAsk, reply: HumanReply, spent?: OpenCodeUsage): Promise<SessionHandle>; private continueAfterRestart; /** What a session spent so far (`session.get`), or nothing when OpenCode does not say. */ private sessionUsage; /** Interrupt the thread's session (cancel). */ interrupt(input: AgentRunInput): Promise<void>; private ensureLive; /** Stop following a run in this process. */ drop(runId: string): void; /** A person was asked something: tell the host (cards in other channels, wake-ups). */ private reached; /** * What the run produced, as the host's settle hooks see it. `usage` is what the run spent — the * runner's sum of its milestones' usage, which a resumed run reads back from its journal — else * what the turn this process follows saw. */ private runResult; /** * The host's last word on the answer before the stream ends: components to append (a guardrail * notice), or, for a failed run, the error the person reads instead of OpenCode's. */ private amend; /** Delivery, telemetry, spend: after the run settled. Errors are the host's, never the run's. */ private settled; private forget; /** * What the run spent. The turn this process follows starts from the runner's sum of the earlier * milestones and adds every call it saw since, so it is never behind that sum — and ahead of it for * a run that failed or was cancelled between two milestones. Without one, the runner's sum. */ private spentBy; /** * End the run's stream and bookkeeping for how its execution ended. `spent` is what the run spent * (the sum of its milestones' `usage`); omit → what the turn this process follows saw. */ settle(runId: string, input: AgentRunInput, outcome: TurnOutcome, durationMs: number, spent?: OpenCodeUsage): Promise<void>; settleFailed(runId: string, input: AgentRunInput, error: string, durationMs?: number, spent?: OpenCodeUsage): Promise<void>; /** As `InlineAgentRunner`: hand the thread to its next queued message, and say so on the stream. */ private handoff; } /** The `_meta` key OpenCode names its session with on every MCP call. */ export declare const SESSION_META_KEY = "ai.opencode/sessionID"; //# sourceMappingURL=turns.d.ts.map