This is the full developer documentation for Foldspace # Build AI-Native Product Experiences > Foldspace is the AI-Native agentic interface for your product: an agent that interprets user intent and acts inside your app, with Analytics and Trust Lab built in. ### [User Guides](/user-guides/) [For product & growth teams](/user-guides/) [Get the most out of your agent. Follow best practices for building and tuning it, use Analytics to see how users engage and where it falls short, and validate every change in Trust Lab before it ships.](/user-guides/) [Best practices Analytics Trust Lab Conversations Knowledge](/user-guides/) ### [Developer Guide](/start/introduction/) [Build agentic experiences in code](/start/introduction/) [Drop in the SDK and make your product act. Bind actions to your backend, render UI components inside the chat, sync live app state, and orchestrate task agents — all through typed, real APIs.](/start/introduction/) [SDK Actions Chatterblocks Shared State Task Agents APIs](/start/introduction/) Shared tools for both teams [ Trust Lab ](/user-guides/trust-lab/)· [ Analytics](/user-guides/analytics/dashboard/) # 404 > The page you were looking for could not be found. ## This page folded out of existence [Section titled “This page folded out of existence”](#this-page-folded-out-of-existence) The page you were looking for doesn’t exist or has moved. [Back to home](/) · [Quickstart](/start/quickstart/) · [API Reference](/reference/overview/) # Cookbook > End-to-end, opinionated recipes that combine Foldspace primitives (actions, task agents, and UI) into a complete capability. A **recipe** is complete and opinionated. Where a [guide](/guides/build-actions/) teaches one primitive (“how to author an action”, “how to call a task agent”), a recipe composes several primitives into a real, shippable capability, with the architecture, the routing logic, and the code all in one place. Reach for a recipe when you know the *outcome* you want (“let users analyze a report and get recommendations”) but not yet the *shape*: which actions, which task agents, and how they wire together. [Report insights (routing action) ](/cookbook/insights-action-routing/)One action asks the user which KPI to optimize, then routes the analysis to a specialized task agent and returns structured insights. ## How a recipe is structured [Section titled “How a recipe is structured”](#how-a-recipe-is-structured) Every recipe follows the same arc, so you can skim or execute: * **What you’ll build**: the outcome, and the core idea in one paragraph. * **Architecture**: a diagram of the pieces and how a request flows through them. * **Steps**: build each piece in order (task agents, then the action, then the handler), with copy-ready configuration and code. * **Test, evaluate, publish**: how to prove it works before it ships. * **Best practices & extend it**: how to grow the pattern without a rewrite. ## Related [Section titled “Related”](#related) * [Build actions](/guides/build-actions/): define and implement the capabilities an agent runs. * [Task agents](/user-guides/task-agents/): specialized sub-agents the main agent delegates to. # Recipe: report insights with a routing action > Build one action that asks the user which KPI to optimize, then routes the analysis to a specialized task agent and returns structured insights. ## What you’ll build [Section titled “What you’ll build”](#what-youll-build) An **Insights** capability: the user points the agent at a reporting or analytics report and gets back *decisions* — the drivers, the anomalies, and what to do next — not just a summary. The trick is a single **routing action**. Instead of one giant “analyze everything” prompt, the action first has the agent ask a **qualifying question** — *what goal or KPI do you want to optimize?* — and then its handler **routes** that intent to the right specialized [task agent](/user-guides/task-agents/): a revenue analyst, a funnel analyst, a retention analyst, and so on. One action, many specialized analysts. Why route instead of writing one mega-prompt: * **Each analyst is focused.** A funnel expert and a churn expert need different reasoning; keeping them separate makes each sharper. * **Each analyst is independently versioned, evaluated, cached, and tuned.** You can put a cheap model behind a simple summary and a stronger one behind cohort analysis. * **The capability grows without a rewrite.** Adding a KPI means adding one task agent and one line in a routing map — the action and its handler don’t change. Prerequisites You should be comfortable [authoring an action](/user-guides/authoring-actions/), wiring its [`execute()` handler](/guides/executing-actions/), and calling a [task agent with `runTask()`](/reference/task-agent-api/). This recipe assembles those three into one capability. ## Architecture [Section titled “Architecture”](#architecture) ``` sequenceDiagram participant U as User participant A as Main Agent participant Act as Routing Action (execute) participant T as Specialist Task Agent U->>A: "Analyze last quarter's report" A->>U: Which goal or KPI do you want to optimize? U->>A: Retention A->>Act: analyze_report_insights({ report, goal, kpi }) Act->>Act: Map kpi → taskKey Act->>T: runTask(retention_churn_analyzer, { report, question }) T-->>Act: Structured insights (JSON) Act-->>A: { kpi, summary, findings, recommendations } A->>U: Reply grounded in the analyst's insights ``` The main agent handles the conversation and the qualifying question. The **routing action** is a thin dispatcher: it maps the chosen KPI to a task-agent key and calls it. The **task agent** does the heavy reasoning and returns a predictable JSON object the agent can relay. ## Step 1 — Build the specialist task agents [Section titled “Step 1 — Build the specialist task agents”](#step-1--build-the-specialist-task-agents) In **Agent Studio → Task Agents**, create one analyst per KPI family. Configure each with a **JSON** response so the handler gets a predictable object back. A single reusable output schema keeps every analyst interchangeable from the handler’s point of view: ```json { "type": "object", "properties": { "summary": { "type": "string", "description": "One-paragraph headline of what the data shows" }, "findings": { "type": "array", "description": "The most important observations, most significant first", "items": { "type": "object", "properties": { "metric": { "type": "string" }, "observation": { "type": "string" }, "impact": { "type": "string", "enum": ["high", "medium", "low"] } }, "required": ["metric", "observation", "impact"], "additionalProperties": false } }, "anomalies": { "type": "array", "description": "Values that look off and are worth a human check", "items": { "type": "object", "properties": { "metric": { "type": "string" }, "note": { "type": "string" } }, "required": ["metric", "note"], "additionalProperties": false } }, "recommendations": { "type": "array", "description": "Concrete next actions to move the chosen KPI", "items": { "type": "string" } }, "confidence": { "type": "string", "enum": ["high", "medium", "low"] } }, "required": ["summary", "findings", "anomalies", "recommendations", "confidence"], "additionalProperties": false } ``` Give each analyst instructions scoped to its KPI. A starting set: | Task agent (`api_name`) | Optimizes for | Instructions focus | | :--------------------------- | :------------ | :------------------------------------------------------------------------ | | `revenue_growth_analyzer` | Revenue | Revenue drivers by segment, expansion vs. new, ARPU, refund/discount drag | | `conversion_funnel_analyzer` | Conversion | Stage-by-stage drop-off, activation, signup→paid, where to intervene | | `retention_churn_analyzer` | Retention | Cohort retention, churn concentration, refunds, at-risk segments | | `engagement_analyzer` | Engagement | Feature usage, active-user trends, engagement anomalies | Example instructions for one analyst — keep them tight and tell it the input to expect and the goal to serve: > You are a retention and churn analyst. You receive `report` (rows of metrics, often by cohort or segment) and a `question` describing the user’s goal. Identify where retention is strongest and weakest, concentrate on churn drivers and at-risk segments, flag anomalies, and return concrete actions to improve retention. Be specific and quantitative; never invent numbers not in the data. Let the agent draft it The Task Agent editor’s **Generate** button (✦) drafts the description and instructions from a plain-English brief. The Knowledge Base also feeds these analysts automatically — if you keep an article on *what good retention analysis looks like for our product*, the analyst applies it. See the [Task Agents guide](/user-guides/task-agents/) for the editor, and [Example: analyze report data](/guides/task-agent-analysis/) for a worked single-analyst call. ## Step 2 — Author the routing action [Section titled “Step 2 — Author the routing action”](#step-2--author-the-routing-action) In **Agent Studio → Actions**, create `analyze_report_insights`. This is the front door; its job is to collect the goal/KPI and hand off. **Description** (the agent reads this to decide *when* to call it — say what it returns): > Analyzes a reporting or analytics report and returns structured insights — summary, key findings, anomalies, and recommended actions — tuned to the KPI the user wants to optimize. **Instructions** (the qualifying-question rule — this is what makes it a *router*, not a guesser): > Use when the user wants to analyze a report, dashboard, or analytics export and get insights or recommendations. **Before calling, make sure you know which goal or KPI to optimize for.** If the user hasn’t said, ask: *“What goal or KPI do you want to optimize — for example revenue, conversion, retention, or engagement?”* When the goal is already clear from context, skip the question and proceed. Pass the user’s objective in `goal` and the matching category in `kpi`. **User Input** parameters (see [Defining inputs](/user-guides/defining-inputs/)): | Parameter | Type | Required | Purpose | | :-------- | :------------- | :------: | :----------------------------------------------------------------- | | `report` | Object / Array | Yes | The report data (or a reference the handler can resolve) | | `goal` | String | No | The user’s objective in their own words, e.g. “reduce churn in EU” | | `kpi` | String (enum) | Yes | One of `revenue`, `conversion`, `retention`, `engagement`, `other` | The `kpi` enum is what the handler routes on; `goal` is the free-text nuance the analyst reads. ## Step 3 — Write the `execute()` router handler [Section titled “Step 3 — Write the execute() router handler”](#step-3--write-the-execute-router-handler) The handler is small on purpose: map `kpi` to a task-agent key, call it, and return the structured result. All the reasoning lives in the analysts. ```javascript // KPI → the specialist task agent that handles it. // Add a KPI by adding a task agent and one entry here — nothing else changes. const ROUTES = { revenue: 'revenue_growth_analyzer', conversion: 'conversion_funnel_analyzer', retention: 'retention_churn_analyzer', engagement: 'engagement_analyzer', }; foldspace('when', 'ready', () => { foldspace.agent({ /* …common setup… */ }) .addActionHandlers({ analyze_report_insights: { // Analysis can take longer than the 5s default — give it room. timeout: 60000, execute: async ({ report, goal, kpi }) => { const taskKey = ROUTES[kpi]; // Routing couldn't decide — hand control back so the agent asks the // qualifying question instead of guessing an analyst. if (!taskKey) { return { needsGoal: true, message: 'Which KPI should I optimize for?', options: Object.keys(ROUTES), }; } try { const insights = await foldspace .agent('YOUR-AGENT-API-NAME') .runTask({ taskKey, data: { report, goal, question: `Optimize for ${goal || kpi}. Surface the drivers, flag anomalies, and recommend concrete next actions.`, }, // Dashboards refresh often; reuse an identical analysis for an hour. cacheOptions: { ttlSeconds: 3600 }, }); // Return the KPI alongside the analyst's output so the agent can // frame its reply ("Here's what matters for retention…"). return { kpi, goal, ...insights }; } catch (error) { console.error('Report analysis failed:', error); return { error: true, kpi, message: 'The analysis could not be completed. Try again shortly.' }; } }, }, }); }); ``` A few things this handler gets right: * **It routes, it doesn’t analyze.** Swapping a model or sharpening a prompt happens in the task agent, never here. * **It closes the loop when routing fails.** The `needsGoal` return nudges the agent back to the qualifying question rather than picking an analyst at random. * **It caches.** Identical `data` returns from cache, so a re-opened dashboard doesn’t pay twice — see [cache options](/reference/task-agent-api/#cache-options). ## Step 4 — The qualifying-question pattern, in depth [Section titled “Step 4 — The qualifying-question pattern, in depth”](#step-4--the-qualifying-question-pattern-in-depth) There are two ways the agent gathers the goal/KPI. Pick based on how much you want to steer: 1. **Conversationally (default, no code).** The action **Instructions** from Step 2 tell the agent to ask *“what goal or KPI do you want to optimize?”* whenever it’s unclear, and to skip the question when the goal is obvious from context. This is enough for most cases and keeps the flow natural. 2. **With an in-chat UI picker.** For a guided experience, render a KPI chooser in the chat from the action instead of returning plain data — see [UI components in chat](/guides/in-chat-ui/). Set `awaitUserInput: true` on the handler so the agent pauses for the user’s selection before the analysis runs. The `needsGoal` fallback in the handler is the safety net for both: if the KPI still isn’t set when the action fires, control returns to the agent to ask, rather than to a wrong analyst. ## Step 5 — Test, evaluate, publish [Section titled “Step 5 — Test, evaluate, publish”](#step-5--test-evaluate-publish) * **Route correctly.** Use the action’s **Evals** tab to confirm real phrasings (“why are we losing customers?”, “where’s revenue coming from?”) extract the right `kpi` and route to the right analyst. * **Analyze correctly.** Use each task agent’s **Evaluation** tab to assert the JSON output meets your criteria on representative reports before it ever reaches the main agent. * **Watch cost and latency.** The task agent **Logs** tab records inputs, output, model, latency, and cost per run — use it to move simple analysts to a cheaper model. * **Publish in order.** Publish the **task agents first** (the handler calls them by `taskKey`), then publish the **routing action**. A task agent must be Published before the agent can delegate to it. ## Best practices & extend it [Section titled “Best practices & extend it”](#best-practices--extend-it) * **One clear job per analyst.** If an analyst’s instructions need “and” several times, it’s probably two analysts. * **Add a KPI in two edits.** Create the task agent, add one `ROUTES` entry. The action, its schema, and the handler are untouched. * **Cache for dashboards, bypass on fresh data.** Set `cacheOptions.bypass: true` when the underlying report changed and you need a fresh run. * **Observe adoption.** Track execution and routing accuracy with Action Insights and [Conversational Analytics](/user-guides/analytics/dashboard/) — they tell you which KPIs users actually ask about, so you know which analyst to build next. ## Related [Section titled “Related”](#related) * [Task agents](/user-guides/task-agents/) — configure the specialist sub-agents. * [Example: analyze report data](/guides/task-agent-analysis/) — a single-analyst `runTask()` call. * [Authoring actions](/user-guides/authoring-actions/) · [Execute actions](/guides/executing-actions/) — the two halves of the routing action. * [UI components in chat](/guides/in-chat-ui/) — render a KPI picker for the qualifying question. # Appearance and greeting > Set your agent's name, logo, entry message, and prompt bar hint to match your brand. These settings control how the agent looks and how it introduces itself. Pass them through `setConfiguration` as described in the [SDK configuration](/customize/configuration/) reference. ## Options [Section titled “Options”](#options) | Key | Type | Default | Description | | :----------------- | :-------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------- | | `agentDisplayName` | `string` | `"Agent"` | The display name shown for the agent in the chat interface. | | `entryMessage` | `string` | `"How can I help you today?"` | The first message users see when the chat interface launches. Sets the initial tone of the conversation. | | `logoUrl` | `string` | `""` | URL of the logo displayed in the agent interface. | | `promptBarHint` | `string` | `""` | Placeholder text inside the prompt input bar, used to guide the user on what to ask (for example, `"Ask me anything..."`). | | `displaySparkIcon` | `boolean` | `true` | Whether the spark icon is visible in the agent interface. | ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ agentDisplayName: "Foldspace Assistant", entryMessage: "What can I help with?", logoUrl: "https://assets.foldspace.com/logos/foldspace-logo-light.svg", promptBarHint: "Ask me anything...", displaySparkIcon: true }); ``` To set colors and typography alongside these settings, see [Theme](/customize/theme/). # Border Animation Settings > Defines animated border effects for highlighting the agent. ## enableBorderAnimation [Section titled “enableBorderAnimation”](#enableborderanimation) Determines whether animated border effects are enabled around the chat interface or agent widget. When active, borders smoothly transition between defined colors. * **Type:** `boolean` * **Default Value:** `true` ## borderColors [Section titled “borderColors”](#bordercolors) Defines the sequence of colors used for the border animation. The colors transition in order to create a dynamic visual effect. * **Type:** `array` * **Default Value:** `["#0054B6", "#ffffff"]` ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ // Border animation settings borderAnimationSettings: { enableBorderAnimation: true, borderColors: ["#0066FF", "#3B82F6", "#60A5FA"] } }); ``` # SDK configuration > Configure how your Foldspace agent looks, behaves, and renders through the SDK. Prerequisites The [SDK is installed](/start/install/) on your page. The SDK accepts a single configuration object that controls how the agent looks, introduces itself, and behaves on the page. Pass it through `setConfiguration` on an agent instance: ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ // settings go here }); ``` ``` flowchart TD SC["setConfiguration({ })"] AP["Appearance & greeting
name, logo, entry message"] PL["Placement & rendering
position, render mode, drag"] TH["Theme
colors, fonts, border"] DM["Dark mode
dark theme, dark logo"] IC["Interaction
timeout, voice, speech"] CS["Conversation starters
prompts, defaults"] SO["Style overrides
CSS-level customization"] SC --> AP SC --> PL SC --> TH SC --> DM SC --> IC SC --> CS SC --> SO ``` Settings are grouped into focused areas. Each grouped setting (such as `theme`) has its own reference page covering every key, its type, and its default value. ## Setting groups [Section titled “Setting groups”](#setting-groups) | Group | What it controls | | :----------------------------------------------------------- | :-------------------------------------------------------------------------- | | [Appearance and greeting](/customize/appearance/) | The agent’s display name, logo, entry message, and prompt bar hint. | | [Placement and rendering](/customize/placement/) | How the agent is embedded, anchored, and positioned on screen. | | [Theme](/customize/theme/) | The colors and typography used in the agent interface. | | [Dark mode](/customize/dark-mode/) | Dark-theme colors and a dark-specific logo. | | [Interaction and communication](/customize/interaction/) | Session timeout, voice chat, speech-to-text, and new-conversation triggers. | | [Conversation starters](/customize/conversation-starters/) | Predefined prompts and behavior for starting conversations. | | [Style overrides](/customize/style-overrides/) | CSS-level overrides for the entry button, speech bubble, and agent frame. | | [Border animation](/customize/border-animation/) | The animated border around the agent entry point. | | [Share current tab](/customize/share-current-tab/) | Let the agent read and understand the current page content. | | [Embedded configuration](/customize/embedded-configuration/) | Render the agent inline inside a specific DOM container. | | [Localization](/customize/localization/) | Language override and developer-translated strings. | See also the [full configuration example](/customize/configuration-example/) for a single snippet that uses every group together. ## Studio and SDK precedence [Section titled “Studio and SDK precedence”](#studio-and-sdk-precedence) ``` flowchart TD ST["Studio settings
dashboard UI"] --> D["Default values"] SDK["SDK setConfiguration
code"] --> D SDK -->|"overrides"| ST D --> R["Rendered agent"] style SDK fill:#1842ef,stroke:#1842ef,color:#ffffff style R fill:#e8ecff,stroke:#1842ef,color:#0a0e1a ``` Configure every one of these settings visually in [Studio](https://app.foldspace.ai/agent/customize). SDK configuration takes precedence over Studio values, so you can override defaults per page or per context directly in code. # Agent Configuration Example > An example showcasing how to customize your Foldspace Agent's look and behavior. ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ // String properties agentDisplayName: "Foldspace Assistant", entryMessage: "What can I help with?", logoUrl: "https://assets.foldspace.com/logos/foldspace-logo-light.svg", renderMode: "IFRAME", promptBarHint: "Ask me anything...", // Boolean properties allowAgentDrag: true, enableVoiceChat: true, enableSpeechToText: true, displaySparkIcon: true, // Position property (format: "VERTICAL_HORIZONTAL") initialAgentPosition: "BOTTOM_RIGHT", // Session timeout in minutes sessionTimeout: 30, // Theme object theme: { primaryColor: "#0066FF", primaryFontColor: "#0B1220", backgroundColor: "#FFFFFF", fontFamily: "Roboto", fontSize: 14, secondaryFontColor: "#6B7280", secondaryColor: "#F3F4F6", borderColor: "#E6E9EE" }, // Dark mode settings darkModeSettings: { darkTheme: { primaryColor: "#3B82F6", primaryFontColor: "#FFFFFF", backgroundColor: "#0F1724", fontFamily: "Roboto", fontSize: 14, secondaryFontColor: "#9CA3AF", secondaryColor: "#111827", borderColor: "#1F2937" }, darkLogoUrl: "https://assets.foldspace.com/logos/foldspace-logo-dark.svg" }, // Border animation settings borderAnimationSettings: { enableBorderAnimation: true, borderColors: ["#0066FF", "#3B82F6", "#60A5FA"] }, // Conversation starters settings conversationStartersSettings: { defaultConversationStarter: "KNOWLEDGE", startNewConversationOnStarterClick: true, conversationStarters: { KNOWLEDGE: [ { title: "What is your return policy?" }, { title: "How do I track my order?" }, { title: "Show me product specifications" } ], ACTION: [ { title: "Schedule a demo" }, { title: "Check my order status" }, { title: "Contact support" } ] } }, // Style override styleOverride: { entryComponentStyle: { header: { horizontalAlignment: "start" }, conversationStarter: { borderColor: "#E6E9EE", borderWidth: 1 } }, speechBubbleStyle: { padding: "14px 16px", borderRadius: "12px", backgroundColor: "#FFFFFF", color: "#0B1220", fontSize: "14px", lineHeight: "1.4" } }, shareStateSettings: { active: true, tooltipText: "Enable Tandem Mode — allow the assistant to read the page to help complete tasks", tooltipStarters: [ "Fill out this form for me", "Suggest values for the fields on this page" ] }, screenSharingSettings: { active: true, tooltipText: "Allow the assistant to understand this page and provide helpful suggestions", tooltipStarters: [ "Summarize this page for me", "Highlight important information here" ], shareMode: "both" } }); ``` # Conversation starters > Configure the KNOWLEDGE and ACTION prompts shown on the agent entry screen, and override them at runtime with the programmatic API. Conversation starters are the predefined prompts shown on the agent entry screen. Configure them statically with `setConfiguration`, or override them at runtime with the programmatic API. To translate starters for different locales, see [Localization](/customize/localization/). ## conversationStarters [Section titled “conversationStarters”](#conversationstarters) The set of starter prompts shown to the user. This is an object with two arrays, `KNOWLEDGE` and `ACTION`, each representing a distinct type of starter: * **KNOWLEDGE**: informational or contextual questions based on existing knowledge. * **ACTION**: task-oriented starters that trigger specific actions. Each item in either array must follow the format `{ title: "" }`. Type: `object`\ Default value: `{ "KNOWLEDGE": [], "ACTION": [] }` ## defaultConversationStarter [Section titled “defaultConversationStarter”](#defaultconversationstarter) Which starter group is shown by default when the chat loads. Type: `string`\ Default value: `"KNOWLEDGE"`\ Accepted values: `"KNOWLEDGE" | "ACTION"` ## startNewConversationOnStarterClick [Section titled “startNewConversationOnStarterClick”](#startnewconversationonstarterclick) Whether clicking a starter begins a new conversation instead of continuing the current one. Type: `boolean`\ Default value: `false` ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ conversationStartersSettings: { defaultConversationStarter: "KNOWLEDGE", startNewConversationOnStarterClick: true, conversationStarters: { KNOWLEDGE: [ { title: "What is your return policy?" }, { title: "How do I track my order?" }, { title: "Show me product specifications" } ], ACTION: [ { title: "Schedule a demo" }, { title: "Check my order status" }, { title: "Contact support" } ] } } }); ``` ## Programmatic API [Section titled “Programmatic API”](#programmatic-api) ### `setConversationStarters(starters, defaultStarterType?)` [Section titled “setConversationStarters(starters, defaultStarterType?)”](#setconversationstartersstarters-defaultstartertype) Overrides the conversation starters shown on the entry screen at runtime. If called before the agent is ready, the call is queued and replayed once the agent initializes. **Parameters:** | Parameter | Type | Required | Description | | -------------------- | ------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------- | | `starters` | `Record<"KNOWLEDGE" \| "ACTION", { title: string }[]>` | Yes | A record with `KNOWLEDGE` and `ACTION` arrays. Pass `null` to restore the original remote configuration starters. | | `defaultStarterType` | `"KNOWLEDGE" \| "ACTION"` | No | Switch which starter tab is shown by default. If omitted or `null`, the original remote default is restored. | **Returns:** the agent instance, enabling method chaining. **Example: override starters** ```javascript const agent = foldspace.agent('YOUR-AGENT-KEY'); agent.setConversationStarters( { KNOWLEDGE: [ { title: "What is your return policy?" }, { title: "How do I track my order?" } ], ACTION: [ { title: "Schedule a demo" }, { title: "Contact support" } ] }, "ACTION" ); ``` **Example: restore original starters from remote config** ```javascript agent.setConversationStarters(null); ``` ### `getConversationStarters()` [Section titled “getConversationStarters()”](#getconversationstarters) Returns the current conversation starters and the default starter type. If starters were overridden via `setConversationStarters`, the overridden values are returned. Otherwise, it reflects the remote configuration. **Parameters:** none. **Returns:** `{ starters, defaultStarterType } | null` Returns `null` if the agent is not yet initialized. | Field | Type | Description | | -------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------- | | `starters` | `Record<"KNOWLEDGE" \| "ACTION", { title: string }[]>` | The starters grouped by type. Each group is guaranteed to contain at least one entry. | | `defaultStarterType` | `"KNOWLEDGE" \| "ACTION"` | Which starter tab is currently the default. | **Example:** ```javascript const result = agent.getConversationStarters(); console.log(result); ``` **Response:** ```json { "starters": { "KNOWLEDGE": [ { "title": "What is your return policy?" }, { "title": "How do I track my order?" } ], "ACTION": [ { "title": "Schedule a demo" }, { "title": "Contact support" } ] }, "defaultStarterType": "ACTION" } ``` # Dark mode > Configure a dark theme and a dark-mode logo so your agent stays on-brand across light and dark appearances. Supply theme overrides and a dedicated logo that apply when the agent renders in dark mode. Pass a `darkModeSettings` object to `setConfiguration`. To override individual component styles instead of the whole theme, see [Style Overrides](/customize/style-overrides/). ## darkLogoUrl [Section titled “darkLogoUrl”](#darklogourl) URL of the logo shown when dark mode is active. Use it to keep branding legible against a dark background. Type: `string`\ Default value: `""` ## darkTheme [Section titled “darkTheme”](#darktheme) Theme overrides applied in dark mode. Its structure mirrors the standard [Theme](/customize/theme/) settings, so you control colors and styles in the dark appearance. Type: `object`\ Default value: `{}` ``` flowchart TD T["Theme settings
primaryColor, fontFamily, ..."] --> L["Light mode"] DT["darkModeSettings.darkTheme
overrides same keys"] --> D["Dark mode"] T -.->|"inherits unset keys"| D DL["darkLogoUrl"] --> D style L fill:#ffffff,stroke:#e2e4ea,color:#0a0e1a style D fill:#0a0e1a,stroke:#1d2336,color:#e2e4ea ``` ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ // Dark mode settings darkModeSettings: { darkTheme: { primaryColor: "#3B82F6", primaryFontColor: "#FFFFFF", backgroundColor: "#0F1724", fontFamily: "Roboto", fontSize: 14, secondaryFontColor: "#9CA3AF", secondaryColor: "#111827", borderColor: "#1F2937" }, darkLogoUrl: "https://assets.foldspace.com/logos/foldspace-logo-dark.svg" } }); ``` # Embedded Mode Configuration > Configure how the agent mounts directly into a specific DOM element within your application layout. Embedded mode allows you to mount the agent directly into a specific DOM element within your application layout. This is ideal for creating integrated chat experiences, custom sidebars, or dedicated support pages where the agent is a native part of the UI rather than a floating widget. ## embeddedConfiguration [Section titled “embeddedConfiguration”](#embeddedconfiguration) Defines the layout and behavior of the agent when mode is set to `EMBEDDED`. This object controls how the agent mounts into your application’s DOM and its initial UI state. **Structure:** * **Container:** The HTML element into which the agent will mount. * **Type:** `HTMLElement` ## Example [Section titled “Example”](#example) ```javascript const containerElement = document.getElementById('agent-container'); window.foldspace.agent({ apiName: 'AGENT_API_NAME', mode: 'EMBEDDED', configuration: { embeddedConfiguration: { container: containerElement, } } }); ``` # Interaction and communication > Enable voice conversations and speech-to-text input so users can talk to your agent instead of typing. These settings control how users communicate with the agent: by typing, by speaking, or both. Pass these flags to `setConfiguration`. ## enableVoiceChat [Section titled “enableVoiceChat”](#enablevoicechat) Lets users hold a real-time voice conversation with the agent instead of typing. Type: `boolean`\ Default value: `false` ## enableSpeechToText [Section titled “enableSpeechToText”](#enablespeechtotext) Adds a microphone button to the chat interface. When a user clicks the mic, their speech is recorded and transcribed into a text prompt. Type: `boolean`\ Default value: `false` ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ enableVoiceChat: true, enableSpeechToText: true, }); ``` # Localization > Serve your agent in your user's language with languageOverride, and translate the strings the server does not handle for you. Pass `languageOverride` when initializing the agent to fetch its configuration in a specific language. The SDK sends the language code to the server, which returns translated content for that locale. ```javascript foldspace.agent('YOUR-AGENT-KEY', { languageOverride: "fr" }); ``` The value must be an **ISO 639-1** language code, such as `"en"`, `"fr"`, `"es"`, `"de"`, or `"ja"`. ## What gets translated automatically [Section titled “What gets translated automatically”](#what-gets-translated-automatically) When `languageOverride` is set, the server returns the agent configuration with **conversation starters** translated into the requested language, provided those translations have been set up in advance (see [Translating conversation starters in the admin UI](#translating-conversation-starters-in-the-admin-ui)). ## What requires developer translation [Section titled “What requires developer translation”](#what-requires-developer-translation) The following UI strings are **not** translated automatically by the server. If your application supports multiple languages, you are responsible for translating them and passing the correct values via `setConfiguration`: | String | Config property | Where it appears | | --------------------- | ---------------------- | ------------------------------------------ | | Entry Message | `entryMessage` | Placeholder text inside the chat input bar | | Agent name | `agentDisplayName` | Header of the agent panel | | Conversation starters | `conversationStarters` | Entry screen prompts shown to the user | For the full conversation starter reference, see [Conversation Starters](/customize/conversation-starters/). ### Example: translating dynamic strings [Section titled “Example: translating dynamic strings”](#example-translating-dynamic-strings) ```javascript const translations = { en: { agentDisplayName: "Support Agent", entryMessage: "Ask me anything...", starters: { KNOWLEDGE: [ { title: "What is your return policy?" }, { title: "How do I track my order?" } ], ACTION: [ { title: "Schedule a demo" }, { title: "Contact support" } ] } }, fr: { agentDisplayName: "Agent d'assistance", entryMessage: "Posez-moi une question...", starters: { KNOWLEDGE: [ { title: "Quelle est votre politique de retour ?" }, { title: "Comment suivre ma commande ?" } ], ACTION: [ { title: "Planifier une démo" }, { title: "Contacter le support" } ] } } }; const userLang = "fr"; const t = translations[userLang]; const agent = foldspace.agent('YOUR-AGENT-KEY', { languageOverride: userLang }); agent.setConfiguration({ agentDisplayName: t.agentDisplayName, entryMessage: t.entryMessage }); agent.setConversationStarters(t.starters); ``` ## Translating conversation starters in the admin UI [Section titled “Translating conversation starters in the admin UI”](#translating-conversation-starters-in-the-admin-ui) You can translate conversation starters directly from the Foldspace admin panel without writing any code. Go to your agent’s **Starters** page under **Customize** and add translations for each language. Translations added there are served automatically when the SDK requests a specific language via `languageOverride`, so no additional SDK code is needed for starters managed through the admin UI. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) If the requested language is not available, the agent falls back to **English (`en`)**. Two console warnings help you identify translation issues: * `[Foldspace] Requested language "xx" is not supported. Serving "en" instead.` The server did not have translations for the requested language and returned English. * `[Foldspace] Requested language "xx" — server did not confirm language resolution. Translations may not be available.` The server did not return an `X-Resolved-Language` header, so the response may or may not be translated. If `languageOverride` is omitted, the agent defaults to English. # Placement and rendering > Control how your Foldspace agent is embedded, positioned, and behaves on screen. These settings control how the agent is embedded into the host page, where it appears, and how it behaves on screen. Pass them through `setConfiguration` as described in the [SDK configuration](/customize/configuration/) reference. ## Options [Section titled “Options”](#options) | Key | Type | Default | Accepted values | Description | | :--------------------- | :-------- | :--------------- | :------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------- | | `allowAgentDrag` | `boolean` | `true` | | Whether users can click and drag the agent widget to reposition it anywhere on the screen. | | `renderMode` | `string` | `"IFRAME"` | `"IFRAME" \| "INLINE"` | How the SDK is embedded within the host page: as an iframe or an inline component. | | `sessionTimeout` | `number` | `30` | `5` to `1440` | The maximum idle time (in minutes) before a new conversation is automatically started. Minimum 5 minutes, maximum 1440 minutes (24 hours). | | `initialAgentPosition` | `string` | `"CENTER_RIGHT"` | `"TOP_RIGHT" \| "TOP_LEFT" \| "BOTTOM_RIGHT" \| "BOTTOM_LEFT" \| "CENTER_RIGHT" \| "CENTER_LEFT"` | The default on-screen location where the agent widget appears when first loaded. | Note `initialAgentPosition` values follow the format `"VERTICAL_HORIZONTAL"`. ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ renderMode: "IFRAME", allowAgentDrag: true, // Position property (format: "VERTICAL_HORIZONTAL") initialAgentPosition: "BOTTOM_RIGHT", // Session timeout in minutes sessionTimeout: 30, }); ``` # Share current tab > Give the agent real-time page context — configured in the dashboard, with an SDK override for programmatic control. Share current tab lets the agent read and interpret the page’s content, giving it full awareness of the page’s structure and elements so it can guide users based on what’s on the page. This is the **page context** the agent reasons over on each turn (see [Context](/guides/context/)). Turn it on and tune it from your agent’s settings in the **dashboard** — that’s the primary way to configure it. The SDK settings below are an **override**: use `setConfiguration({ screenSharingSettings })` to control it programmatically, per page or per session, on top of the dashboard defaults. ## SDK override [Section titled “SDK override”](#sdk-override) ### active [Section titled “active”](#active) Enables or disables the Shared Current Tab feature. When `true`, the agent can read and understand the content of the current page to provide contextual assistance, suggest actions, or highlight relevant information without making any changes directly. * **Type:** `boolean` * **Default Value:** `false` ### tooltipText [Section titled “tooltipText”](#tooltiptext) One-line hover label shown on the Agent control. Use this to clearly explain what enabling Shared Current Tab does in a short, non-technical way that reassures users about control and privacy. * **Type:** `string` * **Default Value:** `"Let the assistant understand this page and help you."` ### tooltipStarters [Section titled “tooltipStarters”](#tooltipstarters) Array of up to 2 short suggested prompts shown in the UI to help users quickly start interactions using Shared Current Tab. Hints should be concise, actionable, and reassure users that the agent only reads the page to assist, without modifying it. * **Type:** `array` * **Default Value:** `[ "Summarize this", "Explain what I'm seeing" ]` ### shareMode [Section titled “shareMode”](#sharemode) Determines what type of data the SDK includes when the user shares their screen with the agent. This controls whether the agent receives only an image of the page, only its HTML structure, or both. * **Type:** `"image" | "html" | "both"` * **Default Value:** `"both"` ### Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ screenSharingSettings: { active: true, tooltipText: "Allow the assistant to understand this page and provide helpful suggestions", tooltipStarters: [ "Summarize this page for me", "Highlight important information here" ], shareMode: "both" } }); ``` # Style overrides > Override the styles of individual SDK components, including the entry component and user message speech bubbles. Restyle individual agent components without rebuilding the theme. Pass a `styleOverride` object to `setConfiguration` to control layout, borders, color, and typography on specific components. For global theming, see [Dark Mode](/customize/dark-mode/). ## entryComponentStyle [Section titled “entryComponentStyle”](#entrycomponentstyle) Styles the entry component, including its header layout and the conversation starter borders. This is an object with nested properties for each area of the entry component. Type: `object` Default value: ```javascript { "header": { "horizontalAlignment": "start" }, "conversationStarter": { "borderWidth": 1, "borderColor": "#000000" } } ``` ### Properties [Section titled “Properties”](#properties) | Property | Type | Default | Description | | --------------------------------- | ------------------------------ | ---------- | ------------------------------------------ | | `header.horizontalAlignment` | `"start" \| "center" \| "end"` | `"center"` | Horizontal alignment of header content. | | `conversationStarter.borderWidth` | `number` | `1` | Border width of each conversation starter. | | `conversationStarter.borderColor` | `string` | `""` | Border color of each conversation starter. | ## speechBubbleStyle [Section titled “speechBubbleStyle”](#speechbubblestyle) Styles the user message speech bubble. Use it to control spacing, color, and typography inside the bubble. Type: `object`\ Default value: `{}` ### Properties [Section titled “Properties”](#properties-1) | Property | Type | Description | | ----------------- | -------- | ------------------------------------------ | | `padding` | `string` | Internal spacing inside the speech bubble. | | `borderRadius` | `string` | Corner rounding of the speech bubble. | | `backgroundColor` | `string` | Background color of the speech bubble. | | `color` | `string` | Text color inside the speech bubble. | | `fontSize` | `string` | Font size for bubble text. | | `lineHeight` | `string` | Line height for text inside the bubble. | ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ styleOverride: { entryComponentStyle: { header: { horizontalAlignment: "start" }, conversationStarter: { borderColor: "#E6E9EE", borderWidth: 1 } }, speechBubbleStyle: { padding: "14px 16px", borderRadius: "12px", backgroundColor: "#FFFFFF", color: "#0B1220", fontSize: "14px", lineHeight: "1.4" } } }); ``` # Theme > Define the colors and typography used across your Foldspace agent interface. The `theme` group controls the colors and typography used across the agent interface. Pass it as a nested object inside `setConfiguration`, as described in the [SDK configuration](/customize/configuration/) reference. ## Options [Section titled “Options”](#options) | Key | Type | Default | Description | | :------------------- | :------- | :---------- | :-------------------------------------------------------------------------------------------- | | `primaryColor` | `string` | `"#0054B6"` | The main accent color used for key interactive and highlight elements across the UI. | | `primaryFontColor` | `string` | `"#121926"` | The main text color applied to primary content and key interface elements. | | `backgroundColor` | `string` | `"#FFFFFF"` | The primary background color for chat containers and other main interface components. | | `fontFamily` | `string` | `"Poppins"` | The typeface used throughout the interface. | | `fontSize` | `number` | `14` | The default font size for text across the interface. | | `borderColor` | `string` | `""` | The color used for borders around static UI elements, such as containers or input fields. | | `secondaryFontColor` | `string` | `"#121926"` | The text color for secondary content, such as hints, timestamps, or less prominent labels. | | `secondaryColor` | `string` | `"#0054B6"` | The secondary accent color, used to complement the primary color and create visual hierarchy. | ## Example [Section titled “Example”](#example) ```javascript foldspace.agent('YOUR-AGENT-KEY').setConfiguration({ theme: { primaryColor: "#0066FF", primaryFontColor: "#0B1220", backgroundColor: "#FFFFFF", fontFamily: "Roboto", fontSize: 14, secondaryFontColor: "#6B7280", secondaryColor: "#F3F4F6", borderColor: "#E6E9EE" } }); ``` # A2A advanced patterns > LangGraph, TypeScript, Java, and native (no-SDK) implementations of an A2A server agent, plus streaming progress and multi-turn user input. This page covers building A2A server agents beyond the [Python quickstart](/guides/a2a-quickstart/): orchestrating with [LangGraph](https://www.langchain.com/langgraph), the TypeScript and Java SDKs, and implementing the protocol natively without an SDK. ## LangGraph integration [Section titled “LangGraph integration”](#langgraph-integration) [LangGraph](https://langchain-ai.github.io/langgraph/) provides a graph-based orchestration layer for building stateful, multi-step agents. If you already orchestrate agents with LangGraph, you can expose a compiled graph as an A2A server agent with a thin `AgentExecutor` bridge — no need to rewrite your agent logic. > New to LangGraph? See the [official LangGraph documentation](https://langchain-ai.github.io/langgraph/) and the [`langchain-ai/langgraph` repository](https://github.com/langchain-ai/langgraph). ### Basic LangGraph agent [Section titled “Basic LangGraph agent”](#basic-langgraph-agent) Wrap a LangGraph research agent using the `AgentExecutor` bridge pattern to expose it as an A2A server. research\_agent.py ```python import uvicorn from langgraph.graph import StateGraph, MessagesState, START, END from langchain_openai import ChatOpenAI from a2a.server.apps.jsonrpc import A2AStarletteApplication from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import InMemoryTaskStore, TaskUpdater from a2a.types import ( AgentCard, AgentCapabilities, AgentSkill, Part, TextPart, ) # --- Step 1: Define the LangGraph agent --- llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) def research_node(state: MessagesState): response = llm.invoke(state["messages"]) return {"messages": [response]} graph_builder = StateGraph(MessagesState) graph_builder.add_node("researcher", research_node) graph_builder.add_edge(START, "researcher") graph_builder.add_edge("researcher", END) research_graph = graph_builder.compile() # --- Step 2: Bridge LangGraph to A2A --- class ResearchAgentExecutor(AgentExecutor): def __init__(self, graph): self.graph = graph async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.start_work() user_input = context.get_user_input() result = await self.graph.ainvoke( {"messages": [{"role": "user", "content": user_input}]} ) response_text = result["messages"][-1].content await updater.add_artifact(parts=[Part(root=TextPart(text=response_text))]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.cancel() # --- Step 3: Agent Card + Server --- agent_card = AgentCard( name="Research Assistant Agent", description="Researches topics and provides detailed, cited answers", url="http://localhost:9000/", version="1.0.0", capabilities=AgentCapabilities(streaming=True), default_input_modes=["text/plain"], default_output_modes=["text/plain"], skills=[ AgentSkill( id="research", name="Deep Research", description="Researches any topic and provides comprehensive answers", tags=["research", "knowledge", "analysis"], examples=["What are the key differences between gRPC and REST?"], ), ], ) request_handler = DefaultRequestHandler( agent_executor=ResearchAgentExecutor(research_graph), task_store=InMemoryTaskStore(), ) server = A2AStarletteApplication( agent_card=agent_card, http_handler=request_handler, ) if __name__ == "__main__": uvicorn.run(server.build(), host="0.0.0.0", port=9000) ``` ### LangGraph with streaming progress [Section titled “LangGraph with streaming progress”](#langgraph-with-streaming-progress) For long-running agents, stream intermediate updates with `astream_events` so users see progress in real time. ```python class StreamingResearchExecutor(AgentExecutor): def __init__(self, graph): self.graph = graph async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.start_work() user_input = context.get_user_input() async for event in self.graph.astream_events( {"messages": [{"role": "user", "content": user_input}]}, version="v2", ): if event["event"] == "on_chat_model_stream": chunk = event["data"]["chunk"].content if chunk: await updater.update_status( state="working", message=Part(root=TextPart(text=chunk)), ) final_result = await self.graph.ainvoke( {"messages": [{"role": "user", "content": user_input}]} ) response_text = final_result["messages"][-1].content await updater.add_artifact(parts=[Part(root=TextPart(text=response_text))]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.cancel() ``` ### LangGraph with multi-turn (user input required) [Section titled “LangGraph with multi-turn (user input required)”](#langgraph-with-multi-turn-user-input-required) When your agent needs clarification before proceeding, transition to `input-required` — the Copilot prompts the user and relays their reply back. ```python class MultiTurnResearchExecutor(AgentExecutor): def __init__(self, graph): self.graph = graph async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.start_work() user_input = context.get_user_input() result = await self.graph.ainvoke( {"messages": [{"role": "user", "content": user_input}]} ) response_text = result["messages"][-1].content if result.get("needs_clarification", False): await updater.update_status( state="input-required", message=Part(root=TextPart(text=response_text)), ) else: await updater.add_artifact(parts=[Part(root=TextPart(text=response_text))]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.cancel() ``` ## TypeScript SDK [Section titled “TypeScript SDK”](#typescript-sdk) Install the SDK and Express: ```bash npm install @a2a-js/sdk express ``` A complete agent implementation demonstrating message handling, custom execution logic, and skill definitions. agent.ts ```typescript import express from 'express'; import { v4 as uuidv4 } from 'uuid'; import { AgentCard, Message } from '@a2a-js/sdk'; import { AgentExecutor, RequestContext, ExecutionEventBus, DefaultRequestHandler, InMemoryTaskStore, } from '@a2a-js/sdk/server'; import { agentCardHandler, jsonRpcHandler, restHandler, } from '@a2a-js/sdk/server/express'; class DocumentAnalysisExecutor implements AgentExecutor { async execute( requestContext: RequestContext, eventBus: ExecutionEventBus ): Promise { const userMessage = requestContext.message; const inputText = userMessage.parts .filter((p) => p.kind === 'text') .map((p) => p.text) .join(' '); const analysisResult = await this.analyzeDocument(inputText); const responseMessage: Message = { kind: 'message', messageId: uuidv4(), role: 'agent', parts: [{ kind: 'text', text: analysisResult }], contextId: requestContext.contextId, }; eventBus.publish(responseMessage); eventBus.finished(); } cancelTask = async (): Promise => {}; private async analyzeDocument(text: string): Promise { return `Analysis complete. Document contains ${text.split(' ').length} words. Key topics identified.`; } } const agentCard: AgentCard = { name: 'Document Analysis Agent', description: 'Analyzes documents for key topics, entities, and summaries', url: 'http://localhost:3000', version: '1.0.0', capabilities: { streaming: true, pushNotifications: false }, defaultInputModes: ['text/plain', 'application/pdf'], defaultOutputModes: ['text/plain', 'application/json'], skills: [ { id: 'summarize', name: 'Summarize Document', description: 'Produces a concise summary of the input document', tags: ['nlp', 'summarization'], examples: ['Summarize this contract for key obligations'], }, { id: 'extract-entities', name: 'Extract Entities', description: 'Identifies people, organizations, dates, and amounts', tags: ['nlp', 'ner', 'extraction'], examples: ['Extract all company names and dates from this filing'], }, ], }; const handler = new DefaultRequestHandler({ agentExecutor: new DocumentAnalysisExecutor(), taskStore: new InMemoryTaskStore(), }); const app = express(); app.use(express.json()); app.get('/.well-known/agent.json', agentCardHandler(agentCard)); app.post('/jsonrpc', jsonRpcHandler(handler)); app.use('/api', restHandler(handler)); app.listen(3000, () => { console.log('Document Analysis Agent running on port 3000'); }); ``` ## Java SDK [Section titled “Java SDK”](#java-sdk) Maven dependency: ```xml org.a2aproject.sdk a2a-java-sdk-reference-jsonrpc 1.0.0.Beta1 ``` A CDI producer configures the agent metadata — capabilities, skills, and supported interfaces: ```java import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.AgentSkill; import org.a2aproject.sdk.spec.TransportProtocol; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import java.util.Collections; import java.util.List; @ApplicationScoped public class CodeReviewAgentCardProducer { private static final String AGENT_URL = "http://localhost:10001"; @Produces @PublicAgentCard public AgentCard agentCard() { return AgentCard.builder() .name("Code Review Agent") .description("Reviews code for bugs, security issues, and style violations") .supportedInterfaces(List.of( new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) .pushNotifications(false) .build()) .defaultInputModes(Collections.singletonList("text/plain")) .defaultOutputModes(List.of("text/plain", "application/json")) .skills(List.of( AgentSkill.builder() .id("review-code") .name("Review Code") .description("Analyzes code for bugs, security vulnerabilities, and improvements") .tags(List.of("code-review", "security", "quality")) .examples(List.of("Review this Java class for thread safety issues")) .build(), AgentSkill.builder() .id("suggest-refactor") .name("Suggest Refactoring") .description("Proposes structural improvements and design patterns") .tags(List.of("refactoring", "design-patterns")) .examples(List.of("Suggest how to refactor this service class")) .build())) .build(); } } ``` An executor processes requests and manages the task lifecycle, including cancellation: ```java import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.JSONRPCError; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskNotCancelableError; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TextPart; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; import java.util.List; @ApplicationScoped public class CodeReviewExecutorProducer { @Inject CodeReviewAgent codeReviewAgent; @Produces public AgentExecutor agentExecutor() { return new CodeReviewAgentExecutor(codeReviewAgent); } private static class CodeReviewAgentExecutor implements AgentExecutor { private final CodeReviewAgent codeReviewAgent; public CodeReviewAgentExecutor(CodeReviewAgent agent) { this.codeReviewAgent = agent; } @Override public void execute(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { if (context.getTask() == null) { agentEmitter.submit(); } agentEmitter.startWork(); String code = extractTextFromMessage(context.getMessage()); String review = codeReviewAgent.review(code); TextPart responsePart = new TextPart(review); agentEmitter.addArtifact(List.of(responsePart)); agentEmitter.complete(); } @Override public void cancel(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { Task task = context.getTask(); if (task.getStatus().state() == TaskState.COMPLETED || task.getStatus().state() == TaskState.CANCELED) { throw new TaskNotCancelableError(); } agentEmitter.cancel(); } private String extractTextFromMessage(Message message) { StringBuilder sb = new StringBuilder(); for (Part part : message.parts()) { if (part instanceof TextPart textPart) { sb.append(textPart.text()); } } return sb.toString(); } } } ``` ## Native implementation (without an SDK) [Section titled “Native implementation (without an SDK)”](#native-implementation-without-an-sdk) If you need full control or work in a language without an official SDK, implement the A2A protocol directly over JSON-RPC 2.0. ### What you must implement [Section titled “What you must implement”](#what-you-must-implement) 1. `GET /.well-known/agent.json` — serve your Agent Card. 2. `POST /` — handle JSON-RPC requests: * `message/send` — accept a message and return a task. * `tasks/get` — return task status. * `message/stream` — SSE stream of updates (for streaming agents). ### Python — native with Flask [Section titled “Python — native with Flask”](#python--native-with-flask) agent.py ```python import json import uuid from flask import Flask, request, jsonify, Response app = Flask(__name__) tasks = {} AGENT_CARD = { "name": "Expense Report Agent", "description": "Processes and categorizes expense reports", "url": "http://localhost:5000", "version": "1.0.0", "capabilities": {"streaming": False, "pushNotifications": False}, "defaultInputModes": ["text/plain", "application/json"], "defaultOutputModes": ["application/json"], "skills": [ { "id": "categorize-expenses", "name": "Categorize Expenses", "description": "Categorizes line items into budget categories", "tags": ["finance", "expenses"], "examples": ["Categorize these 5 expense line items"], } ], } @app.route("/.well-known/agent.json", methods=["GET"]) def agent_card(): return jsonify(AGENT_CARD) @app.route("/", methods=["POST"]) def handle_jsonrpc(): body = request.get_json() method = body.get("method") params = body.get("params", {}) request_id = body.get("id") if method == "message/send": return handle_send_message(params, request_id) elif method == "tasks/get": return handle_get_task(params, request_id) else: return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": {"code": -32601, "message": f"Method not found: {method}"}, }) def handle_send_message(params, request_id): message = params.get("message", {}) context_id = params.get("metadata", {}).get("contextId", str(uuid.uuid4())) parts = message.get("parts", []) user_text = " ".join(p["text"] for p in parts if p.get("kind") == "text") result = categorize_expenses(user_text) task_id = str(uuid.uuid4()) task = { "kind": "task", "id": task_id, "contextId": context_id, "status": { "state": "completed", "message": { "role": "agent", "parts": [{"kind": "data", "data": result}], }, }, "artifacts": [ { "artifactId": str(uuid.uuid4()), "parts": [{"kind": "data", "data": result}], "lastChunk": True, } ], } tasks[task_id] = task return jsonify({"jsonrpc": "2.0", "id": request_id, "result": task}) def handle_get_task(params, request_id): task_id = params.get("id") task = tasks.get(task_id) if not task: return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": {"code": -32602, "message": "Task not found"}, }) return jsonify({"jsonrpc": "2.0", "id": request_id, "result": task}) def categorize_expenses(text: str) -> dict: return { "categories": [ {"item": "Client dinner", "category": "Meals & Entertainment", "amount": 127.50}, {"item": "Uber to airport", "category": "Transportation", "amount": 45.00}, {"item": "Conference ticket", "category": "Professional Development", "amount": 599.00}, ], "total": 771.50, } if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) ``` ### TypeScript — native with Express and SSE streaming [Section titled “TypeScript — native with Express and SSE streaming”](#typescript--native-with-express-and-sse-streaming) Demonstrates SSE streaming via the `message/stream` method to push updates asynchronously. agent.ts ```typescript import express from 'express'; import { v4 as uuidv4 } from 'uuid'; const app = express(); app.use(express.json()); const tasks: Record = {}; const AGENT_CARD = { name: 'Invoice Processing Agent', description: 'Extracts structured data from invoices and receipts', url: 'http://localhost:4000', version: '1.0.0', capabilities: { streaming: true, pushNotifications: false }, defaultInputModes: ['text/plain', 'application/pdf'], defaultOutputModes: ['application/json'], skills: [ { id: 'extract-invoice', name: 'Extract Invoice Data', description: 'Parses invoices and returns structured line items', tags: ['finance', 'ocr', 'extraction'], examples: ['Extract all line items from this invoice'], }, ], }; app.get('/.well-known/agent.json', (req, res) => { res.json(AGENT_CARD); }); app.post('/', (req, res) => { const { method, params, id: requestId } = req.body; switch (method) { case 'message/send': return handleSendMessage(params, requestId, res); case 'message/stream': return handleStreamMessage(params, requestId, res); case 'tasks/get': return handleGetTask(params, requestId, res); default: return res.json({ jsonrpc: '2.0', id: requestId, error: { code: -32601, message: `Method not found: ${method}` }, }); } }); function handleSendMessage(params: any, requestId: string, res: express.Response) { const userText = params.message.parts .filter((p: any) => p.kind === 'text') .map((p: any) => p.text) .join(' '); const taskId = uuidv4(); const result = processInvoice(userText); const task = { kind: 'task', id: taskId, contextId: params.metadata?.contextId || uuidv4(), status: { state: 'completed' }, artifacts: [{ artifactId: uuidv4(), parts: [{ kind: 'data', data: result }], lastChunk: true }], }; tasks[taskId] = task; res.json({ jsonrpc: '2.0', id: requestId, result: task }); } function handleStreamMessage(params: any, requestId: string, res: express.Response) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const taskId = uuidv4(); const contextId = params.metadata?.contextId || uuidv4(); // Emit working status res.write(`data: ${JSON.stringify({ jsonrpc: '2.0', method: 'tasks/statusUpdate', params: { taskId, contextId, status: { state: 'working' } }, })}\n\n`); setTimeout(() => { const result = processInvoice( params.message.parts.filter((p: any) => p.kind === 'text').map((p: any) => p.text).join(' ') ); // Emit artifact res.write(`data: ${JSON.stringify({ jsonrpc: '2.0', method: 'tasks/artifactUpdate', params: { taskId, contextId, artifact: { artifactId: uuidv4(), parts: [{ kind: 'data', data: result }], lastChunk: true }, }, })}\n\n`); // Emit completed status res.write(`data: ${JSON.stringify({ jsonrpc: '2.0', method: 'tasks/statusUpdate', params: { taskId, contextId, status: { state: 'completed' } }, })}\n\n`); res.end(); }, 1000); } function handleGetTask(params: any, requestId: string, res: express.Response) { const task = tasks[params.id]; if (!task) { return res.json({ jsonrpc: '2.0', id: requestId, error: { code: -32602, message: 'Task not found' }, }); } res.json({ jsonrpc: '2.0', id: requestId, result: task }); } function processInvoice(text: string) { return { vendor: 'Acme Corp', invoiceNumber: 'INV-2026-0847', lineItems: [ { description: 'Consulting services', quantity: 40, unitPrice: 150, total: 6000 }, { description: 'Travel expenses', quantity: 1, unitPrice: 1200, total: 1200 }, ], subtotal: 7200, tax: 648, total: 7848, }; } app.listen(4000, () => { console.log('Invoice Processing Agent running on port 4000'); }); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) * **Streaming responses aren’t received by the client** — confirm `capabilities.streaming` is `true` in your Agent Card and that you emit SSE events on `message/stream`. * **Tasks stay in `working` indefinitely** — ensure every path calls `complete()`, `fail()`, or `cancel()`. * **Cancellation isn’t honored** — implement `cancel()` and guard against canceling already-`completed`/`canceled` tasks. ## Further reading [Section titled “Further reading”](#further-reading) * [A2A Protocol Specification](https://a2a-protocol.org/latest/specification/) * Official SDKs: [Python](https://github.com/a2aproject/a2a-python) · [TypeScript](https://github.com/a2aproject/a2a-js) · [Java](https://github.com/a2aproject/a2a-java) * [LangGraph documentation](https://langchain-ai.github.io/langgraph/) · [`langchain-ai/langgraph`](https://github.com/langchain-ai/langgraph) * [Technology Compatibility Kit (TCK)](https://github.com/a2aproject/a2a-tck) * Next: [Connect your agent to Foldspace →](/guides/a2a-connect/) # Connect an A2A agent > Register your A2A server agent in Foldspace Agent Studio — connection details, authentication, user-context forwarding, and timeouts. Once your [A2A server agent](/guides/a2a-server-agents/) is deployed and reachable, connect it to a Copilot in **Agent Studio → A2A Agents**. The Copilot then discovers your agent’s skills and delegates matching tasks to it automatically. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * Your remote A2A server agent must run over **HTTPS** and be reachable. * It must serve an Agent Card at `/.well-known/agent.json` (or a custom URL). * You need the agent’s **base URL** and **authentication credentials**. * A Copilot can connect up to **20** A2A agents. ## The agent list [Section titled “The agent list”](#the-agent-list) **Agent Studio → A2A Agents** lists every agent connected to this Copilot: | Column | What it shows | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | **Name** | The display name you gave the agent. This is also how it appears in the **Actions** filter in [Conversations](/user-guides/conversations/). | | **Base URL** | The root URL of the remote agent. | | **Auth** | The auth type in use — **None**, **Bearer Token**, or **API Key**. | | **Agent Card** | The name from the fetched Agent Card and its skill count. A green check means the card was fetched and validated. | | **Status** | Toggle an agent on or off. A disabled agent stays configured, but the Copilot won’t invoke it. | Use **Add Agent** to connect a new one, or the **⋯** menu on a row for per-agent options. ## Connection details [Section titled “Connection details”](#connection-details) **Path:** Agent Studio → A2A Agents → **Add Agent** | Field | Required | Description | | :------------------- | :------- | :---------------------------------------------------------------------------------------------- | | **Display Name** | Yes | A friendly name for this agent (e.g. “Status Monitor Agent”). Shown in the Copilot’s tool list. | | **Base URL** | Yes | The root URL where the remote A2A agent is hosted (e.g. `https://my-agent.example.com`). | | **Agent Card URL** | No | Override the default card location. Leave blank to use `/.well-known/agent.json`. | | **Protocol Version** | Yes | Currently **A2A v0.3** (auto-selected). | ## Authentication [Section titled “Authentication”](#authentication) | Auth type | When to use | Fields | | :--------------- | :--------------------------------------------------------- | :---------- | | **None** | Agent is open or network-restricted (no auth headers sent) | — | | **Bearer Token** | Agent expects `Authorization: Bearer ` | Token value | | **API Key** | Agent expects `X-API-Key: ` | Key value | Note Secrets are encrypted at rest. Once saved, they display masked (`••••••••`). Click **Replace** to rotate a credential. After saving, click **Fetch Agent Card** to validate the connection and pull in your agent’s skills, then choose which skills to **expose** to the Copilot. ## Advanced settings (optional) [Section titled “Advanced settings (optional)”](#advanced-settings-optional) ### Forward user context [Section titled “Forward user context”](#forward-user-context) Toggle on to pass the current user’s identity to the remote agent via the `X-Foldspace-User-Id` header. You can also include a structured **user-context data part**: | Source field | Default key | Description | | :------------------ | :---------------- | :------------------------------------- | | **User Email** | `user_email` | The authenticated user’s email address | | **User ID** | `user_id` | The user’s unique identifier | | **Subscription ID** | `subscription_id` | The customer’s subscription identifier | ### Additional headers [Section titled “Additional headers”](#additional-headers) Add static custom headers (for API versioning, routing, etc.) sent on every request to your agent. ### Connection timeouts [Section titled “Connection timeouts”](#connection-timeouts) | Setting | Default | Range | Description | | :------------------ | :------ | :----- | :------------------------------------------ | | **Connect Timeout** | 10s | 1–300s | Max time to establish a connection | | **Request Timeout** | 120s | 1–600s | Max time to wait for a single HTTP response | | **Task Timeout** | 300s | 1–900s | Max total time for an A2A task to complete | ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) Start in Agent Studio → **Conversations** and open the **Actions** filter — your A2A agent is listed there by its display name. Select it and click **Apply** to narrow the list to conversations that delegated to it. Open one, click **View Analysis** on the agent message, and check the **Actions** tab: each A2A call shows a **Remote Agent** badge, its status, and — when expanded — the **Arguments** sent, the **Response** returned, and the execution time. That’s usually enough to tell a timeout apart from an auth failure or a bad payload. | Problem | Solution | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------- | | **”Failed to fetch agent card”** | Verify the Base URL is reachable, the Agent Card URL serves valid JSON, and your auth credentials are correct. | | **Agent shows but the Copilot doesn’t invoke it** | Check that the agent is **Enabled** and has at least one **Exposed** skill. | | **Authentication errors at runtime** | Confirm the token or API key hasn’t expired or been revoked. | | **Agent times out during execution** | Increase the **Task Timeout** in Advanced settings (default 300s). | ## Related [Section titled “Related”](#related) * [A2A server agents](/guides/a2a-server-agents/) — concepts and the Agent Card. * [A2A quickstart](/guides/a2a-quickstart/) — build your first agent. * [A2A advanced patterns](/guides/a2a-advanced/) — LangGraph, TypeScript, Java, native. * [A2A agents (Agent Studio)](/user-guides/a2a-agents/) — the product overview. # A2A quickstart > Build, run, and test your first A2A server agent in Python using the official A2A SDK, then connect it to the Foldspace Copilot. This quickstart builds a Travel Booking A2A server agent in Python with the official A2A SDK, then runs and tests it locally. For the concepts behind A2A, start with [A2A server agents](/guides/a2a-server-agents/). ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Python 3.10+ * Familiarity with async Python ## Step 1: Install the SDK [Section titled “Step 1: Install the SDK”](#step-1-install-the-sdk) Install the official Python A2A SDK plus `uvicorn` to serve the app. ```bash pip install a2a-sdk uvicorn ``` ## Step 2: Implement your agent [Section titled “Step 2: Implement your agent”](#step-2-implement-your-agent) The `AgentExecutor` holds your business logic, the `AgentCard` describes your capabilities, and `A2AFastAPIApplication` provides the HTTP layer. agent.py ```python import uvicorn from a2a.server.apps.jsonrpc import A2AFastAPIApplication from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import InMemoryTaskStore, TaskUpdater from a2a.types import ( AgentCard, AgentCapabilities, AgentSkill, Part, TextPart, ) class TravelBookingExecutor(AgentExecutor): """Handles flight and hotel search requests.""" async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.start_work() user_input = context.get_user_input() result = await self._search_travel_options(user_input) await updater.add_artifact(parts=[Part(root=TextPart(text=result))]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.cancel() async def _search_travel_options(self, query: str) -> str: # Replace with your actual logic — call an LLM, query APIs, etc. return f"Found 3 flight options and 5 hotels matching: {query}" # --- Agent Card: describes what your agent can do --- agent_card = AgentCard( name="Travel Booking Agent", description="Searches flights, hotels, and packages based on travel preferences", url="http://localhost:8080/", version="1.0.0", capabilities=AgentCapabilities(streaming=True), default_input_modes=["text/plain"], default_output_modes=["text/plain", "application/json"], skills=[ AgentSkill( id="search-flights", name="Search Flights", description="Find available flights between destinations", tags=["travel", "flights", "booking"], examples=["Find flights from NYC to London next Friday"], ), AgentSkill( id="search-hotels", name="Search Hotels", description="Find hotels at a destination with filters", tags=["travel", "hotels", "accommodation"], examples=["Find 4-star hotels in Paris for 3 nights"], ), ], ) # --- Wire up and start the server --- request_handler = DefaultRequestHandler( agent_executor=TravelBookingExecutor(), task_store=InMemoryTaskStore(), ) app = A2AFastAPIApplication( agent_card=agent_card, http_handler=request_handler, ).build() if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080) ``` ## Step 3: Run it [Section titled “Step 3: Run it”](#step-3-run-it) ```bash python agent.py ``` ## Step 4: Test it [Section titled “Step 4: Test it”](#step-4-test-it) Fetch the published Agent Card: ```bash curl http://localhost:8080/.well-known/agent.json | jq . ``` Send a message via JSON-RPC `message/send`: ```bash curl -X POST http://localhost:8080/ \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": "test-001", "method": "message/send", "params": { "message": { "role": "user", "parts": [{"kind": "text", "text": "Find flights from SF to Tokyo next week"}], "messageId": "msg-001" } } }' ``` Validate protocol compliance with the A2A Inspector: ```bash npx @a2a-js/inspector http://localhost:8080 ``` ## How it works [Section titled “How it works”](#how-it-works) The A2A server comprises three key components: 1. **AgentExecutor** — implements business logic, receiving a `RequestContext` (the user’s message) and publishing results to an `EventQueue`. 2. **DefaultRequestHandler** — handles protocol details, routing JSON-RPC calls to your executor and managing task lifecycle and storage. 3. **A2AFastAPIApplication** — the HTTP layer that builds a FastAPI/Starlette app with endpoints like `/.well-known/agent.json` and the JSON-RPC POST. Request flow: Client → `A2AFastAPIApplication` (HTTP) → `DefaultRequestHandler` (task lifecycle) → your executor → `TaskUpdater` publishes status/artifacts to the `EventQueue` → events return as an SSE stream (streaming) or a single JSON-RPC response (non-streaming). Note The text you send via `add_artifact(...)` is what the Foldspace Copilot shows as the **answer**. Status updates (`update_status(state="working", ...)`) are treated as **progress** only. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Problem | Fix | | :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | **Agent Card not found at `/.well-known/agent.json`** | Verify the correct port and an error-free server startup. | | **Tasks remain in `working` state** | Ensure your executor calls `complete()`, `fail()`, or `cancel()` in all code paths, including errors. | | **Authentication fails when Foldspace invokes the agent** | Confirm your Agent Card’s `authentication.schemes` matches what the server validates. | ## Next steps [Section titled “Next steps”](#next-steps) * **[Advanced patterns →](/guides/a2a-advanced/)** — LangGraph, TypeScript, Java, native (no-SDK), and streaming. * **[Connect to Foldspace →](/guides/a2a-connect/)** — register your agent in Agent Studio. # A2A server agents > Build an Agent-to-Agent (A2A) server agent that the Foldspace Copilot can discover, delegate tasks to, and stream results from — with Python, TypeScript, Java, and LangGraph examples. Agent-to-Agent (A2A) lets your Foldspace Copilot delegate work to an external AI agent — your **A2A server agent** — over a standard protocol, then stream the result back into the conversation. This is the developer guide for **building** an A2A server agent. For the product / no-code side of connecting one in Agent Studio, see the [A2A agents user guide](/user-guides/a2a-agents/). ## What is an A2A server agent? [Section titled “What is an A2A server agent?”](#what-is-an-a2a-server-agent) An A2A server agent (also called a “remote agent”) is an AI-powered service that exposes its capabilities over HTTP using the [A2A protocol](https://a2a-protocol.org). Any A2A-compatible client — including orchestrators, platforms like Foldspace, or other agents — can discover your agent via its **Agent Card**, send it tasks, and receive results through a standardized interface. The protocol is built on **JSON-RPC 2.0 over HTTP(S)**, supports **Server-Sent Events (SSE)** for streaming, and uses **Agent Cards** for capability discovery. ## How Foldspace uses A2A [Section titled “How Foldspace uses A2A”](#how-foldspace-uses-a2a) In Foldspace, the **Copilot agent acts as the A2A client**. When a user interacts with a Copilot, it discovers your server agent via its Agent Card, delegates tasks to it, and presents results back to the user. Your job is to build the server side. ```text User → Foldspace Copilot (A2A client) → Your server agent (A2A server) ← Task results / streaming updates ``` Your A2A server agent is invoked automatically when the Copilot determines your agent’s skills match the user’s intent. You only need to build and register the server side — Foldspace handles all client-side orchestration. ### What Foldspace supports [Section titled “What Foldspace supports”](#what-foldspace-supports) | Capability | How it works | | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Streaming** | Real-time SSE updates. When your agent emits status or artifact events, users see progress live in the conversation. | | **Tool progress** | When your agent invokes tools during execution, Foldspace surfaces progress indicators to users automatically. | | **Authentication** | Configure your Agent Card’s auth schemes (Bearer, API key) and Foldspace manages credential passing. | | **User input (multi-turn)** | Set task state to `input-required` and the Copilot prompts the user, then relays their response back to your agent. | ## Key concepts [Section titled “Key concepts”](#key-concepts) | Concept | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Agent Card** | A JSON manifest describing your agent’s name, skills, endpoints, authentication, and capabilities. Hosted at `/.well-known/agent.json`. | | **Task** | The unit of work. Has a lifecycle: `submitted` → `working` → `completed` / `failed` / `canceled`. | | **Message** | Carries multimodal content (text, files, structured data) between client and server. | | **Artifact** | The output/result of a completed task, streamed incrementally. | | **Streaming (SSE)** | Real-time status updates and partial results over Server-Sent Events. | Note The text you send via `add_artifact(...)` is what the Foldspace Copilot shows as the **answer**. Status updates (`update_status(state="working", ...)`) are treated as **progress** only. ## Agent Card reference [Section titled “Agent Card reference”](#agent-card-reference) The Agent Card is the entry point for all A2A interactions. It tells clients what your agent can do and how to reach it, and is served at `/.well-known/agent.json`. /.well-known/agent.json ```json { "name": "Your Agent Name", "description": "What your agent does in one sentence", "url": "https://your-agent.example.com/a2a", "version": "1.0.0", "capabilities": { "streaming": true, "pushNotifications": false }, "default_input_modes": ["text/plain"], "default_output_modes": ["text/plain", "application/json"], "skills": [ { "id": "skill-identifier", "name": "Human-Readable Skill Name", "description": "What this skill does", "tags": ["relevant", "tags"], "examples": ["Example input that triggers this skill"] } ], "authentication": { "schemes": ["bearer"] } } ``` **Required fields:** `name`, `url`, `version`, `capabilities`, `skills` ## Implementation approaches [Section titled “Implementation approaches”](#implementation-approaches) There are three ways to build an A2A server agent: | Approach | Best for | Languages | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------ | | **Official SDK** | Most developers — handles protocol compliance automatically | Python, TypeScript, Java | | **[LangGraph](https://www.langchain.com/langgraph) + SDK** | Teams already using [LangGraph](https://langchain-ai.github.io/langgraph/) for agent orchestration | Python | | **Native (no SDK)** | Full control, unsupported languages, or minimal dependencies | Any language with HTTP support | Already orchestrating agents with [LangGraph](https://github.com/langchain-ai/langgraph)? You can expose an existing LangGraph graph as an A2A server agent with a thin executor bridge — see [Advanced patterns](/guides/a2a-advanced/#langgraph-integration). ## Next steps [Section titled “Next steps”](#next-steps) * **[Quickstart →](/guides/a2a-quickstart/)** — build and test your first A2A server agent in Python. * **[Advanced patterns →](/guides/a2a-advanced/)** — LangGraph, TypeScript, Java, native (no-SDK), and streaming. * **[Connect to Foldspace →](/guides/a2a-connect/)** — register your agent in Agent Studio. # How Our Agent Thinks, Acts, and Prevents Loops > Understand how the Foldspace agent plans, selects tools, executes actions, manages iterations, and prevents infinite loops for safe, reliable automation. ## Agentic Capabilities: Planning, Tool Selection, and Iterative Execution [Section titled “Agentic Capabilities: Planning, Tool Selection, and Iterative Execution”](#agentic-capabilities-planning-tool-selection-and-iterative-execution) Foldspace’s embedded agent is designed to make SaaS applications smart, personalized, and easy to use by transforming natural language input into actions, workflows, and interactive UI. This article explains how Foldspace supports agentic capabilities across the lifecycle of intent → planning → execution → iteration. ## 1. Intent Determination and Tool Selection [Section titled “1. Intent Determination and Tool Selection”](#1-intent-determination-and-tool-selection) The agent understands their intent, triggers the right workflows, and can also display interactive UI components (like forms, cards, or dashboards) within the conversation. * Users can simply describe what they want in text or voice. * The agent uses natural language understanding to parse the request, determine intent, and map it to the correct workflow. * Actions and Schemas are defined in Agent Studio, allowing the agent to dynamically select and execute them. ## 2. Action Planning and Execution [Section titled “2. Action Planning and Execution”](#2-action-planning-and-execution) Foldspace supports AI Actions, which are custom functions that the agent can trigger. Actions can be: * **Execute-only** (e.g., trigger a workflow, fetch data) * **Interactive** (execute + render results via Chatterblocks, such as forms, cards, or dashboards) The agent can: * Collect required parameters from the user. * Execute the action. * Render results or UI components inline in the conversation. ## 3. Plan Generation [Section titled “3. Plan Generation”](#3-plan-generation) By design, the agent can detect dependencies between different agentic experiences and tie them to the user’s intent. This allows the same high-level intent to drive different execution paths, depending on the user’s context. You can design an action that: * Calls other agentic functions in sequence, or * Uses a deterministic execution block to orchestrate a clear, ordered flow. This gives you flexibility: workflows can be both adaptive to user needs and deterministic when required. ## 4. Iterative Execution and Looping [Section titled “4. Iterative Execution and Looping”](#4-iterative-execution-and-looping) * The agent can handle stepwise workflows where it waits for user input before proceeding. * It supports chaining actions (e.g., query → refine → confirm → execute). * Iteration is typically user-driven: the user edits data, the agent processes it, and the user confirms or cancels. ## 5. Exit Criteria and Loop Termination [Section titled “5. Exit Criteria and Loop Termination”](#5-exit-criteria-and-loop-termination) Actions can be configured to complete when conditions are met, such as: * User saves or cancels changes. * Required inputs are collected. * The agent awaits user input before proceeding to the next step. Safety mechanisms are in place to prevent infinite loops by detecting repetitive cycles and enforcing termination safeguards. These mechanisms ensure that the agent does not continue indefinitely without resolution. ## 6. Summary Table [Section titled “6. Summary Table”](#6-summary-table) | Capability Aspect | Support Level | Notes | | :------------------------ | :-------------------- | :--------------------------------------------------------------------------------------------------------- | | Intent Determination | Yes | Natural language understanding and workflow triggering | | Tool/Action Selection | Yes | Actions and schemas defined in Agent Studio | | Plan Generation | Yes | Detects dependencies across agentic experiences; supports chaining, function calls, or deterministic flows | | Iterative Execution | Partial (User-driven) | Stepwise, with user-in-the-loop; not fully autonomous | | Exit Criteria/Termination | Yes | User-driven completion and built-in safeguards against infinite loops | ## 7. Conclusion [Section titled “7. Conclusion”](#7-conclusion) Foldspace provides strong support for: * Intent determination * Tool/action selection * Dependency-aware planning and deterministic flows * Stepwise execution with interactive UI In addition, the platform includes safety mechanisms to prevent infinite loops, ensuring reliable agent behavior. For fully autonomous, multi-step execution with internal planning, you may extend logic inside your Action Handlers or integrate Foldspace with external orchestration systems for advanced loop control. # The agentic loop > The Foldspace SDK runs the same evaluate-act-repeat agent loop that powers the Claude Agent SDK, chaining multiple actions until the request is done. A user prompt doesn’t get answered in a single pass. Foldspace runs an *agentic loop*: it evaluates the prompt against real-time context, makes one or more action calls, feeds each action result back into its next evaluation, and repeats until there’s nothing left to call. This is the same loop that powers the [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/agent-loop) — the Foldspace SDK runs it inside your product, grounded in your users, your knowledge, and your actions. ## Core concept [Section titled “Core concept”](#core-concept) A single prompt turns into a cycle rather than a one-shot reply: 1. **User prompt** — what the user asks, in natural language. 2. **Foldspace real-time context** — the agent assembles [usage, knowledge, and action context](/guides/context/) for this turn: who the user is, the page they’re on, and what it can do. 3. **Foldspace evaluates** — the agent reasons over the current state and decides the next step: make action calls, or reply. 4. **Action calls** — **one or more actions** run, and each **action result** feeds back into the next evaluation. 5. **Final reply** — when no actions are left to call, the agent answers. The loop is what separates an agent from a chatbot. A chatbot maps a message to a response. An agent evaluates, acts on your product, reads each result, and keeps going until the prompt is actually resolved. ## It chains more than one action [Section titled “It chains more than one action”](#it-chains-more-than-one-action) A request rarely resolves in a single action. Within one evaluation the agent can request **several actions at once**, and across the loop it **chains many actions in sequence** — each one informed by the result of the last. * **Multiple actions per turn.** When the agent needs several things at once, it requests them together. Independent, read-only actions can run in parallel; actions that change state run in order. * **Many actions across turns.** A high-level request — “refund this order and email the customer” — becomes a chain: look up the order, issue the refund, then send the confirmation, with the agent re-evaluating after each result. * **Adaptive, not scripted.** The agent decides the next action from what came back, so the same request can take a different path depending on the user, the page, and the data it sees. This is exactly how the Claude Agent SDK behaves: a quick request might call one action, while a complex one chains many actions across turns before the agent is done. ## The same loop as the Claude Agent SDK [Section titled “The same loop as the Claude Agent SDK”](#the-same-loop-as-the-claude-agent-sdk) The Claude Agent SDK runs a cycle — evaluate, call tools, feed results back, repeat — that continues until the model responds with no tool calls. The Foldspace SDK runs that identical loop, with each stage bound to the parts of your product the agent is embedded in. | Claude Agent SDK stage | What the Foldspace SDK does | | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Receive prompt** | Takes in the request with its [assembled context](/guides/context/) — who the user is, the page they’re on, the facts it can draw on, and the actions available. | | **Evaluate and respond** | Reasons over the current state and either calls **one or more [actions](/guides/build-actions/)** or produces a reply. | | **Execute tools** | Runs each action’s [`execute()` handler](/guides/executing-actions/), fetching data or [rendering an interactive component](/guides/in-chat-ui/) in the chat, and feeds the result back. | | **Repeat** | Cycles through evaluation and execution, chaining actions across turns until no further action is needed. | | **Return result** | Delivers the final reply once the agent responds with no more actions to run. | Because the loop shape is the same, patterns you know from the Claude Agent SDK carry over: give the agent good context, expose the right actions, and let it iterate. ## How the loop runs [Section titled “How the loop runs”](#how-the-loop-runs) ### Evaluate [Section titled “Evaluate”](#evaluate) The agent doesn’t load everything up front. For each pass it assembles *only* the context relevant to the request — usage context (identity, subscription, live page), knowledge context (the facts it answers from), and action context (what it can do) — and reasons over it to decide the next step. See [Context](/guides/context/) for how each source is wired up. ### Act [Section titled “Act”](#act) When the agent decides to act, it extracts the parameters each action needs from the user’s intent and calls it — often more than one at a time. An action can run pure logic and return data for the agent to reason over, or [render UI in the chat](/guides/in-chat-ui/) — a form, card, or dashboard — when the request calls for interaction. ### Repeat [Section titled “Repeat”](#repeat) Each action’s result feeds back into the next evaluation. The agent chains as many actions as the request needs — query, refine, confirm, execute — re-evaluating after each one. Where a step needs the user, an action can [pause for input](/guides/executing-actions/) with `awaitUserInput` before the loop continues. ### Finish [Section titled “Finish”](#finish) The loop ends when the agent responds with no further actions to run, or when an exit condition is met — the user saves or cancels, required inputs are collected, or a safeguard halts a repetitive cycle. Built-in loop-termination safeguards keep the agent from running indefinitely. ## Why it matters [Section titled “Why it matters”](#why-it-matters) The agentic loop is why the same intent can resolve different ways depending on who’s asking and what they’re doing. Instead of hard-coding one path per request, you give the agent context and actions, and the loop composes the steps — chaining as many actions as it takes — at runtime: adaptive when the situation calls for it, and deterministic where you need a fixed order. ## Related [Section titled “Related”](#related) * [Context](/guides/context/) — the three kinds of context the agent evaluates each pass. * [Execute actions](/guides/executing-actions/) — the handler behind each action the agent runs. * [How our agent thinks, acts, and prevents loops](/guides/agentic-capabilities/) — planning, tool selection, and termination in more detail. # Build actions > Actions are the capabilities the agent runs in your product — defined in Agent Studio, implemented in your code. Actions are what the agent can *do*: call your APIs, submit forms, navigate your UI, or render a component. Each action has two halves — you **define** it in Agent Studio (name, description, input schema) and **implement** it in your code (a handler registered under the action’s key). The agent decides *when* to call an action from the user’s request; your handler runs it and returns data or renders UI. [What are Actions ](/guides/text-driven-actions/)The model: text-driven capabilities the agent routes requests to. [Connect actions ](/guides/connecting-actions/)Register a handler in your code under the action's key. [Execute actions ](/guides/executing-actions/)What runs at call time, and how to return data or render UI. ## Where to start [Section titled “Where to start”](#where-to-start) Read [What are Actions](/guides/text-driven-actions/) for the model, then [Connect actions](/guides/connecting-actions/) to wire a handler, then [Execute actions](/guides/executing-actions/) for the runtime. ## Related [Section titled “Related”](#related) * [Authoring actions](/user-guides/authoring-actions/): define actions in the Agent Studio dashboard. * [Agentic UI](/guides/recipes/): render UI components from an action. # Connect actions > Register a handler in your code for an action defined in Agent Studio, so the agent can run real logic in your product. An action has two halves. In **Agent Studio**, you define what it is — a name, a description, and the input schema the agent fills in from the user’s request. In **your code**, you implement what it does. Connecting an action wires those halves together: you register a handler under the action’s key, and the agent calls it whenever it routes a request to that action. ``` flowchart LR subgraph STUDIO["Agent Studio"] D[Define action
name & description] S[Set input schema
& response mock] P[Publish version] end subgraph CODE["Your application"] C[Take action key
& schema] I[Register handler
under that key] W[Run logic, return
data or render UI] end D --> S --> P P -- "key, schema,
expected output" --> C C --> I --> W ``` ## What you need from Studio [Section titled “What you need from Studio”](#what-you-need-from-studio) To connect an action, take three things from its definition in Agent Studio: | Item | What it is | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **Action Key** | The action’s unique identifier (e.g. `create_task`, `invite_user`). You register your handler under this exact key. | | **Input Schema** | The fields the agent extracts and passes to your handler as `params` (e.g. `email`, `name`, `organizationRoleType`). | | **Expected Output** | What the action should do, plus any data the agent should use in its reply — for example, a link to deep-link the user to a page. | ## Register a handler [Section titled “Register a handler”](#register-a-handler) Register handlers on the agent with `addActionHandlers`. Each key matches an Action Key from Studio, and the agent calls that handler with the schema fields as `params`: ```javascript foldspace("when", "ready", () => { foldspace.agent({ /* …common setup… */ }) .addActionHandlers({ create_task: { execute: async (params) => { // `params` matches the action's input schema, e.g. { title, body } // …do the work, then return data for the agent… }, }, }); }); ``` What goes inside the handler depends on what the action should return: * **[Execute actions](/guides/executing-actions/)** — run logic and return data the agent relays in its next reply. * **[UI components in chat](/guides/in-chat-ui/)** — render an interactive component in the chat and send the user’s input back to the agent. Note See the **Developer Guide → [Coding Actions](https://foldspace.readme.io/docs/ai-actions-2)** for full implementation details. Tip Think of Studio as the **blueprint** for an action, and your application code as the **engine** that runs it. ## Next steps [Section titled “Next steps”](#next-steps) * [Execute actions](/guides/executing-actions/): what runs when the agent calls your handler, and how to return data. * [UI components in chat](/guides/in-chat-ui/): render an interactive component from an action. # Context > The agent assembles three kinds of context — usage, knowledge, and action — in real time to answer with low latency, high quality, and accuracy. Every time the agent responds, it assembles three kinds of context in real time and reasons over them together. Getting the *right* context in front of the model, and only the right context, is what makes the agentic interface fast, accurate, and genuinely useful. ## The three contexts [Section titled “The three contexts”](#the-three-contexts) | Context | What it is | Where it comes from | | :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Usage context** *(real-time)* | Who the user is and what they’re doing right now: identity, subscription, and custom attributes, plus the live page they’re looking at. | [`identify()`](/start/user-context/) for the user and subscription; [Shared State (Tandem)](/guides/shared-state/) and [Share current tab](/customize/share-current-tab/) for the page. | | **Knowledge context** | The facts the agent can draw on to answer. | [Knowledge Base](/user-guides/knowledge-base/) | | **Action context** | What the agent can do: the actions available to run. | [Actions](/guides/build-actions/) | ## Assembled in real time [Section titled “Assembled in real time”](#assembled-in-real-time) The agent doesn’t load everything up front. For each turn it selects only the context relevant to the request and assembles it on the fly. That real-time assembly is what maximizes: * **Latency** — only the context that matters is sent to the model, so responses come back fast. * **Quality** — replies reflect who the user is and what they’re doing on the page right now. * **Accuracy** — answers are grounded in your knowledge and act through your actions, instead of guessing. ## Wire up each context [Section titled “Wire up each context”](#wire-up-each-context) [Usage context ](/start/user-context/)Identify users and subscriptions with attributes, and share live page state (Shared State / Share current tab). [Knowledge context ](/user-guides/knowledge-base/)Give the agent the content it answers from. [Action context ](/guides/build-actions/)Define the actions the agent can run in your product. # Embedded mode > Mount the Foldspace agent as a web component inside your application by initializing it into a DOM container. Embedded mode renders the agent as a web component directly inside your application, anchored to a DOM element you control. Use it when you want the chat to live inline in your layout rather than as a floating overlay. Prerequisite This page assumes you’ve already installed the [SDK](/start/install/). ## Initialize the agent [Section titled “Initialize the agent”](#initialize-the-agent) The snippet below waits for the SDK to be ready, then mounts the agent into the container element you pass. ```javascript // Replace with the API name from Agent Studio → Setup const AGENT_API_NAME = "Foo"; const agentContainer = document.getElementById('agentContainer'); foldspace('when', 'ready', () => { foldspace.agent({ apiName: AGENT_API_NAME, mode: 'EMBEDDED', configuration: { embeddedConfiguration: { container: agentContainer } } }); }); ``` ## Embedded configuration [Section titled “Embedded configuration”](#embedded-configuration) The `embeddedConfiguration` object controls how the agent renders inside your application. The property relevant to initialization: | Property | Type | Required | Default | Description | | :-------- | :------------ | :------- | :------ | :----------------------------------------------------------------------------------------------- | | container | `HTMLElement` | Yes | - | The DOM element where the embedded agent will be mounted. Must be an existing element reference. | Note For all other appearance, layout, and behavioral options, visit the full configuration reference. Tip Configure the agent’s look and feel in **Agent Studio** > **Style & Behavior**. ## Next steps [Section titled “Next steps”](#next-steps) * [Actions](/guides/connecting-actions/): give the agent AI-Native actions that run workflows and return context-aware responses. * [Knowledge Base](/user-guides/knowledge-base/): connect a Knowledge Base for grounded answers. # Execute actions > Write the execute() handler that runs your action's logic and returns data the agent uses in its next reply. `execute` is the code behind an action. When the agent invokes the action, it awaits your `execute(params)` function and uses whatever you return in its next chat message. There’s no UI — just logic that runs and data that comes back. Reach for this whenever the result is information the agent should reason over or relay: creating a record, calling an API, looking up a value. To draw an interactive component in the chat instead, see [UI components in chat](/guides/in-chat-ui/) under **Agentic UI**. Prerequisites You’ve [defined an action](/user-guides/authoring-actions/) in Agent Studio and [connected a handler](/guides/connecting-actions/) for it. ## Action handler options [Section titled “Action handler options”](#action-handler-options) | Property | Type | Default | Required | Description | | :--------------- | :----------------------------------------------- | :------ | :------: | :-------------------------------------------------------------------------------------------------------------------------------------- | | `execute` | `(params: any) => Promise` | N/A | Yes | Handler for your async task; must return a `Promise` with data. | | `render` | `(data, host, header, callback, cancel) => void` | N/A | No | Optional. Draw an interactive component in the chat from the data `execute` returns — see [UI components in chat](/guides/in-chat-ui/). | | `awaitUserInput` | `boolean` | false | No | When `true`, the agent will pause and wait for user interaction before continuing. | | `timeout` | `number` | 5000 | No | Milliseconds to abort the `execute` call (default `5000`). | ``` sequenceDiagram participant U as User participant A as Agent participant H as execute(params) participant API as Your API U->>A: Natural-language request A->>A: Extract params from intent A->>H: Call action handler H->>API: fetch / POST / query API-->>H: Response data H-->>A: Return result object A->>U: Chat message using returned data ``` ## Example [Section titled “Example”](#example) This `create_task` action POSTs the agent-supplied `params` to an API and returns the created record. Return every field the agent might need later in the conversation: here, including a `link` means the agent can answer a follow-up like “What’s the link?” without another call. ```javascript foldspace("when", "ready", () => { foldspace.agent({ /* …common setup… */ }) .addActionHandlers({ create_task: { execute: async (params) => { const {title, body} = params; const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify({ title, body, userId: 'playground', }), headers: { 'Content-type': 'application/json; charset=UTF-8', }, }); const data = await response.json() // Return all relevant fields so the agent can reference them later return { id: data.id, title: data.title, body: data.body, userId: data.userId, // Include a link property—if the user asks "What's the link?", the agent can supply this link: `https://jsonplaceholder.typicode.com/posts/${data.id}` }; } } }); }); ``` ## Related [Section titled “Related”](#related) * [UI components in chat](/guides/in-chat-ui/): render an interactive component in the chat instead of returning plain data. * [Connecting actions in your application](/guides/connecting-actions/): wire an action key and schema into your product code. # UI components in chat > Let an action render an interactive component — a form, card, or confirmation — directly in the chat, then send the user's input back to the agent. Most actions return data the agent relays as text. An action that renders a **UI component in the chat** goes one step further: after it runs, it draws an interactive component — a form, a card, a confirmation dialog — directly into the chat. The user acts on it inline, and their input flows back to the agent. An action and its UI are two halves of one feature. `execute(params)` owns the **logic** — fetch or write data. `render(data)` owns the **presentation** — build the component the user sees and interacts with. Together they make up an *execute-and-render* action. ## When to use it [Section titled “When to use it”](#when-to-use-it) * **Show** data the user should take in at a glance — a table, a summary card, a dashboard. * **Collect** input without leaving the chat — a form, a picker, an editable record. * **Confirm** a consequential change before it happens — an approve / reject dialog. When you only need data back as text, with no UI, use a plain [execute action](/guides/executing-actions/) instead. ## How it works [Section titled “How it works”](#how-it-works) When `execute(params)` resolves, the agent hands its result to your `render` function. You build DOM nodes into the container you’re given; when the user submits, you call `callback(value)` to send their input back to the agent, or `cancel()` to abort. ``` sequenceDiagram participant U as User participant A as Agent participant E as execute(params) participant R as render(data, host, ...) U->>A: Natural-language request A->>E: Call execute handler E-->>A: Return data object A->>R: Pass data to render R->>R: Build the UI component R->>U: Interactive component in chat U->>R: Interact (edit, confirm) R->>A: callback(value) A->>U: Continue with the submitted data ``` Note For a component that takes input or a confirmation, set `awaitUserInput: true` so the agent pauses until the user submits or cancels. ## Render options [Section titled “Render options”](#render-options) Your `render` function receives these arguments, in order. | Parameter | Type | Description | | ---------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `data` | `any` | The value returned by your `execute` handler (e.g., an object or array of results). | | `host` | `HTMLElement` | The container element where you append your UI nodes (forms, cards, lists). | | `header` | `HTMLElement` | A header element you can style or populate with a title, instructions, or status. | | `callback` | `(value: string, disableOnSubmit?: boolean) => void` | Call this when the user submits; the value is passed to the agent. `disableOnSubmit` defaults to `true` (the SDK disables your UI after submit) — set it to `false` to keep the component interactive. | | `cancel` | `() => void` | Call this if the user aborts, so the agent resumes without new data. A default “Cancel” control is also rendered. | ## Example [Section titled “Example”](#example) This `show_task` action fetches a task in `execute`, then renders an editable card in `render`. `awaitUserInput: true` pauses the agent until the user saves or cancels: **Save** passes the edited fields to `callback`, and **Cancel** aborts. ```javascript foldspace("when", "ready", () => { foldspace.agent({ /* …common setup… */ }) .addActionHandlers({ show_task: { // 1. Fetch the task by ID execute: async (params) => { const { taskId } = params; const response = await fetch( `https://jsonplaceholder.typicode.com/posts/${taskId}` ); const data = await response.json(); // Return the task object to both agent and renderer return data; }, // 2. Tell the agent to wait for user input before continuing awaitUserInput: true, // 3. Render an editable card UI render: (task, host, header, callback, cancel) => { const style = document.createElement('style'); style.textContent = ` .task-card { border: 1px solid #e3e3e3; border-radius: 8px; margin-top: 8px; font-family: sans-serif; overflow: hidden; } .task-card-header { background-color: #f5f5f5; padding: 12px 16px; font-weight: bold; } .task-card-body { padding: 16px; } .task-card-body label { display: block; margin-bottom: 4px; font-size: 14px; } .task-card-body input, .task-card-body textarea { width: 100%; margin-bottom: 12px; padding: 8px; box-sizing: border-box; font-size: 14px; } .task-card-footer { padding: 12px 16px; text-align: right; background-color: #fafafa; } .task-card-footer button { margin-left: 8px; padding: 8px 16px; cursor: pointer; } `; header.appendChild(style); // Build the card container const card = document.createElement('div'); card.className = 'task-card'; // Header section (title) const cardHeader = document.createElement('div'); cardHeader.className = 'task-card-header'; cardHeader.textContent = `Edit Task #${task.id}`; // Body section (form fields) const body = document.createElement('div'); body.className = 'task-card-body'; const titleLabel = document.createElement('label'); titleLabel.textContent = 'Title:'; const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.value = task.title; const bodyLabel = document.createElement('label'); bodyLabel.textContent = 'Body:'; const bodyInput = document.createElement('textarea'); bodyInput.rows = 4; bodyInput.value = task.body; body.append(titleLabel, titleInput, bodyLabel, bodyInput); // Footer section (buttons) const footer = document.createElement('div'); footer.className = 'task-card-footer'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.onclick = () => { callback({ id: task.id, title: titleInput.value, body: bodyInput.value, }); }; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = cancel; footer.append(cancelBtn, saveBtn); // Assemble and mount card.append(cardHeader, body, footer); host.appendChild(card); } } }); }); ``` ## Related [Section titled “Related”](#related) * [Execute actions](/guides/executing-actions/): return data to the agent without rendering UI. * [What are Actions](/guides/text-driven-actions/): the action model these build on. # Connecting Foldspace to Intercom: Automated Support Handoff > Step-by-step walkthrough for implementing an automated handoff from your Foldspace Agent to a live Intercom support session. ## Overview [Section titled “Overview”](#overview) By implementing this action, your Foldspace Agent can intelligently recognize when it has reached its limits and seamlessly transition the conversation to your human support team. This ensures that frustrated users or complex technical issues are always handled with the highest level of care. There are two ways to hand off to Intercom. Pick based on how your team works: | Handoff type | What happens | Use when | | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------ | | **Live chat** | The Agent opens the Intercom Messenger on the page, seeded with the conversation summary, then steps aside. | You staff the Intercom Inbox for live chat. | | **Email → conversation** | The Agent emails the summary and context to your Intercom-connected support address, which opens a conversation in the Inbox. | You handle support asynchronously. | Both are triggered the same way; only the destination differs. The steps below cover the live-chat handoff — the email option is at the end of this guide. Leave the built-in handoff off This handoff is driven by your own `support_handoff` action, not by Foldspace’s built-in escalation. Keep **Offer support handoff** OFF in **Style & Behavior → Data Access & Handoff** so the agent escalates through your handler rather than the built-in ticket channel. The toggle belongs to the email path only. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * An active Intercom subscription with the Intercom Messenger installed on your site. * Access to your Foldspace Dashboard. * The Foldspace Web SDK integrated into your web application. ## Step 1: Create the “Support Handoff” Action [Section titled “Step 1: Create the “Support Handoff” Action”](#step-1-create-the-support-handoff-action) First, we need to define the action inside the Foldspace App so the Agent knows how to “call” for human help. 1. Log in to your Foldspace App. 2. Navigate to the **Actions** section and click **Create New Action**. 3. Fill in the fields below. Each one has a **Copy** button, so you can paste it straight into the form. Let the Foldspace assistant build it Copy prompt The Actions editor has a Foldspace assistant beside the form. Paste this prompt into it and it fills in every field below for you, then leaves the action as a draft for you to review before publishing. Create a new action for this agent with exactly these values. Name: Support Handoff Action key: support\_handoff Description: Triggers a live support hand-off via Intercom when the Agent cannot resolve an issue, detects a bug, or the user is frustrated. Instructions: Use this action ONLY when: - The user explicitly asks for a human. - You have failed to find an answer after multiple attempts. - The user mentions a 'bug' or technical error. - The user's sentiment is highly frustrated. You must synthesize a clear, one-sentence summary of the user's problem for the 'user\_prompt' property. User input, one field: - Name: user\_prompt - Type: string - Required: yes - Description: A concise summary of the user's issue and why the hand-off is being triggered (e.g. 'User is unable to save settings'). A front-end action handler in my own app runs this one: it opens the Intercom Messenger seeded with the summary. There is no API call and no server-side execution to configure. Save it as a draft so I can review it before publishing. ### Fields [Section titled “Fields”](#fields) Name Copy Support Handoff Action key Copy support\_handoff Description Copy Triggers a live support hand-off via Intercom when the Agent cannot resolve an issue, detects a bug, or the user is frustrated. ### Logic & Instructions [Section titled “Logic & Instructions”](#logic--instructions) Paste this into the **Instructions** field. It stops the Agent from escalating on ordinary questions: Instructions Copy Use this action ONLY when: - The user explicitly asks for a human. - You have failed to find an answer after multiple attempts. - The user mentions a 'bug' or technical error. - The user's sentiment is highly frustrated. You must synthesize a clear, one-sentence summary of the user's problem for the 'user\_prompt' property. ### User Input Configuration [Section titled “User Input Configuration”](#user-input-configuration) Create a required input field so the Support Agent receives context immediately upon handoff. Set **Type** to `string` and **Required** to yes. Input name Copy user\_prompt Input description Copy A concise summary of the user's issue and why the hand-off is being triggered (e.g. 'User is unable to save settings'). ## Step 2: Code Implementation [Section titled “Step 2: Code Implementation”](#step-2-code-implementation) Once the action is defined in the dashboard, you must handle the execution on your front-end. Add the following code to your application where you initialize the Foldspace Agent: ```javascript window.foldspace("when", "ready", () => { const agent = window.foldspace.agent("planning-copilot"); agent.addActionHandlers({ support_handoff: { execute: async (params) => { try { const HANDOFF_DELAY_MS = 5000; // 5-second delay for smooth UX // Verify Intercom is loaded on the page if (!window.Intercom) { return { message: "Sorry, I'm unable to connect to the Support Agent right now. How else can I assist you?", }; } const message = params?.user_prompt?.trim() || "User requested human assistance."; // Trigger the handoff sequence setTimeout(() => { window.Intercom("showNewMessage", message); agent.hide(); // Hide the Foldspace agent to let Intercom take over }, HANDOFF_DELAY_MS); return { message: "Transferring you to a Support Agent now. One moment please.", }; } catch (error) { return { message: "Something went wrong while connecting to support. Please try again or reach out directly.", }; } }, }, }); agent.show(); }); ``` ## Email handoff (opens an Intercom conversation) [Section titled “Email handoff (opens an Intercom conversation)”](#email-handoff-opens-an-intercom-conversation) When you don’t staff live chat, the Agent can hand off by **email** instead. This isn’t a real-time transfer — Foldspace sends the conversation summary and context to your support address, and Intercom turns that email into a conversation in the Inbox. No custom code required: 1. Enable handoff in **Style & Behavior → Data Access & Handoff** ([Style & Behavior](/user-guides/building-your-agent/)). 2. Set the destination to **Email** in [Integrations](/user-guides/integrations/). 3. Point it at an address that reaches your **Intercom Inbox**. Intercom only receives mail sent to your workspace’s inbound address — `[app_id]@[workspace].intercom-mail.com` — so either send there directly, or send to a support address you’ve configured to auto-forward to it (**Settings → Channels → Email → Domains & addresses**). **Don’t send to a plain mailbox.** An address that doesn’t forward to your workspace’s inbound address never appears in the Intercom Inbox, so the escalation sits in someone’s email — unassigned, unrouted, and untracked. ## Best Practices [Section titled “Best Practices”](#best-practices) * **User Expectations:** The 5-second delay defined in `HANDOFF_DELAY_MS` gives the user time to read the Agent’s confirmation message before the UI switches. * **Fallback:** If the Intercom script fails to load (e.g., due to an ad-blocker), the code includes a fallback message to ensure the user isn’t left in a “dead end.” * **Context is King:** The `user_prompt` is automatically sent to Intercom, meaning your support team starts the conversation with a summary of the issue already in hand. ## Related [Section titled “Related”](#related) * [Connecting Foldspace to Zendesk](/guides/zendesk-handoff/) — the same live-handoff pattern for Zendesk, plus an email-to-ticket option. * [Integrations](/user-guides/integrations/) — where handoff destinations (email, HubSpot, webhook) are configured. ## Intercom references [Section titled “Intercom references”](#intercom-references) Official Intercom documentation for the APIs and setup this guide relies on: * [JavaScript API: Methods](https://developers.intercom.com/installing-intercom/web/methods) — `Intercom("showNewMessage", prePopulatedContent)` and the other Messenger methods. * [Installing the Intercom Messenger for web](https://developers.intercom.com/installing-intercom/web/installation) — get the Messenger snippet onto your page so `window.Intercom` is available. * [Get started with Intercom Inbox](https://www.intercom.com/help/en/articles/6274899-get-started-with-intercom-inbox) — the Inbox that handles inbound support email. * [Forward your email to your team inbox](https://www.intercom.com/help/en/articles/6522819-forward-your-email-to-your-team-inbox-using-the-next-gen-inbox) — the workspace inbound address and forwarding setup. # A2A / MCP Protocols > Connect the agent to external tools, data, and other agents over MCP and A2A. Two protocols extend the agent beyond your own app: * **MCP** (Model Context Protocol) gives the agent tools and data from an MCP server. * **A2A** (Agent-to-Agent) lets the agent delegate to, and receive tasks from, other AI agents. ## MCP [Section titled “MCP”](#mcp) [MCP server integration ](/reference/mcp-server/)Connect an MCP server so the agent can use its tools and data. ## A2A [Section titled “A2A”](#a2a) [A2A server agents ](/guides/a2a-server-agents/)Expose your agent as an A2A server. [A2A quickstart ](/guides/a2a-quickstart/)The fastest path to a working A2A connection. [A2A advanced patterns ](/guides/a2a-advanced/)Streaming, multi-agent, and other advanced flows. [Connect an A2A agent ](/guides/a2a-connect/)Delegate to an external A2A agent from yours. ## Support-desk handoff [Section titled “Support-desk handoff”](#support-desk-handoff) The code for handing a conversation to a human desk also lives here: [Intercom handoff ](/guides/intercom-handoff/)Hand off into a live Intercom session. [Zendesk handoff ](/guides/zendesk-handoff/)Hand off into Zendesk chat or a ticket. ## Where to start [Section titled “Where to start”](#where-to-start) For tools and data, start with [MCP server integration](/reference/mcp-server/). For agent-to-agent, start with the [A2A quickstart](/guides/a2a-quickstart/). # Agentic UI > The UI components the agent renders and drives, plus end-to-end use cases you can copy from. Agentic UI is how the agent operates your interface: the components it renders and drives inside the conversation, plus worked end-to-end use cases you can adapt. ## Interaction modalities [Section titled “Interaction modalities”](#interaction-modalities) Users interact with the agent in more than one way, and an action can respond in whichever fits the moment — or a combination: * **Text** — the default chat conversation. * **Voice** — spoken interaction; configure it in [Voice](/user-guides/voice/). * **Visual** — interactive UI components (Chatterblocks) rendered in the chat; see [UI components in chat](/guides/in-chat-ui/). * **Combination** — mix them, for example a spoken request that returns a visual component to act on. ## Agentic UI components [Section titled “Agentic UI components”](#agentic-ui-components) The building blocks the agent renders and drives inside your product. [UI components in chat ](/guides/in-chat-ui/)Render forms, cards, and dashboards (Chatterblocks) inline in the conversation. [Shared State (Tandem) ](/guides/shared-state/)Keep the agent and your live page in sync so responses match what the user sees. ### Shared State examples [Section titled “Shared State examples”](#shared-state-examples) [React meeting form ](/guides/shared-state-react-meeting/)Two-way sync between the agent and a React form. [React filter panel ](/guides/shared-state-react-filter/)The agent drives a filter panel as the user browses. [Backbone ](/guides/shared-state-backbone/)Shared State wired into a Backbone app. [Vanilla JS ](/guides/shared-state-vanilla/)Shared State with no framework. ## Use cases [Section titled “Use cases”](#use-cases) End-to-end walkthroughs that put the pieces together. [Analyze report data ](/guides/task-agent-analysis/)Run a task agent over report data and render the result inline. [Embed the agent ](/guides/embedded-agent/)Run the agent as an embedded, always-on surface in your app. Tip Want to start from a prompt instead of a pattern? See [Vibe coding](/guides/vibe-coding/). # Shared State (Tandem) > Sync your app's UI state with the agent in real time for two-way, state-aware experiences. ## What is Shared State? [Section titled “What is Shared State?”](#what-is-shared-state) While AI Actions let the agent perform tasks, Shared State gives the agent eyes and hands inside your application. It creates a two-way sync between your app’s UI state (like a form or settings panel) and the agent. The agent reads the current state of your UI in real time and writes changes back to it. ## Why use Shared State? [Section titled “Why use Shared State?”](#why-use-shared-state) * **Contextual awareness** — the agent sees exactly what the user sees, so it can give relevant help without asking clarifying questions. * **Interactive guidance** — instead of just describing steps, the agent can highlight fields, pre-fill values, and walk users through complex workflows. * **Seamless task automation** — the agent can update your UI directly, turning multi-step processes into a single conversation. ## How it works: a two-way street [Section titled “How it works: a two-way street”](#how-it-works-a-two-way-street) * **Your App → Agent**: your app calls `shareState()` whenever the UI changes, keeping the agent’s picture of the current screen up to date. * **Agent → Your App**: when the agent decides to act, it pushes updates through the handler you registered, and your UI re-renders instantly. ``` flowchart LR subgraph Your App UI[UI Component] SF["shareState(key, state)"] end subgraph Foldspace AG[Agent] end UI -- "User types ➜ setForm()" --> SF SF -- "State snapshot" --> AG AG -- "Agent updates ➜ handler(key, next)" --> UI ``` Note This page assumes you’ve already initialized your agent. ## Methods [Section titled “Methods”](#methods) Two methods on the agent instance manage the whole process. ### `shareState(key, state, handler, stateDescription)` [Section titled “shareState(key, state, handler, stateDescription)”](#sharestatekey-state-handler-statedescription) Establish and update synchronized state. Call it once on initialization, then again every time the user-side state changes. | Parameter | Type | Required | Description | | :------------------- | :-------------------------------------- | :------: | :---------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `string` | Yes | A unique string identifier for this piece of state (e.g. `"contact_form"`). | | `state` | `object` | Yes | A snapshot of the current UI state object (e.g. `{ name: "", email: "" }`). | | `handler` | `(key: string, nextState: any) => void` | No | Optional, but required to let the agent push updates back to your UI. Pass it for two-way sync. | | `stateDescription` | `string` | No | A free-form string that adds context about the state. Can describe the type/model, usage instructions, or provide a human-readable explanation. | | `persistAcrossPages` | `boolean` | No | | ```javascript // Initial sync: Register the state key, initial data, and the handler const agentKey = "YOUR_AGENT_KEY"; const agent = window.foldspace?.agent(agentKey); agent.current?.shareState(stateKey, form, handleStateChange, stateDescription); ``` #### Guide the agent with `stateDescription` [Section titled “Guide the agent with stateDescription”](#guide-the-agent-with-statedescription) `stateDescription` is optional but gives context to both developers and the agent. Though concise by design, it can carry different kinds of guidance: * **Naming conventions**: e.g. `"All keys in this state should be snake_case"`. * **Model structure hints**: e.g. `"If 'items' is an empty array, each entry should contain { id: string, quantity: number }"`. * **Purpose description**: e.g. `"Tracks the shipping address details required for checkout"`. `stateDescription` enriches the state with descriptive or instructional metadata, so the UI and the agent interpret the state consistently. ### `clearState(key)` [Section titled “clearState(key)”](#clearstatekey) Tell the agent to stop tracking a piece of state. Call it for cleanup when a component unmounts or the user navigates away, preventing memory leaks. | Parameter | Type | Required | Description | | :-------- | :------- | :------: | :------------------------------------------------------------ | | `key` | `string` | Yes | The unique string identifier for the state you wish to clear. | ## Sync a React form [Section titled “Sync a React form”](#sync-a-react-form) Synchronize a React form component’s state with the agent using hooks. ```typescript import { useEffect, useRef, useState } from "react"; type FormState = { name: string; email: string; company?: string; }; interface Props { agentKey: string; } export default function SharedStateForm({ agentKey }: Props) { // 1. Local UI state is kept minimal. const [form, setForm] = useState({ name: "", email: "", company: "" }); // 2. The agent instance is created once and stored in a ref. const agentRef = useRef(null); const stateKey = "contact_form"; const stateDescription = ` type FormState = { name: string; email: string; company?: string; }; interface Props { agentKey: string; }`; // 3. `useEffect` handles the entire lifecycle: setup, subscription, and cleanup. useEffect(() => { // Initialize the agent agentRef.current = window.foldspace?.agent(agentKey); // Agent -> UI: Define the handler to receive updates FROM the agent const handleStateChange = (key: string, nextState: unknown) => { if (key === stateKey) { setForm(nextState as FormState); // Update local state } }; // Initial sync: Register the state key, initial data, and the handler agentRef.current?.shareState(stateKey, form, handleStateChange, stateDescription); // Cleanup function: Tell the agent to stop tracking on unmount return () => { agentRef.current?.clearState(stateKey); agentRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [agentKey]); // 4. UI -> Agent: Call this on user input to keep the agent's snapshot fresh. const updateField = (key: K, value: FormState[K]) => { const next = { ...form, [key]: value }; setForm(next); // Send the fresh snapshot to the agent agentRef.current?.shareState(stateKey, next); }; // 5. Render your form inputs and call updateField on change. return <>...; } ``` ``` sequenceDiagram participant App as Your App participant SDK as Foldspace SDK participant Agent as Agent App->>SDK: shareState("form", data, handler) SDK->>Agent: State snapshot Note right of Agent: Agent reads form context Agent-->>SDK: Updated state SDK-->>App: handler("form", nextState) App->>App: setForm(nextState) ``` ## Related [Section titled “Related”](#related) * [AI Actions](./actions-1): give your agent custom actions that automate workflows and return context-aware responses. * [Knowledge Base](./knowledge-base): connect a knowledge base for accurate, context-rich answers. # Backbone 'Reporting Filter' Shared State Example > Integrate Foldspace Shared State in a Backbone app to sync filter data with your agent in real time for interactive, context-driven experiences. ```javascript // Bind Backbone to jQuery (defensive) if (!Backbone.$ && window.jQuery) Backbone.$ = window.jQuery; // --- Share-state Backbone example (filters) --- const stateKey = "bb_filters"; const agentKey = "YOUR_AGENT_KEY"; const agent = window.foldspace?.agent?.(agentKey); const FiltersModel = Backbone.Model.extend({ defaults: { location: "", account: "" } }); const FiltersView = Backbone.View.extend({ el: "#filters-root", className: "filters-panel", template: _.template(`

  `),


  events: {
    "change select[name=location]": "onChange",
    "change select[name=account]" : "onChange"
  },


  initialize() {
    this.listenTo(this.model, "change", this.renderSnapshot);


    // Agent -> UI: subscribe for shared state updates
    this._handler = (key, nextState) => {
      if (key === stateKey) this.model.set(nextState);
    };
    agent?.shareState(stateKey, this.model.toJSON(), this._handler);


    this.render();  // ensure element exists & template rendered
  },


  render() {
    this.$el.html(this.template());
    // Sync current values into selects
    this.$("select[name=location]").val(this.model.get("location"));
    this.$("select[name=account]").val(this.model.get("account"));
    this.renderSnapshot();
    return this;
  },


  renderSnapshot() {
    this.$(".snapshot").text(JSON.stringify(this.model.toJSON()));
  },


  onChange(e) {
    const $t = this.$(e.currentTarget);
    const next = { ...this.model.toJSON(), [$t.attr("name")]: $t.val() };
    this.model.set(next);
    // UI -> Agent
    agent?.shareState(stateKey, next);
    this.renderSnapshot();
  },


  remove() {
    agent?.clearState(stateKey);
    Backbone.View.prototype.remove.call(this);
  }
});


// Mount after #filters-root exists
new FiltersView({ model: new FiltersModel() });
```

