Skip to content

Agent reference ​

Package: @kestrel/agent · depends on @kestrel/kernel and @kestrel/protocol.

The runtime owns sessions, the model/tool registries and the execution loop. It performs no I/O except through SessionStore and the model implementations it is given.

SessionManager ​

ts
interface SessionManagerOptions {
  models: ModelRegistry;
  tools: ToolRegistry;
  logger: Logger;
  defaultWorkspace: string;
  store?: SessionStore;
  systemPrompt?: (workspace: string) => string;
  defaultModel?: string;
  maxSteps?: number;
  temperature?: number;
  maxTokens?: number;
  approve?: ApprovalHandler;
  resolvePermission?: PermissionResolver;
  retry?: RetryOptions;          // { attempts, baseDelayMs, maxDelayMs }
}

class SessionManager {
  constructor(options: SessionManagerOptions);
  initialize(): Promise<void>;
  subscribe(listener: (event: SessionEvent) => void): () => void;
  publish(event: SessionEvent): void;          // used by adapters/tests
  addGuard(guard: SessionGuard): () => void;   // veto create/run; see Quotas
  checkGuards(context: SessionGuardContext): void;  // throws SessionGuardError
  create(request?: CreateSessionRequest, tenantId?: string): AgentSession;
  get(id: string): AgentSession;               // throws SessionNotFoundError
  has(id: string): boolean;
  list(): SessionSummary[];
  send(id: string, content: string): { runId: string; done: Promise<RunResult> };
  cancel(id: string): boolean;
  delete(id: string): Promise<void>;
  close(): Promise<void>;
}

initialize() loads persisted sessions from the store once. subscribe receives every session event; the HTTP event hub is built on it. close() cancels active runs and drops in-memory sessions during runtime shutdown. Nothing is deleted from the store.

SessionNotFoundError has code = "session_not_found", which the HTTP layer maps to 404.

Model errors and retries ​

ModelRequestError carries status, retryable and retryAfterMs. isModelRequestError() detects it structurally, so a provider bundled with its own copy of the package still classifies correctly. Adapters use modelHttpError() and modelNetworkError() to build one.

retryDelay(error, attempt, options, random?) computes equal-jitter exponential backoff and lets a Retry-After hint win, capped by maxDelayMs. sleep(ms, signal) is abortable. The session loop retries only errors marked retryable, only before the first token reaches the client, and reports each wait as a notice event.

AgentSession ​

ts
class AgentSession {
  readonly id: string;
  readonly isRunning: boolean;
  readonly model: string;
  readonly workspace: string;

  summary(): SessionSummary;
  detail(): SessionDetail;
  rename(title: string): void;
  setModel(model: string): void;
  setSystemPrompt(prompt: string): void;
  reset(): void;
  start(input: string, options?: { images?: ImageAttachment[]; messageId?: string }):
    { runId: string; done: Promise<RunResult> };
  rewind(messageId: string): { removed: number; messages: ChatMessage[] };
  cancel(): boolean;
  touch(): void;
  subscribe(listener: (event: SessionEvent) => void): () => void;
}

start() appends the user message, emits message.completed, emits run.started and begins execution without awaiting it. The returned done promise resolves with a RunResult:

ts
type RunResult = {
  runId: string;
  status: "completed" | "cancelled" | "max_steps";
  usage: Usage;
};

A model or transport failure rejects done after emitting run.failed; it does not produce an "error" status. Callers that need to distinguish outcomes must catch the rejection.

Starting a second run while isRunning throws. cancel() aborts the signal; the run resolves with status: "cancelled".

rewind() removes a user message and every message after it, persists the transcript, emits session.rewound (with the surviving list) and session.updated, and returns the count. It refuses unknown ids, non-user targets and active runs with RewindError (message_not_found, invalid_rewind_target, session_busy). It never touches files: the server pairs it with a checkpoint restore, keeping the runtime free of storage. messageId on start() lets a caller take a checkpoint under the same id before the message exists.

ModelRegistry ​

ts
class ModelRegistry {
  constructor(options?: { defaultModel?: string });
  register(model: LanguageModel, options?: { setDefault?: boolean }): () => void;
  get(id?: string): LanguageModel;   // throws when missing or no default
  has(id: string): boolean;
  setDefault(id: string): void;
  get defaultModel(): string | undefined;
  list(): ModelInfo[];
  get size(): number;
}

