@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
179 lines • 7.77 kB
TypeScript
import type { ModelProvider } from '../../spi/model-provider.js';
/**
* The structured side of every tree-LLM request: what is being asked, as data. The prompt says the
* same thing in prose; `task` exists so a deterministic fake (tests, offline dev — see
* {@link import('./keyword-tree-llm.js').keywordTreeLlm}) can answer without parsing prose, and so a
* host can log or route requests by kind.
*/
export type TreeLlmTask = {
kind: 'structure';
/** The window of pages the LLM is asked to find section starts in. */
units: {
unit: number;
page?: number;
text: string;
}[];
} | {
kind: 'summarize';
title: string;
text: string;
} | {
kind: 'describe';
title?: string;
outline: {
title: string;
summary?: string;
}[];
} | {
kind: 'navigate';
query: string;
/** `single-pass`: the whole tree is shown; `beam`: one level of candidates at a time. */
mode: 'single-pass' | 'beam';
candidates: TreeLlmCandidate[];
/** Max nodes the answer may name. */
maxNodes: number;
} | {
kind: 'select';
query: string;
documents: {
id: string;
title?: string;
description?: string;
}[];
maxDocuments: number;
};
/** A node as shown to the navigating LLM. */
export interface TreeLlmCandidate {
id: string;
title: string;
summary?: string;
pages?: string;
/** Present on beam candidates: whether the node can be expanded. */
hasChildren?: boolean;
/** Present in single-pass mode: depth (0 = top level). */
depth?: number;
}
export interface TreeLlmRequest {
task: TreeLlmTask;
system: string;
prompt: string;
/** A cap for the reply. Adapters pass it as `max_tokens` when they can. */
maxOutputTokens: number;
/** Aborted when the call's timeout or the build/navigation deadline passes. */
signal: AbortSignal;
}
export interface TreeLlmResponse {
text: string;
/** Provider-reported usage. When absent, the budget estimates ~4 characters per token. */
usage?: {
inputTokens?: number;
outputTokens?: number;
};
}
/**
* The only LLM surface tree building and navigation need: one prompt in, one text reply out. Replies
* are expected to be JSON (for structure / navigate / select) or plain text (summaries); parsing is
* lenient (code fences, prose around the object). Bring any model:
* {@link treeLlmFromModelProvider} (the agent's own `ModelProvider`), {@link openAiChatTreeLlm}
* (any OpenAI-compatible `/chat/completions`), or a function of your own. Use a deterministic
* setting (temperature 0) — trees are cached by content, so a flaky model makes rebuilds noisy.
*/
export type TreeLlm = (request: TreeLlmRequest) => Promise<TreeLlmResponse>;
/**
* Hard limits for one document build or one navigation. Every LLM call is checked against them
* **before** it is made (with the prompt's estimated size), so a budget is never overshot by more
* than one reply, and nothing retries past it: the failure mode this exists for is an indexer that
* burned ~1,240 calls on one PDF it could not structure.
*/
export interface TreeBudget {
/** Max LLM calls. */
maxCalls?: number;
/** Max input (prompt) tokens across all calls, estimated at ~4 chars/token before each call. */
maxInputTokens?: number;
/** Max output tokens across all calls. */
maxOutputTokens?: number;
/** Wall-clock deadline for the whole operation, ms. */
timeoutMs?: number;
/** Per-call timeout, ms. Default: whatever is left of `timeoutMs`. */
callTimeoutMs?: number;
}
/** Thrown internally when a budget or deadline would be exceeded; callers turn it into a fallback. */
export declare class TreeBudgetExceededError extends Error {
readonly limit: 'calls' | 'inputTokens' | 'outputTokens' | 'timeout';
constructor(limit: 'calls' | 'inputTokens' | 'outputTokens' | 'timeout');
}
/** ~4 characters per token: the budget's estimate when a provider reports no usage. */
export declare function estimateTokens(text: string): number;
/** Tracks spend against a {@link TreeBudget} and runs calls under it. Not exported from the package. */
export declare class BudgetedLlm {
private readonly llm;
private readonly budget;
private readonly outerSignal?;
calls: number;
inputTokens: number;
outputTokens: number;
exhausted: boolean;
private readonly deadline;
constructor(llm: TreeLlm, budget: Required<Pick<TreeBudget, 'maxCalls' | 'maxInputTokens' | 'maxOutputTokens'>> & TreeBudget, outerSignal?: AbortSignal | undefined);
/** Time left before the deadline, ms (`Infinity` without one). */
remainingMs(): number;
/** Would a call with this prompt fit? Does not reserve anything; a `false` marks the budget exhausted. */
fits(prompt: string, maxOutputTokens: number): boolean;
private limitHit;
/**
* Make one call, or throw {@link TreeBudgetExceededError} without calling. Provider errors
* propagate (after being counted). Never retries.
*/
call(task: TreeLlmTask, system: string, prompt: string, maxOutputTokens: number): Promise<string>;
}
/**
* Pull the JSON object out of a model reply: bare, fenced in ```json, or surrounded by prose.
* Returns `undefined` when there is none (never throws).
*/
export declare function parseJsonReply(text: string): Record<string, unknown> | undefined;
/** The strings in a JSON array value (numbers stringified); anything else yields `[]`. */
export declare function stringList(value: unknown): string[];
/**
* A {@link TreeLlm} over the agent's own {@link ModelProvider}: one `runTurn` with no tools and a
* discarding sink. Usage comes from the provider's report.
*/
export declare function treeLlmFromModelProvider(provider: ModelProvider): TreeLlm;
export interface OpenAiChatTreeLlmOptions {
model: string;
/** Base URL up to and including `/v1`. Default `https://api.openai.com/v1`. */
baseUrl?: string;
apiKey?: string;
headers?: Record<string, string>;
/** Default 0 — trees are cached by content, determinism keeps rebuilds quiet. */
temperature?: number;
/**
* Send `response_format: { type: 'json_object' }` on JSON tasks. Default `false`: not every
* OpenAI-compatible server accepts it, and replies are parsed leniently anyway.
*/
jsonMode?: boolean;
/** Extra body fields (a reasoning switch, provider routing). */
body?: Record<string, unknown>;
fetch?: typeof fetch;
}
/**
* A {@link TreeLlm} over any OpenAI-compatible `POST /v1/chat/completions` — OpenAI, a gateway,
* vLLM, Ollama, DeepSeek, Qwen… — with no SDK dependency. Throws `HttpModelError` on a non-2xx.
*/
export declare function openAiChatTreeLlm(options: OpenAiChatTreeLlmOptions): TreeLlm;
/** A get/set cache — a `Map`, an LRU, a Redis wrapper. Values are whole replies. */
export interface TreeLlmCache {
get(key: string): TreeLlmResponse | undefined | Promise<TreeLlmResponse | undefined>;
set(key: string, value: TreeLlmResponse): unknown;
}
/**
* Memoize a {@link TreeLlm} by the exact request (task kind, system prompt, prompt, output cap).
* With a deterministic model that makes a rebuild of an unchanged section, or a repeated question,
* free. Cached replies report zero usage, so they cost nothing against a budget but a call slot.
*/
export declare function cachedTreeLlm(llm: TreeLlm, cache?: TreeLlmCache): TreeLlm;
/**
* A fast, deterministic, non-cryptographic 64-bit hash (two independent 32-bit FNV-1a/murmur-mixed
* lanes) as 16 hex chars — content fingerprints and cache keys, not security.
*/
export declare function stableHash(text: string): string;
//# sourceMappingURL=llm.d.ts.map