Appam
Guides

Agent Skill

Copy the canonical Appam implementation skill as raw Markdown for reuse in other LLM coding agents.

This page renders the canonical docs/SKILL.md file as a single copyable Markdown block.

If you update the Appam skill, update docs/SKILL.md. This page reads that file directly so the Fumadocs docs stay aligned with the standalone skill file.

SKILL.md
---name: appam-agent-implementationdescription: Implement, extend, debug, or review Rust AI agents built with Appam. Use when working with `appam::prelude::*`, `Agent::quick`, `AgentBuilder`, `TomlAgent`, Appam tool macros, streaming consumers, session persistence, trace output, provider configuration, or Appam-specific runtime behavior and caveats.---# Appam Agent ImplementationTreat this file as the source-backed operating manual for building agents with Appam.## Keep the mental model straightAppam is a Rust agent runtime with four core layers:- Construction: `Agent::quick(...)`, `Agent::new(...)`, `AgentBuilder`, and `TomlAgent`.- Tools: Rust `Tool` implementations, `#[tool]` macro-generated tools, or TOML-declared Python tools.- Runtime loop: provider selection, streaming, tool execution, continuation handling, tracing, and history.- Provider adapters: OpenAI, OpenAI Codex, Anthropic, OpenRouter, Vertex, Azure OpenAI, Azure Anthropic, and Bedrock.The important design point is that most user code never talks to a provider client directly. It builds an agent, streams a prompt, and lets Appam run the tool-calling loop.## Use the right source of truthIf documentation and code disagree, trust them in this order:1. `src/agent/runtime.rs` and `src/agent/runtime_agent.rs`2. `src/agent/builder.rs`, `src/agent/toml_agent.rs`, `src/agent/quick.rs`3. `src/tools/*.rs` and `appam_macros/src/lib.rs`4. `tests/*.rs`5. `examples/coding-agent-*.rs`6. `README.md`7. `docs/content/docs/**/*.mdx`Read these files first for repo work:- `README.md`- `src/lib.rs`- `src/agent/mod.rs`- `src/agent/builder.rs`- `src/agent/quick.rs`- `src/agent/runtime.rs`- `src/agent/runtime_agent.rs`- `src/agent/toml_agent.rs`- `src/agent/streaming.rs`- `src/agent/streaming_builder.rs`- `src/agent/history.rs`- `src/agent/errors.rs`- `src/tools/mod.rs`- `src/tools/registry.rs`- `src/tools/loader.rs`- `src/tools/register.rs`- `appam_macros/src/lib.rs`- `src/llm/provider.rs`- `src/llm/unified.rs`- `src/llm/openai/config.rs`- `src/llm/openai_codex/config.rs`- `src/llm/anthropic/config.rs`- `src/llm/openrouter/config.rs`- `src/llm/vertex/config.rs`- `examples/coding-agent-anthropic.rs`- `examples/coding-agent-openai-responses.rs`- `examples/coding-agent-openai-codex.rs`- `examples/coding-agent-openrouter-responses.rs`- `examples/coding-agent-vertex.rs`- `tests/agent_builder.rs`- `tests/tool_macro.rs`- `tests/streaming.rs`- `tests/logging_tracing_history.rs`- `tests/continuation_tests.rs`- `tests/hybrid_config.rs`- `tests/provider_switching.rs`## Understand the naming trap in the prelude`use appam::prelude::*;` is the right default import, but it exports two different “Agent” concepts:- `Agent` in the prelude is the quick-constructor helper namespace from `src/agent/quick.rs`.- `AgentTrait` in the prelude is the actual trait.If you need to implement the trait manually, import `appam::agent::Agent` or use `AgentTrait`. Do not confuse that trait with `Agent::quick(...)`.## Choose the construction style deliberatelyUse `Agent::quick(...)` when:- You need the smallest working agent.- You can express the system prompt inline.- You do not need unusual provider tuning beyond model, prompt, and tools.Use `Agent::new(name, model)` when:- You want builder ergonomics but still want automatic provider detection from the model string.- You want to write `.prompt(...)`, `.tool(...)`, and `.build()?`.Use `AgentBuilder` when:- You need production-oriented configuration.- You need Anthropic thinking, caching, retries, beta features, rate limiting, or tool choice.- You need OpenAI reasoning or service-tier controls.- You need traces, history, continuation requirements, or explicit provider selection.Use `TomlAgent::from_file(...)` when:- The system prompt and tool declarations should live on disk.- Non-Rust contributors need to edit agent behavior.- You are okay with the current TOML tool reality: Python tools are the practical dynamic path.Use `RuntimeAgent` directly only when you are already assembling your own `ToolRegistry` and want a concrete in-memory agent type.## Remember the runtime configuration realityThis is one of the most important Appam-specific facts:- The default runtime path does not automatically read `appam.toml`.- `Agent::quick(...)`, `AgentBuilder::build()`, `RuntimeAgent::run()`, and `TomlAgent::run()` start from `load_config_from_env()`, then apply agent/runtime overrides.- `load_global_config(...)` exists, but the standard run path does not call it for you.Practical consequence:- Use environment variables for provider credentials and other global defaults.- Use builder methods or TOML agent fields for agent-specific settings.- Do not assume dropping an `appam.toml` file into the repo changes runtime behavior unless your own code loads it explicitly.## Start from the preludeFor most code, begin with:```rustuse appam::prelude::*;```That gives you:- `Agent`, `AgentBuilder`, `TomlAgent`, `RuntimeAgent`- `Tool`, `ToolRegistry`, `ClosureTool`, `ToolRegistryExt`- `tool`, `Schema`- `StreamBuilder`, `StreamEvent`, `StreamConsumer`- `ConsoleConsumer`, `ChannelConsumer`, `CallbackConsumer`, `TraceConsumer`- `SessionHistory`, `SessionSummary`- `Result`, `Context`, `json`, `Value`, `Arc`, and serde derivesIf you need lower-level types like `SqliteTraceConsumer`, import them explicitly.## Build the smallest correct agentUse this as the canonical minimal pattern:```rustuse appam::prelude::*;#[tokio::main]async fn main() -> Result<()> {    let agent = Agent::quick(        "anthropic/claude-sonnet-4-5",        "You are a concise Rust assistant.",        vec![],    )?;    agent        .stream("Explain ownership in three sentences.")        .on_content(|text| print!("{text}"))        .run()        .await?;    println!();    Ok(())}```Current `Agent::quick(...)` behavior worth remembering:- It returns a `RuntimeAgent`.- It auto-detects the provider from the model string.- It sets `temperature(0.7)`, `max_tokens(4096)`, and `top_p(0.9)`.- Unknown model prefixes fall back to `LlmProvider::OpenRouterResponses`.## Prefer `AgentBuilder` for real agent implementationsUse this shape for most serious work:```rustuse appam::prelude::*;#[derive(Deserialize, Schema)]struct AddInput {    #[description = "First number"]    a: f64,    #[description = "Second number"]    b: f64,}#[derive(Serialize)]struct AddOutput {    sum: f64,}#[tool(description = "Add two numbers together")]fn add(input: AddInput) -> Result<AddOutput> {    Ok(AddOutput { sum: input.a + input.b })}#[tokio::main]async fn main() -> Result<()> {    let agent = AgentBuilder::new("calculator")        .provider(LlmProvider::OpenAI)        .model("gpt-5.4")        .system_prompt("You are a careful calculator. Use the add tool for exact arithmetic.")        .tool(add())        .build()?;    agent        .stream("What is 42 + 58?")        .on_content(|text| print!("{text}"))        .on_tool_call(|name, args| println!("\ncalling {name}: {args}"))        .on_tool_result(|name, result| println!("\n{name} -> {result}"))        .run()        .await?;    Ok(())}```Prefer `.tool(my_tool())` for single concrete tools. Prefer `.with_tools(vec![...])` if you already have `Vec<Arc<dyn Tool>>`.## Implement tools the Appam wayPrefer the `#[tool]` macro for Rust tools.### Use structured inputs for nontrivial toolsUse:- `#[derive(Deserialize, Schema)]` on the input struct- `#[description = "..."]` on each field- `#[tool(description = "...")]` on the functionThis produces the cleanest JSON Schema for the model and is the dominant pattern across the docs, examples, and tests.### Use inline parameters only for simple toolsThis is valid:```rust#[tool(description = "Multiply two numbers")]fn multiply(    #[arg(description = "First number")] a: f64,    #[arg(description = "Second number")] b: f64,) -> Result<f64> {    Ok(a * b)}```Use `#[arg(default = ...)]` only when the parameter should be optional.### Know what the macro actually does`#[tool]` currently expects:- A synchronous function body- A return type of `Result<T>`- `T` to be serializableThe macro generates:- A concrete tool type named from the function in PascalCase- A constructor function with the original function name- A `Tool` implementation- JSON Schema generation- JSON argument parsingSuccessful macro-generated tool outputs are wrapped as:```json{ "output": ... }```### Use the correct manual `ToolSpec` shapeIf you implement `Tool` manually, the spec shape is:```rustToolSpec {    type_field: "function".to_string(),    name: "read_file".to_string(),    description: "Read a file".to_string(),    parameters: json!({        "type": "object",        "properties": {            "file_path": { "type": "string", "description": "Absolute path to the file" }        },        "required": ["file_path"]    }),    strict: None,}```Do not use a nested OpenAI-style `{ "function": { ... } }` wrapper when constructing `appam::llm::ToolSpec`. Appam’s `ToolSpec` is the direct flattened struct above.### Validate tool inputs aggressivelyTreat tool arguments as untrusted model output.Always:- Validate required fields.- Validate path expectations.- Bound resource usage.- Return structured error context.- Avoid shell injection, path traversal, and silent fallthrough.Examples in `examples/coding-agent-*.rs` are intentionally simple. For production code, tighten them.## Use closure tools when that is genuinely simpler`ClosureTool` and `ToolRegistryExt::register_fn(...)` are good for:- Fast prototypes- Tests- Small inline helpersThey are not the best default for larger tools that benefit from typed inputs and compile-time structure.## Use TOML agents with the current limitations in mindThe current TOML agent shape is:```toml[agent]name = "assistant"model = "openai/gpt-5.4"system_prompt = "prompt.txt"description = "A helpful assistant"[[tools]]name = "echo"schema = "tools/echo.json"implementation = { type = "python", script = "tools/echo.py" }```Remember these implementation realities:- Paths are resolved relative to the TOML file’s directory.- `TomlAgent::from_file(...)` validates the prompt path and tool entries immediately.- `TomlAgent::model()` falls back to `"openai/gpt-5"` if the TOML file omits a model.- Python tools only work when Appam is built with `--features python`.- TOML-declared Rust module loading is currently disabled in `src/tools/loader.rs`.If you need TOML-defined prompts plus Rust tools, use the hybrid path:```rustlet agent = TomlAgent::from_file("agents/assistant/agent.toml")?    .with_additional_tool(Arc::new(my_tool()));```That hybrid pattern is covered in `tests/hybrid_config.rs`.## Stream the agent instead of waiting for a final blobAppam is streaming-first.Use `.stream(prompt)` and attach handlers:- `on_session_started`- `on_content`- `on_reasoning`- `on_tool_call`- `on_tool_result`- `on_tool_failed`- `on_error`- `on_done`- `on_tool_call_async`- `on_tool_result_async`This is the canonical runtime shape for CLI, TUI, and app integrations.Use `StreamConsumer` implementations when you need reusable sinks or multiple destinations.Built-in consumers:- `ConsoleConsumer`: default for `run()`- `ChannelConsumer`: forward `StreamEvent`s into a Tokio mpsc channel- `CallbackConsumer`: wrap a closure- `TraceConsumer`: write JSONL traces### Handle `StreamEvent::Error` as a rich eventThe current `StreamEvent::Error` variant is richer than older docs imply. It can include:- `message`- `failure_kind`- `provider`- `model`- `http_status`- `request_payload`- `response_payload`- `provider_response_id`If you build custom consumers, log or persist those fields.## Enable tracing and history explicitlyHistory and traces are opt-in.For a runtime-built agent:```rustlet agent = AgentBuilder::new("stateful-agent")    .provider(LlmProvider::Anthropic)    .model("claude-sonnet-4-5")    .system_prompt("You are helpful.")    .enable_history()    .history_db_path("data/sessions.db")    .auto_save_sessions(true)    .enable_traces()    .trace_format(TraceFormat::Detailed)    .build()?;```What happens when enabled:- History uses `SessionHistory` over SQLite.- The default DB path is `data/sessions.db`.- `continue_session(...)` only works when the agent’s runtime points at the same history database.- `enable_traces()` causes the runtime to attach `TraceConsumer`.- Trace output writes `session-<id>.jsonl`.- The runtime also writes `session-<id>.json` at the end of the session when traces are enabled.- `TraceFormat::Compact` omits reasoning entries.## Use continuation enforcement when the agent must finish by calling a toolAppam supports required completion tools via:- `require_completion_tools(...)`- `max_continuations(...)`- `continuation_message(...)`Use this when the agent is not allowed to “just answer” and must call a finalizing tool.Source-backed behavior:- The runtime injects a continuation user message if the session ends without a required tool call.- Default continuation budget is `2`.- The continuation counter is based on exact message matching against the configured continuation message.- Exhaustion produces a structured `SessionFailureKind::RequiredCompletionToolMissing`.See `src/agent/runtime.rs` and `tests/continuation_tests.rs`.## Know the provider-specific controls### AnthropicUse:- `.provider(LlmProvider::Anthropic)`- `.thinking(...)`- `.caching(...)`- `.tool_choice(...)`- `.effort(...)`- `.beta_features(...)`- `.retry(...)`- `.rate_limiter(...)`This is the richest builder surface in Appam. `examples/coding-agent-anthropic.rs` is the best source example.### OpenAIUse:- `.provider(LlmProvider::OpenAI)`- `.openai_reasoning(...)`- `.openai_text_verbosity(...)`- `.openai_service_tier(...)`- `.openai_pricing_model(...)`OpenAI defaults live in `src/llm/openai/config.rs`.### OpenAI CodexUse:- `.provider(LlmProvider::OpenAICodex)`- `.openai_codex_access_token(...)`- `.openai_reasoning(...)`Auth precedence is:1. `OpenAICodexConfig.access_token`2. `OPENAI_CODEX_ACCESS_TOKEN`3. cached OAuth credentials in `OPENAI_CODEX_AUTH_FILE` or the default auth fileUse the interactive login helpers only for trusted local developer flows. The example is `examples/coding-agent-openai-codex.rs`.### OpenRouterChoose the API flavor explicitly:- `LlmProvider::OpenRouterCompletions`- `LlmProvider::OpenRouterResponses`Use:- `.openrouter_api_key(...)`- `.openrouter_reasoning(...)`- `.openrouter_provider_routing(...)`- `.openrouter_transforms(...)`- `.openrouter_models(...)``Agent::quick("openrouter/...")` resolves to `OpenRouterResponses`.### VertexUse:- `.provider(LlmProvider::Vertex)`- `.vertex_api_key(...)`Current caveat:- `AgentBuilder` does not expose every `VertexConfig` field.- Advanced Vertex settings like `thinking`, `function_calling_mode`, `allowed_function_names`, and `include_thoughts` are currently configured through env vars or lower-level config/client work, not rich builder methods.See `src/llm/vertex/config.rs` and `examples/coding-agent-vertex.rs`.### Azure OpenAIUse explicit provider selection:```rust.provider(LlmProvider::AzureOpenAI {    resource_name: "my-resource".to_string(),    api_version: "2025-04-01-preview".to_string(),})```Do not expect quick provider detection to infer Azure.### Azure AnthropicUse explicit provider selection:```rust.provider(LlmProvider::AzureAnthropic {    base_url: "https://my-resource.services.ai.azure.com/anthropic".to_string(),    auth_method: appam::llm::anthropic::AzureAnthropicAuthMethod::XApiKey,})```Treat `.model(...)` as the Azure deployment/model name for that endpoint.### BedrockUse explicit provider selection:```rust.provider(LlmProvider::Bedrock {    region: "us-east-1".to_string(),    model_id: "us.anthropic.claude-sonnet-4-5-20250514-v1:0".to_string(),    auth_method: appam::llm::anthropic::BedrockAuthMethod::SigV4,})```Important:- SigV4 is the streaming-friendly default.- Bearer token auth is the non-streaming path.## Set the right environment variablesUse environment variables as the safest standard runtime path.Minimum credentials by provider:- OpenAI: `OPENAI_API_KEY`- OpenAI Codex: `OPENAI_CODEX_ACCESS_TOKEN` or cached auth file- Anthropic: `ANTHROPIC_API_KEY`- OpenRouter: `OPENROUTER_API_KEY`- Vertex API key mode: `GOOGLE_VERTEX_API_KEY` or `GOOGLE_API_KEY` or `GEMINI_API_KEY`- Vertex bearer mode: `GOOGLE_VERTEX_ACCESS_TOKEN`- Azure OpenAI: `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_RESOURCE`- Azure Anthropic: `AZURE_API_KEY` or `AZURE_ANTHROPIC_API_KEY`, plus `AZURE_ANTHROPIC_BASE_URL` or `AZURE_ANTHROPIC_RESOURCE`- Bedrock SigV4: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`- Bedrock bearer: `AWS_BEARER_TOKEN_BEDROCK`Useful Appam runtime env vars:- `APPAM_PROVIDER`- `APPAM_LOG_LEVEL`- `APPAM_LOGS_DIR`- `APPAM_LOG_FORMAT`- `APPAM_TRACE_FORMAT`- `APPAM_ENABLE_TRACES`- `APPAM_ENABLE_LOGS`- `APPAM_HISTORY_ENABLED`- `APPAM_HISTORY_DB_PATH`## Use the current CLI accuratelyThe CLI surface is real, but a few capabilities are intentionally limited.Supported commands:- `appam new <name>`- `appam validate agents/.../agent.toml`- `appam run agents/.../agent.toml "prompt"`- `appam tracing --traces-dir logs`Current caveats:- `appam run` is single-shot.- Omitting the prompt does not start a real interactive session yet.- `appam serve` exists as a command, but the underlying web API is hard-disabled.## Respect the current web API realityThe legacy Axum API is present in-tree but disabled.Facts from `src/web/mod.rs`:- `serve(...)` always refuses to start.- The disablement is intentional and security-related.- The route code is kept for redesign/reference, not for production use today.Do not implement new user-facing features assuming `appam serve` is available unless you are explicitly reworking that subsystem.The trace visualizer server is separate and still usable through `appam tracing` or `web::serve_tracing(...)`.## Know the biggest sharp edgesThese are the Appam-specific traps most likely to waste time:- `appam.toml` is not auto-loaded by the normal run path.- `appam::tools::builtin::*` is not implemented. `src/tools/builtin/mod.rs` is a placeholder.- `ToolRegistry::with_builtins()` currently returns an empty registry.- TOML Rust tool loading is disabled in `src/tools/loader.rs`.- TOML Python tools require the `python` feature.- `AgentBuilder::tools(...)` takes `Vec<Arc<dyn Tool>>`; it does not accept raw concrete tools.- Use repeated `.tool(...)` calls if you want no-`Arc` ergonomics.- `build_with_stream()` returns a receiver but does not automatically wire agent output into it. Prefer `run_streaming(..., Box::new(ChannelConsumer::new(tx)))` for real channel streaming.- `RuntimeAgent::model()` and `TomlAgent::model()` fall back to `"openai/gpt-5"`.- `AppConfig::default()` starts with `LlmProvider::OpenRouterCompletions`, but `Agent::quick(...)` falls back to `OpenRouterResponses` when provider detection fails.## Use the examples as implementation templatesMap your task to the nearest example first:- `examples/coding-agent-anthropic.rs`: richest builder configuration and Anthropic-specific controls- `examples/coding-agent-openai-responses.rs`: OpenAI Responses path- `examples/coding-agent-openai-codex.rs`: ChatGPT Codex auth flow- `examples/coding-agent-openrouter-responses.rs`: OpenRouter Responses path- `examples/coding-agent-openrouter-completions.rs`: OpenRouter Completions path- `examples/coding-agent-vertex.rs`: Vertex/Gemini path- `examples/coding-agent-azure-openai.rs`: Azure OpenAI path- `examples/coding-agent-azure-anthropic.rs`: Azure Anthropic path- `examples/coding-agent-bedrock.rs`: Bedrock pathOne important pattern those examples reveal:- They define their own `read_file`, `write_file`, `bash`, and `list_files` tools inline because the built-in Rust tool module is not ready.## Debug Appam agents methodicallyWhen an Appam agent misbehaves:1. Verify provider credentials and model selection first.2. Confirm which provider was actually selected.3. Check whether history and traces were explicitly enabled.4. Inspect `logs/session-<id>.jsonl` and `logs/session-<id>.json` if traces are on.5. Inspect `StreamEvent::Error` payloads, not just the display string.6. Verify tool names and schema field names match exactly.7. Remember that tool macro outputs are wrapped under `"output"`.8. For TOML agents, confirm every path is relative to the TOML file directory.9. If using continuation enforcement, confirm the required tool names and continuation message are exactly what the runtime expects.Use these tests as behavior references while debugging:- `tests/tool_macro.rs`- `tests/streaming.rs`- `tests/logging_tracing_history.rs`- `tests/continuation_tests.rs`- `tests/hybrid_config.rs`- `tests/provider_switching.rs`## Follow this implementation checklist- Import `appam::prelude::*`.- Pick the construction path that matches the task.- Set credentials via environment variables unless there is a strong reason not to.- Prefer `#[tool]` plus `#[derive(Deserialize, Schema)]` for Rust tools.- Use `AgentBuilder` for anything beyond the most minimal prototype.- Stream responses instead of only calling `run()` blindly.- Enable history and traces explicitly if you need continuity or observability.- Use an example matching the provider as your template.- Avoid the disabled TOML Rust tool path and the disabled web API path.- If you modify framework behavior inside this repo, update the closest example, test, and doc page alongside the source.