Agent
The Agent is the only thing most apps construct directly. Everything else hangs off it — providers, tools, permissions, sessions, skills, subagents. Construction is synchronous and cheap; the resources that cost something (the SQLite session store, MCP child processes, the on-disk skill set) are wired up lazily on the first session() call.
import { Agent } from "@skawld/agent-sdk"; import { OpenAIResponsesProvider } from "@skawld/agent-sdk/providers"; import { defaultTools } from "@skawld/agent-sdk/tools"; const agent = new Agent({ provider: new OpenAIResponsesProvider({ apiKey: process.env.OPENAI_API_KEY }), model: "gpt-5", tools: defaultTools(), permissions: { mode: "default" }, systemPrompt: "You are a careful release engineer.", });
#Constructor
Build an agent. Throws ConfigError synchronously when provider or model is missing, or when maxRetries is a negative or non-integer value. Everything else is just option capture — no provider call, no disk write, no MCP connect, no skill loader running.
What the constructor actually does, in order:
Validates
provider,model, andmaxRetries.Resolves
cwd(defaults toprocess.cwd()) andconfigDir(defaults to.skawldresolved againstcwd).Materialises the tool registry (
opts.toolsordefaultTools()) and constructs aPermissionEnginewithprojectRoot: cwd.Builds the static system blocks — OS info, shell, Node version, skawld version, permission mode, sorted tool-name list, and any
systemPromptyou supplied. These blocks are rebuilt later when MCP, skills, or subagents register additional tools.Resolves
toolConcurrencyonce viagetDefaultToolConcurrency()so a mid-process env-var change cannot race the scheduler.Stashes lazy closures for the session store, MCP client, skills, and subagents. None of them fire until you call
session().
No constructor field is readonly except opts.
Mutating opts after construction does not retroactively
change the agent — values are copied into internal state at construction
time.
#Options
Every field of AgentOptions exported from @skawld/agent-sdk. Only provider and model are required.
An instance of AnthropicProvider,
OpenAIResponsesProvider,
OpenAIChatCompletionsProvider, or your own subclass of
BaseProvider. The agent never instantiates a provider for
you — bring your own keys and transport. See
Providers.
Provider-specific model id, e.g. "claude-opus-4-5"
or "gpt-5". ModelId is just
string — skawld does no validation here and ships no
built-in default. An unknown id surfaces as a provider error on the first
turn.
Tools the agent can call. Defaults to defaultTools() — Read,
Write, Edit, Bash, Glob, Grep, and the task tools. MCP tools, the Skill
tool, and the Subagent tool are appended onto this same registry on first
session(). See Tools.
External MCP servers keyed by name. Tools are exposed to the model as
mcp__<server>__<tool>. Servers connect lazily on
the first session() call and disconnect on
close(); a failed connect throws and the memo clears so the
next session() retries. See MCP.
Policy that gates every tool call. Shape:
{ mode?: "default" | "acceptEdits" | "yolo", rules?: PermissionRule[], canUseTool?: CanUseTool }. Defaults to { mode: "default", rules: [] }. Rules are
first-match-wins. See Permissions.
Where sessions persist. When omitted, a SqliteSessionStore
at .skawld/sessions.db is constructed on the first
session() — supplying your own store means the file is never
touched. Use InMemorySessionStore in tests. See
Sessions.
Working directory for tools and the default resolution root for
configDir. Defaults to process.cwd(). Path
rules in the permission engine resolve relative paths against this.
Per-project config directory. Skills are loaded from
<configDir>/skills/<name>/SKILL.md; subagents
from <configDir>/agents/. Defaults to
".skawld" resolved against cwd. Pass
an absolute path to override the location entirely.
Additional system-prompt text appended after the default skawld blocks.
Set this once at construction rather than threading it through every
session.run() call — it’s baked into the static system
blocks and benefits from prompt caching.
Override compaction. Defaults to the built-in strategy that fires at 80%
of the context window. Pass a function (or strategy object) to customise
summarisation; pass undefined only if you have wrapped the
agent in a controller that handles ContextLengthError
itself. See Compaction.
Max retries for retryable provider errors (5xx, 429, network drops).
Defaults to 5, so up to 6 attempts per provider call. Must
be a non-negative integer or the constructor throws.
Max output tokens per turn. Omitted by default. When
unset, OpenAI providers drop max_tokens from the wire and
the API default applies; the Anthropic provider falls back to
32768 because its Messages API requires the field. Pass an
explicit number to override both behaviours. Can also be set per-run via
RunOptions.maxOutputTokens.
Hard cap on assistant turns per session.run(). Unbounded by
default (Infinity) — a run continues until the model stops
calling tools, errors, or is aborted. Hitting the cap yields a
TurnLimitError result event. Set a small number for
adversarial or untrusted prompts.
Prompt-cache TTL hint. "5m" by default —
Anthropic’s standard ephemeral cache. Pass
"1h" for long-idle sessions whose gaps between
turns may exceed 5 minutes; the first write costs 2x base but the prefix
stays warm for an hour. Only Anthropic honours this; OpenAI providers
silently ignore it.
Emit partial_assistant events carrying token deltas as they
stream. Defaults to false. Turn on only when you render the
stream to a UI — every consumer pays the iterator cost.
maxOutputTokens is easy to miss. Leaving it unset means a
long-thinking Anthropic turn is capped at 32768 tokens,
while an equivalent OpenAI turn is bounded only by the API default. If
you have a deterministic output budget across providers, set it
explicitly.
There is no additionalDirectories, hooks, or
abortSignal field on AgentOptions. Run
cancellation lives on the session — pass signal to
session.run() or call session.abort().
#configDir resolution and override
The directory used for on-disk configuration — skills and subagents — is computed once at construction:
- When
configDiris omitted, the agent usespath.resolve(cwd, ".skawld"). - When
configDiris a relative path, it is resolved againstcwd. - When
configDiris an absolute path, it is used as-is — handy for multi-tenant servers that keep one config tree per user.
Setting it programmatically is the recommended path; there is no environment variable that overrides it. See Configuration for the on-disk layout.
import path from "node:path"; import { Agent } from "@skawld/agent-sdk"; import { AnthropicProvider } from "@skawld/agent-sdk/providers"; const agent = new Agent({ provider: new AnthropicProvider(), model: "claude-opus-4-5", // Use a per-tenant config tree instead of the default ./.skawld. configDir: path.join(process.cwd(), ".my-agent"), });
#Methods
Open a new session or resume an existing one. Pass id to resume — the store creates a fresh record if no session with that id exists, then loads persisted messages into the providerView. The very first call to session() also kicks off lazy setup: MCP servers connect, the SQLite store is allocated (if one wasn't injected), skills are loaded from disk and the Skill tool is registered, and the Subagent tool is registered.
Existing session id to resume. When omitted, the store assigns a new id and returns an empty session. When provided but unknown, the store still creates a record with that id.
Arbitrary metadata persisted alongside the session record. Shallow-
merged on subsequent session.updateMeta(patch) calls.
Lazy setup happens exactly once per agent. Concurrent session() calls share one in-flight MCP connect, one skill load, and one subagent load — they each await the same memoised promise. A failed connect clears its memo, so the next session() call retries from scratch.
Release resources held by the agent. Closes MCP child processes if any connected, and closes the session store only if it was ever allocated. Always await before your process exits.
close() is intentionally narrow:
- It does not abort in-flight runs. Cancel sessions via
session.abort()or the run-scopedAbortSignalfirst. - It is a no-op for resources that were never allocated. An agent that never called
session()can safelyclose()without creating a SQLite file or starting an MCP child process — important for short-lived CLI invocations and tests. - It does not tear down the provider client. Providers are caller-owned; if you injected a custom one with its own connection pool, close it yourself.
#Agent lifetime
Construct an Agent once per process and reuse it for every session. Construction is synchronous and cheap (object allocation plus a small system-prompt assembly), but the agent owns durable resources — the SQLite store, MCP child processes, the in-memory skill set, and the per-session registry that SkillTool walks to find the active session. Rebuilding the agent per request would re-open the SQLite file, re-spawn MCP children, and re-scan the skills directory on every request.
| Resource | Created in | Released by |
|---|---|---|
| System blocks, permission engine, tool registry | Constructor (sync) | GC |
| SQLite session store | First session() call (lazy) | close() |
| MCP child processes | First session() call (lazy, memoised) | close() |
| Loaded skills + Skill tool registration | First session() call (lazy, memoised) | GC |
| Subagent registry + Subagent tool registration | First session() call (lazy, memoised) | GC |
| Per-session entry in the SkillTool lookup map | Every session() call | Agent GC (bounded by your session-creation rate — call close() for the agent in long-running servers if you spin up many sessions) |
The agent registers each opened session into an internal map so the
SkillTool can find the active session by id. Entries stay
for the agent’s lifetime — memory is bounded by your
session-creation rate. For very long-lived processes that create large
numbers of sessions, prefer one agent per logical tenant and
close() it when the tenant goes idle.