From 92b9d5d8e1b6cf8e7bd9958b9b0cd776368dc459 Mon Sep 17 00:00:00 2001 From: timonwa Date: Wed, 6 May 2026 12:18:18 +0100 Subject: [PATCH 1/3] Refactor callbacks documentation for clarity and structure - Updated the introduction to emphasize the lifecycle and functionality of callbacks in ADK-TS. - Enhanced the descriptions of callback firing points and their purposes. - Improved the callback registration section with clearer examples and return value explanations. - Added a visual representation of the callback flow using Mermaid diagrams. - Streamlined the examples for each callback type to demonstrate practical usage. - Removed redundant content and ensured consistency in terminology across the documentation. - Adjusted the meta.json to remove the index page from the callbacks section. --- .../framework/callbacks/callback-patterns.mdx | 581 +++++++++++------- .../docs/framework/callbacks/index.mdx | 205 +++--- .../docs/framework/callbacks/meta.json | 2 +- .../docs/framework/callbacks/types.mdx | 565 ++++++++--------- 4 files changed, 701 insertions(+), 652 deletions(-) diff --git a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx index 7f74ee608..b1f355fcc 100644 --- a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx +++ b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx @@ -1,255 +1,382 @@ --- title: Callback Patterns -description: Design patterns and best practices for agent, model, and tool callbacks in ADK-TS +description: Practical patterns for ADK-TS callbacks — guardrails, caching, state management, logging, and conditional execution. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; -Callbacks give you precise control over the agent lifecycle, LLM requests/responses, and tool execution. This guide presents callback-centric patterns using ADK-TS, with examples built around `AgentBuilder` and references to `CallbackContext` and `ToolContext`. - -## Callback & Context Overview - -- `CallbackContext` is passed to agent and model callbacks. - - Key fields commonly used: `state` (mutable session state), `invocationId`, `agent` (current agent), `logger` (if configured). -- `ToolContext` is passed to tool callbacks. - - Key fields commonly used: `state`, `invocationId`, `tool` (current tool), `logger`. - -State writes inside callbacks are tracked and persisted by the `SessionService`. This makes it simple to pass information across the agent run. - - - -These examples use the ADK-TS API. Configure callbacks via -`AgentBuilder` chain methods and rely on canonicalization to support single -callbacks or arrays. +These patterns cover the most common reasons to use callbacks: enforcing policies, caching expensive calls, propagating data across steps, and controlling what runs. Each pattern shows the minimum code needed to solve a specific problem. + + State writes inside callbacks are automatically tracked and persisted by the + `SessionService`. You don't need to do anything extra to save state changes + made inside a callback. -## Builder Setup +## Guardrails and policy enforcement + +Block unsafe prompts before they reach the LLM, and reject unauthorized tool calls before they execute. Both work by returning early from a `before_` callback. + +```typescript +import { AgentBuilder, LlmResponse } from "@iqai/adk"; +import type { + CallbackContext, + LlmRequest, + BaseTool, + ToolContext, +} from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("policy_agent") + .withModel("gemini-2.5-flash") + .withBeforeModelCallback( + ({ + callbackContext, + llmRequest, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }): LlmResponse | null => { + const text = llmRequest.contents + .flatMap(c => c.parts ?? []) + .map(p => p.text ?? "") + .join(" "); + + if (text.includes("confidential") || text.includes("internal policy")) { + return new LlmResponse({ + content: { + role: "model", + parts: [{ text: "I can't help with that topic." }], + }, + }); + } + + return null; + }, + ) + .withBeforeToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + ): Record | null => { + if ( + tool.name === "transfer_funds" && + toolContext.state.get("user_tier") !== "pro" + ) { + return { error: "This operation requires a Pro account." }; + } + return null; + }, + ) + .build(); +``` -```ts -import { AgentBuilder } from '@iqai/adk'; -import { BaseTool } from '@iqai/adk'; +## State management across steps -// Example tool (shape depends on your tool implementation) -const searchApi: BaseTool = /* ... */; +Use callbacks to read and write session state so data flows naturally between agent steps, LLM calls, and tool executions without manual wiring. -// Create builder using static method and chain configuration -const { agent, runner } = await AgentBuilder - .create('context-patterns-agent') - .withModel('') - .withTools(searchApi) - .withBeforeAgentCallback(async (ctx) => { - // Warm up state; skip agent run by returning Content if desired - ctx.state['start_time'] = Date.now(); - }) - .withAfterAgentCallback(async (ctx) => { - // Post-run logging or final content override by returning Content - ctx.logger?.info?.(`Invocation ${ctx.invocationId} completed`); - }) - .withBeforeModelCallback(async (ctx, req) => { - // Inspect/mutate LLM request; optionally return a response to short-circuit - if (ctx.state['block_llm'] === true) { - // Returning a response here skips the LLM call - return { contents: [{ type: 'text', text: 'LLM call blocked by policy.' }] }; - } - - // Example mutation: add system instruction dynamically - req.config = { - ...req.config, - systemInstruction: `${req.config?.systemInstruction ?? ''}\nUser tier: ${ctx.state['user_tier'] ?? 'free'}`, - }; - return req; - }) - .withAfterModelCallback(async (ctx, res) => { - // Modify/shape LLM response (e.g., trim text or add metadata) - const first = res.contents?.[0]; - if (first?.type === 'text') { - first.text = first.text.trim(); - } - return res; - }) - .withBeforeToolCallback(async (tool, args, toolCtx) => { - // Validate or enrich tool args; return a result to short-circuit execution - if (tool.name === 'searchApi' && typeof args.query !== 'string') { - return { error: 'query must be a string' }; - } - - // Enrich args from shared state - args.locale = toolCtx.state['locale'] ?? 'en'; - }) - .withAfterToolCallback(async (tool, args, toolCtx, result) => { - // Persist useful values into state for later steps - if (result && typeof result === 'object' && 'top_hit' in result) { - toolCtx.state['last_top_hit'] = (result as { top_hit: any }).top_hit; - } - return result; - }) - .build(); - -// Now you can use the agent and runner -export { agent, runner }; -``` +```typescript +import { AgentBuilder } from "@iqai/adk"; +import type { CallbackContext, BaseTool, ToolContext } from "@iqai/adk"; -## Design Patterns - -### 1. Guardrails & Policy Enforcement - -- Use `withBeforeModelCallback` to inspect `req.contents` and block unsafe prompts by returning a response. -- Use `withBeforeToolCallback` to validate `call.args` and return an error result when needed. - -```ts -builder - .withBeforeModelCallback(async (ctx, req) => { - const forbidden = ["forbidden-topic", "profanity"]; - const text = (req.contents ?? []) - .filter(c => c.type === "text") - .map((c: { text: string }) => c.text) - .join("\n"); - if (forbidden.some(k => text.includes(k))) { - return { - contents: [{ type: "text", text: "Sorry, I can’t help with that." }], - }; - } - return req; +const { runner } = await AgentBuilder.create("stateful_agent") + .withModel("gemini-2.5-flash") + .withBeforeAgentCallback((callbackContext: CallbackContext): undefined => { + // Stamp the run start time so downstream callbacks can measure duration + callbackContext.state.set("run_start_ms", Date.now()); + return undefined; }) - .withBeforeToolCallback(async (tool, args, toolCtx) => { - if (tool.name === "transferFunds" && toolCtx.state["user_tier"] !== "pro") { - return { error: "This operation requires a pro account." }; - } - }); -``` - -### 2. Dynamic State Management - -- Read/write `ctx.state` and `toolCtx.state` to carry data between steps. -- Changes are tracked automatically and persisted. - -```ts -builder - .withAfterToolCallback(async (tool, args, toolCtx, result) => { - // Save transaction id for later use - if (result && typeof result === "object" && "transaction_id" in result) { - toolCtx.state["last_transaction_id"] = ( - result as { transaction_id: any } - ).transaction_id; - } - return result; + .withAfterToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + toolResponse: Record, + ): Record | null => { + // Persist key results so later tool calls or the agent can reference them + if (toolResponse.order_id) { + toolContext.state.set("last_order_id", toolResponse.order_id); + } + return null; + }, + ) + .withAfterAgentCallback((callbackContext: CallbackContext): undefined => { + const startMs = callbackContext.state.get("run_start_ms") ?? Date.now(); + console.log(`Run completed in ${Date.now() - startMs}ms`); + return undefined; }) - .withBeforeAgentCallback(async ctx => { - // Personalize behavior based on prior state - const tier = ctx.state["user_tier"] ?? "free"; - ctx.logger?.info?.(`Running agent for tier: ${tier}`); - }); + .build(); ``` -### 3. Logging and Monitoring - -- Add structured logs in `before/after` callbacks for observability. - -```ts -builder - .withBeforeToolCallback(async (tool, args, toolCtx) => { - toolCtx.logger?.info?.( - `Before Tool: ${tool.name} (invocation: ${ - toolCtx.invocationId - }) args=${JSON.stringify(args)}`, +## Logging and observability + +Add structured logs at each lifecycle stage for monitoring without changing any business logic. + +```typescript +import { AgentBuilder } from "@iqai/adk"; +import type { + CallbackContext, + LlmRequest, + LlmResponse, + BaseTool, + ToolContext, +} from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("monitored_agent") + .withModel("gemini-2.5-flash") + .withBeforeAgentCallback((callbackContext: CallbackContext): undefined => { + console.log( + `[agent:start] ${callbackContext.agentName} invocation=${callbackContext.invocationId}`, ); + return undefined; }) - .withAfterModelCallback(async (ctx, res) => { - ctx.logger?.info?.( - `After Model: ${ctx.agent.name} (invocation: ${ctx.invocationId})`, - ); - return res; - }); + .withBeforeToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + ): Record | null => { + console.log( + `[tool:start] ${tool.name} args=${JSON.stringify(args)} call=${toolContext.functionCallId}`, + ); + return null; + }, + ) + .withAfterModelCallback( + ({ + callbackContext, + llmResponse, + }: { + callbackContext: CallbackContext; + llmResponse: LlmResponse; + }): LlmResponse | null => { + const chars = llmResponse.content?.parts?.[0]?.text?.length ?? 0; + console.log( + `[model:done] ${callbackContext.agentName} response=${chars} chars`, + ); + return null; + }, + ) + .build(); ``` -### 4. Caching - -- Avoid redundant calls by checking a cache key in `state`. -- If found, return directly from a `before_` callback; otherwise, persist in the corresponding `after_` callback. - -```ts -builder - .withBeforeToolCallback(async (tool, args, toolCtx) => { - const key = `cache:search:${args.query}`; - const hit = toolCtx.state[key]; - if (hit) return hit; // short-circuit tool - }) - .withAfterToolCallback(async (tool, args, toolCtx, result) => { - const key = `cache:search:${args.query}`; - toolCtx.state[key] = result; - return result; - }); +## Tool result caching + +Avoid redundant tool calls by checking a cache key in state before execution. If the result is cached, return it immediately; if not, save it after the tool runs. + +```typescript +import { AgentBuilder } from "@iqai/adk"; +import type { BaseTool, ToolContext } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("caching_agent") + .withModel("gemini-2.5-flash") + .withBeforeToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + ): Record | null => { + const cacheKey = `cache:${tool.name}:${JSON.stringify(args)}`; + const cached = toolContext.state.get(cacheKey); + + if (cached) { + console.log(`Cache hit for ${tool.name}`); + return cached; + } + + return null; + }, + ) + .withAfterToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + toolResponse: Record, + ): Record | null => { + const cacheKey = `cache:${tool.name}:${JSON.stringify(args)}`; + toolContext.state.set(cacheKey, toolResponse); + return null; + }, + ) + .build(); ``` -### 5. Request/Response Modification - -- Mutate the outgoing LLM request or the returned response to fit your UX. - -```ts -builder - .withBeforeModelCallback(async (ctx, req) => { - req.config = { - ...req.config, - systemInstruction: `${req.config?.systemInstruction ?? ""}\nSession: ${ - ctx.invocationId - }`, - }; - return req; - }) - .withAfterModelCallback(async (ctx, res) => { - const contents = res.contents ?? []; - res.contents = contents.map((c: { type: string; text?: string }) => - c.type === "text" ? { ...c, text: c.text?.replaceAll("\n\n", "\n") } : c, - ); - return res; - }); +## Request and response modification + +Mutate the LLM request to inject dynamic context, or modify the response to clean up formatting before the agent processes it further. + +```typescript +import { AgentBuilder } from "@iqai/adk"; +import type { CallbackContext, LlmRequest, LlmResponse } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("enriched_agent") + .withModel("gemini-2.5-flash") + .withBeforeModelCallback( + ({ + callbackContext, + llmRequest, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }): LlmResponse | null => { + // Append per-user context to the system instruction + const userTier = callbackContext.state.get("user_tier") ?? "free"; + if (llmRequest.config) { + llmRequest.config.systemInstruction = + `${llmRequest.config.systemInstruction ?? ""}\nUser tier: ${userTier}`.trim(); + } + return null; + }, + ) + .withAfterModelCallback( + ({ + callbackContext, + llmResponse, + }: { + callbackContext: CallbackContext; + llmResponse: LlmResponse; + }): LlmResponse | null => { + // Collapse multiple blank lines in the response text + if (llmResponse.content?.parts) { + llmResponse.content.parts = llmResponse.content.parts.map(part => + part.text + ? { ...part, text: part.text.replace(/\n{3,}/g, "\n\n") } + : part, + ); + } + return null; + }, + ) + .build(); ``` -### 6. Conditional Skipping of Steps - -- Return a value from a `before_` callback to skip the normal operation. - -```ts -builder - .withBeforeAgentCallback(async ctx => { - if (ctx.state["maintenance_mode"] === true) { - // Returning Content here completes the run immediately - return [{ type: "text", text: "Agent is under maintenance." }]; - } - }) - .withBeforeModelCallback(async (ctx, req) => { - if (ctx.state["use_cached_response"]) { - return { contents: [{ type: "text", text: "Cached response" }] }; - } - return req; - }) - .withBeforeToolCallback(async (tool, args, toolCtx) => { - if (toolCtx.state["api_quota_exceeded"]) { - return { error: "API quota exceeded" }; - } - }); +## Conditional step skipping + +Return from a `before_` callback to skip a step entirely — useful for maintenance modes, quota enforcement, or feature flags. + +```typescript +import { AgentBuilder, LlmResponse } from "@iqai/adk"; +import type { + CallbackContext, + LlmRequest, + BaseTool, + ToolContext, +} from "@iqai/adk"; +import type { Content } from "@google/genai"; + +const { runner } = await AgentBuilder.create("conditional_agent") + .withModel("gemini-2.5-flash") + .withBeforeAgentCallback( + (callbackContext: CallbackContext): Content | undefined => { + if (callbackContext.state.get("maintenance_mode")) { + return { + role: "model", + parts: [ + { text: "Service is temporarily unavailable for maintenance." }, + ], + }; + } + return undefined; + }, + ) + .withBeforeModelCallback( + ({ + callbackContext, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }): LlmResponse | null => { + if (callbackContext.state.get("use_cached_response")) { + const cached = + callbackContext.state.get("cached_response_text") ?? + "No cached response."; + return new LlmResponse({ + content: { role: "model", parts: [{ text: cached }] }, + }); + } + return null; + }, + ) + .withBeforeToolCallback( + ( + _tool: BaseTool, + _args: Record, + toolContext: ToolContext, + ): Record | null => { + if (toolContext.state.get("api_quota_exceeded")) { + return { error: "API quota exceeded. Please try again later." }; + } + return null; + }, + ) + .build(); ``` -### 7. Tool-Specific Actions (Auth & Summarization Control) - -- Implement per-tool behaviors like auth injection, summarization toggles, or rate-limiting. - -```ts -builder - .withBeforeToolCallback(async (tool, args, toolCtx) => { - if (tool.name === "searchApi") { - args.headers = { - ...(args.headers ?? {}), - Authorization: `Bearer ${toolCtx.state["api_token"] ?? ""}`, - }; - } - }) - .withAfterToolCallback(async (tool, args, toolCtx, result) => { - if (tool.name === "searchApi" && toolCtx.state["summarize"] === true) { - // Example: mark for summarization in subsequent steps - toolCtx.state["needs_summary"] = true; - } - return result; - }); +## Per-tool auth injection + +Inject authentication tokens or enrich arguments for specific tools without changing the tool implementations. + +```typescript +import { AgentBuilder } from "@iqai/adk"; +import type { BaseTool, ToolContext } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("auth_agent") + .withModel("gemini-2.5-flash") + .withBeforeToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + ): Record | null => { + // Only inject auth for tools that need it + if (["search_api", "fetch_data"].includes(tool.name)) { + const token = toolContext.state.get("api_token"); + if (!token) { + return { error: "No API token found. Please authenticate first." }; + } + args.authorization = `Bearer ${token}`; + } + return null; + }, + ) + .withAfterToolCallback( + ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + toolResponse: Record, + ): Record | null => { + // Flag the result for summarization if the agent requested it + if ( + tool.name === "search_api" && + toolContext.state.get("auto_summarize") + ) { + return { ...toolResponse, _needs_summary: true }; + } + return null; + }, + ) + .build(); ``` + +## Next steps + + + + + + diff --git a/apps/docs/content/docs/framework/callbacks/index.mdx b/apps/docs/content/docs/framework/callbacks/index.mdx index 3ca767c90..5b861c704 100644 --- a/apps/docs/content/docs/framework/callbacks/index.mdx +++ b/apps/docs/content/docs/framework/callbacks/index.mdx @@ -1,120 +1,115 @@ --- title: Callbacks -description: Observe, customize, and control agent behavior with powerful callback mechanisms +description: Hook into the ADK-TS agent lifecycle to observe, modify, and control behavior at six defined execution points — without changing framework code. --- import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; +import { Mermaid } from "@/components/mdx/mermaid"; -Callbacks are a cornerstone feature of ADK-TS, providing a powerful mechanism to hook into an agent's execution process. They allow you to observe, customize, and even control the agent's behavior at specific, predefined points without modifying the core ADK-TS framework code. +Callbacks are functions you attach to an agent that the framework calls automatically at six defined points in every run — before and after the agent itself, the LLM, and each tool. They let you add logging, enforce guardrails, modify data, or short-circuit execution without touching framework internals. -**What are they?** In essence, callbacks are standard functions that you define. You then associate these functions with an agent when you create it. The ADK-TS framework automatically calls your functions at key stages, letting you observe or intervene. Think of it like checkpoints during the agent's process: +## Where callbacks fire -- **Before the agent starts its main work on a request, and after it finishes:** When you ask an agent to do something (e.g., answer a question), it runs its internal logic to figure out the response. -- The `Before Agent` callback executes _right before_ this main work begins for that specific request. -- The `After Agent` callback executes _right after_ the agent has finished all its steps for that request and has prepared the final result, but just before the result is returned. -- This "main work" encompasses the agent's _entire_ process for handling that single request. This might involve deciding to call an LLM, actually calling the LLM, deciding to use a tool, using the tool, processing the results, and finally putting together the answer. These callbacks essentially wrap the whole sequence from receiving the input to producing the final output for that one interaction. -- **Before sending a request to, or after receiving a response from, the Large Language Model (LLM):** These callbacks (`Before Model`, `After Model`) allow you to inspect or modify the data going to and coming from the LLM specifically. -- **Before executing a tool (a function or another agent) or after it finishes:** Similarly, `Before Tool` and `After Tool` callbacks give you control points specifically around the execution of tools invoked by the agent. +Each callback fires at a fixed point in the execution chain. The agent invokes the LLM and tools as needed, and each of those steps has a before/after hook: - -**Why use them?** Callbacks unlock significant flexibility and enable advanced agent capabilities: - -- **Observe & Debug:** Log detailed information at critical steps for monitoring and troubleshooting. -- **Customize & Control:** Modify data flowing through the agent (like LLM requests or tool results) or even bypass certain steps entirely based on your logic. -- **Implement Guardrails:** Enforce safety rules, validate inputs/outputs, or prevent disallowed operations. -- **Manage State:** Read or dynamically update the agent's session state during execution. -- **Integrate & Enhance:** Trigger external actions (API calls, notifications) or add features like caching. - -## How Callbacks Work - -### Callback Registration - -Callbacks are registered during agent creation: + chart={` +flowchart TD + BA[beforeAgentCallback] --> AG[Agent logic] + AG --> BM[beforeModelCallback] --> LLM[LLM call] --> AM[afterModelCallback] + AG --> BT[beforeToolCallback] --> TL[tool.runAsync] --> AT[afterToolCallback] + AM --> AA[afterAgentCallback] + AT --> AA +`} +/> + +## Available callbacks + +| Callback | Available on | Fires | +| --------------------- | --------------- | ------------------------------------ | +| `beforeAgentCallback` | All agents | Before the agent's main logic begins | +| `afterAgentCallback` | All agents | After the agent's logic completes | +| `beforeModelCallback` | `LlmAgent` only | Before each LLM call | +| `afterModelCallback` | `LlmAgent` only | After each LLM response | +| `beforeToolCallback` | `LlmAgent` only | Before each tool executes | +| `afterToolCallback` | `LlmAgent` only | After each tool completes | + +## Return values + +What you return from a callback tells the framework whether to proceed or override: + +| Callback | Return to continue | Return to override | +| --------------------- | --------------------- | --------------------------------------------------------------- | +| `beforeAgentCallback` | `undefined` | `Content` — skips the agent, uses this as its output | +| `afterAgentCallback` | `undefined` | `Content` — replaces the agent's output | +| `beforeModelCallback` | `null` or `undefined` | `LlmResponse` — skips the LLM call | +| `afterModelCallback` | `null` or `undefined` | `LlmResponse` — replaces the LLM's response | +| `beforeToolCallback` | `null` or `undefined` | `Record` — skips the tool, uses this as its result | +| `afterToolCallback` | `null` or `undefined` | `Record` — replaces the tool's result | + + + You can also mutate inputs in-place before returning the continue sentinel. + For example, modify `llmRequest.config.systemInstruction` in + `beforeModelCallback` and return `null` to proceed with the modified request. + + +## Quick start + +Attach callbacks via `AgentBuilder`. This example logs every LLM call and blocks a specific keyword before it reaches the model: ```typescript -import { LlmAgent, CallbackContext, LlmRequest, LlmResponse } from "@iqai/adk"; - -// Define callback function -const beforeModelCallback = ({ - callbackContext, - llmRequest, -}: { - callbackContext: CallbackContext; - llmRequest: LlmRequest; -}): LlmResponse | null => { - console.log(`Processing request for agent: ${callbackContext.agentName}`); - - // Return null to proceed normally - // Return LlmResponse to skip LLM call - return null; -}; - -// Create agent with callback -const agent = new LlmAgent({ - name: "callback_agent", - model: "gemini-2.5-flash", - description: "Agent with callbacks", - instruction: "You are helpful", - beforeModelCallback, -}); +import { AgentBuilder, CallbackContext, LlmResponse } from "@iqai/adk"; +import type { LlmRequest } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withBeforeModelCallback( + ({ + callbackContext, + llmRequest, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }): LlmResponse | null => { + const lastMessage = + llmRequest.contents[llmRequest.contents.length - 1]?.parts?.[0]?.text ?? + ""; + + if (lastMessage.includes("confidential")) { + return new LlmResponse({ + content: { + role: "model", + parts: [{ text: "I cannot process that request." }], + }, + }); + } + + console.log(`LLM call from agent: ${callbackContext.agentName}`); + return null; + }, + ) + .build(); ``` -## The Callback Mechanism: Interception and Control - -When the ADK-TS framework reaches a callback point (for example, just before calling the LLM), it checks whether the agent was configured with a corresponding callback and, if so, executes it. - -**Context is key:** Your callback receives a context object to understand and affect the current run: - -- `CallbackContext` for agent/model-level callbacks -- `ToolContext` (extends `CallbackContext`) for tool callbacks - -These include invocation/session info, a delta-aware `state`, and helper methods like `saveArtifact`, `loadArtifact`, `listArtifacts`, and `searchMemory`. - -**Controlling the flow:** In TypeScript, the value you return determines whether ADK-TS proceeds or overrides behavior. The exact “continue vs override” sentinel differs by callback kind: - -1. Continue with default behavior - -- before_agent / after_agent: return `undefined` -- before_model / after_model: return `null` -- before_tool / after_tool: return `null` - -You can still mutate mutable inputs in-place (for example, tweak `llmRequest` in a before_model callback or adjust `args` in a before_tool callback) and then return the sentinel to proceed. - -2. Override default behavior by returning a specific object - -- before_agent → return `Content` (from `@google/genai`) to skip the agent’s run and immediately reply. -- after_agent → return `Content` to replace the agent’s produced reply. -- before_model → return `LlmResponse` to skip the external LLM call (great for guardrails or caching). -- after_model → return `LlmResponse` to replace the received LLM response. -- before_tool → return `Record` to skip actual tool execution and use this as the tool result. -- after_tool → return `Record` to replace the tool’s result before it’s sent back to the LLM. - -Notes - -- Agent callbacks are typed to return `Content | undefined`. -- Model callbacks are typed to return `LlmResponse | null | undefined`. -- Tool callbacks are typed to return `Record | null | undefined`. -- All callbacks may be `async` and you can configure a single callback or an array; when multiple are provided, they’re invoked in order until one returns a non-sentinel value. +All six callbacks accept either a single function or an array. When you pass an array, callbacks run in order until one returns a non-sentinel value — the rest are skipped. + +## Next steps + + + + + + diff --git a/apps/docs/content/docs/framework/callbacks/meta.json b/apps/docs/content/docs/framework/callbacks/meta.json index 4bcb52ae1..d9f110f52 100644 --- a/apps/docs/content/docs/framework/callbacks/meta.json +++ b/apps/docs/content/docs/framework/callbacks/meta.json @@ -1,4 +1,4 @@ { "icon": "GitBranch", - "pages": ["index", "types", "callback-patterns"] + "pages": ["types", "callback-patterns"] } diff --git a/apps/docs/content/docs/framework/callbacks/types.mdx b/apps/docs/content/docs/framework/callbacks/types.mdx index f4444bee8..71eb0b042 100644 --- a/apps/docs/content/docs/framework/callbacks/types.mdx +++ b/apps/docs/content/docs/framework/callbacks/types.mdx @@ -1,450 +1,377 @@ --- title: Callback Types -description: Understanding different types of callbacks and when each type fires in the agent lifecycle +description: Signatures, trigger points, and return behavior for all six ADK-TS callback types — agent, model, and tool. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; -The framework provides different types of callbacks that trigger at various stages of an agent's execution. Understanding when each callback fires and what context it receives is key to using them effectively. +ADK-TS provides six callback types across three lifecycle stages: agent execution, LLM interaction, and tool execution. This page covers the exact signature for each, when it fires, and what each return value does. -## Agent Lifecycle Callbacks +## Agent lifecycle callbacks -These callbacks are available on any agent that inherits from `BaseAgent` (including `LlmAgent`, `SequentialAgent`, `ParallelAgent`, `LoopAgent`, etc.). +These fire around the agent's own execution and are available on all agent types — `LlmAgent`, `SequentialAgent`, `ParallelAgent`, `LoopAgent`, and any custom agent extending `BaseAgent`. -### Before Agent Callback +### beforeAgentCallback -**When:** Called immediately before the agent's `runAsyncImpl` method is executed. It runs after the agent's `InvocationContext` is created but before its core logic begins. - -**Purpose:** Ideal for setting up resources or state needed only for this specific agent's run, performing validation checks on the session state, logging the entry point of the agent's activity, or potentially skipping the agent execution entirely. - -**Signature:** +Fires immediately before the agent's `runAsyncImpl` begins. Use it to validate state, initialise resources, log entry points, or skip the agent entirely by returning early. ```typescript +import type { Content } from "@google/genai"; +import type { CallbackContext } from "@iqai/adk"; + type BeforeAgentCallback = ( callbackContext: CallbackContext, ) => Promise | Content | undefined; ``` -**Example:** +Returning `undefined` lets the agent run normally. Returning a `Content` object skips the agent entirely and uses that content as its output. ```typescript -import { LlmAgent, CallbackContext, Content } from "@iqai/adk"; - -const beforeAgentCallback = ( - callbackContext: CallbackContext, -): Content | undefined => { - // Check if agent should be skipped - const skipAgent = callbackContext.state.get("skip_agent"); - - if (skipAgent) { - // Return content to skip agent execution - return { - role: "model", - parts: [{ text: "Agent execution was skipped based on state." }], - }; - } +import { LlmAgent } from "@iqai/adk"; +import type { CallbackContext } from "@iqai/adk"; +import type { Content } from "@google/genai"; - // Log agent start - console.log(`Starting agent: ${callbackContext.agentName}`); +const agent = new LlmAgent({ + name: "guarded_agent", + model: "gemini-2.5-flash", + instruction: "You are a helpful assistant.", + description: "An agent with a guardrail on availability.", + beforeAgentCallback: ( + callbackContext: CallbackContext, + ): Content | undefined => { + const isBlocked = callbackContext.state.get("agent_blocked"); + + if (isBlocked) { + return { + role: "model", + parts: [{ text: "This agent is currently unavailable." }], + }; + } - return undefined; // Proceed with normal execution -}; + console.log(`Agent started: ${callbackContext.agentName}`); + return undefined; + }, +}); ``` - - -**Gatekeeping Pattern:** `beforeAgentCallback` acts as a gatekeeper, allowing -you to intercept execution before a major step and potentially prevent it -based on checks like state, input validation, or permissions. +### afterAgentCallback - - -### After Agent Callback - -**When:** Called immediately after the agent's `runAsyncImpl` method successfully completes. It does not run if the agent was skipped due to `beforeAgentCallback` returning content or if `endInvocation` was set during the agent's run. - -**Purpose:** Useful for cleanup tasks, post-execution validation, logging the completion of an agent's activity, modifying final state, or augmenting/replacing the agent's final output. - -**Signature:** +Fires immediately after `runAsyncImpl` completes successfully. Does not fire if the agent was skipped by `beforeAgentCallback`. Use it for cleanup, post-processing, or replacing the agent's final output. ```typescript +import type { Content } from "@google/genai"; +import type { CallbackContext } from "@iqai/adk"; + type AfterAgentCallback = ( callbackContext: CallbackContext, ) => Promise | Content | undefined; ``` -**Example:** +Returning `undefined` passes through the agent's original output. Returning a `Content` object replaces it. ```typescript +import type { CallbackContext } from "@iqai/adk"; +import type { Content } from "@google/genai"; + const afterAgentCallback = ( callbackContext: CallbackContext, ): Content | undefined => { - // Update completion count - const count = callbackContext.state.get("completion_count") || 0; - callbackContext.state.set("completion_count", count + 1); - - // Log completion - console.log( - `Agent ${callbackContext.agentName} completed. Total completions: ${ - count + 1 - }`, - ); - - // Optionally modify the output - const shouldAddNote = callbackContext.state.get("add_completion_note"); - if (shouldAddNote) { - return { - role: "model", - parts: [ - { text: "Task completed successfully with additional processing." }, - ], - }; - } - - return undefined; // Use original output + const count = (callbackContext.state.get("run_count") ?? 0) + 1; + callbackContext.state.set("run_count", count); + console.log(`Agent ${callbackContext.agentName} completed (run #${count})`); + return undefined; }; ``` - - -**Post-Processing Pattern:** `afterAgentCallback` allows post-processing or -modification. You can inspect the result of a step and decide whether to let -it pass through, change it, or completely replace it. +## LLM interaction callbacks - - -## LLM Interaction Callbacks - -These callbacks are specific to `LlmAgent` and provide hooks around the interaction with the Large Language Model. - -### Before Model Callback +These are specific to `LlmAgent` and fire around each call to the underlying language model. A single agent turn may involve multiple LLM calls, so these can fire more than once per invocation. -**When:** Called just before the `generateContentAsync` request is sent to the LLM within an `LlmAgent`'s flow. +### beforeModelCallback -**Purpose:** Allows inspection and modification of the request going to the LLM. Use cases include adding dynamic instructions, injecting few-shot examples based on state, modifying model config, implementing guardrails, or implementing request-level caching. - -**Signature:** +Fires just before the LLM request is sent. Use it to inspect or modify the request — inject dynamic instructions, add few-shot examples, enforce content policies, or return a cached response to skip the network call entirely. ```typescript -type BeforeModelCallback = ({ - callbackContext, - llmRequest, -}: { +import type { LlmRequest, LlmResponse, CallbackContext } from "@iqai/adk"; + +type BeforeModelCallback = (args: { callbackContext: CallbackContext; llmRequest: LlmRequest; -}) => Promise | LlmResponse | undefined; +}) => Promise | LlmResponse | null | undefined; ``` -**Example:** +Returning `null` or `undefined` lets the LLM call proceed (with any in-place modifications you made to `llmRequest`). Returning an `LlmResponse` skips the LLM call and uses that response directly. ```typescript -import { LlmAgent, CallbackContext, LlmRequest, LlmResponse } from "@iqai/adk"; - -const beforeModelCallback = ({ - callbackContext, - llmRequest, -}: { - callbackContext: CallbackContext; - llmRequest: LlmRequest; -}): LlmResponse | undefined => { - // Add user preferences to system instruction - const userLang = callbackContext.state.get("user_language"); - if (userLang && llmRequest.config.systemInstruction) { - const existingInstruction = - llmRequest.config.systemInstruction.parts?.[0]?.text || ""; - llmRequest.config.systemInstruction.parts = [ - { - text: `${existingInstruction}\n\nUser language preference: ${userLang}`, - }, - ]; - } +import { LlmAgent, LlmResponse } from "@iqai/adk"; +import type { CallbackContext, LlmRequest } from "@iqai/adk"; - // Check for blocked content - const lastContent = llmRequest.contents?.[llmRequest.contents.length - 1]; - const userMessage = lastContent?.parts?.[0]?.text || ""; +const agent = new LlmAgent({ + name: "policy_agent", + model: "gemini-2.5-flash", + instruction: "You are a helpful assistant.", + description: "An agent with a content policy on certain topics.", + beforeModelCallback: ({ + callbackContext, + llmRequest, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }): LlmResponse | null => { + const lastMessage = + llmRequest.contents[llmRequest.contents.length - 1]?.parts?.[0]?.text ?? + ""; + + if (lastMessage.toLowerCase().includes("restricted")) { + return new LlmResponse({ + content: { + role: "model", + parts: [{ text: "I cannot help with that topic." }], + }, + }); + } - if (userMessage.toLowerCase().includes("blocked_keyword")) { - // Return response to skip LLM call - return new LlmResponse({ - content: { - role: "model", - parts: [ - { text: "I cannot process requests containing blocked content." }, - ], - }, - }); - } + // Inject the user's preferred language into the system instruction + const lang = callbackContext.state.get("preferred_language"); + if (lang && llmRequest.config) { + llmRequest.config.systemInstruction = + `${llmRequest.config.systemInstruction ?? ""}\nRespond in: ${lang}`.trim(); + } - return undefined; // Proceed with LLM call -}; + return null; + }, +}); ``` -**Return Value Effect:** - -- If the callback returns `undefined`, the LLM continues its normal workflow -- If the callback returns an `LlmResponse` object, the call to the LLM is **skipped** and the returned response is used directly - -### After Model Callback - -**When:** Called just after a response (`LlmResponse`) is received from the LLM, before it's processed further by the invoking agent. +### afterModelCallback -**Purpose:** Allows inspection or modification of the raw LLM response. Use cases include logging model outputs, reformatting responses, censoring sensitive information generated by the model, parsing structured data from the LLM response and storing it in state, or handling specific error codes. - -**Signature:** +Fires after a response is received from the LLM, before it's processed further. Use it to log model outputs, extract structured data into state, censor sensitive content, or reformat the response. ```typescript -type AfterModelCallback = ({ - callbackContext, - llmResponse, -}: { +import type { LlmResponse, CallbackContext } from "@iqai/adk"; + +type AfterModelCallback = (args: { callbackContext: CallbackContext; llmResponse: LlmResponse; -}) => Promise | LlmResponse | undefined; +}) => Promise | LlmResponse | null | undefined; ``` -**Example:** +Returning `null` or `undefined` passes the original response through. Returning a new `LlmResponse` replaces it. ````typescript +import type { CallbackContext, LlmResponse } from "@iqai/adk"; + const afterModelCallback = ({ callbackContext, llmResponse, }: { callbackContext: CallbackContext; llmResponse: LlmResponse; -}): LlmResponse | undefined => { - // Log model response - console.log(`LLM responded for agent: ${callbackContext.agentName}`); - - // Extract and save structured data - const responseText = llmResponse.content?.parts?.[0]?.text || ""; - const jsonMatch = responseText.match(/```json\n(.*?)\n```/s); - if (jsonMatch) { +}): LlmResponse | null => { + const text = llmResponse.content?.parts?.[0]?.text ?? ""; + + // Pull out any JSON block and store it in state for downstream tools + const match = text.match(/```json\n([\s\S]*?)\n```/); + if (match) { try { - const data = JSON.parse(jsonMatch[1]); - callbackContext.state.set("extracted_data", data); - } catch (error) { - console.warn("Failed to parse JSON from response:", error); + callbackContext.state.set("extracted_json", JSON.parse(match[1])); + } catch { + // Malformed JSON — ignore } } - // Add disclaimer if needed - const addDisclaimer = callbackContext.state.get("add_disclaimer"); - if (addDisclaimer && llmResponse.content) { - const modifiedResponse = { ...llmResponse }; - const originalText = modifiedResponse.content.parts?.[0]?.text || ""; - modifiedResponse.content.parts = [ - { - text: `${originalText}\n\n*This response was generated by AI and should be verified.*`, - }, - ]; - return modifiedResponse; - } - - return undefined; // Use original response + return null; }; ```` -## Tool Execution Callbacks +## Tool execution callbacks -These callbacks are also specific to `LlmAgent` and trigger around the execution of tools (including `FunctionTool`, agent tools, etc.) that the LLM might request. +These are specific to `LlmAgent` and fire around each tool call the LLM requests. They receive a `ToolContext` which extends `CallbackContext` with tool-specific methods like `listArtifacts` and `searchMemory`. - +### beforeToolCallback -Status: Tool execution callbacks are implemented in the current TypeScript -ADK-TS. You can use them today to validate/modify arguments, short-circuit -execution, and post-process tool results. - - - -### Before Tool Callback - -**When:** Called just before a specific tool's `runAsync` method is invoked, after the LLM has generated a function call for it. - -**Purpose:** Allows inspection and modification of tool arguments, performing authorization checks before execution, logging tool usage attempts, or implementing tool-level caching. - -**Signature:** +Fires just before a tool's `runAsync` is invoked. Use it to validate arguments, check authorization, inject auth tokens, or return a cached result to skip execution. ```typescript +import type { BaseTool, ToolContext } from "@iqai/adk"; + type BeforeToolCallback = ( tool: BaseTool, - toolArgs: Record, + args: Record, toolContext: ToolContext, -) => Promise | undefined> | Record | undefined; +) => + | Promise | null | undefined> + | Record + | null + | undefined; ``` -**Example:** +Returning `null` or `undefined` lets the tool run (with any in-place changes to `args`). Returning a `Record` skips execution and uses it as the tool result. ```typescript -const beforeToolCallback: BeforeToolCallback = ( - tool, - toolArgs, - toolContext, -) => { - // Check authentication - const hasAuth = toolContext.state.get("api_token"); - if (!hasAuth) { - // Return error result to skip tool execution - return { - error: "Authentication required for this tool", - }; - } +import type { BaseTool, ToolContext } from "@iqai/adk"; - // Log tool usage - console.log( - `Executing tool with function call ID: ${toolContext.functionCallId}`, - ); +const beforeToolCallback = ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, +): Record | null => { + const token = toolContext.state.get("api_token"); - // Validate arguments - if (!toolArgs.query || toolArgs.query.length < 3) { - return { - error: "Query must be at least 3 characters long", - }; + if (!token) { + return { error: "Authentication required. Please sign in first." }; } - return undefined; // Proceed with tool execution + // Inject the token so the tool can use it + args.authToken = token; + + console.log( + `Running tool: ${tool.name} (call ID: ${toolContext.functionCallId})`, + ); + return null; }; ``` -**Return Value Effect:** - -1. If the callback returns `undefined`, the tool's `runAsync` method is executed with the (potentially modified) `toolArgs` -2. If a dictionary is returned, the tool's `runAsync` method is **skipped** and the returned dictionary is used directly as the tool result +### afterToolCallback -### After Tool Callback - -**When:** Called just after the tool's `runAsync` method completes successfully. - -**Purpose:** Allows inspection and modification of the tool's result before it's sent back to the LLM. Useful for logging tool results, post-processing or formatting results, or saving specific parts of the result to the session state. - -**Signature:** +Fires after the tool's `runAsync` completes. Use it to log results, save important values into state, filter sensitive fields, or reformat the output before the LLM sees it. ```typescript +import type { BaseTool, ToolContext } from "@iqai/adk"; + type AfterToolCallback = ( tool: BaseTool, - toolArgs: Record, + args: Record, toolContext: ToolContext, - toolResult: Record, -) => Promise | undefined> | Record | undefined; + toolResponse: Record, +) => + | Promise | null | undefined> + | Record + | null + | undefined; ``` -**Example:** +Returning `null` or `undefined` passes the original result through. Returning a new object replaces it. ```typescript -const afterToolCallback: AfterToolCallback = ( - tool, - toolArgs, - toolContext, - toolResult, -) => { - // Log tool result - console.log( - `Tool completed with function call ID: ${toolContext.functionCallId}`, - ); +import type { BaseTool, ToolContext } from "@iqai/adk"; - // Save important data to state - if (toolResult.transaction_id) { - toolContext.state.set("last_transaction_id", toolResult.transaction_id); +const afterToolCallback = ( + tool: BaseTool, + args: Record, + toolContext: ToolContext, + toolResponse: Record, +): Record | null => { + // Persist the transaction ID for later reference + if (toolResponse.transaction_id) { + toolContext.state.set("last_transaction_id", toolResponse.transaction_id); } - // Filter sensitive information - if (toolResult.api_key) { - const filteredResult = { ...toolResult }; - delete filteredResult.api_key; - return filteredResult; + // Strip API keys from the result before the LLM sees them + if (toolResponse.api_key) { + const { api_key, ...safe } = toolResponse; + return safe; } - return undefined; // Use original result + return null; }; ``` -**Return Value Effect:** - -1. If the callback returns `undefined`, the original `toolResult` is used -2. If a new dictionary is returned, it **replaces** the original `toolResult` +## Callback arrays -## Context Objects +Every callback property accepts either a single function or an array. When you pass an array, the framework calls each function in order and stops as soon as one returns a non-sentinel value. -Callbacks receive different context objects depending on their type: +```typescript +import { LlmAgent } from "@iqai/adk"; +import type { CallbackContext } from "@iqai/adk"; +import type { Content } from "@google/genai"; -### CallbackContext +const loggingCallback = (ctx: CallbackContext): Content | undefined => { + console.log(`[log] Agent started: ${ctx.agentName}`); + return undefined; +}; -Used in agent lifecycle and LLM interaction callbacks: +const guardCallback = (ctx: CallbackContext): Content | undefined => { + if (ctx.state.get("maintenance_mode")) { + return { role: "model", parts: [{ text: "Under maintenance." }] }; + } + return undefined; +}; -```typescript -class CallbackContext { - // Invocation metadata - readonly invocationId: string; - readonly agentName: string; - readonly appName: string; - readonly userId: string; - readonly sessionId: string; - - // State management (delta-aware) - readonly state: State; - - // Artifact operations - async loadArtifact( - filename: string, - version?: number, - ): Promise; - async saveArtifact(filename: string, artifact: Part): Promise; - - // Event actions (for tracking changes) - readonly eventActions: EventActions; -} +const agent = new LlmAgent({ + name: "multi_callback_agent", + model: "gemini-2.5-flash", + instruction: "You are a helpful assistant.", + description: "An agent that demonstrates multiple beforeAgentCallbacks.", + // loggingCallback always runs; guardCallback only blocks if maintenance_mode is set + beforeAgentCallback: [loggingCallback, guardCallback], +}); ``` -### ToolContext +## Single callback types + +Each callback type has a corresponding `Single*` variant that represents one function (before wrapping in an array). These are useful when defining a callback separately and you want TypeScript to infer the exact signature without accepting arrays: -Used in tool execution callbacks, extends CallbackContext: +| Array type | Single type | +| --------------------- | --------------------------- | +| `BeforeAgentCallback` | `SingleAgentCallback` | +| `AfterAgentCallback` | `SingleAgentCallback` | +| `BeforeModelCallback` | `SingleBeforeModelCallback` | +| `AfterModelCallback` | `SingleAfterModelCallback` | +| `BeforeToolCallback` | `SingleBeforeToolCallback` | +| `AfterToolCallback` | `SingleAfterToolCallback` | ```typescript -class ToolContext extends CallbackContext { - // Tool-specific properties - readonly functionCallId?: string; +import type { + SingleBeforeModelCallback, + SingleBeforeToolCallback, +} from "@iqai/adk"; - // Additional tool operations - async listArtifacts(): Promise; - async searchMemory(query: string): Promise; +// Explicitly typed — TypeScript enforces the exact function signature +const addUserContext: SingleBeforeModelCallback = ({ + callbackContext, + llmRequest, +}) => { + const lang = callbackContext.state.get("language") ?? "en"; + if (llmRequest.config) { + llmRequest.config.systemInstruction = + `${llmRequest.config.systemInstruction ?? ""}\nLanguage: ${lang}`.trim(); + } + return null; +}; - // Action controls - readonly actions: EventActions; // alias for eventActions -} +const checkAuth: SingleBeforeToolCallback = (tool, args, toolContext) => { + if (!toolContext.state.get("api_token")) { + return { error: "Not authenticated." }; + } + return null; +}; ``` - -Always use the specific context type provided (`CallbackContext` for -agent/model, `ToolContext` for tools) to ensure access to the appropriate -methods and properties. - + `CallbackContext` and `ToolContext` provide state, artifact, and memory + access. See [CallbackContext](/docs/framework/context/callback-context) and + [ToolContext](/docs/framework/context/tool-context) for the full property and + method reference. -## Callback Arrays - -All callback types support both single callbacks and arrays of callbacks: - -```typescript -const agent = new LlmAgent({ - name: "multi_callback_agent", - model: "gemini-2.5-flash", - description: "Agent with multiple callbacks", - instruction: "You are helpful", - - // Single callback - beforeAgentCallback: singleBeforeCallback, - - // Array of callbacks (executed in order until one returns a value) - afterAgentCallback: [ - firstAfterCallback, - secondAfterCallback, - thirdAfterCallback, - ], -}); -``` - -When using callback arrays: - -- Callbacks are executed in the order they appear in the array -- Execution stops when a callback returns a non-undefined value -- If all callbacks return undefined, normal execution continues +## Next steps + + + + + + From 9af1657981deb9b9e9fd88fb5ed5b065abdb98d5 Mon Sep 17 00:00:00 2001 From: timonwa Date: Wed, 6 May 2026 12:47:43 +0100 Subject: [PATCH 2/3] Fix Mermaid lifecycle diagram and add null safety in guardrail example Co-Authored-By: Claude Sonnet 4.6 --- .../content/docs/framework/callbacks/callback-patterns.mdx | 2 +- apps/docs/content/docs/framework/callbacks/index.mdx | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx index b1f355fcc..15dfa7415 100644 --- a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx +++ b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx @@ -37,7 +37,7 @@ const { runner } = await AgentBuilder.create("policy_agent") callbackContext: CallbackContext; llmRequest: LlmRequest; }): LlmResponse | null => { - const text = llmRequest.contents + const text = (llmRequest.contents ?? []) .flatMap(c => c.parts ?? []) .map(p => p.text ?? "") .join(" "); diff --git a/apps/docs/content/docs/framework/callbacks/index.mdx b/apps/docs/content/docs/framework/callbacks/index.mdx index 5b861c704..9528ed189 100644 --- a/apps/docs/content/docs/framework/callbacks/index.mdx +++ b/apps/docs/content/docs/framework/callbacks/index.mdx @@ -17,10 +17,9 @@ Each callback fires at a fixed point in the execution chain. The agent invokes t chart={` flowchart TD BA[beforeAgentCallback] --> AG[Agent logic] - AG --> BM[beforeModelCallback] --> LLM[LLM call] --> AM[afterModelCallback] - AG --> BT[beforeToolCallback] --> TL[tool.runAsync] --> AT[afterToolCallback] - AM --> AA[afterAgentCallback] - AT --> AA + AG --> BM[beforeModelCallback] --> LLM[LLM call] --> AM[afterModelCallback] --> AG + AG --> BT[beforeToolCallback] --> TL[tool.runAsync] --> AT[afterToolCallback] --> AG + AG --> AA[afterAgentCallback] `} /> From 47504b8ba45ece68c8cc33e97fb21d85b89a1f50 Mon Sep 17 00:00:00 2001 From: timonwa Date: Wed, 6 May 2026 17:40:00 +0100 Subject: [PATCH 3/3] Use getSystemInstructionText() in examples and remove unused LlmRequest import Co-Authored-By: Claude Sonnet 4.6 --- .../docs/framework/callbacks/callback-patterns.mdx | 11 +++-------- apps/docs/content/docs/framework/callbacks/types.mdx | 3 ++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx index 15dfa7415..85762a917 100644 --- a/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx +++ b/apps/docs/content/docs/framework/callbacks/callback-patterns.mdx @@ -224,8 +224,9 @@ const { runner } = await AgentBuilder.create("enriched_agent") // Append per-user context to the system instruction const userTier = callbackContext.state.get("user_tier") ?? "free"; if (llmRequest.config) { + const current = llmRequest.getSystemInstructionText() ?? ""; llmRequest.config.systemInstruction = - `${llmRequest.config.systemInstruction ?? ""}\nUser tier: ${userTier}`.trim(); + `${current}\nUser tier: ${userTier}`.trim(); } return null; }, @@ -258,12 +259,7 @@ Return from a `before_` callback to skip a step entirely — useful for maintena ```typescript import { AgentBuilder, LlmResponse } from "@iqai/adk"; -import type { - CallbackContext, - LlmRequest, - BaseTool, - ToolContext, -} from "@iqai/adk"; +import type { CallbackContext, BaseTool, ToolContext } from "@iqai/adk"; import type { Content } from "@google/genai"; const { runner } = await AgentBuilder.create("conditional_agent") @@ -286,7 +282,6 @@ const { runner } = await AgentBuilder.create("conditional_agent") callbackContext, }: { callbackContext: CallbackContext; - llmRequest: LlmRequest; }): LlmResponse | null => { if (callbackContext.state.get("use_cached_response")) { const cached = diff --git a/apps/docs/content/docs/framework/callbacks/types.mdx b/apps/docs/content/docs/framework/callbacks/types.mdx index 71eb0b042..5a509d650 100644 --- a/apps/docs/content/docs/framework/callbacks/types.mdx +++ b/apps/docs/content/docs/framework/callbacks/types.mdx @@ -135,8 +135,9 @@ const agent = new LlmAgent({ // Inject the user's preferred language into the system instruction const lang = callbackContext.state.get("preferred_language"); if (lang && llmRequest.config) { + const current = llmRequest.getSystemInstructionText() ?? ""; llmRequest.config.systemInstruction = - `${llmRequest.config.systemInstruction ?? ""}\nRespond in: ${lang}`.trim(); + `${current}\nRespond in: ${lang}`.trim(); } return null;