The main public class is alancode.AlanCodeAgent. This page documents the methods and properties you're likely to use.
For a tutorial-style introduction, see guides/building-agents.md.
from alancode import AlanCodeAgent
AlanCodeAgent(
backend: str | LLMBackend | None = None,
*,
model: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
request_timeout: int | str | None = None,
context_window: int | str | None = None,
cwd: str | None = None,
permission_mode: str | None = None,
max_iterations_per_turn: int | None = None,
max_output_tokens: int | str | None = None,
escalated_max_tokens: int | None = None,
empty_response_retries: int | None = None,
no_verbalize_warning: bool | None = None,
persist_thinking: bool | None = None,
disable_thinking: bool | None = None,
memory: str | None = None,
tool_call_format: str | None = None,
session_id: str | None = None,
ask_callback: Callable | None = None,
verbose: bool | None = None,
extra_tools: list[Tool] | None = None,
custom_system_prompt: str | None = None,
append_system_prompt: str | None = None,
gui_label: str | None = None,
programmatic: bool = False,
tools: list[Tool] | None = None,
disabled_tools: list[str] | None = None,
**backend_kwargs: Any,
)All settings omitted (None) fall through to the resumed session snapshot when
session_id is supplied, otherwise .alan/settings.json, then built-in
defaults. Explicitly supplied declared settings pass through their registered
per-key validators. See guides/configuration.md.
Key arguments:
cwd— working directory the agent operates in. Defaults toos.getcwd().backend— either a string ("auto","anthropic-native","scripted") or a concreteLLMBackendinstance. If omitted, it is inferred frommodel.request_timeout— positive seconds or"auto". A custombase_urlgets a 3,600-second automatic timeout for slow local inference.context_window— positive token count or"auto"; also exposed as the resolvedagent.context_windowproperty.max_output_tokens- starting per-call output budget."auto"/Noneuses the model default capped at one quarter of its context window.escalated_max_tokens- retry budget used after output truncation when it is higher than the resolved starting budget. Set it at or belowmax_output_tokensfor a hard ceiling.empty_response_retries- corrective retries for wholly empty or reasoning-only replies with no visible answer/tool (2by default;0disables).no_verbalize_warning- when a turn calls tools with no visible text, send a<system-reminder>asking the model to narrate (Falseby default). Not a retry: the tool calls still run and their results are kept.persist_thinking- re-inject reasoning already returned by the backend into subsequent requests and compaction summaries (Falseby default). It does not enable provider-side thinking.disable_thinking- sendchat_template_kwargs={"enable_thinking": false}to ask a server-side chat template to stop emitting reasoning (Falseby default). LiteLLM (backend="auto") only; the native Anthropic backend ignores it.session_id— if set, resume an existing session; otherwise a new session ID is generated.ask_callback—async def callback(question: str, options: list[str]) -> str. Called when a tool needs user approval. Return the chosen option text (or any string to use as a free-text answer).extra_tools— additional tools appended to the agent's tool list. See guides/building-agents.md for embedding patterns.custom_system_prompt— full prompt replacement; normal skills, memory, scratchpad, andALAN.mdsections are omitted.append_system_prompt— additive instructions after the normal prompt, or aftercustom_system_promptwhen both are set.gui_label— URL path segment for the GUI bridge. Defaults to the cwd basename.programmatic— whenTrue, runs Alan as a library component rather than a developer assistant. See Programmatic mode below.tools— explicit base tool list, replacing the default builtins. Composes withdisabled_toolsandextra_tools. See Tool selection below.disabled_tools— list of tool names to remove from the base set (e.g.["WebFetch", "GitCommit"]).
The 2×2 matrix:
| Sync | Async | |
|---|---|---|
| Final text only | query(prompt) -> str |
query_async(prompt) -> str |
| All events | query_events(prompt) -> list[StreamEvent | Message] |
query_events_async(prompt) -> AsyncGenerator[StreamEvent | Message, None] |
Run a turn synchronously. Returns the assistant's final text response.
answer = agent.query("Explain the compaction system")Internally runs asyncio.run, or dispatches to a worker thread if an event loop is already running (Jupyter-safe).
Same as query but awaitable.
answer = await agent.query_async("Explain the compaction system")Synchronous; returns a full list of events after the turn completes. Useful for post-hoc inspection.
The real primitive and the only live-streaming query method. Yields events as they're produced:
async for event in agent.query_events_async("Summarize README.md"):
# handle each event
passThe union aliases and message dataclasses live in alancode.messages.types:
| Event | When |
|---|---|
RequestStartEvent |
Each API call begins (useful for "Thinking..." indicators). |
AssistantMessage with hide_in_api=True |
Streaming delta — text chunks, thinking chunks. |
AssistantMessage with hide_in_api=False |
Final assembled message after the stream completes. Has tool calls. |
UserMessage |
Injected (interactive system reminders, tool results). The automatic date/time reminder is omitted with programmatic=True. |
SystemMessage |
Informational (compaction markers, etc.). |
AttachmentMessage |
Structured metadata (e.g., max_iterations_per_turn_reached). |
ProgressMessage |
Reserved public/serialization type for long-running updates; the current query/tool path does not construct one. |
Filter on hide_in_api to distinguish streaming deltas from final messages — see the streaming example in guides/building-agents.md.
query_async() is asynchronous in the normal Python sense (it does not block the event loop), but it still waits for the whole turn and returns one final string. For live text, iterate query_events_async() and select virtual AssistantMessage text deltas. There is currently no separate query_stream() convenience method.
Consume query_events_async() to exhaustion. A final assembled AssistantMessage is an event within an iteration, not necessarily the end of the turn: usage for that completed call is already recorded, but advancing the generator after the event performs requested tools and reaches later model rounds. If a caller intentionally stops early, it must await stream.aclose(); breaking at the first final assistant can otherwise skip tools and leave cleanup deferred.
| Property | Type | Description |
|---|---|---|
agent.session_id |
str |
Current session ID (auto-generated or passed in). |
agent.messages |
list[Message] |
Copy of the current conversation (safe to mutate the returned list). |
agent.usage |
Usage |
Cumulative tokens across the session. |
agent.last_usage |
Usage |
Usage from the most recent successful API call. |
agent.cost_usd |
float |
Cumulative estimated cost. |
agent.cost_unknown |
bool |
True if the model's pricing isn't known. |
agent.cwd |
str |
Working directory. |
agent.turn_count |
int |
Number of user messages processed this session. |
agent.context_window |
int |
Resolved context-window size after settings/model metadata. |
Usage has: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, plus a total_input property summing the three input types.
agent.abort()Sets the abort event. Alan checks it at loop checkpoints and after streaming; it does not forcibly terminate an in-flight SDK request.
agent.inject_message("Actually, focus on calc.py only.")Queues a user message to be delivered at the start of the next iteration. Useful for orchestration frameworks that steer mid-turn.
error = agent.update_session_setting("permission_mode", "yolo")
if error:
print("Invalid:", error)Validates and updates a setting in memory and on disk. Returns an error message string on validation/backend-creation failure, or None on success. backend, model, api_key, base_url, request_timeout, and context_window trigger transactional backend recreation.
await agent.close()Idempotently fires session_end hooks, closes the backend's owned client/server resources, and releases the session lock. Call it when done; the CLI does this on /exit.
Use programmatic=True when Alan is being driven by another program (a benchmark harness, a parent agent, an automated pipeline) rather than a developer at a terminal. It detaches Alan from project- and host-level state that's normally helpful for an interactive assistant but contaminates a controlled run.
agent = AlanCodeAgent(
model="claude-sonnet-4-6",
cwd="/path/to/experiment",
permission_mode="yolo",
programmatic=True,
)When programmatic=True:
~/.alan/ALAN.md(global instructions) is not loaded.<cwd>/ALAN.md(project instructions) is not loaded.~/.alan/memory/MEMORY.md(global memory index) is not loaded.- The default tool set excludes
WebFetch,GitCommit, andAskUserQuestion.SkillToolis also not appended.
Project-scoped state in <cwd>/.alan/sessions/<id>/ (transcript, state, scratchpad) is unchanged — that's the agent's own working memory and is needed for resume.
You can override the curated tool set with tools= or refine it with disabled_tools= (see below).
Three knobs control the agent's tool list, applied in order:
- Base set. Resolved from the first of:
tools=[...]if passed (explicit replacement),- the curated programmatic set if
programmatic=True, - all enabled built-in tools otherwise (the
SkillToolis appended in this case).
- Subtract any names listed in
disabled_tools. - Append anything in
extra_tools.
# Read-only assistant: drop write/exec tools entirely
agent = AlanCodeAgent(disabled_tools=["Bash", "Edit", "Write", "GitCommit"])
# Custom tool list (e.g. for a domain-specific agent)
agent = AlanCodeAgent(tools=[MyDomainTool(), MyOtherTool()])
# Programmatic mode plus an extra custom tool
agent = AlanCodeAgent(programmatic=True, extra_tools=[MyTool()])SessionState takes an exclusive flock on <cwd>/.alan/sessions/<session_id>/session.lock at construction. A second process attempting to open the same session raises alancode.session.SessionLockedError. The lock is released by agent.close() and on process exit.
Distinct session IDs have separate state and transcript files. Project settings, project allow rules, and the context-window cache use lock-backed atomic read-modify-write operations, so concurrent updates do not corrupt JSON or silently lose unrelated entries.
This does not make two coding agents transactionally safe in the same source tree: separate sessions can still edit the same file from stale views. Use a separate Git worktree per concurrent coding agent and merge their changes normally. Within one agent, read-only tool calls may run concurrently while write/exec calls are serialized; separate agents do not share that scheduler.
The remaining process-global behavior is library configuration (for example LiteLLM logger verbosity), not conversation/request state. Each agent owns its backend instance, message list, abort event, session start time, and lifecycle.
async def my_ask(question: str, options: list[str]) -> str:
print(f"\n{question}")
for i, opt in enumerate(options, 1):
print(f" {i}) {opt}")
choice = input("> ").strip()
if choice.isdigit() and 1 <= int(choice) <= len(options):
return options[int(choice) - 1]
return choice # free-text answer
agent = AlanCodeAgent(ask_callback=my_ask, permission_mode="edit")The callback is awaited when a tool needs approval. Return one of the option strings to accept the corresponding action (Allow, Deny, Allow always), or return any other string — that string becomes the "tool result" sent back to the model (so the user can deny with a reason in one step).
Ctrl+C from within the callback should raise KeyboardInterrupt → Alan converts it to asyncio.CancelledError → the turn aborts cleanly.
from alancode import AlanCodeAgent
agent = AlanCodeAgent(
model="openrouter/google/gemini-2.5-flash",
permission_mode="yolo", # auto-approve for automation
)
answer = agent.query("What's 2+2?")
print(answer)
print(f"Cost: ${agent.cost_usd:.4f}")
print(f"Tokens: {agent.usage.total_input} in, {agent.usage.output_tokens} out")
import asyncio
asyncio.run(agent.close())import asyncio
from alancode import AlanCodeAgent
from alancode.messages.types import AssistantMessage, TextBlock, ToolUseBlock
async def main():
agent = AlanCodeAgent(permission_mode="yolo")
try:
async for event in agent.query_events_async("List files and summarize."):
if not isinstance(event, AssistantMessage):
continue
for block in event.content:
if event.hide_in_api and isinstance(block, TextBlock):
print(block.text, end="", flush=True)
elif not event.hide_in_api and isinstance(block, ToolUseBlock):
print(f"\n[tool: {block.name}({block.input})]")
finally:
await agent.close()
asyncio.run(main())from alancode import AlanCodeAgent
from alancode.backends.base import LLMBackend
class MyBackend(LLMBackend):
async def stream(self, messages, system, tools, *, model, max_tokens, thinking, **kwargs):
# yield BackendStreamEvent objects
...
def get_model_info(self, model):
...
async def close(self):
...
agent = AlanCodeAgent(backend=MyBackend(...))AlanCodeAgent(backend="scripted", model="remote", ...) starts an embedded HTTP server and waits for an external caller (a human or another agent) to act as the LLM. Useful for debugging tool wiring, system prompts, and framework integrations without burning tokens.
See guides/remote-scripted-backend.md for the endpoints, payload shapes, and a typical curl loop.
- guides/building-agents.md — tutorial-style introduction.
- guides/remote-scripted-backend.md — HTTP-driven impersonation backend.
- reference/tools.md — what tools the agent has access to.
- reference/settings.md — what kwargs are valid and what defaults apply.
- architecture/query-loop.md — what happens inside each
query_events_asynccall.