@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
254 lines • 10.5 kB
TypeScript
import { type ElicitationRequest, type HumanReply } from '../elicitation.js';
import type { AgentStore, CostSource } from '../spi/agent-store.js';
import type { ApprovalRequirement } from '../spi/approval-policy.js';
import type { SinkWriter } from '../spi/token-stream-sink.js';
import type { AgentUiComponent } from '../stream-events.js';
import { type ToolPresentation } from '../tool-presentation.js';
import type { AgentRunInput, MessageUsage } from '../types.js';
import type { OpenCodeClient, OpenCodeEvent, OpenCodeForm, OpenCodeTokens } from './client.js';
/** How an OpenCode execution ended. */
export type TurnOutcome = {
status: 'succeeded';
} | {
status: 'failed';
error: string;
} | {
status: 'interrupted';
};
/**
* Something the turn put to a person, with everything needed to hand the answer back to OpenCode
* later — serializable, so a durable run journals it and replies from another process.
*/
export type PendingAsk = {
kind: 'approval';
/** The OpenCode permission request id — also the tool-call id the decision is signalled under. */
id: string;
action: string;
approver: string;
expiresAt?: string;
/** The assistant message the call hangs on (absent for a request recovered after a restart). */
messageId?: string;
} | {
kind: 'form';
id: string;
form: OpenCodeForm;
request: ElicitationRequest;
messageId?: string;
};
/**
* What a turn spent. `inputTokens` is the whole input side, cached input included (as the library's
* `MessageUsage` counts it — OpenCode's own `tokens.input` leaves the cache out); `cacheReadTokens`,
* `cacheWriteTokens` and `reasoningTokens` are subsets. `costUsd` sums every priced call; `steps`
* counts the model steps.
*/
export interface OpenCodeUsage {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
reasoningTokens: number;
costUsd: number;
steps: number;
}
export declare function emptyUsage(): OpenCodeUsage;
/** `a + b`; a missing side (a milestone journaled before usage travelled) counts as nothing. */
export declare function addUsage(a: OpenCodeUsage | undefined, b: Partial<OpenCodeUsage> | undefined): OpenCodeUsage;
/** The model a step ran on, as OpenCode names it (`session.step.started`). */
export interface StepModel {
providerID: string;
id: string;
}
/** What a model call cost, and where the figure came from. */
export interface StepCost {
costUsd?: number;
costSource?: CostSource;
}
/**
* Where a turn stands: it needs a person, or it is over. `usage` is what the turn spent since its
* previous milestone — journaled with the milestone, so a run resumed in another process still adds
* up what every process saw.
*/
export type Milestone = {
kind: 'ask';
ask: PendingAsk;
timeoutMs?: number;
usage?: OpenCodeUsage;
} | {
kind: 'finished';
outcome: TurnOutcome;
usage?: OpenCodeUsage;
};
export interface OpenCodeTurnArgs {
runId: string;
input: AgentRunInput;
client: OpenCodeClient;
sessionId: string;
writer: SinkWriter;
store: AgentStore;
/** What an action needs before it runs (the agent's `ApprovalPolicy`). */
approvalFor: (action: string) => Promise<ApprovalRequirement>;
/**
* The app tool a permission `action` names, with its presentation: its `confirm` templates (filled
* from the call's arguments) word the approval card. `undefined` (or omitted) → an OpenCode
* built-in, worded from its name.
*/
approvalPresentation?: (action: string) => {
name: string;
presentation?: ToolPresentation;
} | undefined;
/** The model label usage is recorded under until OpenCode names the step's model. */
modelLabel: string;
/**
* What a model call cost: OpenCode's reported figure, or the library's estimate. Omit → OpenCode's
* figure as reported.
*/
priceStep?: (step: {
model: StepModel | undefined;
usage: MessageUsage;
reportedCostUsd: number | undefined;
}) => Promise<StepCost>;
/** What the run spent before this turn object existed (an earlier process, an earlier session). */
carried?: OpenCodeUsage;
/**
* An action OpenCode was allowed to run — by a person, a remembered approval or the policy. The
* engine's MCP endpoint runs an `action` tool only against one of these.
*/
onGranted?: (action: string, permissionId: string) => void;
}
/**
* One turn of an OpenCode session, as the library's stream and store see it. Fed the session's
* events in order, it writes the protocol's frames and persists what the loop would have — the
* assistant messages with their tool calls and results, the calls put to a person (so the
* approve/answer routes find their run), usage per model step — and reports {@link Milestone}s: a
* person was asked something, or the execution ended.
*
* The turn does not wait for people itself. Whoever drives it (`OpenCodeTurns`, under an in-memory
* or a durable runner) takes the `ask` milestone, waits however it waits, and hands the answer back
* with {@link decide} — which is what lets a durable run wait across a restart and reply from
* another process, with a fresh turn object fed the journaled {@link PendingAsk}.
*/
export declare class OpenCodeTurn {
private readonly a;
/** Serializes the handling of events, so frames and rows land in the order OpenCode sent them. */
private chain;
private segment;
private readonly tools;
/** Results already persisted per message, so a late settlement adds to them rather than replacing. */
private readonly messageResults;
private readonly asked;
/** Calls of {@link HIDDEN_TOOLS}, by id. */
private readonly hidden;
private readonly milestones;
private waiting;
/** What this turn object saw spent, and how much of it a milestone already reported. */
private spent;
private reported;
private priced;
private model;
/** What the session had spent when this turn prompted it; what OpenCode reported since. */
private baseline;
private readonly seen;
private compacted;
private stepOpen;
private sawText;
private separator;
private reasoningSince;
private wroteMessage;
private ended;
private stepError;
constructor(a: OpenCodeTurnArgs);
/**
* What the session had spent before this turn prompted it (`session.get`). When the execution
* ends, what the session spent since that this turn's events did not report — the title OpenCode
* generates, a compaction: OpenCode records those calls on the session without streaming them — is
* recorded as a usage row of its own.
*/
setBaseline(info: {
cost?: number;
tokens?: OpenCodeTokens;
} | undefined): void;
/** What the run spent so far: carried from before this turn object, plus what it saw. */
totalUsage(): OpenCodeUsage;
get sessionId(): string;
get finished(): boolean;
/** The run's totals so far (see {@link totalUsage}): tokens, cost, model steps. */
totals(): {
inputTokens: number;
outputTokens: number;
costUsd?: number;
steps: number;
};
private isPriced;
/** Feed one of the session's events. */
handle(event: OpenCodeEvent): void;
/** End the turn as failed without an event (the prompt was refused, the stream is gone). */
fail(error: string): void;
/**
* A component a tool pushed (`ctx.emitUi`, through the engine's MCP endpoint): streamed as a `ui`
* frame and persisted on the message being written, in the order it arrived. Pushing the same id
* again replaces it.
*/
pushUi(component: AgentUiComponent): Promise<void>;
/** The next milestone: one already reached, or the next one to come. */
next(): Promise<Milestone>;
/** Whether a milestone is waiting to be taken. */
hasMilestone(): boolean;
/**
* Requests OpenCode raised while nobody listened (this process started after them, or the event
* stream dropped): its open permissions and forms, handled as if their events had just arrived.
* One already recorded in the store is reported without being recorded or streamed again.
*/
catchUp(): Promise<void>;
/**
* Hand a person's answer to what the turn asked, and let OpenCode go on. `tellOpenCode: false`
* only settles the call (stream and store): the request died with a restarted server, and the
* new session is told the answer in its prompt instead.
*/
decide(ask: PendingAsk, reply: HumanReply, opts?: {
tellOpenCode?: boolean;
}): Promise<void>;
private reach;
private enqueue;
private write;
private event;
private step;
private endReasoning;
private tool;
private announce;
private makeAvailable;
private setInput;
/** Code-mode calls made inside a tool (`metadata.toolCalls`), nested under it by `parentId`. */
private innerCalls;
private onEvent;
private settleTool;
private modelId;
/** One model call's usage and cost, into the ledger and the run's total. */
private spend;
private stepEnded;
/** The run's usage as its last message carries it: the whole run, cache tokens included. */
private messageUsage;
/**
* Persist what the turn produced since the last flush as one assistant message — the message a
* call put to a person hangs on, or the turn's last one. Returns its id.
*/
private flush;
/** A request recorded by an earlier process (a turn resumed after a restart). */
private alreadyRecorded;
/** The persisted assistant message carrying call `id`, so its result lands on the same message. */
private messageOf;
private onPermission;
private decided;
private onForm;
private answered;
private addResult;
/** What the session spent since the baseline that no event of this turn reported. */
private unreported;
private end;
}
/**
* OpenCode's token counts as the library's `MessageUsage`. OpenCode reports the uncached input in
* `input` and the cache reads and writes beside it; the library counts the whole input side in
* `inputTokens`, the cache counts being subsets of it.
*/
export declare function usageOf(tokens: any): MessageUsage;
//# sourceMappingURL=turn.d.ts.map