# React 'Reporting Filter' Shared State Example

> Learn how to sync a reporting filter form with your Foldspace agent using React hooks and Shared State for real-time, context-aware data filtering.

Shared State example of a ‘Reporting Filter’ React form with the agent using hooks.

FilterPanel.tsx

```typescript
import { useEffect, useRef, useState } from "react";


type FilterState = {
  location: string;  // e.g., "NYC"
  account: string;   // e.g., "Acme Inc"
};


interface Props {
  agentKey: string;
  initial?: Partial;
}


export default function FilterPanel({ agentKey, initial }: Props) {
  const [filters, setFilters] = useState({
    location: initial?.location ?? "",
    account: initial?.account ?? "",
  });


  const agentRef = useRef(null);
  const stateKey = "filters_panel";
  const stateDescription = `
    type FilterState = {
      location: string;  // e.g., "NYC"
      account: string;   // e.g., "Acme Inc"
    };


    interface Props {
      agentKey: string;
      initial?: Partial;
    }`;


  useEffect(() => {
    agentRef.current = (window as any).foldspace?.agent(agentKey);


    const handleStateChange = (key: string, nextState: unknown) => {
      if (key === stateKey) setFilters(nextState as FilterState);
    };


    // initial sync
    agentRef.current?.shareState(stateKey, filters, handleStateChange, stateDescription);


    return () => {
      agentRef.current?.clearState(stateKey);
      agentRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [agentKey]);


  const updateField = (k: K, v: FilterState[K]) => {
    const next = { ...filters, [k]: v };
    setFilters(next);
    agentRef.current?.shareState(stateKey, next);
  };


  const LOCATIONS = ["NYC", "London", "Tel Aviv", "Remote"];
  const ACCOUNTS  = ["Acme Inc", "Globex", "Initech", "Umbrella"];


  return (
    
{/* The agent now has a fresh snapshot at state key "filters_panel". */} Current: {JSON.stringify(filters)}
); } ``` # React 'Schedule a Meeting' Shared State Example > See a Shared State example in React. Learn how to sync meeting details with your agent in real time for interactive, context-aware automation. Shared State example of a ‘Schedule Meeting’ React form with the agent using hooks. ScheduleMeetingForm.tsx ```typescript import { useEffect, useRef, useState } from "react"; type MeetingState = { title: string; date: string; // "2025-09-01" startTime: string; // "14:00" endTime: string; // "15:00" attendees: string; // comma-separated emails location: string; // "Zoom" | "Office TLV" | "Google Meet" | custom notes?: string; }; interface Props { agentKey: string; initial?: Partial; } export default function ScheduleMeetingForm({ agentKey, initial }: Props) { const [form, setForm] = useState({ title: initial?.title ?? "", date: initial?.date ?? "", startTime: initial?.startTime ?? "", endTime: initial?.endTime ?? "", attendees: initial?.attendees ?? "", location: initial?.location ?? "", notes: initial?.notes ?? "", }); const agentRef = useRef(null); const stateKey = "meeting_form"; const stateDescription = ` type MeetingState = { title: string; date: string; // "2025-09-01" startTime: string; // "14:00" endTime: string; // "15:00" attendees: string; // comma-separated emails location: string; // "Zoom" | "Office TLV" | "Google Meet" | custom notes?: string; }; interface Props { agentKey: string; initial?: Partial; }`; useEffect(() => { agentRef.current = (window as any).foldspace?.agent(agentKey); const handleStateChange = (key: string, nextState: unknown) => { if (key === stateKey) setForm(nextState as MeetingState); }; agentRef.current?.shareState(stateKey, form, handleStateChange, stateDescription); return () => { agentRef.current?.clearState(stateKey); agentRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [agentKey]); const updateField = (k: K, v: MeetingState[K]) => { const next = { ...form, [k]: v }; setForm(next); agentRef.current?.shareState(stateKey, next); }; return (