# v0.3.0 Source: https://docs.symbolica.ai/changes/0.3.0_release First public release of the Agentica SDK The Agentica SDK v0.3.0 is available! # v0.3.1 Source: https://docs.symbolica.ai/changes/0.3.1 Changes from v0.3.0 to v0.3.1 ## Improvements * Updated runtime to latest wasmtime version for improved stability ## Bug Fixes * Fixed `random` module virtualization ## Documentation * Updated Python and TypeScript SDK READMEs with clearer examples * Updated contact email to `hello@symbolica.ai` # v0.3.2 Source: https://docs.symbolica.ai/changes/0.3.2 Changes from v0.3.1 to v0.3.2 ## Reasoning Effort Parameter Feature Initial support for a **reasoning effort** has been added to control the thinking budget on reasoning models (GPT 5.2, Sonnet 4.5, Gemini 3, etc.). Higher values use more reasoning tokens but may produce better results. ```python theme={null} from agentica import spawn agent = await spawn( reasoning_effort='high' # 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | None ) ``` ```typescript theme={null} import { spawn } from '@symbolica/agentica'; const agent = await spawn({ reasoningEffort: 'high' // 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' }); ``` ## Multi-threaded Support for Python Improvement The Python SDK now supports **multi-threaded usage** with separate asyncio event loops per thread. Each thread with its own event loop gets its own client session manager instance, enabling concurrent agent operations across threads. ## Bug Fixes * **Thread safety**: Fixed crashes when spawning agents from non-main threads * **Race condition fix**: Added thread-safe agent ID assignment to prevent race conditions when multiple agents spawn concurrently * **AttributeError fix**: Fixed potential error in logger cleanup by checking attribute existence before access ## Improvements * Log file paths are now shown relative to the current working directory when possible for cleaner output # v0.4.0 Source: https://docs.symbolica.ai/changes/0.4.0 Changes from v0.3.2 to v0.4.0 ## Summary Associated with our [blog post about ARC](https://www.symbolica.ai/blog/arcgentica), this release of the Agentica SDK allows a wider variety of Python objects to be exchanged between your Python session and agent REPLs. This brings us one step closer to the vision of agents and humans collaborating in a unified Python environment. You can pass agents data-heavy or stateful objects like `numpy.ndarray`, `pandas.DataFrame`, and `sqlite3.Connection`. Agents can use the normal APIs provided by these objects. These objects are [warped by reference](/concepts/how-it-works#the-mental-model), meaning that the agent can modify them in-place, and the changes will be reflected in your local Python session. Agents can be warped entire Python modules, such as `numpy`, `scipy`, `sympy`, `sqlite3` and `pandas`, to gain access to the full APIs of these libraries. Commonly used Python standard libraries types are also now supported, including `NamedTuple`, `TypedDict`, `date`, `time`, `timedelta`, `IPv4Address`, etc. ## Improvements * Improvements to warping of Python objects to agents: * Warped by *reference*: * `Path` objects, which confer the ability to read and write files * subclasses of builtin classes like `list`, `dict`, `set`, etc. * `NamedTuple`, `TypedDict`, `BaseModel` instances * data containers like `numpy.ndarray`, `pandas.DataFrame`, etc. * user modules (`ModuleType` objects) * stateful built-in system iterator objects * Warped by *value*: * `lambda` functions * temporal: `date`, `datetime`, `time`, `timedelta`, etc. * numeric: `complex`, and big int numeric values * regexes: `Pattern`, `Match` * stateless system iterators: `zip`, `map`, `filter`, `slice`, `range`, `enumerate`, `iter(seq)`, `iter(fn, stop)`. * Agents can catch and raise client-side exceptions * Support for C-implemented class and instance properties * More accurate warping of Python function and method signatures * positional-only, keyword-only, variadic, and default arguments * signatures of C-implemented functions via `__text_signature__` * low-overhead support for `@overload` signatures * Additional options to `spawn`: * `reasoning_effort` to set the thinking budget for reasoning models * `cache_ttl` to control Anthropic prompt cache duration * Improvement to streaming and logging: * `StreamLogger` takes `on_chunk` callback and `include_usage` filter * `Chunk.type` field: `reasoning`, `output_text`, `usage`, etc. * reasoning traces now visible in `StandardLogger` output * `ResponseUsage` replaces `Usage` in Python, and includes new usage stats (cached tokens, reasoning tokens) * Performance improvements: * Reduced excessive blocking in `asyncio` event loop * Improvements to inference: * OpenAI/Anthropic models routed to native APIs; others via OpenRouter * 128-agent concurrency cap removed ## Bug Fixes * Fixed possible deadlocks associated with spawning multiple calls to the same agentic function concurrently * Reduced cases of classes that warp incorrectly due to their metaclass ## Licensing * The [Agentica Python SDK](https://github.com/symbolica-ai/agentica-python-sdk) is now licensed under the MIT License. * The [Agentica Typescript SDK](https://github.com/symbolica-ai/agentica-typescript-sdk) is now licensed under the MIT License. * The [Agentica Server](https://github.com/symbolica-ai/agentica-server) is now licensed under the MIT License. * The [Agentica Internal Libraries](https://github.com/symbolica-ai/agentica-internal-libraries) are now licensed under the MIT License. # Agent Source: https://docs.symbolica.ai/concepts/agent Use the Agentica SDK to build agents. **This is detailed usage documentation.** New to the Agentica SDK? Start with the [Quickstart](/quickstart) or learn [when to use agents vs agentic functions](/concepts/agentic-vs-agents). **Agents** are stateful, long‑lived LLM workers you spawn and call repeatedly; they keep conversational history across invocations. We expose two ways to instantiate an agent: * **direct** instantiation via `Agent.__init__` * **awaitable** instantiation via the `spawn` function Both return an instance of the `Agent` class. Direct instantiation is often useful in functions that **must be synchronous** e.g. setting attributes of objects in protected methods such as `__init__`. See [here](/references/python/agents) for the API reference of `Agent`. ## Using agents Agents built with the Agentica SDK accomplish specific tasks using native libraries, code, APIs, and SDKs available in your programming language's runtime. A single agent represents an **evolving history of invocations**, each of which may be provided a specific task and set of resources. ### When to use agents Agents work best for **longer-running, multi-step tasks** where each action depends on prior outcomes, state is preserved, and task-appropriate sets of resources need to be delegated. For single, well-bounded tasks without cross-step context, see [Agentic functions](/concepts/agentic). ## The basics Agents can be created with the Agentica SDK using `spawn` and later **called to perform tasks**. An agent's **history evolves across its invocations**, so you can follow up with tasks in the context of previous results. In Python, provide a **return type** to receive a result of that runtime type (defaulting to `str`). In TypeScript, the return type is specified via the generic `` type parameter. ```python Python wrap theme={null} agent = await spawn(premise="You are a helpful assistant.") c: float = await agent.call(float, "What is the lattice constant of silicon in Ångströms?") print("Lattice constant of silicon:", c) derivation: str = await agent.call("And how is this constant derived?") print("Derivation of lattice constant:", derivation) ``` ```typescript TypeScript wrap theme={null} await using agent = await spawn({ premise: "You are a helpful assistant." }); const c = await agent.call("What is the lattice constant of silicon in Ångströms?"); console.log("Lattice constant of silicon:", c); const derivation = await agent.call("And how is this constant derived?"); console.log("Derivation of lattice constant:", derivation); ``` See the API references: [Python](/references/python/agents) | [TypeScript](/references/ts/agents). ## Use your tools and types **Any** function, object, method, or other runtime value can be directly exposed as resources your agent can interact with. No need to set up MCP servers. Expose the full programmatic power of an SDK or API directly to your agent. Make them available when spawning the agent and/or pass per-invocation resources. You can also **expose existing remote or local MCP tools** by passing an MCP configuration path. See [here](/concepts/unmcp) for more information. ```python Python wrap theme={null} agent = await spawn(premise="You are a helpful researcher.") gdp: float = await agent.call( float, "What percentage of US GDP is from California?", web_search=web_search, ) print(f"Percentage: {gdp:.1f}%") ``` ```typescript TypeScript wrap theme={null} await using agent = await spawn({ premise: 'You are a helpful researcher.' }); const gdp = await agent.call( 'What percentage of US GDP is from California?', { webSearch }, ); console.log(`Percentage: ${gdp.toFixed(2)}%`); ``` * Objects passed to `scope` or as arguments are presented **without** private methods or field names (fields with a leading `_`). * Async functions in `scope` are exposed to the REPL as synchronous functions returning `Future[T]`. The REPL includes a top-level event loop, so agents can `await` these futures directly and use standard patterns like `asyncio.gather()`. ## Multi-agent orchestration Multi-agent orchestration becomes straightforward. Agents can trigger sub-agents by passing `spawn` in `scope`, enabling completely dynamic agent delegation. ```python Python wrap theme={null} agent = await spawn(premise="You are an agent orchestrator.", model="openai/gpt-5.2") result = await agent.call( tuple[int, int], "Use one sub-agent to compute 3**32 and another to compute 3**34, then return both results.", spawn=spawn, ) assert result == (3**32, 3**34) print(result) ``` ```typescript TypeScript wrap theme={null} await using agent = await spawn({ premise: 'You are an agent orchestrator.', model: 'openai/gpt-5.2', }); async function subAgent(task: string): Promise { return await agentic(task, { pow: Math.pow }); } const result = await agent.call<[number, number]>( 'Use one sub-agent to compute 3**32 and another to compute 3**34, then return both results.', { subAgent }, ); console.log(result); ``` ## Streaming Stream responses as they are being generated. ```python Python wrap theme={null} import asyncio from agentica import spawn from agentica.logging.loggers import StreamLogger agent = await spawn(premise='You are a mathematician.', model='openai/gpt-5.2') stream = StreamLogger() with stream: root = asyncio.create_task( agent.call(float, 'Define a Newton–Raphson solver, and use it to solve for a root of a polynomial of your choice.') ) role = None async for chunk in stream: if role is None and chunk.role == 'user': continue # Skip first user message if role != chunk.role: print(f"\n\n--- {chunk.role} ---") role = chunk.role print(chunk, end='', flush=True) print('\n') print('root =', await root) ``` ```typescript TypeScript wrap theme={null} await using agent = await spawn({ premise: 'You are a mathematician.', model: 'openai/gpt-5.2' }); let role: string | null = null; function print(iid: string, chunk: any) { if (role === null && chunk.role === 'user') { return; // Skip first user message } if (role !== chunk.role) { process.stdout.write(`\n\n--- ${chunk.role} ---\n`); role = chunk.role; } process.stdout.write(chunk.content); } const root = await agent.call( 'Define a Newton–Raphson solver, and use it to solve for a root of a polynomial of your choice.', { }, { listener: print } ); console.log('\n'); console.log('root =', root); ``` ```` --- agent --- ```python def newton_raphson(f, df, x0, tol=1e-8, max_iter=100): x = x0 for _ in range(max_iter): fx = f(x) dfx = df(x) if abs(dfx) < 1e-12: break # Avoid division by zero x_new = x - fx / dfx if abs(x_new - x) < tol: return float(x_new) x = x_new return float(x) # Polynomial: x^3 - x - 2 = 0 def f(x): return x**3 - x - 2 def df(x): return 3*x**2 - 1 return newton_raphson(f, df, 1.5) ``` --- user --- 1.5213797068045676 root = 1.5213797068045676 ```` ## Chat with your agents Create a simple chat loop using streaming. Consume the stream before awaiting the final result to see live generation. ```python Python wrap theme={null} import asyncio from agentica import spawn from agentica.logging import set_default_agent_listener from agentica.logging.loggers import StreamLogger RED = "\033[91m" GREEN = "\033[92m" PURPLE = "\033[95m" RESET = "\033[0m" GREY = "\033[90m" set_default_agent_listener(None) async def chat(): agent = await spawn(premise='You are a helpful assistant.', model='openai/gpt-5.2') while user_input := input(f"\n{PURPLE}User{RESET}: "): try: # Invoke agent against user prompt stream = StreamLogger() with stream: result = asyncio.create_task( agent.call(str, user_input) ) # Stream intermediate "thinking" to console print(GREY) async for chunk in stream: if chunk.role == 'agent': print(chunk, end="", flush=True) print(RESET) # Print final result print(f"\n{GREEN}Agent{RESET}: {await result}") except Exception as agent_error: print(f"\n{RED}Error: {agent_error}{RESET}") if __name__ == '__main__': asyncio.run(chat()) ``` ```typescript TypeScript theme={null} import { spawn } from '@symbolica/agentica'; import * as readline from 'readline'; const RED = '\x1b[91m'; const GREEN = '\x1b[92m'; const PURPLE = '\x1b[95m'; const RESET = '\x1b[0m'; const GREY = '\x1b[90m'; async function chat() { await using agent = await spawn({ premise: 'You are a helpful assistant.', model: 'openai/gpt-5.2' }); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const question = (prompt: string): Promise => { return new Promise((resolve) => rl.question(prompt, resolve)); }; while (true) { const userInput = await question(`\n${PURPLE}User${RESET}: `); if (!userInput) break; try { // Invoke agent against user prompt process.stdout.write('\n' + GREY); const result = await agent.call(userInput, {}, { listener: (iid: string, chunk: any) => { if (chunk.role === 'agent' && chunk.content) { process.stdout.write(chunk.content); } } } ); process.stdout.write(RESET + '\n'); // Print final result console.log(`\n${GREEN}Agent${RESET}: ${result}`); } catch (error) { console.log(`\n${RED}Error: ${error}${RESET}`); } } rl.close(); } chat(); ``` That's all it takes! ## Advanced You can expose custom exceptions in scope so they can be raised from within execution (see [Advanced](/guides/agent-errors), including information on logging, retries, rate-limiting and prefix caching). For more examples, see [Examples](/guides/examples). # Agentic Functions Source: https://docs.symbolica.ai/concepts/agentic Use agentic to execute any function with an agent. **This is detailed usage documentation.** New to the Agentica SDK? Start with the [Quickstart](/quickstart) or learn [when to use agentic functions vs agents](/concepts/agentic-vs-agents). ## Using agentic **Agentic functions** are stateless, decorator‑based functions implemented by the model; each call is independent. The de facto way to implement an agentic function is via the `@agentic` decorator. ### When to use agentic functions Agentic functions work best for completing simple, well-defined tasks that **don't benefit from maintaining context** across multiple operations. For longer-running, multi-task, contextual problem solving, see [Agents](/concepts/agent). ## The basics The Agentica SDK's `agentic` decorator enables you to **turn regular Python or TypeScript functions into agent-backed functions**. Define a function with a descriptive prompt or docstring and simply call it like any other function. Function bodies in Python should contain a descriptive doc-string, but otherwise be empty (body contains `...`). See [here](/guides/prompting#writing-effective-prompts) for best practices on writing effective doc-strings. ```python Python wrap theme={null} from agentica import agentic @agentic() async def rhymes(word_a: str, word_b: str) -> bool: """ Returns whether `word_a` rhymes with `word_b`. """ ... ``` ```typescript TypeScript wrap theme={null} import { agentic } from '@symbolica/agentica'; async function rhymes(wordA: string, wordB: string): Promise { return await agentic( `Returns whether wordA rhymes with wordB`, { wordA, wordB } ); } ``` See the full API in the references: [Python](/references/python/agentic) | [TypeScript](/references/ts/agentic). ## Use your tools and types Expose the full programmatic power of an SDK or API. No MCP server is required. Simply pass it into your agentic functions `scope`. You can also **expose existing remote or local MCP tools** by passing an MCP configuration path. See [here](/concepts/unmcp) for more information. **Prerequisites**: * **Python**: run `pip install art` or `uv add art` * **TypeScript**: run `npm install figlet` (or use `pnpm`, `bun`) ```python Python wrap theme={null} from agentica.logging import set_default_agent_listener set_default_agent_listener(None) from agentica import agentic from art import text2art @agentic(text2art) async def greet(name: str) -> str: """ Use the provided function to create a fancy greeting. """ ... print(await greet("agentica")) ``` ```typescript TypeScript wrap theme={null} import { agentic } from '@symbolica/agentica'; import figlet from 'figlet'; async function greet(name: string): Promise { return await agentic( `Use the provided function to create a fancy greeting`, { figletText: figlet.text, name }, ); } console.log(await greet("agentica")); ```
``` __ __ ____ ___ __ _ / / / /__ / / /___ / | ____ ____ ____ / /_(_)________ _ / /_/ / _ \/ / / __ \ / /| |/ __ `/ _ \/ __ \/ __/ / ___/ __ `/ / __ / __/ / / /_/ / / ___ / /_/ / __/ / / / /_/ / /__/ /_/ / /_/ /_/\___/_/_/\____( ) /_/ |_\__, /\___/_/ /_/\__/_/\___/\__,_/ |/ /____/ ```
* Objects passed to `scope` or as arguments are presented **without** private methods or field names (fields with a leading `_`). * Async functions in `scope` are exposed to the REPL as synchronous functions returning `Future[T]`. The REPL includes a top-level event loop, so agents can `await` these futures directly and use standard patterns like `asyncio.gather()`. ## Streaming Stream responses as they are being generated. ```python Python theme={null} import asyncio from agentica import agentic from agentica.logging.loggers import StreamLogger @agentic(model='openai/gpt-5.2') async def word_counter(corpus: str) -> int: """Returns the number of words in the corpus.""" ... stream = StreamLogger() with stream: res = asyncio.create_task( word_counter("True! -- nervous -- very, very dreadfully nervous I had been and am; but why will you say that I am mad? The disease had sharpened my senses -- not destroyed -- not dulled them. Above all was the sense of hearing acute. I heard all things in the heaven and in the earth. I heard many things in hell. How, then, am I mad? Hearken! and observe how healthily -- how calmly I can tell you the whole story.") ) async for chunk in stream: if chunk.role == 'agent': print(chunk, end="", flush=True) print() print(await res) ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; async function summarize(text: string): Promise { const result = await agentic( `Summarize the following text in two sentences.`, { text }, { listener: (iid, chunk) => process.stdout.write(chunk.content) } ); return result; } const summary = await summarize( 'True! -- nervous -- very, very dreadfully nervous I had been and am; but why will you say that I am mad? The disease had sharpened my senses -- not destroyed -- not dulled them. Above all was the sense of hearing acute. I heard all things in the heaven and in the earth. I heard many things in hell. How, then, am I mad? Hearken! and observe how healthily -- how calmly I can tell you the whole story.' ); console.log('\nFinal summary:', summary); ``` ## Using MCP ```python Python wrap theme={null} from dataclasses import dataclass from agentica import agentic @dataclass class Report: name: str blurb: str @agentic(mcp="./my-mcp.json") async def run_report(company: str) -> Report: """ Create a brief company report for the given company name. Returns a report with: - name: The official company name - blurb: A 1-2 sentence description of the company's main business focus """ ... ``` ```typescript TypeScript wrap theme={null} // MCP support via unMCP coming soon for TypeScript ``` ## Advanced You can expose custom exceptions in scope so they can be raised from within execution (see [here](/guides/agent-errors), including information on logging, retries, rate-limiting and prefix caching). For more examples, see [Examples](/guides/examples). # Agentic Functions vs Agents Source: https://docs.symbolica.ai/concepts/agentic-vs-agents Understanding when to use each pattern ## The Core Difference * [**Agentic Functions**](#agentic-functions) are **stateless** -- each call is independent * [**Agents**](#agents) are **stateful** -- they maintain context across calls ## Choosing Between Them * **Single jump vs journey**: Use an agentic function when the job is “Given X, return Y” in one call; use an agent when you naturally say “First do A, then based on that do B, then refine with C…”. * **Isolation vs shared context**: Agentic calls are independent and great for extraction, transformation, and batch jobs; agents keep conversation and REPL history, so later steps can build on earlier reasoning. * **Pipeline step vs orchestrator**: Agentic functions plug in as pure steps inside existing pipelines; agents own longer-lived workflows, conversations, and tool orchestration where they steer the process. * **Cost profile**: Agentic functions scale to many cheap calls with predictable behavior; agents are heavier but better suited for deeper, higher-value tasks where the extra context and adaptability pay off. Decision matrix: Agentic Functions vs. Agents
} icon="table"> | Situation / Requirement | Agentic Functions | Agents | Why | | -------------------------------------------------------------------- | ---------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------- | | You can phrase the job as “Given X, produce Y” in one shot | ✅ **Best fit** | ⚠️ Overkill | Agentic functions are optimized for single, stateless transforms. | | Each input item is independent (batches, lists, records) | ✅ **Best fit** | ❌ Not appropriate | No cross-item memory needed; agent state adds no benefit. | | You need to keep and reuse context across multiple steps | ❌ Loses context each call | ✅ **Best fit** | Agents maintain conversation and REPL history. | | Later steps depend tightly on earlier agent outputs | ⚠️ Must manually pass everything back in | ✅ **Best fit** | With agents, the prior reasoning is “already in the room.” | | The workflow is conversational or exploratory | ❌ Awkward (you’d simulate a chat) | ✅ **Best fit** | Agents are built for back-and-forth refinement. | | You want strict, predictable “pure function” behavior | ✅ **Best fit** | ⚠️ Possible but harder to reason about | Stateless calls are easier to test, debug, and cache. | | You want agents to orchestrate tools over time (plan → act → adjust) | ⚠️ Only for simple tool calls | ✅ **Best fit** | Agents shine in multi-step orchestration loops. | | You need to run at scale (thousands of similar calls) | ✅ **Best fit** | ❌ Expensive & complex | Stateless calls parallelize and scale horizontally. | | You have strict latency or cost budgets per operation | ✅ Typically cheaper, more predictable | ⚠️ Higher overhead | Agent state and multiple turns increase cost/latency. | | You’re integrating into an existing pipeline as a “smart function” | ✅ Natural drop-in | ⚠️ Integration overhead | Agentic functions look like any other function; agents introduce sessions. | | You need to inspect / audit behavior per call | ✅ Narrow, local reasoning | ⚠️ State makes it harder to replay | Stateless calls are easier to log and replay in isolation. | ## Agentic Functions Stateless AI operations. Each call has a fresh [REPL](/concepts/how-it-works), and is independent with no memory of previous calls. Use these for: * Extraction, transformation, loading data into structures * Batch processing independent items * Single-shot generation or classification * Pure functions with AI logic ```python Python theme={null} @agentic() async def extract_order_from_email(email: str) -> Order: """Extract the customer order details""" ... @agentic(get_bug_reports) async def group_by_severity() -> dict[Literal["severe", "medium", "low"], list[BugReport]]: """Obtain the bug reports, read them, and group by severity""" ... ``` ```typescript TypeScript theme={null} async function extractOrderFromEmail(email: string): Promise { return agentic("Extract the customer order details", { email }); } async function groupBySeverity(): Promise> { return agentic("Obtain the bug reports, read them, and group by severity", { getBugReports }); } ``` ## Agents Stateful AI workflows. Maintains conversation and [REPL history](/concepts/how-it-works) and builds on previous interactions. Use these for: * Multi-step workflows where the steps have serial dependencies * Conversational interfaces * Iterative refinement * Complex orchestration ```python Python theme={null} agent = await spawn(premise="You are a data analyst") analysis = await agent.call(str, "Analyze these sales figures", sales_data=data) recommendation = await agent.call(str, "Based on those trends, what should we focus on?") revised = await agent.call(str, "Revise that assuming a 20% budget cut") ``` ```typescript TypeScript theme={null} await using agent = await spawn({ premise: "You are a data analyst" }); const analysis = await agent.call("Analyze these sales figures", { data }); const recommendation = await agent.call("Based on those trends, what should we focus on?"); const revised = await agent.call("Revise that assuming a 20% budget cut"); ``` ## Next Steps Documentation Documentation UnMCP Working examples # How It Works Source: https://docs.symbolica.ai/concepts/how-it-works Understanding the execution model ## Overview The Agentica SDK allows agents to write and execute **arbitrary Python code** in a **sandboxed execution environment**, while maintaining **direct access to objects in your runtime**. ## Why? The Agentica SDK is built on the premise that **code is the most expressive interface through which models can interact with their environment**; with the Agentica SDK, agents can manage and **engineer their own context**, manipulate and return objects **by reference** and dynamically **create their own tools**. Check out the blog post [Beyond Code Mode: The Agentica SDK](https://symbolica.ai/blog/introducing-agentica). ## The Mental Model This is achieved by **Warp**, the Agentica SDK's protocol that combines **Remote Procedure Call (RPC)** with **transparent proxying in a Python REPL** using a language-agnostic object model. * **Sandboxed execution**: Agents write Python code that is executed in a safe, isolated environment * **Warp bridge**: Functions you pass in [scope](/concepts/scope) appear in the sandbox as stubs, but execute in your runtime * **Your code stays local**: All actual computation happens in your process with full access to your dependencies * **Objects are warped**: Return values from your functions are represented as lightweight proxies in the sandbox, not fully serialized * **Type safety enforced**: Return values are validated against your type annotations ## Anatomy of an Invocation Let's walk through exactly what happens when you call an agentic function (much like an agent). Consider the simplified example below where we have elided portions of the code for clarity. ```python Python theme={null} from agentica import agentic # Your existing types and functions class OrderResult: ... class CustomerTier: ... def get_customer_tier(name: str) -> CustomerTier: ... def calculate_discount(tier: CustomerTier) -> float: ... @agentic(get_customer_tier, calculate_discount) async def process_order(customer_name: str, base_price: float) -> OrderResult: """Look up customer tier, calculate discount, and create order""" ... # Call the function result = await process_order("Alice", 100.0) ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; // Your existing types and functions class OrderResult { ... } class CustomerTier { ... } function getCustomerTier(name: string): CustomerTier { ... } function calculateDiscount(tier: CustomerTier): number { ... } async function processOrder(customerName: string, basePrice: number): Promise { return agentic( "Look up customer tier, calculate discount, and create order", { customerName, basePrice, getCustomerTier, calculateDiscount } ); } // Call the function const result = await processOrder("Alice", 100.0); ``` ### What Happens Behind the Scenes When `process_order("Alice", 100.0)``await processOrder("Alice", 100.0)` is called, it triggers the following interaction. **1. The Agentica SDK sends a request to the underlying model** with: * The instruction: `"Look up customer tier, calculate discount, and create order"` * The input parameters: `customer_name = "Alice"`, `base_price = 100.0``customerName = "Alice"`, `basePrice = 100.0` * The signatures and docstrings of functions in [scope](/concepts/scope): `get_customer_tier()`, `calculate_discount()``getCustomerTier()`, `calculateDiscount()` * The details of the types in [scope](/concepts/scope): `OrderResult`, `CustomerTier` * The details of the expected return type, `OrderResult` **2. The agent interactively writes and evaluates code in a sandboxed REPL environment**. The agent always writes **Python code** in the sandbox, regardless of whether you're using the Python or TypeScript SDK. From its perspective, the Python REPL in the sandbox has everything in [scope](/concepts/scope): `get_customer_tier`, `calculate_discount`, `OrderResult`, `CustomerTier`, etc. The agent can write and evaluate code like normal. Here's what a sample REPL session could look like. ````markdown wrap expandable An example of an agent's output in the Agentica SDK. theme={null} [Agent] I should begin by obtaining the customer tier ```python tier = get_customer_tier(customer_name) tier ``` [Execution] ``` CustomerTier(level='gold', benefits=['free_shipping', 'priority_support']) ``` [Agent] This must be an important customer ```python I need to calculate the discount factor discount = calculate_discount(tier) discount ``` [Execution] ``` 0.15 ``` [Agent] That looks reasonable, a 15% discount. I should compute the price using the discount ```python final_price = base_price * (1 - discount) final_price ``` [Execution] ``` 85.0 ``` [Agent] Good, let me assemble the order ```python return OrderResult( customer=customer_name, original_price=base_price, discount=discount, final_price=final_price ) ``` [Execution] ``` No output was produced. ``` ```` **Async Functions in the REPL:** The REPL includes a top-level event loop, so async functions work naturally. When you pass async functions from your runtime, they appear in the REPL as functions returning `Future[T]` (Python `async def foo(...) -> T` becomes `def foo(...) -> Future[T]`, TypeScript `async function foo(...): Promise` similarly translates). The agent can use top-level `await`, and standard patterns like `asyncio.gather()` work as expected. ### A breakdown Let's break down the key lines from the agent's output above: ```python theme={null} tier = get_customer_tier(customer_name) ``` This calls a **stub function** in the sandbox — `get_customer_tier` was never defined in the sandbox, but Warp makes it *appear* to be present in the agent's execution environment. The stub intercepts the call and sends an RPC to **your runtime**, where your actual `get_customer_tier()` executes with access to your database, environment, etc. ```python theme={null} tier CustomerTier(level='gold', benefits=['free_shipping', 'priority_support']) ``` The return value is **warped** back to the sandbox. On inspection, the agent sees what looks like a `CustomerTier` object, but it's actually a lightweight reference to the real object in your runtime. ```python theme={null} discount = calculate_discount(tier) ``` When the agent passes `tier` to another function, Warp resolves the proxy back to the real object. Your actual `calculate_discount()` executes in your runtime with the real `CustomerTier` object. ```python theme={null} final_price = base_price * (1 - discount) ``` Simple calculations execute directly in the sandbox -- no RPC needed for basic operations. ```python theme={null} result = OrderResult(...) ``` Instantiating `OrderResult` triggers your actual class constructor in your runtime via Warp, returning another proxy. **3. The result type is validated and returned** to your code: ```python theme={null} result = await process_order("Alice", 100.0) # Returns: OrderResult(customer="Alice", original_price=100.0, discount=0.15, final_price=85.0) ``` Observe that no schema was generated or needed to return a value to your code. Instead an object was instantiated in your runtime and the Agentica SDK ensures that the type of `result` matches the required return type (`OrderResult`), which ensures type safety. ### Current limitations Agents and agentic functions currently cannot: * define a type and then return an instance of that type * return functions, types, or generators that they have defined themselves **Expanded module support**: Entire modules (e.g. `numpy`, `pandas`, `scipy`, `sympy`), C-implemented functions, and pure lambdas can now be passed from the client into agent scope and warped (proxied by reference) to the sandbox. ### Bug reports To report bugs and errors to the Agentica team, please create an issue in the relevant GitHub repo ([Python SDK](https://github.com/symbolica-ai/agentica-python-sdk) or [TypeScript SDK](https://github.com/symbolica-ai/agentica-typescript-sdk)) and include the agent logs (filenames are printed to output). Happy programming! ## Next Steps Learn about passing functions, state, and types Understand when to use each Dive into agentic function usage See practical examples # Python Source: https://docs.symbolica.ai/concepts/logging_and_streaming/python Learn how to log and stream agents and agentic functions in Python. The Agentica Python SDK exposes **utilities that listen to and log all agent and agentic function invocations and interactions**. This includes both chat histories in and out of the REPL, as well as outputs of code execution in the REPL. This is useful for debugging, monitoring, and understanding your agent's behavior. In Python, the `logging` module has the following structure: ``` agentica.logging ├── AgentListener ├── PrintOnlyListener ├── FileOnlyListener ├── StandardListener ├── AgentLogger ├── NoLogging └── loggers ├── StreamLogger ├── PrintLogger ├── FileLogger └── StandardLogger ``` In short, an `AgentListener` listens to an HTTP endpoint, while an `AgentLogger` defines the logging behaviour itself e.g. printing and file logging. Think of listeners as the "when" and "where" of logging, while loggers are the "what" and the "how". ## Specifying logging behaviour The Agentica Python SDK provides **three ways** to specify how agents and agentic functions are logged, each with **different scopes and priorities**. Understanding the hierarchy helps you control logging behavior precisely for your use case. ### Listener priority hierarchy When an agent or agentic function is invoked, listener resolution follows this priority (highest to lowest): 1. **Contextual loggers** (via context manager) - highest priority, temporary 2. **Per-agent/agentic function listener** (via `listener` parameter) - medium priority, per-instance 3. **Default agent listener** (via `set_default_agent_listener`) - lowest priority, global If contextual loggers are active, they are **added to** (not replace) any configured listener, creating a `CompositeLogger` that routes events to all loggers simultaneously. ### Method 1: Default listener (global) The **default agent listener** for all agents and agentic functions is the `StandardListener`. You can change this globally with `set_default_agent_listener`. ### Method 2: Per-agent/agentic function listener Override the default for specific agents or agentic functions: ```python wrap theme={null} from agentica.logging import ( PrintOnlyListener, FileOnlyListener ) # Attach listener to a specific agent agent = await spawn( premise="Agent's task", listener=PrintOnlyListener ) # Attach listener to a specific agentic function @agentic(listener=FileOnlyListener) async def my_func(a: int) -> str: ... ``` This is useful when you want different logging behavior for different agents or functions in your application. ### Method 3: Contextual loggers (scoped) Use any `AgentLogger` to temporarily control logging for all agents and agentic functions spawned and invoked within that scope: This is particularly useful for **temporarily changing logging behavior** for a specific section of code - for example, to debug a particular workflow or to separate logs for different operations. ```python wrap theme={null} from agentica import spawn from agentica.logging.loggers import ( FileLogger, StandardLogger ) agent = await spawn(premise="Helpful agent.") # Temporarily log to file only with FileLogger(): await agent.call(int, "Calculate 2 + 2") # FileLogger is ADDED to any configured listener # Outside context, normal behavior resumes await agent.call(float, "Calculate the fifth root of 93") ``` **Multiple contextual loggers** can be nested - all will be active: ```python wrap theme={null} from agentica.logging.loggers import PrintLogger, FileLogger with PrintLogger(): with FileLogger(): # Both PrintLogger AND FileLogger are active agent = await spawn(premise="Dual logging") await agent.call(str, "Hello") ``` **Disable logging temporarily** with `NoLogging`: ```python wrap theme={null} from agentica.logging.agent_logger import NoLogging with NoLogging(): # No logging occurs for agents spawned here agent = await spawn(premise="Silent agent") await agent.call(int, "Calculate something") ``` ## Built-in listeners and loggers The Agentica Python SDK offers built-in listeners and loggers with various combinations of behaviour, notably printing to `stdout` and writing to `.log` files. | Logger | Listener | `stdout` | `.log` | | :--------------: | :-----------------: | :-------------------: | :-------------------: | | `StandardLogger` | `StandardListener` | | | | `PrintLogger` | `PrintOnlyListener` | | | | `FileLogger` | `FileOnlyListener` | | | | `NoLogging` | | | | | `CaptionLogger` | | | | | `StreamLogger` | | | | The `.log` files include * writing full chat histories to per‑agent files under `./logs/` (e.g., `agent-7.log`), * allocates incrementing agent IDs based on existing files, and * auto‑creating the logs directory (with a `.gitignore`). ````xml wrap theme={null} Work out the 32nd power of 3 ```python result = 3**32 ``` ```` Printing to `stdout` includes * assigning stable colors per agent, * printing on spawning an agent, and * printing the result of an invocation an agent or an agentic function. ```shell wrap theme={null} Spawned Agent 25 (./logs/agent-25.log) ► Agent 25: Get a subagent to work out the 32nd power of 3, then another subagent to work out the 34th power, then return both results. Spawned Agent 26 (./logs/agent-26.log) ► Agent 26: Work out the 32nd power of 3 ◄ Agent 26: 1853020188851841 Spawned Agent 27 (./logs/agent-27.log) ► Agent 27: Work out the 34th power of 3 ◄ Agent 27: 16677181699666569 ◄ Agent 25: (1853020188851841, 16677181699666569) ``` `StandardLogger` also prints reasoning content inside `` tags for both OpenAI and Anthropic models. ## Streaming ### `Chunk` Each streamed piece of content is a `Chunk` with an optional `type` field for distinguishing different kinds of streamed content: ```python theme={null} @dataclass class Chunk: role: Role # 'agent', 'user', or 'system' content: str # The text content of the chunk type: str | None # 'reasoning', 'output_text', 'code', 'usage', 'invocation_exit', or None ``` **Handling typed chunks:** ```python theme={null} async def on_chunk(chunk: Chunk): if chunk.type == "reasoning": print(f"[thinking] {chunk.content}") elif chunk.type == "output_text": print(chunk.content, end="") elif chunk.type == "usage": print(f"[usage] {chunk.content}") ``` ### `StreamLogger` `StreamLogger` exposes an async iterator over the agent's text-generation stream. It also accepts an `on_chunk` callback and an `include_usage` flag: ```python theme={null} stream = StreamLogger( on_chunk=my_async_handler, # Optional: async callback for each chunk include_usage=True, # Optional: include usage chunks (default False) ) ``` **Using `on_chunk` for real-time forwarding:** ```python theme={null} async def forward(chunk: Chunk): await websocket.send(chunk.content) stream = StreamLogger(on_chunk=forward, include_usage=True) ``` By default, usage chunks (`chunk.type == 'usage'`) are filtered out to avoid breaking existing consumers. Set `include_usage=True` to receive them. # TypeScript Source: https://docs.symbolica.ai/concepts/logging_and_streaming/typescript Learn how to log and stream agents and agentic functions in TypeScript. The Agentica TypeScript SDK exposes **utilities that listen to and log all agent and agentic function invocations and interactions**. This includes both chat histories in and out of the REPL, as well as outputs of code execution in the REPL. This is useful for debugging, monitoring, and understanding your agent's behavior. ## Listeners Listeners enable you to **observe and log agent and agentic function invocations, chat histories, and interactions**. This includes both chat histories in and out of the REPL, as well as outputs of code execution in the REPL. In TypeScript, this takes the form of callbacks referred to as listeners; every time a text chunk is generated (a token in the case of the `agent`), this callback is triggered, sending the chunk along with the ID for which the generation is a part of. ```typescript wrap theme={null} (iid: string, chunk: Chunk) => void ``` The parameters are: * `iid`: a unique invocation ID for the specific invocation/call * `chunk`: a `Chunk` object: ```typescript theme={null} interface Chunk { role: 'user' | 'agent' | 'system'; content: string; type?: string; // 'reasoning', 'output_text', 'code', 'usage', etc. } ``` The `type` field allows consumers to handle different kinds of streamed content (e.g. distinguishing reasoning traces from output text). The TypeScript SDK allows users to provide a callback to * `spawn` in `AgentSpawnConfig`, * `Agent.call` in `AgentCallConfig` and * `agentic` in `AgenticConfig` via the parameter `listener`. ### Usage Chunks By default, usage-reporting chunks (`chunk.type === 'usage'`) are filtered out of the listener stream. To include them, set `listenerIncludeUsage: true`: ```typescript theme={null} // At spawn time (applies to all calls) const agent = await spawn({ premise: "...", listener: (iid, chunk) => console.log(chunk.content), listenerIncludeUsage: true, }); // Or per-call const result = await agent.call("...", {}, { listener: (iid, chunk) => console.log(chunk.content), listenerIncludeUsage: true, }); ``` ## Streaming # Scope Source: https://docs.symbolica.ai/concepts/scope The unified interface for functions, state, and types ## Overview While other frameworks have separate concepts for tools, state, and schemas, the Agentica SDK's unique model unifies all of these into a single concept: **scope**. After all, it's just code. Agents built with the Agentica SDK operate via [REPL and Warp RPC](/concepts/how-it-works), so they can directly access anything you pass to them. This includes: * **Functions** which replace the need for bespoke "tools" * **Variables** which replace the need for explicit "state" objects * **Types** which replace the need for special "schemas" This means you may write normal code with normal objects, and agents can use them directly. ## Types Replace Schemas Instead of defining schemas in a special format, just pass your classes. The agent will instantiate them, and if your validation logic fails, the error is passed back to the agent to fix. ```python Python theme={null} from agentica import agentic from dataclasses import dataclass from typing import Literal @dataclass class PaymentRequest: amount: float currency: Literal["USD", "EUR", "GBP"] customer_email: str def __post_init__(self): if self.amount <= 0: raise ValueError("Amount must be positive") if "@" not in self.customer_email: raise ValueError("Invalid email address") @agentic(PaymentRequest) async def extract_payment_info(invoice_text: str) -> PaymentRequest: """Parse invoice and return a validated PaymentRequest""" ... ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; type Currency = 'USD' | 'EUR' | 'GBP'; class PaymentRequest { amount: number; currency: Currency; customerEmail: string; constructor(amount: number, currency: Currency, customerEmail: string) { if (amount <= 0) { throw new Error("Amount must be positive"); } if (!customerEmail.includes("@")) { throw new Error("Invalid email address"); } this.amount = amount; this.currency = currency; this.customerEmail = customerEmail; } } async function extractPaymentInfo(invoiceText: string): Promise { return agentic( "Parse invoice and return a validated PaymentRequest", { invoiceText, PaymentRequest } ); } ``` ## Functions Replace Tools Instead of wrapping functions in special tool definitions or separating the concept of functions and tools, just pass your functions directly. ```python Python theme={null} from agentica import agentic # Your existing classes class OrderResult: ... class CustomerTier: ... # Your existing functions def get_customer_tier(name: str) -> CustomerTier: ... def calculate_price(tier: CustomerTier, base_price: float) -> float: ... @agentic(get_customer_tier, calculate_price) async def process_order(customer_name: str, base_price: float) -> OrderResult: """Look up customer tier, calculate price, and create order""" ... ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; // Your existing classes class OrderResult { ... } class CustomerTier { ... } // Your existing functions function getCustomerTier(name: string): CustomerTier { ... } function calculateDiscount(tier: CustomerTier): number { ... } async function processOrder(customerName: string, basePrice: number): Promise { return agentic( "Look up customer tier, calculate price, and create order", { customerName, basePrice, getCustomerTier, calculateDiscount } ); } ``` ## Variables Replace State Because the Agentica SDK can warp objects by reference as well as by value, you can give agents access to stateful resources like functions, database connections, third party modules, instantiated API clients, even custom classes! You can either provide such resources to an agentic function by passing them through function arguments -- so they can differ from call to call -- or as global resources present at the time you define the agentic function itself. ```python Python theme={null} from agentica import agentic import numpy as np import sqlite3 db = sqlite3.connect("inventory.db") # Stateful resources that agentic function can interact with resources = dict( db=db, cache=RedisClient(url="redis://localhost"), current_user=User(id="123", tier="gold") ) # Pass entire modules and live connections directly @agentic(np, scope=resources) async def reorder_check(threshold: float) -> list[str]: """Query the products table for items whose stock is below `threshold` standard deviations from the mean, and return their names.""" ... low_stock = await reorder_check(1.5) ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; // Your existing class class Product { ... } // Stateful objects that can't be serialized const dbConnection = new DatabaseConnection({ host: "prod-db" }); const cache = new RedisClient({ url: "redis://localhost" }); const currentUser = new User({ id: "123", tier: "gold" }); async function getPersonalizedRecommendations(): Promise { return agentic( "Query database for user's history, check cache, and generate recommendations", { dbConnection, cache, currentUser } ); } ``` ## Summary: Why Scope **Scope matches how programming works.** In any programming language, functions have scope -- they can access variables, call other functions, and use types. The Agentica SDK's agentic functions and agents work the same way. If agentic functions are just functions, they naturally have scope and we match that expectation. **Everything is data.** Because the Agentica SDK treats everything -- functions, variables, types, even SDK clients -- as data, you can pass anything to anything. This includes: * Passing functions to agentic functions and vice versa; typed function composition and hand-offs. * Passing `Agent` objects to other agents (enabling dynamic multi-agent systems). * Passing SDK clients, database connections, or any object that is live in your codebase. **One concept instead of three.** Other frameworks force you to learn separate APIs for tools, state, and schemas. The Agentica SDK just uses scope -- the same concept you already know from programming. After all, it's just code. ## Next Steps Understand when to use each Dive into agentic function usage Learn about agent usage Learn how we unwrap MCP # Un-MCP Source: https://docs.symbolica.ai/concepts/unmcp Un-MCP support is currently only available in Python. ## Why Some **tools are only exposed via MCP servers**, so the Agentica Python SDK allows users to **provide an MCP config for agents and agentic functions** to use inside the REPL. This makes the Agentica Python SDK **backwards compatible** with things like **VSCode**, **Cursor** and **Claude Code** MCP configurations! ## How We provide backwards compatibility with MCP by **turning MCP tools back into regular functions** to be compatible with the Agentica SDK's execution model. We call this process **un-MCP**. Both **remote** and **local** MCP servers are **connected to from your local machine** meaning all sensitive information (e.g. API keys) is secure. See the [Python API reference](/references/python/agents) for more details on MCP configurations. ## Example Below is an example of an agent and an agentic function that can use tools from the Playwright MCP server in their REPL. ```python Agent wrap theme={null} from agentica import spawn from dataclasses import dataclass @dataclass class Report: """ Args: name: The official company name. blurb: A 1-2 sentence description of the company's main business focus. """ name: str blurb: str agent = await spawn(premise="You are a helpful assistant.") report = await agent.call(Report, f"Create a report on Google.", mcp="./mcp-config.json") ``` ```python Agentic function wrap theme={null} from agentica import agentic from dataclasses import dataclass @dataclass class Report: """ Args: name: The official company name. blurb: A 1-2 sentence description of the company's main business focus. """ name: str blurb: str @agentic(mcp="./mcp-config.json") async def run_report(company: str) -> Report: """ Create a brief company report for the given company name. """ ... ``` ```json mcp-config.json theme={null} { "mcpServers": { "playwright": { "command": "npx", "args": [ "@playwright/mcp@latest" ] } } } ``` # Agent Errors Source: https://docs.symbolica.ai/guides/agent-errors Handle custom exceptions raised by agents ## Overview When working with agents, you can design them to raise exceptions back to you when they encounter specific conditions. This allows you to handle domain-specific error cases gracefully. **Agent errors** occur when the agent intentionally raises an exception. This could happen if: * You told the agent to raise an exception in certain situations * The agent believes your task to be impossible or contradictory * The tools you provided to the agent are not working as expected Agent errors are different from [operational errors](/guides/operational-errors), which are platform-level failures like network issues, API timeouts, or sandbox errors. ## Basic Error Handling There are three approaches to handling agent failures: 1. **Result types**: Allow the agent to return a union -- either the desired type, or an error type (for example `None``null`). 2. **Builtin exceptions**: The agent may throw any of the builtin exceptions in your language's runtime (e.g., `ValueError`, `TypeError`). 3. **Custom exceptions**: Define your own exception classes and the agent can raise them based on your documented error conditions. ### Using Result Types The simplest approach is to allow the agent to return `None`/`null` when it cannot complete the task: ```python Python theme={null} from agentica import agentic @agentic() async def extract_date(text: str) -> tuple[int, int, int] | None: """ Extract date in YYYY-MM-DD format. Return None if no date found. """ ... try: date = await extract_date(document) if date is None: # Handle missing date date = "unknown" except Exception as e: logger.error(f"Failed to extract date: {e}") # Fallback logic ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; async function extractDate(text: string): Promise { return agentic( `Extract date in YYYY-MM-DD format. Return null if no date found.`, { text } ); } try { let date = await extractDate(document); if (date === null) { // Handle missing date date = "unknown"; } } catch (e) { logger.error(`Failed to extract date: ${e}`); // Fallback logic } ``` ## Custom Exceptions You can define your own exception classes and have the agent raise them when specific error conditions occur. This is useful for domain-specific error handling that goes beyond builtin exceptions. **Best practices for custom exceptions:** * Pass custom exceptions into the function or agent scope so they are available to raise * Clearly document when each exception should be raised in your docstringJSDoc comments (`/** ... */`) * Use descriptive exception names that indicate the error condition * Provide clear error messages that help diagnose the issue * The agent can see and understand your documentation to know when to raise each exception ```python Python expandable theme={null} from agentica import agentic # Define custom exceptions class InsufficientDataError(Exception): """Raised when the input data is incomplete or insufficient for analysis.""" pass class DataQualityError(Exception): """Raised when data quality is too poor for reliable results.""" pass class UnsupportedFormatError(Exception): """Raised when the data format is not supported.""" pass @agentic(InsufficientDataError, DataQualityError, UnsupportedFormatError) async def analyze_dataset(data: str) -> dict: """ Analyze the dataset and return insights. Raises: InsufficientDataError: If the dataset has fewer than 10 rows DataQualityError: If more than 50% of values are missing or invalid UnsupportedFormatError: If the data format is not CSV or JSON ValueError: If the data cannot be parsed Returns a dictionary with analysis results. """ ... # Use with try/except try: results = await analyze_dataset(raw_data) print(f"Analysis complete: {results}") except InsufficientDataError as e: logger.warning(f"Not enough data: {e}") results = {"status": "insufficient_data", "message": str(e)} except DataQualityError as e: logger.warning(f"Poor data quality: {e}") results = perform_basic_analysis(raw_data) # Fallback except UnsupportedFormatError as e: logger.error(f"Format not supported: {e}") results = {"status": "error", "message": "Please provide CSV or JSON"} except ValueError as e: logger.error(f"Parsing failed: {e}") raise ``` ```typescript TypeScript expandable theme={null} import { agentic } from '@symbolica/agentica'; // Define custom exceptions /** * Raised when the input data is incomplete or insufficient for analysis. */ class InsufficientDataError extends Error { constructor(message: string) { super(message); this.name = 'InsufficientDataError'; } } /** * Raised when data quality is too poor for reliable results. */ class DataQualityError extends Error { constructor(message: string) { super(message); this.name = 'DataQualityError'; } } /** * Raised when the data format is not supported. */ class UnsupportedFormatError extends Error { constructor(message: string) { super(message); this.name = 'UnsupportedFormatError'; } } interface AnalysisResult { status: string; insights?: string[]; message?: string; } /** * Analyze the dataset and return insights. * * @throws {InsufficientDataError} If the dataset has fewer than 10 rows * @throws {DataQualityError} If more than 50% of values are missing or invalid * @throws {UnsupportedFormatError} If the data format is not CSV or JSON * @throws {Error} If the data cannot be parsed */ async function analyzeDataset(data: string): Promise { return agentic( `Analyze the dataset and return insights. Throw InsufficientDataError if the dataset has fewer than 10 rows. Throw DataQualityError if more than 50% of values are missing or invalid. Throw UnsupportedFormatError if the data format is not CSV or JSON. Throw Error if the data cannot be parsed. Return a dictionary with analysis results.`, { data, InsufficientDataError, DataQualityError, UnsupportedFormatError } ); } // Use with try/catch try { const results = await analyzeDataset(rawData); console.log(`Analysis complete: ${results}`); } catch (e) { if (e instanceof InsufficientDataError) { logger.warn(`Not enough data: ${e}`); results = { status: "insufficient_data", message: e.message }; } else if (e instanceof DataQualityError) { logger.warn(`Poor data quality: ${e}`); results = await performBasicAnalysis(rawData); // Fallback } else if (e instanceof UnsupportedFormatError) { logger.error(`Format not supported: ${e}`); results = { status: "error", message: "Please provide CSV or JSON" }; } else if (e instanceof Error) { logger.error(`Parsing failed: ${e}`); throw e; } } ``` The agent can see your docstringsJSDoc comments (`/** ... */`)! Be specific about the conditions that should trigger each exception. The more precise your documentation, the more reliably the agent will raise the appropriate exception. ## Validation After Invocation Type annotations help guide agents, and constrain the types the agent is capable of returning, but sometimes you need additional validation logic not expressible in the type system. ### Agent-Visible Validation Validation logic may be realized in the type itself, such as during initialization of custom classes. In these cases, the agent can see validation errors and self-correct when returning back to you. ```python Python theme={null} from dataclasses import dataclass from agentica import agentic @dataclass class Price: amount: float currency: str def __post_init__(self): if self.amount < 0: raise ValueError("Price must be positive") if self.currency not in ['USD', 'EUR', 'GBP']: raise ValueError(f"Unsupported currency: {self.currency}") @agentic() async def extract_price(text: str) -> Price: """Extract price from text.""" ... ``` ```typescript TypeScript theme={null} class Price { amount: number; currency: string; constructor(amount: number, currency: string) { if (amount < 0) throw new Error("Price must be positive"); if (!['USD', 'EUR', 'GBP'].includes(currency)) throw new Error(`Unsupported currency: ${currency}`); this.amount = amount; this.currency = currency; } } async function extractPrice(text: string): Promise { return agentic("Extract price from text.", { text }); } ``` Here `Price` cannot be instantiated without satisfying the validation logic, and therefore cannot be returned by the agent until it is satisfied. ### Fine-Grained Validation Off-the-shelf validation libraries such as Pydantic (Python) or Zod (TypeScript) may be used to integrate with existing validation logic or describe more complex validation requirements. Pydantic provides powerful declarative validation through field constraints and custom validators. **Field-level constraints** can specify numeric ranges, string lengths, and other basic requirements: ```python Python theme={null} from pydantic import BaseModel, Field from typing import Literal class ProductReview(BaseModel): rating: int = Field(ge=1, le=5, description="Rating from 1-5") sentiment: Literal["positive", "negative", "neutral"] categories: list[str] = Field(min_length=1, max_length=5) summary: str = Field(min_length=10, max_length=200) ``` Not only do these fields provide basic validation, but they also provide excellent documentation for the agent. **Custom field validators** handle complex logic on individual fields using `@field_validator`: ```python Python theme={null} from pydantic import field_validator class ProductReview(BaseModel): # ... fields as above ... @field_validator('categories') @classmethod def validate_categories(cls, v: list[str]) -> list[str]: allowed = {'quality', 'price', 'service', 'delivery', 'packaging'} for category in v: if category not in allowed: raise ValueError(f"Invalid category: {category}") return v ``` **Cross-field validation** uses `@model_validator` to validate relationships between fields: ```python Python theme={null} from pydantic import model_validator class ProductReview(BaseModel): # ... fields and field_validator as above ... @model_validator(mode='after') def validate_sentiment_matches_rating(self) -> 'ProductReview': if self.rating >= 4 and self.sentiment == 'negative': raise ValueError("High rating inconsistent with negative sentiment") if self.rating <= 2 and self.sentiment == 'positive': raise ValueError("Low rating inconsistent with positive sentiment") return self ``` The agent sees Pydantic validation errors and adjusts its output to satisfy all constraints: ```python Python theme={null} from agentica import agentic @agentic() async def analyze_review(review_text: str) -> ProductReview: """Analyze this product review and extract structured information.""" ... review = await analyze_review("Great product! Fast shipping and excellent quality. 5 stars!") # All constraints are guaranteed to be satisfied ``` Zod provides schema-based runtime validation similar to Pydantic. **Define the schema** with field types and constraints: ```typescript TypeScript theme={null} import { z } from 'zod'; const ProductReviewSchema = z.object({ rating: z.number().int().min(1).max(5), sentiment: z.enum(['positive', 'negative', 'neutral']), categories: z.array( z.enum(['quality', 'price', 'service', 'delivery', 'packaging']) ).min(1).max(5), summary: z.string().min(10).max(200) }); ``` **Cross-field validation** uses `.refine()` to validate relationships: ```typescript TypeScript theme={null} const ProductReviewSchema = z.object({ // ... fields as above ... }).refine(data => { // Validate sentiment matches rating if (data.rating >= 4 && data.sentiment === 'negative') return false; if (data.rating <= 2 && data.sentiment === 'positive') return false; return true; }, { message: "Sentiment must be consistent with rating" }); ``` **Type inference and validation** -- Zod infers TypeScript types from schemas and validates at runtime: ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; type ProductReview = z.infer; async function analyzeReview(reviewText: string): Promise { const result = await agentic( "Analyze this product review and extract structured information.", { reviewText } ); // Validate with Zod - throws if validation fails return ProductReviewSchema.parse(result); } const review = await analyzeReview("Great product! Fast shipping and excellent quality. 5 stars!"); // All constraints are guaranteed to be satisfied ``` ## Graceful Degradation You may encounter edge cases where a task is genuinely impossible (missing required data, contradictory constraints, etc.). In these cases, you can design your application to degrade gracefully, maintaining basic functionality even when an agent cannot complete the full task. Frequent fallbacks indicate an opportunity to refine your approach -- adjusting prompts, choosing a different model, or providing more context. Use fallback patterns to handle genuine edge cases. ### Fallback to Simpler Logic If a complex agent operation fails, fall back to simpler approaches. This example shows agents generating database migrations, with fallbacks to safer manual approaches. First, define your agent-backed function that attempts the complex task: ```python Python theme={null} from agentica import agentic @agentic() async def generate_migration(schema_old: dict, schema_new: dict) -> str: """ Generate a SQL migration script to transform the old schema to the new one. Handle complex cases like: - Column renames (detect via similarity, not just adds/drops) - Data type changes with appropriate conversions - Foreign key updates - Index optimizations Return valid SQL that preserves data. """ ... ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; async function generateMigration(schemaOld: object, schemaNew: object): Promise { return agentic( `Generate a SQL migration script to transform the old schema to the new one. Handle column renames, type changes, foreign keys, and index optimizations. Return valid SQL that preserves data.`, { schemaOld, schemaNew } ); } ``` Then create a simpler, safer fallback that generates a basic migration: ```python Python theme={null} def generate_basic_migration(schema_old: dict, schema_new: dict) -> str: """Generate simple ADD/DROP column migration without smart renames.""" old_cols = set(schema_old.get('columns', [])) new_cols = set(schema_new.get('columns', [])) added = new_cols - old_cols dropped = old_cols - new_cols sql_lines = [] table = schema_new.get('table_name', 'table') for col in dropped: sql_lines.append(f"ALTER TABLE {table} DROP COLUMN {col};") for col in added: sql_lines.append(f"ALTER TABLE {table} ADD COLUMN {col} VARCHAR(255);") return "\n".join(sql_lines) if sql_lines else "-- No changes detected" ``` ```typescript TypeScript theme={null} function generateBasicMigration(schemaOld: any, schemaNew: any): string { const oldCols = new Set(schemaOld.columns || []); const newCols = new Set(schemaNew.columns || []); const added = [...newCols].filter(col => !oldCols.has(col)); const dropped = [...oldCols].filter(col => !newCols.has(col)); const sqlLines: string[] = []; const table = schemaNew.tableName || 'table'; for (const col of dropped) { sqlLines.push(`ALTER TABLE ${table} DROP COLUMN ${col};`); } for (const col of added) { sqlLines.push(`ALTER TABLE ${table} ADD COLUMN ${col} VARCHAR(255);`); } return sqlLines.length > 0 ? sqlLines.join('\n') : '-- No changes detected'; } ``` Attempt the smart migration first, falling back to basic if it fails: ```python Python theme={null} async def create_migration(schema_old: dict, schema_new: dict) -> str: """Generate agent-backed migration, fallback to basic diff.""" try: migration = await generate_migration(schema_old, schema_new) logger.info("Generated smart migration with an agent") return migration except Exception as e: logger.warning(f"Agent-backed migration generation failed: {e}, using basic diff") return generate_basic_migration(schema_old, schema_new) ``` ```typescript TypeScript theme={null} async function createMigration(schemaOld: object, schemaNew: object): Promise { try { const migration = await generateMigration(schemaOld, schemaNew); logger.info("Generated smart migration with an agent"); return migration; } catch (e) { logger.warn(`Agent-backed migration generation failed: ${e}, using basic diff`); return generateBasicMigration(schemaOld, schemaNew); } } ``` ### Partial Success Handling Sometimes an agent-backed operation can partially succeed. Instead of treating this as complete failure, design your workflow to continue with whatever succeeded. This example shows an agent refactoring code across multiple files. Define a workflow where agents process multiple items, tracking successes and failures: ```python Python expandable theme={null} from dataclasses import dataclass from agentica import agentic @dataclass class RefactorResult: file_path: str success: bool updated_code: str | None error: str | None @agentic() async def refactor_file(code: str, instruction: str) -> str: """ Refactor the given code according to the instruction. Preserve functionality while improving code quality. """ ... async def refactor_codebase(files: dict[str, str], instruction: str) -> list[RefactorResult]: """Refactor multiple files, continuing even if some fail.""" results = [] for file_path, code in files.items(): try: updated = await refactor_file(code, instruction) results.append(RefactorResult( file_path=file_path, success=True, updated_code=updated, error=None )) logger.info(f"Successfully refactored {file_path}") except Exception as e: results.append(RefactorResult( file_path=file_path, success=False, updated_code=None, error=str(e) )) logger.warning(f"Failed to refactor {file_path}: {e}") return results ``` ```typescript TypeScript expandable theme={null} import { agentic } from '@symbolica/agentica'; interface RefactorResult { filePath: string; success: boolean; updatedCode?: string; error?: string; } async function refactorFile(code: string, instruction: string): Promise { return agentic( `Refactor the given code according to the instruction. Preserve functionality while improving code quality.`, { code, instruction } ); } async function refactorCodebase( files: Record, instruction: string ): Promise { const results: RefactorResult[] = []; for (const [filePath, code] of Object.entries(files)) { try { const updated = await refactorFile(code, instruction); results.push({ filePath, success: true, updatedCode: updated }); logger.info(`Successfully refactored ${filePath}`); } catch (e) { results.push({ filePath, success: false, error: String(e) }); logger.warn(`Failed to refactor ${filePath}: ${e}`); } } return results; } ``` Then act on partial results, applying successful changes while reporting failures: ```python Python expandable theme={null} async def apply_refactoring(files: dict[str, str], instruction: str) -> dict: """Apply refactoring and report on partial success.""" results = await refactor_codebase(files, instruction) successful = [r for r in results if r.success] failed = [r for r in results if not r.success] # Write successful refactorings for result in successful: with open(result.file_path, 'w') as f: f.write(result.updated_code) # Log summary if len(successful) == len(results): logger.info(f"All {len(results)} files refactored successfully") elif len(successful) > 0: logger.warning( f"Partial success: {len(successful)}/{len(results)} files refactored. " f"Failed: {[r.file_path for r in failed]}" ) else: logger.error("All refactoring attempts failed") return { "total": len(results), "successful": len(successful), "failed": len(failed), "failed_files": [r.file_path for r in failed] } ``` ```typescript TypeScript expandable theme={null} import { writeFile } from 'fs/promises'; async function applyRefactoring( files: Record, instruction: string ): Promise<{ total: number; successful: number; failed: number; failedFiles: string[]; }> { const results = await refactorCodebase(files, instruction); const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); // Write successful refactorings await Promise.all( successful.map(result => writeFile(result.filePath, result.updatedCode!) ) ); // Log summary if (successful.length === results.length) { logger.info(`All ${results.length} files refactored successfully`); } else if (successful.length > 0) { logger.warn( `Partial success: ${successful.length}/${results.length} files refactored. ` + `Failed: ${failed.map(r => r.filePath).join(', ')}` ); } else { logger.error("All refactoring attempts failed"); } return { total: results.length, successful: successful.length, failed: failed.length, failedFiles: failed.map(r => r.filePath) }; } ``` ### Multi-Level Fallback Chain For critical operations, implement progressively simpler agentic tasks as fallbacks. When a task requires data that isn't available or constraints that can't be met, agents may raise an error. Simpler fallback tasks with relaxed requirements are more likely to succeed. Define multiple agentic approaches with decreasing strictness: ```python Python theme={null} from dataclasses import dataclass from agentica import agentic @dataclass class ShippingAddress: name: str street: str city: str state: str zip_code: str country: str @agentic() async def extract_validated_address(text: str) -> ShippingAddress: """ Extract complete shipping address with ALL required fields. Fields: name, street, city, state, zip_code, country Raise an error if ANY field is missing from the text. """ ... @agentic() async def extract_partial_address(text: str) -> ShippingAddress | None: """ Extract shipping address. Return None if no address is found. Fill in 'unknown' for any missing fields. """ ... @agentic() async def extract_location_mentions(text: str) -> str: """ Extract any location information mentioned (city, state, country, etc). Return as a simple string description of what was found. """ ... ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; interface ShippingAddress { name: string; street: string; city: string; state: string; zipCode: string; country: string; } async function extractValidatedAddress(text: string): Promise { return agentic( `Extract complete shipping address with ALL required fields: name, street, city, state, zipCode, country. Raise an error if ANY field is missing from the text.`, { text } ); } async function extractPartialAddress(text: string): Promise { return agentic( `Extract shipping address. Return null if no address is found. Fill in 'unknown' for any missing fields.`, { text } ); } async function extractLocationMentions(text: string): Promise { return agentic( `Extract any location information mentioned (city, state, country, etc). Return as a simple string description of what was found.`, { text } ); } ``` Attempt each approach, falling back when required data is missing: ```python Python theme={null} async def process_shipping_info(text: str) -> dict: """Extract shipping information with fallback levels.""" # Try complete validated extraction try: address = await extract_validated_address(text) logger.info("Complete shipping address extracted") return {"address": address, "completeness": "complete"} except Exception as e: logger.warning(f"Complete address extraction failed: {e}") # Try partial extraction try: address = await extract_partial_address(text) if address: logger.warning("Partial address extracted, manual review needed") return {"address": address, "completeness": "partial"} else: logger.warning("No structured address found") except Exception as e: logger.error(f"Partial address extraction failed: {e}") # Final fallback - just get location mentions location_text = await extract_location_mentions(text) logger.error("Could not extract structured address, only location mentions") return {"address": None, "location_text": location_text, "completeness": "minimal"} ``` ```typescript TypeScript theme={null} async function processShippingInfo(text: string): Promise<{ address?: ShippingAddress | null; locationText?: string; completeness: string; }> { // Try complete validated extraction try { const address = await extractValidatedAddress(text); logger.info("Complete shipping address extracted"); return { address, completeness: "complete" }; } catch (e) { logger.warn(`Complete address extraction failed: ${e}`); } // Try partial extraction try { const address = await extractPartialAddress(text); if (address) { logger.warn("Partial address extracted, manual review needed"); return { address, completeness: "partial" }; } else { logger.warn("No structured address found"); } } catch (e) { logger.error(`Partial address extraction failed: ${e}`); } // Final fallback - just get location mentions const locationText = await extractLocationMentions(text); logger.error("Could not extract structured address, only location mentions"); return { address: null, locationText, completeness: "minimal" }; } ``` When the text only mentions "Send it to John in Seattle", the validated extraction fails (missing street, state, zip, country), but the minimal extraction can still return "Seattle" as the location. ## Custom Exceptions You can define your own exception classes and have the agent raise them when specific error conditions occur. The agent can raise these exceptions from within its execution environment, and they are automatically bubbled back up to your code. ### Defining Custom Exceptions Custom exceptions are useful for domain-specific error handling. To use them: 1. Define your custom exception classes 2. Pass them into the `@agentic()` decoratoragentic function's scope object 3. Document when each exception should be raised so the agent knows when to use them ```python Python expandable theme={null} from dataclasses import dataclass, field from enum import Enum from time import time from agentica import agentic class TaskCategory(Enum): BUSINESS = "business" PERSONAL = "personal" FREELANCE = "freelance" @dataclass class Task: user: str category: TaskCategory description: str time_created: float = field(default_factory=time) class TaskTooComplicatedError(Exception): """Raised when a task is too complex to complete automatically.""" pass class InsufficientPermissionsError(Exception): """Raised when the user lacks permissions for the requested task.""" pass @agentic(TaskTooComplicatedError, InsufficientPermissionsError) async def perform_task(task: Task) -> str: """ Perform the task and return the result. Raises: TaskTooComplicatedError: If the task requires human intervention InsufficientPermissionsError: If the user lacks necessary permissions ValueError: If the task description is empty or invalid Returns: A description of the completed task. """ ... # Usage with error handling try: result = await perform_task(task) print(f"Task completed: {result}") except TaskTooComplicatedError as e: print(f"Manual intervention required: {e}") # Escalate to human assign_to_human(task) except InsufficientPermissionsError as e: print(f"Permission denied: {e}") # Request additional permissions request_permissions(task.user, task.category) except ValueError as e: print(f"Invalid task: {e}") ``` ```typescript TypeScript expandable theme={null} import { agentic } from '@symbolica/agentica'; enum TaskCategory { BUSINESS = "business", PERSONAL = "personal", FREELANCE = "freelance" } interface Task { user: string; category: TaskCategory; description: string; timeCreated: number; } /** * Raised when a task is too complex to complete automatically. */ class TaskTooComplicatedError extends Error { constructor(message: string) { super(message); this.name = 'TaskTooComplicatedError'; } } /** * Raised when the user lacks permissions for the requested task. */ class InsufficientPermissionsError extends Error { constructor(message: string) { super(message); this.name = 'InsufficientPermissionsError'; } } /** * Perform the task and return the result. * * @throws {TaskTooComplicatedError} If the task requires human intervention * @throws {InsufficientPermissionsError} If the user lacks necessary permissions * @throws {Error} If the task description is empty or invalid */ async function performTask(task: Task): Promise { return agentic( `Perform the task and return the result. Throw TaskTooComplicatedError if the task requires human intervention. Throw InsufficientPermissionsError if the user lacks necessary permissions. Throw Error if the task description is empty or invalid. Return a description of the completed task.`, { task, TaskTooComplicatedError, InsufficientPermissionsError } ); } // Usage with error handling try { const result = await performTask(task); console.log(`Task completed: ${result}`); } catch (e) { if (e instanceof TaskTooComplicatedError) { console.log(`Manual intervention required: ${e}`); // Escalate to human assignToHuman(task); } else if (e instanceof InsufficientPermissionsError) { console.log(`Permission denied: ${e}`); // Request additional permissions requestPermissions(task.user, task.category); } else if (e instanceof Error) { console.log(`Invalid task: ${e}`); } } ``` The agent can see your docstringsJSDoc comments (`/** ... */`)! Be specific about when each exception should be raised. The agent uses this documentation to understand when to throw each exception type. For comprehensive error handling patterns and best practices, see the [Error Handling Guide](/guides/operational-errors). ## Next Steps Handle platform-level errors (network, API, sandbox) Production deployment best practices Add human oversight to your agents Custom system prompts and templating # Best Practices Source: https://docs.symbolica.ai/guides/best-practices Production-ready guidelines for building with the Agentica SDK ## Type Safety Strong type hints do two things: they guide agents toward the correct output structure, and they give you type-safe returns in your code. The more specific your types, the more constrained an agent's output will be. **Use literal types** to restrict outputs to specific values: ```python Python theme={null} from typing import Literal @agentic() async def classify(text: str) -> Literal['positive', 'negative', 'neutral']: """Classify sentiment""" ... # The agent can only return one of these three exact strings result = await classify("Great product!") # Type is Literal['positive', 'negative', 'neutral'] ``` ```typescript TypeScript theme={null} type Sentiment = 'positive' | 'negative' | 'neutral'; async function classify(text: string): Promise { return agentic( "Classify the sentiment of the text as positive, negative, or neutral", { text } ); } // The agent can only return one of these three exact strings const result = await classify("Great product!"); // Type is Sentiment ``` **Use structured types** for complex outputs. The agent will match your type structure exactly: ```python Python theme={null} from dataclasses import dataclass from typing import Literal @dataclass class Review: rating: Literal[1, 2, 3, 4, 5] sentiment: Literal['positive', 'negative', 'neutral'] categories: list[str] summary: str @agentic() async def analyze_review(text: str) -> Review: """Analyze a product review""" ... # Returns a fully typed Review object review = await analyze_review("Great product, fast shipping!") print(review.rating) # Type-safe access ``` ```typescript TypeScript theme={null} interface Review { rating: 1 | 2 | 3 | 4 | 5; sentiment: 'positive' | 'negative' | 'neutral'; categories: string[]; summary: string; } async function analyzeReview(text: string): Promise { return agentic("Analyze a product review", { text }); } // Returns a fully typed Review object const review = await analyzeReview("Great product, fast shipping!"); console.log(review.rating); // Type-safe access ``` **Combine types with validation** for even stronger guarantees. See [Error Handling](/guides/agent-errors#validation-after-invocation) for validation patterns using Pydantic and Zod. ## Security ### Credential Management Never hardcode API keys or secrets. **Use environment variables.** This keeps credentials out of your codebase and allows different values per environment. ```python Python theme={null} import os # Good - use environment variables api_key = os.environ["API_KEY"] database_url = os.environ.get("DATABASE_URL") # Bad - hardcoded secrets api_key = "sk-proj-abc123..." # Never commit this ``` ```typescript TypeScript theme={null} // Good - use environment variables const apiKey = process.env.API_KEY!; const databaseUrl = process.env.DATABASE_URL; // Bad - hardcoded secrets const apiKey = "sk-proj-abc123..."; // Never commit this ``` **Never pass raw API keys to agents.** Instead, pass pre-authenticated SDK clients or specific methods. The agent uses the functionality without ever seeing the credentials: ```python Python theme={null} from agentica import spawn from github import Github # Good - pass authenticated client methods gh = Github(os.environ["GITHUB_TOKEN"]) agent = await spawn(premise="You are a GitHub analyst") result = await agent.call( Report, "Analyze the repository's recent activity", get_repo=gh.get_repo, search_issues=gh.search_issues ) # Agent can use GitHub API without accessing the token # Bad - passing raw credentials result = await agent.call( Report, "Analyze repository", github_token=os.environ["GITHUB_TOKEN"] # Never do this ) ``` ```typescript TypeScript theme={null} import { spawn } from '@symbolica/agentica'; import { Octokit } from '@octokit/rest'; // Good - pass authenticated client methods const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); const agent = await spawn({ premise: "You are a GitHub analyst" }); const result = await agent.call( "Analyze the repository's recent activity", { getRepo: octokit.repos.get.bind(octokit.repos), searchIssues: octokit.search.issuesAndPullRequests.bind(octokit.search) } ); // Agent can use GitHub API without accessing the token // Bad - passing raw credentials const result = await agent.call( "Analyze repository", { githubToken: process.env.GITHUB_TOKEN } // Never do this ); ``` ### Input Validation **Validate user input before passing it to agentic functions.** This prevents injection attacks and ensures your agentic functions receive clean data. ```python Python theme={null} from agentica import agentic @agentic() async def query_database(user_input: str, schema: dict) -> list[dict]: """ Generate and execute a database query based on user input. Only generate SELECT queries. Use the schema to validate table/column names. """ ... async def safe_query(user_input: str) -> list[dict]: # Validate input length if len(user_input) > 500: raise ValueError("Input too long") # Check for suspicious patterns dangerous_keywords = ['drop', 'delete', 'truncate', 'insert', 'update'] if any(keyword in user_input.lower() for keyword in dangerous_keywords): raise ValueError("Invalid query keywords") # Now safe to pass to an agent return await query_database(user_input, schema) ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; async function queryDatabase(userInput: string, schema: object): Promise { return agentic( "Generate and execute a database query based on user input. Only generate SELECT queries. Use the schema to validate table/column names.", { userInput, schema } ); } async function safeQuery(userInput: string): Promise { // Validate input length if (userInput.length > 500) { throw new Error("Input too long"); } // Check for suspicious patterns const dangerousKeywords = ['drop', 'delete', 'truncate', 'insert', 'update']; if (dangerousKeywords.some(kw => userInput.toLowerCase().includes(kw))) { throw new Error("Invalid query keywords"); } // Now safe to pass to an agent return queryDatabase(userInput, schema); } ``` ### File Access Scope Agents that can open arbitrary paths can easily escape their intended sandbox (for example by traversing `../`) and read, modify, or delete files across your system. **Avoid passing `Path` objects or unrestricted file paths directly to agents or agentic functions.** Instead, pre-open only the specific files you want the agent to access and pass those file handles in scope. ```python Python theme={null} from typing import TextIO from agentica import agentic @agentic() async def summarize_report(report_file: TextIO) -> str: """ Read the already-open report_file and summarize its contents. """ ... with open("/var/reports/weekly.csv", "r", encoding="utf-8") as f: # The agent only sees this specific handle, not your whole filesystem summary = await summarize_report(f) ``` ```typescript TypeScript theme={null} import { promises as fs } from 'fs'; import { agentic } from '@symbolica/agentica'; // Read-only handle example async function summarizeReport(reportHandle: fs.FileHandle): Promise { return agentic( "Read the already-open reportHandle and summarize its contents.", { reportHandle } ); } const handle = await fs.open("/var/reports/weekly.csv", "r"); try { const summary = await summarizeReport(handle); // use summary... } finally { await handle.close(); } ``` ### Rate Limiting **Implement rate limiting** to protect against abuse and manage costs. This is especially important for user-facing features. ```python Python theme={null} from collections import defaultdict from time import time class RateLimiter: def __init__(self, max_calls: int, window_seconds: int): self.max_calls = max_calls self.window = window_seconds self.calls: dict[str, list[float]] = defaultdict(list) def allow(self, user_id: str) -> bool: now = time() # Remove old calls outside window self.calls[user_id] = [t for t in self.calls[user_id] if now - t < self.window] if len(self.calls[user_id]) >= self.max_calls: return False self.calls[user_id].append(now) return True limiter = RateLimiter(max_calls=10, window_seconds=60) @agentic() async def summarize(text: str) -> str: """Summarize the text""" ... async def rate_limited_summarize(user_id: str, text: str) -> str: if not limiter.allow(user_id): raise Exception("Rate limit exceeded. Try again in a minute.") return await summarize(text) ``` ```typescript TypeScript theme={null} class RateLimiter { private calls: Map = new Map(); constructor( private maxCalls: number, private windowSeconds: number ) {} allow(userId: string): boolean { const now = Date.now() / 1000; const userCalls = this.calls.get(userId) || []; // Remove old calls outside window const recentCalls = userCalls.filter(t => now - t < this.windowSeconds); if (recentCalls.length >= this.maxCalls) { return false; } recentCalls.push(now); this.calls.set(userId, recentCalls); return true; } } const limiter = new RateLimiter(10, 60); async function summarize(text: string): Promise { return agentic("Summarize the text", { text }); } async function rateLimitedSummarize(userId: string, text: string): Promise { if (!limiter.allow(userId)) { throw new Error("Rate limit exceeded. Try again in a minute."); } return summarize(text); } ``` **Exponential backoff** handles transient failures when agentic functions or agents call external APIs that may be rate-limited. ```python Python theme={null} import asyncio from agentica import agentic from dataclasses import dataclass from typing import Literal @dataclass class FetchResult: status: Literal['success', 'rate_limited', 'error'] data: list[dict] | None message: str @agentic() async def fetch_github_data(query: str, api_search) -> FetchResult: """ Search GitHub using the provided api_search function. If you encounter a rate limit response, return status='rate_limited'. If successful, return status='success' with the data. If other error, return status='error' with a message. """ ... async def fetch_with_backoff(query: str, api_search, max_retries: int = 3) -> FetchResult: for attempt in range(max_retries): result = await fetch_github_data(query, api_search) if result.status == 'success': return result elif result.status == 'rate_limited' and attempt < max_retries - 1: # Exponential backoff: 1s, 2s, 4s wait_time = 2 ** attempt await asyncio.sleep(wait_time) continue else: return result return FetchResult('error', None, 'Max retries exceeded') ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; interface FetchResult { status: 'success' | 'rate_limited' | 'error'; data: object[] | null; message: string; } async function fetchGithubData(query: string, apiSearch: Function): Promise { return agentic( `Search GitHub using the provided apiSearch function. If you encounter a rate limit response, return status='rate_limited'. If successful, return status='success' with the data. If other error, return status='error' with a message.`, { query, apiSearch } ); } async function fetchWithBackoff( query: string, apiSearch: (q: string) => Promise, maxRetries: number = 3 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { const result = await fetchGithubData(query, apiSearch); if (result.status === 'success') { return result; } else if (result.status === 'rate_limited' && attempt < maxRetries - 1) { // Exponential backoff: 1s, 2s, 4s const waitTime = Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, waitTime)); continue; } else { return result; } } return { status: 'error', data: null, message: 'Max retries exceeded' }; } ``` ## Monitoring Track these key metrics in production to understand your agentic operations: * **Latency.** How long do agentic functions and agents take to respond? * **Error rates.** What percentage of agentic calls fail or timeout? * **Usage patterns.** Which functions are called most? By which users? * **Output quality.** Are results meeting expectations? Use sampling to review outputs. ### Logging **Log agentic operations with structured data.** Include the operation name, input size, model used, and timing. This helps debug issues and identify patterns. ```python Python theme={null} import logging import time logger = logging.getLogger(__name__) @agentic() async def classify(text: str) -> str: """Classify sentiment""" ... async def monitored_classify(text: str) -> str: start = time.time() try: result = await classify(text) logger.info("Agentic operation succeeded", extra={ "operation": "classify", "input_length": len(text), "latency_ms": (time.time() - start) * 1000, "model": "gpt-4" }) return result except Exception as e: logger.error("Agentic operation failed", extra={ "operation": "classify", "error": str(e), "input_length": len(text), "latency_ms": (time.time() - start) * 1000 }) raise ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; async function classify(text: string): Promise { return agentic("Classify sentiment", { text }); } async function monitoredClassify(text: string): Promise { const start = Date.now(); try { const result = await classify(text); logger.info("Agentic operation succeeded", { operation: "classify", inputLength: text.length, latencyMs: Date.now() - start, model: "gpt-4" }); return result; } catch (e) { logger.error("Agentic operation failed", { operation: "classify", error: String(e), inputLength: text.length, latencyMs: Date.now() - start }); throw e; } } ``` **Never log sensitive data.** User inputs, API keys, or PII should not appear in logs. See [Error Handling › Sensitive Data Handling](/guides/operational-errors#sensitive-data-handling) for examples of safe logging practices. ## Performance ### Caching **Cache agent responses** when the same inputs produce the same outputs. This reduces latency and costs for repeated operations. Use caching for: * Reference data that changes infrequently (product descriptions, documentation) * Expensive operations called repeatedly with the same inputs * Read-heavy workflows where consistency is acceptable ```python Python theme={null} from functools import lru_cache # Decorate the agentic function directly @lru_cache(maxsize=1000) @agentic() async def categorize_product(description: str) -> str: """Categorize product into a department""" ... # Same description returns cached result category1 = await categorize_product("Red cotton t-shirt") # Calls agent category2 = await categorize_product("Red cotton t-shirt") # Returns cached ``` ```typescript TypeScript theme={null} import { agentic } from '@symbolica/agentica'; const cache = new Map>(); async function categorizeProduct(description: string): Promise { // Check cache first if (cache.has(description)) { return cache.get(description)!; } // Cache the promise to avoid duplicate concurrent calls const promise = agentic("Categorize product into a department", { description }); cache.set(description, promise); return promise; } // Same description returns cached result const category1 = await categorizeProduct("Red cotton t-shirt"); // Calls agent const category2 = await categorizeProduct("Red cotton t-shirt"); // Returns cached ``` **Advanced: Best-of-N caching with retries.** Like JIT compilation that eventually compiles hot code paths, you can combine caching with [retry strategies](/guides/operational-errors#retry-strategies) to create a "best-of-N" pattern: retry failed operations until you get a high-quality result, then cache that successful response. Future calls skip the retry logic entirely and use the cached "compiled" result. This is particularly useful for expensive operations where you want to pay the retry cost once, then reuse the validated output. ### Parallel Processing **Process multiple items in parallel** when they're independent. This is faster than sequential processing. ```python Python theme={null} import asyncio @agentic() async def analyze(text: str) -> dict: """Analyze the text""" ... # Process all texts in parallel texts = ["text 1", "text 2", "text 3"] results = await asyncio.gather(*[analyze(text) for text in texts]) ``` ```typescript TypeScript theme={null} async function analyze(text: string): Promise { return agentic("Analyze the text", { text }); } // Process all texts in parallel const texts = ["text 1", "text 2", "text 3"]; const results = await Promise.all(texts.map(text => analyze(text))); ``` ### Stateful Workflows with Agents **Use agents for multi-step workflows** where later steps depend on earlier results. Agents maintain context across invocations, allowing them to make decisions based on what they've already done. Here's an agent that debugs code by analyzing, then deciding whether to fix or explain based on what it finds: ```python Python theme={null} from agentica import spawn agent = await spawn( premise=""" You are a code debugger. When given code with an error: 1. First analyze the error to understand the root cause 2. If it's a simple fix (syntax, typo), fix it and return the corrected code 3. If it's a logic error requiring design changes, explain the issue instead """, model="openai/gpt-5.2" ) # First invocation: analyze await agent.call(None, "Analyze this error", code=broken_code, error=error_msg) # Second invocation: agent decides to fix or explain based on analysis result = await agent.call( str, "Based on your analysis, either fix the code or explain what needs to change" ) # The agent remembers its analysis and chooses the appropriate action ``` ```typescript TypeScript theme={null} import { spawn } from '@symbolica/agentica'; const agent = await spawn({ premise: `You are a code debugger. When given code with an error: 1. First analyze the error to understand the root cause 2. If it's a simple fix (syntax, typo), fix it and return the corrected code 3. If it's a logic error requiring design changes, explain the issue instead`, model: "openai/gpt-5.2" }); // First invocation: analyze await agent.call( "Analyze this error", { code: brokenCode, error: errorMsg } ); // Second invocation: agent decides to fix or explain based on analysis const result = await agent.call( "Based on your analysis, either fix the code or explain what needs to change" ); // The agent remembers its analysis and chooses the appropriate action ``` For truly independent operations, use agentic functions and process in parallel. For dependent workflows where context matters, use a single agent across multiple calls. ## Cost Optimization Inference costs money -- optimize by choosing the right model, caching responses, and using agents only when needed. **Choose the right model for the task.** Use cheaper models for simple operations, more expensive models for complex reasoning. See [Model Selection](/guides/prompting#choosing-models-with-the-agentica-sdk) for guidance. **Cache aggressively.** Every cache hit is a cost you don't pay. See [Caching](#caching) above. **Keep prompts concise.** Longer prompts cost more. Remove unnecessary context or examples once you've validated your agentic function works. **Use agents strategically.** Agents maintain conversation history, which grows with each call and costs more. For stateless operations, use agentic functions instead. **Bad: Using an agent for independent operations** ```python Python theme={null} # Inefficient - agent maintains unnecessary history agent = await spawn(premise="You are a data processor") for item in items: result = await agent.call(dict, f"Process this item: {item}") # Each call adds to history, increasing cost ``` ```typescript TypeScript theme={null} // Inefficient - agent maintains unnecessary history const agent = await spawn({ premise: "You are a data processor" }); for (const item of items) { const result = await agent.call(`Process this item: ${item}`); // Each call adds to history, increasing cost } ``` **Good: Using agentic function for independent operations** ```python Python theme={null} @agentic() async def process_item(item: str) -> dict: """Process the item""" ... # Each call is independent, no growing history for item in items: result = await process_item(item) ``` ```typescript TypeScript theme={null} async function processItem(item: string): Promise { return agentic("Process the item", { item }); } // Each call is independent, no growing history for (const item of items) { const result = await processItem(item); } ``` **Good: Using agent when context matters** ```python Python theme={null} # Agent remembers context across steps agent = await spawn(premise="You are a research assistant") # Step 1: Find relevant papers papers = await agent.call(list[str], "Search for papers on quantum computing", web_search=search) # Step 2: Agent remembers which papers it found summary = await agent.call(str, "Summarize the key findings from these papers") # Step 3: Agent has full context to compare comparison = await agent.call(str, "Which paper has the most practical applications?") ``` ```typescript TypeScript theme={null} // Agent remembers context across steps const agent = await spawn({ premise: "You are a research assistant" }); // Step 1: Find relevant papers const papers = await agent.call( "Search for papers on quantum computing", { webSearch: search } ); // Step 2: Agent remembers which papers it found const summary = await agent.call("Summarize the key findings from these papers"); // Step 3: Agent has full context to compare const comparison = await agent.call("Which paper has the most practical applications?"); ``` ## Deployment Checklist Before deploying agentic features to production: **Environment & Configuration** Environment variables configured for all environments (dev, staging, prod) API keys secured and not hardcoded Model selections appropriate for each environment (cheaper models for dev/test) **Error Handling & Reliability** Try/catch blocks around all agents operations Fallback strategies for critical paths Retry logic for transient failures Validation on agent outputs where needed **Security** Input validation on user-provided data Rate limiting implemented for user-facing features Sensitive data excluded from logs Authenticated SDK clients passed instead of raw API keys **Monitoring & Observability** Structured logging in place for agentic operations Metrics tracked (latency, error rates, usage) Alerts configured for error spikes or high latency Sample-based output quality monitoring **Testing** Unit tests for agentic functions with representative inputs Integration tests for multi-agent workflows Load tests if serving high-volume traffic Manual review of agent outputs on diverse test cases **Cost Management** Caching implemented for repeated operations Model selection optimized (avoid expensive models for simple tasks) Budget alerts configured with your agent provider Rate limits prevent runaway costs ## Next Steps Add human oversight to your agents See production-ready examples Custom system prompts and templating Complete API documentation # Example Projects & Walk-throughs Source: https://docs.symbolica.ai/guides/examples A walk-through of the Agentica SDK's features by example.
``` __ __ ____ ___ __ _ / / / /__ / / /___ / | ____ ____ ____ / /_(_)________ _ / /_/ / _ \/ / / __ \ / /| |/ __ `/ _ \/ __ \/ __/ / ___/ __ `/ / __ / __/ / / /_/ / / ___ / /_/ / __/ / / / /_/ / /__/ /_/ / /_/ /_/\___/_/_/\____( ) /_/ |_\__, /\___/_/ /_/\__/_/\___/\__,_/ |/ /____/ ```
Like this? Get an agent to [make it](/concepts/agentic#art). ### How do you use the Agentica SDK? **Prerequisites**: * Install `agentica` * Add your `AGENTICA_API_KEY` There are two main ways to use the Agentica SDK. They are: * creating an **agentic function** * **spawning an agent** with the `spawn` function See the [references](/references/python/agents) for more details. ### What can you use the Agentica SDK for? Below are a few examples that we believe highlight some of the best features of the Agentica SDK! ### Grab and go Install any prerequisites, copy and off you go. **Prerequisites**: * Run `pip install slack-sdk` or `uv add slack-sdk` * Add your `SLACK_BOT_TOKEN` [Read these instructions](https://docs.slack.dev/authentication/tokens) to generate a `SLACK_BOT_TOKEN` ! ```python expandable theme={null} import os import asyncio from agentica import agentic from slack_sdk import WebClient SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN") # We know we will want to list users and send a message slack_conn = WebClient(token=SLACK_BOT_TOKEN) send_direct_message = slack_conn.chat_postMessage @agentic(send_direct_message, model="openai/gpt-4.1") async def send_morning_message(user_name: str) -> None: """ Uses the Slack API to send a direct message to a user. Light and cheerful! """ ... if __name__ == "__main__": import asyncio asyncio.run(send_morning_message('@Samuel')) print("Morning message sent!") ``` **Prerequisites**: * Run `pip install matplotlib pandas ipynb jupyter` or `uv add matplotlib pandas ipynb jupyter` * Download the CSV and save as `/movie_metadata.csv` * Run `jupyter notebook data_science.ipynb` ```python data_science.ipynb expandable wrap theme={null} { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from agentica import spawn\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "agent = await spawn()\n", "result = await agent.call(\n", " dict[str, int],\n", " \"Show the number of movies for each major genre. The results can be in any order.\",\n", " movie_metadata_dataset=pd.read_csv(\"./movie_metadata.csv\").to_dict(),\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12, 8))\n", "plt.bar(list(result.keys()), list(result.values()))\n", "plt.xticks(rotation=45, ha='right')\n", "plt.tight_layout()\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "result = await agent.call(\n", " dict[str, int],\n", " \"Update the result to only contain the genres that have more than 1000 movies.\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12, 8))\n", "plt.bar(list(result.keys()), list(result.values()))\n", "plt.xticks(rotation=45, ha='right')\n", "plt.tight_layout()\n", "plt.show()\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" } }, "nbformat": 4, "nbformat_minor": 4 } ``` **Prerequisites**: * If on macOS, install system dependencies with `brew install pkg-config cairo meson ninja` * Run `pip install exa-py validators markdown xhtml2pdf` * Create an EXA account, create an `EXA_SERVICE_API_KEY` and run `export EXA_SERVICE_API_KEY=""` ```python expandable theme={null} """ Deep Research Demo - Multi-agent research with web search and citations. """ import asyncio import json import re from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Literal import markdown from xhtml2pdf import pisa from agentica import Agent from agentica.logging import AgentListener from agentica.std.caption import CaptionLogger from agentica.std.web import ExaAdmin, ExaClient, SearchResult type SourceType = Literal[ "primary", "secondary", "vendor", "press", "blog", "forum", "unknown", ] LEAD_RESEARCHER_MODEL = "anthropic/claude-opus-4.5" SUBAGENT_MODEL = "anthropic/claude-sonnet-4.5" CITATION_MODEL = "openai/gpt-4.1" CITATION_PREMISE = """ You are a citation agent. # Task You must: 1. Review the research report provided to you as `research_report` line by line. 2. Identify which lines of the research report use information that could be from web search results. 3. List the web search results that were used in creating the research report. 4. For each of these lines, use the `load_search_result` function to load the web search result that was used. 5. Add a markdown citation with the URL of the web search result to the claim in the research report by modifying the `research_report` variable. 6. Once this is done, make sure the `research_report` is valid markdown - if not, change the markdown to make it valid. 7. Use the `save_report` function to save the research report to memory as a markdown file at the end. 8. Return saying you have finished. # Rules - Your citations MUST be consistent throughout the `research_report`. - Any URL in the final markdown MUST be formatted as a markdown link, not a bare URL. - You MUST use the `list_search_results` function to list the web search results that were used in creating the research report - You MUST use the `load_search_result` function to load the web search results. - You MUST use the `research_report` variable provided to you to modify the research report by adding citations. - You MUST make sure the `research_report` is valid markdown. - You MUST use the `save_report` function to save the research report to memory at the end. - You MUST inspect the report before saving it to make sure it is valid and what you intended. Iterate until it is valid. ## Citation format - Prefer inline citations like: `... claim ... ([source](https://example.com))` - If multiple sources support a sentence, include multiple links: `... ([s1](...), [s2](...))` """ LEAD_RESEARCHER_PREMISE = """ You are a lead researcher. You have access to web-search enabled subagents. # Task You must: 1. Create a plan to research the user query. 2. Determine how many specialised subagents (with access to the web) are necessary, each with a different specific research task. 3. Call ALL subagents in parallel using asyncio.gather with return_exceptions=True so partial results are preserved. 4. Summarise the results of the subagents in a final research report as markdown. Use sections, sub-sections, list and formatting to make the report easy to read and understand. The formatting should be consistent and easy to follow. 5. Check the final research report, as this will be shown to the user. 6. Return the final research report using `return` at the very end. # Rules - Do NOT construct the final report until you have run the subagents. - Do NOT return the final report in the REPL until planning, assigning subagents and returning the final report is complete. - Do NOT add citations to the final research report yourself, this will be done afterwards. - Do NOT repeat yourself in the final research report. - You MUST raise an AgentError if you cannot complete the task with what you have available. - You MUST check the final research report string before returning it to the user. ## Planning - You MUST write the plan yourself. - You MUST write the plan before assigning subagents to tasks. - You MUST break down the task into small individual tasks. ## Subagents - You MUST assign each small individual task to a subagent. - For each task, YOU MUST create a **new** SubAgent, and provide it with a task via `.call()`. - You MUST NOT assign multiple unrelated tasks to the same SubAgent. - You should only call a SubAgent repeatedly if you feel you failed to get enough information from a single call, instructing them with what they were missing. - You MUST instruct subagents to use the web_search and save_search_result functions if the task requires it. - Do NOT ask subagents to cite the web, instead instruct them to use the save_search_result function. - Subagents MUST be assigned independent tasks. - IF after subagents have returned their findings more research is needed, you can assign more subagents to tasks. - DO NOT try to preemptively *parse* the output of the subagents, **just look at the output yourself**. - Subagents may fail! `asyncio.gather` will raise an exception if any of the subagents fail. Instead, you should pass `return_exceptions=True` to `asyncio.gather` to not lose the results of the successful subagents. ## Final Report - Do NOT write the final report yourself without running subagents to do so. - Do NOT add citations to the final research report yourself, this will be done afterwards by another agent. - Do NOT repeat yourself in the final research report. - Do NOT return a report with missing information, omitted fields or `N/A` values. If more work needs to be done, you must assign more subagents to tasks, or reuse the necessary subagents to extract more information. - You MUST load the plan from memory before returning the final research report to check that you have followed the plan. - You MUST check the final research report before returning it to the user. - Check the final report for quality, completeness and consistency. If up to standard, return using a single `return` as the sole statement in its very own - Your final report MUST include a short "Sources consulted" section: - List each source URL you relied on - Include its source_type and 1-2 extracted claims - Any URL you include MUST be a markdown hyperlink (not a bare URL). - Do NOT put the whole report in a table. """ SUBAGENT_PREMISE = """ You are a helpful assistant. # Task You must: 1. Construct a list of things to search for using the web_search function. 2. Execute ALL web_search calls in parallel using asyncio.gather and asyncio.run. 3. For each search result, `print()` relevant sections using SearchResult.content_with_line_numbers(start=..., end=...). 4. Identify which lines of content you are going to use in your report. 5. Use the save_search_result function to save the SearchResult to memory and include the lines of the content that you have used. - Include the specific `query` you searched for. - Include `extracted_claims`: a list of short claims you will rely on (derived from the saved lines). - Include `source_type`: one of ["primary", "secondary", "vendor", "press", "blog", "forum", "unknown"]. Use your best judgment based on the URL/domain and the content. - IMPORTANT: save_search_result returns a saved artifact path; keep it and include it in SourceInfo.artifact_path 6. Condense the search results into a single report with what you have found. 7. Return the report using `return` at the very end in a separate REPL session. # Rules - You MUST use `print()` to print the content of each search result by via SearchResult.content_with_line_numbers(). - You MUST use the web_search function if instructed to do so OR if the task requires finding information. - Do NOT assume that the web_search function will return the information you need, you must go through the content of each search result line by line by combing through the content with SearchResult.content_with_line_numbers(start=, end=). - Do NOT assume which lines of content you are going to use in your report, you must go through the content of each search result line by line via SearchResult.content_with_line_numbers(start=, end=). - If you cannot find any information, do NOT provide information yourself, instead raise an error for the lead researcher in the REPL. - You MUST save the SearchResult of any research that you have used to memory and include the lines of the content that you have used (are relevant). - When saving, pass `query`, `extracted_claims`, and `source_type` to save_search_result. - Your returned SubAgentReport MUST include `sources`: one entry per saved source, including url, source_type, query, extracted_claims, artifact_path, and lines_used. - Return the report using `return` at the very end in a separate REPL session. """ STORAGE_DIR = Path("deep_research_test") @dataclass class Storage: """Centralized storage for all research artifacts.""" directory: Path = field(default=STORAGE_DIR) _result_counts: dict[int, int] = field(default_factory=dict) def __post_init__(self): self.directory.mkdir(parents=True, exist_ok=True) # Plan def save_plan(self, plan: str) -> None: """Save the research plan.""" (self.directory / "plan.md").write_text(plan) def load_plan(self) -> str: """Load the research plan.""" path = self.directory / "plan.md" if not path.exists(): raise FileNotFoundError("Plan file not created yet.") return path.read_text() # Search Results def save_search_result( self, subagent_id: int, result: SearchResult, lines_used: list[tuple[int, int]], *, query: str | None = None, extracted_claims: list[str] | None = None, source_type: SourceType | None = None, source_notes: str | None = None, ) -> str: count = self._result_counts.get(subagent_id, 0) + 1 self._result_counts[subagent_id] = count path = self.directory / f"subagent_{subagent_id}" / f"result_{count}.json" path.parent.mkdir(parents=True, exist_ok=True) # Extract only the relevant lines filtered_lines: list[str] = [] for start, end in lines_used: filtered_lines.extend(result.content_lines[start - 1 : end]) data = { "title": result.title, "url": result.url, "content_lines": filtered_lines, "score": result.score, # Rich artifact metadata (kept compatible with SearchResult.load()). "saved_at": datetime.now().isoformat(), "subagent_id": subagent_id, "query": query, "lines_used": lines_used, "extracted_claims": extracted_claims or [], "source_type": source_type, "source_notes": source_notes, } path.write_text(json.dumps(data)) return str(path) def load_search_result(self, path: str) -> SearchResult: """ Load a previously saved search-result artifact (JSON) and return it as a SearchResult. Note: artifacts may include extra metadata fields, but SearchResult.load() only uses: - title - url - content_lines - score """ p = Path(path) if not p.is_relative_to(self.directory): raise ValueError(f"Path must be within {self.directory}") return SearchResult.load(p) def list_search_results(self) -> list[str]: """List all saved search result paths.""" files: list[str] = [] for subagent_dir in self.directory.glob("subagent_*"): if not subagent_dir.is_dir(): continue for file in subagent_dir.iterdir(): if file.suffix == ".json" and re.match(r"^result_\d+$", file.stem): files.append(str(file)) return files # Report def save_report(self, md_report: str) -> str: """Save the final report as markdown and PDF.""" md_path = self.directory / "report.md" pdf_path = self.directory / "report.pdf" md_path.write_text(md_report) try: html = markdown.markdown(md_report, extensions=['tables']) with pdf_path.open("wb") as pdf: pisa.CreatePDF(html, dest=pdf) except Exception as e: print(f"Warning: PDF conversion failed: {e}") return str(pdf_path) @property def report_path(self) -> Path: return self.directory / "report.pdf" def report_exists(self) -> bool: return (self.directory / "report.md").exists() # Summary def summary(self) -> str: """Return a summary of all stored artifacts.""" lines = [ "", "━" * 40, f"📁 Research stored in: {self.directory.resolve()}", "━" * 40, ] if self.report_exists(): lines.append(f"📄 Report: {self.report_path.name}") if (self.directory / "report.md").exists(): lines.append(f" {(self.directory / 'report.md').name}") if (self.directory / "plan.md").exists(): lines.append("📋 Plan: plan.md") search_results = self.list_search_results() if search_results: lines.append(f"🔍 Search results: {len(search_results)} files") by_subagent: dict[str, list[str]] = {} for path in search_results: p = Path(path) subagent = p.parent.name by_subagent.setdefault(subagent, []).append(p.name) for subagent, files in sorted(by_subagent.items()): lines.append(f" {subagent}/: {len(files)} results") lines.append("━" * 40) return "\n".join(lines) storage = Storage() class SubAgent: """ A subagent with web search capabilities. For each task, a subagent must be **created**, then **run** with `.call()`. If a subagent needs to be reused, perhaps because it got something wrong, it may be run **again** with a second `.call()`, persisting its history. """ _id: int _exa: ExaClient | None _agent: Agent _initialized: bool def __init__(self): self._id = 0 self._exa = None async def web_search(query: str) -> list[SearchResult]: """Tool: search the web for `query`. Returns a small list of SearchResult objects.""" print(f"Searching: {query}") await self._ensure_init() assert self._exa is not None return await self._exa.search(query, num_results=2) def save_search_result( result: SearchResult, lines_used: list[tuple[int, int]], query: str | None = None, extracted_claims: list[str] | None = None, source_type: SourceType | None = None, source_notes: str | None = None, ) -> str: """ Tool: save a SearchResult artifact for later citation/inspection. Parameters ---------- result: The SearchResult you are using. lines_used: 1-indexed (inclusive) line ranges from result.content_lines that support your claims. query: The web query you used to find this result (optional but recommended). extracted_claims: Short bullet claims you will rely on, derived from the saved lines. source_type: Optional coarse label, e.g. "primary", "secondary", "vendor", "press", "blog", "forum", "unknown". source_notes: Optional brief notes justifying the label / quality. Returns ------- str: Path to the saved JSON artifact (within the storage directory). """ return storage.save_search_result( self._id, result, lines_used, query=query, extracted_claims=extracted_claims, source_type=source_type, source_notes=source_notes, ) self._agent = Agent( model=SUBAGENT_MODEL, premise=SUBAGENT_PREMISE, scope=dict( web_search=web_search, save_search_result=save_search_result, SearchResult=SearchResult, SubAgentReport=SubAgentReport, SourceInfo=SourceInfo, ), ) self._initialized = False async def _ensure_init(self) -> None: if self._initialized: return self._initialized = True # Get agent ID from listener await self._agent._ensure_init() if (listener := self._agent._listener) and listener.logger.local_id: self._id = int(listener.logger.local_id) else: raise ValueError("Agent listener not found") # Create ephemeral Exa API key for this subagent admin = ExaAdmin() key_name = f"SubAgent_{self._id}" api_key = await admin.create_key(key_name) print(f"Created Exa API key for subagent {self._id}: {api_key[:4]}...{api_key[-4:]}") self._exa = ExaClient(api_key=api_key) async def call(self, task: str) -> 'SubAgentReport': """Run the subagent on a task.""" await self._ensure_init() print(f"Running web-search subagent ({self._id})") with CaptionLogger(): return await self._agent.call(SubAgentReport, task) @dataclass class SourceInfo: """ A single source you used in your research. Fill this out in your SubAgentReport so the coordinator can understand: - what URL you relied on, - what you searched for to find it, - what claims you are taking from it, - and an approximate source category (primary/secondary/vendor/press/blog/forum/unknown). """ url: str source_type: SourceType | None = None query: str | None = None extracted_claims: list[str] = field(default_factory=list) artifact_path: str | None = None lines_used: list[tuple[int, int]] = field(default_factory=list) @dataclass class SubAgentReport: """ Your final output for one subagent task. Requirements: - `content` may be paraphrased, but MUST be supported by the saved `lines_used`. - `sources` must include one SourceInfo per source you relied on. """ title: str content: str sources: list[SourceInfo] = field(default_factory=list) class CitationAgent: """Agent that adds citations to a research report.""" def __init__(self): self._agent = Agent( model=CITATION_MODEL, premise=CITATION_PREMISE, scope=dict( list_search_results=storage.list_search_results, load_search_result=storage.load_search_result, save_report=storage.save_report, ), ) async def call(self, md_report: str) -> None: """Add citations to a research report.""" print("Running citation agent") return await self._agent.call( None, f"The `research_report = '{md_report[:10]}...' [truncated]` has been provided to you in the REPL.", research_report=md_report, ) class DeepResearchSession: """Orchestrates a deep research session with multiple agents.""" def __init__(self): self._lead_researcher = Agent( model=LEAD_RESEARCHER_MODEL, premise=LEAD_RESEARCHER_PREMISE, scope=dict( save_plan=storage.save_plan, load_plan=storage.load_plan, list_search_results=storage.list_search_results, load_search_result=storage.load_search_result, SubAgent=SubAgent, SubAgentReport=SubAgentReport, SourceInfo=SourceInfo, SearchResult=SearchResult, ), listener=lambda: AgentListener(CaptionLogger("Lead Researcher")), ) self._citation_agent = CitationAgent() async def call(self, query: str) -> str: """Run the deep research process.""" try: # Research phase report = await self._lead_researcher.call(str, query) # Citation phase with CaptionLogger(): await self._citation_agent.call(report) if not storage.report_exists(): raise RuntimeError("Report was not created") print(storage.summary()) return f"Check out the research report at {storage.report_path}. Ask me any questions!" finally: # Clean up ephemeral API keys print("Pruning Exa API keys...") deleted = await ExaAdmin().prune_keys(prefix="SubAgent_") if deleted: print(f"Pruned {deleted} key(s)") if __name__ == "__main__": session = DeepResearchSession() result = asyncio.run( session.call( "What are all of the companies in the US working on AI agents in 2025? " "Make a list of at least 10. For each, include the name, website and product, " "description of what they do, type of agents they build, and their vertical/industry." ) ) print(result) ``` View the generated report