register returns a disposer. The first registered model becomes the default unless defaultModel was configured; setDefault marks an explicit choice, which later registrations cannot override.

ToolRegistry ​

ts
class ToolRegistry {
  register(tool: Tool): () => void;  // throws on duplicate name
  get(name: string): Tool | undefined;
  list(): Tool[];                    // sorted by name
  schemas(): ToolSchema[];
  get size(): number;
}

schemas() is what the runtime sends to the model. list() and schemas() are sorted by tool name so prompts and console output are stable across restarts.

Provider types ​

ts
interface LanguageModel {
  readonly info: ModelInfo;
  generate(request: CompletionRequest, onDelta?: (delta: TokenDelta) => void): Promise<CompletionResponse>;
}

interface CompletionRequest {
  model: string;
  messages: ChatMessage[];
  tools: ToolSchema[];
  temperature?: number;
  maxTokens?: number;
  signal?: AbortSignal;
}

interface CompletionResponse {
  content: string;
  reasoning?: string;
  toolCalls: ToolCall[];
  usage: Usage;
  finishReason: "stop" | "tool_calls" | "length" | "content_filter" | "error" | (string & {});
}

ModelInfo is defined in @kestrel/protocol:

ts
interface ModelInfo {
  id: string;
  provider: string;
  label?: string;
  contextWindow?: number;
  supportsTools?: boolean;
  supportsReasoning?: boolean;
}

Tool types ​

ts
interface Tool {
  readonly schema: ToolSchema;
  execute(args: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> | ToolExecutionResult;
}

interface ToolExecutionResult {
  content: string;        // sent to the model
  data?: unknown;         // sent to clients only
  isError?: boolean;      // expected failure, rendered as an error
  terminate?: boolean;    // stop the run after this result
}

interface ToolContext {
  readonly session: ToolSession;   // id, workspace, messages, metadata
  readonly callId: string;
  readonly signal: AbortSignal;
  readonly logger: Logger;
  progress(message: string): void;
}

ToolSession.metadata is a per-session Map shared by every tool invocation. Use it for caches or counters, not for large payloads.

Permissions and approvals ​

ts
type PermissionResolver = (
  tool: Tool,
  call: ToolCall,
  session: ToolSession,
) => ToolPermission | "deny" | Promise<ToolPermission | "deny">;

type ApprovalHandler = (request: ApprovalRequest, signal: AbortSignal) => Promise<boolean>;

Resolution order: resolvePermission if provided, otherwise tool.schema.permission ?? "auto". "ask" calls the approval handler; "deny" returns an error result without asking. The default handler used by the server waits on the approval broker, which is fed by the HTTP endpoint.

Stores ​

ts
interface SessionStore {
  list(): Promise<SessionSummary[]>;
  load(id: string): Promise<SessionDetail | undefined>;
  save(session: SessionDetail): Promise<void>;
  delete(id: string): Promise<void>;
}

class MemorySessionStore implements SessionStore {}
class FileSessionStore implements SessionStore {
  constructor(directory: string);
}

FileSessionStore writes <directory>/sessions/<id>/session.json via a temporary file and rename. It ignores corrupt files rather than failing startup.

Prompt construction ​

ts
defaultSystemPrompt(options: {
  workspace: string;
  toolNames: string[];
  extra?: string;
}): string;

The default prompt states the workspace, the available tool names, the rule to prefer small verified changes, and that tool output is untrusted data. config.agent.systemPrompt is appended as additional instructions.

Message helpers ​

FunctionPurpose
createMessage(role, content)New message with id and timestamp.
assistantMessage(partial)Builds an assistant message from a completion.
toolMessage(call, content)Correlates a result with its call id.
estimateTokens(text)Rough token estimate (~4 chars/token).
titleFromInput(text)Derives a session title from the first message.

Validation ​

validateArguments(schema, args) returns JsonValidationIssue[]; formatValidationIssues renders them as path: message. The agent loop validates before calling a tool and turns issues into a tool error the model can read.

Released under the MIT License.