Back to Lowdefy

Agent System

code-docs/architecture/agent-system.md

5.3.033.8 KB
Original Source

Agent System

Overview

The agent system adds AI chat capabilities to Lowdefy apps. Agents are config-driven (YAML) and follow the same type + connectionId + properties pattern used by requests. A top-level agents: key in the Lowdefy config defines one or more agents, each specifying a model, system instructions, tools, and streaming behavior. The system integrates the Vercel AI SDK v6 for multi-provider model access, streaming, and tool execution.

A minimal agent config:

yaml
agents:
  - id: support_agent
    type: ClaudeAgent
    connectionId: anthropic
    properties:
      model: claude-sonnet-4-20250514
      instructions: You are a helpful support agent.
    tools:
      - search_knowledge_base

Four-Layer Architecture

The agent system separates concerns into four layers, each configured independently:

  1. Connection -- AI SDK provider (Anthropic, OpenAI, Google, Vercel AI Gateway) or config container (Mcp). Holds credentials and creates the provider instance. Configured under connections: just like database connections.

  2. Agent -- The orchestration unit. References a connection, specifies model/instructions/tools/hooks, and controls the tool loop (max steps, stop conditions, pruning). Configured under the top-level agents: key. At runtime, a ToolLoopAgent from the AI SDK manages the multi-step model interaction.

  3. Tool -- API endpoints with description + payloadSchema that make them callable by models. Endpoints own their tool metadata; agents simply reference endpoint IDs. MCP servers, sub-agents, and file system access provide additional tool sources.

  4. Block -- AgentChat and AgentConversations UI components. Pure rendering, no AI config. The block connects to the agent API route via a transport layer and renders the streaming conversation.

This separation means the same endpoint can serve a page button, another endpoint, or an agent tool without duplication. The same agent can power different UIs.

Request Flow (Chat Message to Streaming Response)

Client Side

  1. User types a message in AgentChat. The handleSend function fires onBeforeSend for validation, then calls sendMessage() on the useChat hook.
  2. useChat uses a DefaultChatTransport that POSTs to /api/agent/{pageId}/{agentId}?conversationId=... with { messages, urlQuery, sharedState } in the body. sharedState is read from sharedStateRef.current at send time (see Shared State) so the payload always reflects the latest evaluated state.

Server Side

  1. The Next.js catch-all route (pages/api/agent/[...path].js) extracts pageId and agentId from URL segments, validates the request body, and calls callAgent(). See packages/servers/server/pages/api/agent/[...path].js.

  2. callAgent (packages/api/src/routes/agent/callAgent.js) orchestrates the server-side flow:

    • Loads agent config from agents/{agentId}.json via getAgentConfig
    • Builds agentContext ({ conversationId, pageId, sharedState, urlQuery, userId })
    • Evaluates operators in agent properties (_user, _secret, _payload) with agentContext as payload
    • Loads connection config, creates the provider instance (e.g., Anthropic SDK client)
    • Resolves MCP connection references to inline transport config
    • Looks up the agent type resolver from the plugin registry via getAgentResolver
    • Builds a resolverContext with callEndpoint, getEndpointConfig, getAgentConfig, getConnectionForAgent, and resolveMcpSources -- these are the capabilities available to the agent runtime
    • Calls agentType.resolver() with the connection, properties, and context
  3. The agent resolver (e.g., ClaudeAgent) maps provider-specific properties into providerOptions and delegates to handleAgentChat().

  4. handleAgentChat (packages/utils/ai-utils/src/handleAgentChat.js) is the shared runtime:

    • Calls buildAgentTools to merge endpoint, MCP, sub-agent, and file system tools (after rejecting any user tool whose name collides with a platform reserved name like update-page-state)
    • If the request carries a non-empty sharedState, builds the update-page-state tool via buildUpdatePageStateTool and includes the snapshot in the agent's context block
    • Creates a ToolLoopAgent with model, instructions, tools, stop conditions, and hook callbacks
    • Opens a createUIMessageStream, runs the agent inside the stream's execute function
    • If prune config is set, decomposes the stream creation to insert pruneMessages between UI-to-model conversion and agent execution
    • After the agent stream completes, calls onFinish hooks (awaited) and writes any returned dataParts to the stream
    • Cleans up MCP clients

Back to Client

  1. The response streams as SSE events back through Next.js to the browser.
  2. The useChat hook updates messages reactively. useAgentEvents watches message changes and fires Lowdefy events (onMessageComplete, onToolCall, onToolResult, onUserMessage, onTitleGenerated, onDataPart).

Build Pipeline

Three build steps handle agent configuration:

buildAgents

packages/build/src/build/buildAgents.js

Validates and normalizes agent configs in a two-pass approach:

First pass (per agent):

  • Checks for duplicate agent IDs
  • Validates connectionId references an existing connection
  • Validates model is defined in properties
  • Normalizes shorthand: tool strings become { endpointId } objects, MCP strings become { connectionId } objects, sub-agent strings become { agentId } objects
  • Validates each tool endpoint exists and has both description and payloadSchema
  • Validates MCP sources: connection references exist, stdio sources have command, HTTP sources have url
  • Validates hook endpoints exist
  • Validates fileSystem basePath exists on disk
  • Renames id to agentId (internal format: agent:{agentId})
  • Counts server operators for type resolution

Second pass (cross-references):

  • Validates sub-agent references point to existing agents
  • Checks for name collisions between sub-agent IDs and endpoint tool IDs
  • Warns when sub-agents have confirm: true tools (unsupported in sub-agent context)

Cycle detection: Uses DFS with an in-stack set to detect circular sub-agent references. Throws ConfigError if a cycle is found.

writeAgents

packages/build/src/build/writeAgents.js

Serializes each agent config to agents/{agentId}.json using serializer.serializeToString, which preserves ~k markers for error tracing at runtime.

writeAgentImports

packages/build/src/build/writePluginImports/writeAgentImports.js

Generates plugins/agents.js -- an import registry that maps agent type names to their resolver modules. This allows the server to look up agent types dynamically.

copyAgentFileSystems

packages/build/src/build/copyAgentFileSystems.js

When the config directory differs from the server directory (production builds), copies each unique fileSystem.basePath directory to the server output. Uses a Set to avoid copying the same path twice when multiple agents share a base directory.

Tool System

buildAgentTools (packages/utils/ai-utils/src/buildAgentTools.js) merges four tool sources into a single tools object for the AI SDK.

Endpoint Tools

Configured as agent.tools[], each referencing an API endpoint ID. At runtime:

  • Loads endpoint config via context.getEndpointConfig()
  • Creates an AI SDK tool() with the endpoint's description and payloadSchema
  • Cleans build artifact markers (~k, ~r, ~l) from the schema before passing to the AI SDK
  • Executes via context.callEndpoint(), which runs the endpoint's full routine (auth, operators, requests)
  • Optional confirm: true sets needsApproval on the tool for client-side approval UI

MCP Tools

Configured as agent.mcp[], each source specifying either an HTTP URL or stdio command. MCP connections can be referenced by connectionId -- callAgent resolves these to inline config before the agent runs. At runtime:

  • Creates MCP clients via createMCPClient from @ai-sdk/mcp
  • Supports http (default) and stdio transports
  • Retrieves tools via client.tools() and merges them into the tools object
  • Warns and skips on name conflicts with endpoint tools
  • Optional confirm: true adds needsApproval to all tools from that source
  • Unreachable servers are warned, not fatal

Sub-Agent Tools

Configured as agent.agents[], each referencing another agent ID. At runtime:

  • Loads the sub-agent's config and connection
  • Recursively calls buildAgentTools with depth + 1 (max depth: 5)
  • Creates a nested ToolLoopAgent with the sub-agent's own tools and instructions
  • Wraps it as a tool with description (defaults to "Delegate task to the {agentId} agent") and inputSchema (defaults to { task: string })
  • Uses toModelOutput to extract the text response for the parent agent
  • Cleans up sub-agent MCP clients after execution

FileSystem Tools

Configured via agent.properties.fileSystem with a basePath. Automatically adds four tools:

ToolPurposeLimits
read-fileRead file contents512KB max, truncates with notice
list-filesList directory with optional globNo inherent limit
search-filesCase-insensitive text search200 match limit, skips files >1MB
stat-fileFile metadata (size, type, date)None

All tools use resolvePath() (packages/utils/ai-utils/src/fileSystem/resolvePath.js) which normalizes the requested path against the base directory and throws if the resolved path escapes the base, preventing path traversal attacks.

Deployment Considerations

The fileSystem tools call Node's fs/promises and glob directly, so they require a Node.js runtime. Edge runtimes (Vercel Edge, Cloudflare Workers, Deno Deploy edge) and browsers cannot execute them, fs/promises is unavailable. Lowdefy's pages router (pages/api/agent/[...path].js in both @lowdefy/server and @lowdefy/server-dev) does not declare runtime: 'edge', so the default Node runtime applies.

The tools are read-only. There is no write-file or delete-file. Serverless platforms with a read-only deployment filesystem (Vercel, AWS Lambda) work without needing /tmp workarounds.

Standard next start (running the built server directory directly): copyAgentFileSystems copies each unique basePath into context.directories.server at build time, so the data sits alongside the built server. This works as long as the entire server directory ships to the host (Docker, Fly.io, Railway, EC2, etc.).

Next.js standalone output (LOWDEFY_BUILD_OUTPUT_STANDALONE=1 in @lowdefy/server), Vercel, and other tracer-based bundlers need extra wiring. Next's file tracer follows static imports to decide what to include in the bundle. basePath is read from agent config at runtime in buildAgentTools.js:202, so the directory is not statically traceable. Without help, the files copied by copyAgentFileSystems would sit on the build host but never make it into the deployed bundle.

The build handles this automatically. copyAgentFileSystems writes a agentFileSystems.json manifest to the server build directory listing every unique basePath. packages/servers/server/next.config.js reads the manifest and feeds the paths into outputFileTracingIncludes under the /api/agent/* route, so the tracer pulls each basePath directory into the standalone output and the Vercel function bundle. App developers don't need to configure anything.

The trade-off is bundle size: pointing an agent at a large directory will bloat the deployment. That's the explicit intent of granting fileSystem access, but worth flagging when sizing deployments.

Reserved Platform Tool Names

packages/utils/ai-utils/src/reservedToolNames.js exports RESERVED_PLATFORM_TOOL_NAMES:

update-page-state, read-file, list-files, search-files, stat-file

buildAgentTools rejects any endpoint, MCP, or sub-agent whose name collides with this list with a ConfigError. MCP sources can collide in a non-fatal way: the conflicting MCP tool is skipped and a warning is logged. These names are owned by the platform: update-page-state is injected by handleAgentChat when the chat has sharedState; the four *-file* tools are injected when an agent has a fileSystem basePath.

Shared State

The sharedState system lets an agent read from, and write back to, the calling page's Lowdefy state. Two things get wired up when AgentChat.sharedState is a non-empty object:

  1. The object is sent to the server on every turn and injected into the agent's instructions as <context>sharedState: ...</context> (when pageContext: true).
  2. The server injects a platform tool named update-page-state into the agent's tool set. The model calls it with { updates: { key: value, ... } } and the client applies the writes to Lowdefy page state.

Client: AgentChat.sharedState

AgentChat takes a sharedState property (evaluated by operators per render, like any other block property):

yaml
type: AgentChat
properties:
  agentId: support_agent
  sharedState:
    _state: true # expose whole page state
    # or a curated shape:
    # legal: { _state: legal_name }
    # cart:  { _state: cart.items }
  • The block mirrors the evaluated object into sharedStateRef and reads it inside the transport's body() at send time, so the agent sees the freshest value without re-creating the transport on every state change.
  • Empty objects are coerced to null. The server only injects the tool when sharedState is a non-empty object.
  • The transport POSTs { messages, urlQuery, sharedState } to /api/agent/{pageId}/{agentId}.

Server: Tool Injection

callAgent extracts sharedState from the request body, defaults to {}, and puts it on context.agentContext.sharedState. handleAgentChat then:

  1. Calls buildUpdatePageStateTool({ sharedState }). If sharedState is not an object, it returns null. Otherwise, it returns an AI SDK tool() whose:
    • description enumerates each top-level key with its JS typeof (or null / array), so the model knows exactly which fields it can write.
    • inputSchema is { updates: { type: object, additionalProperties: true } }.
  2. If a tool was returned, merges it into tools under the name update-page-state.
  3. When pageContext: true, appends sharedState: {JSON} to the <context> block prepended to the system instructions.

Client-Side Allowlist and Write Path

update-page-state is a client-executed tool (no server execute). The browser handles the call in AgentChat's onToolCall:

  1. Reads toolCall.input.updates.

  2. Filters writes against Object.keys(sharedStateRef.current). Any key the model hallucinates outside the exposed shape is dropped into ignored and not written. This defends against prompt-injection writes to arbitrary page state.

  3. For the writable subset, fires a registered internal event via methods.triggerEvent({ name: '__updatePageState', event: writable }). The event is registered at block-mount time:

    js
    methods.registerEvent({
      name: '__updatePageState',
      actions: [{ id: 'setState', type: 'SetState', params: { _event: true } }],
    });
    

    Routing through registerEvent + triggerEvent with _event: true reuses the action-runner's existing state-write path rather than introducing new block-level setState/getState methods. (An earlier prototype that added block.setState/block.getState was reverted in favour of this pattern, see the merge history for agent-shared-state.)

  4. Responds via addToolOutput({ tool: 'update-page-state', output: { ok: true, written: [...], ignored: [...] } }) so the model can see what took effect.

Sharp Edges

  • Allowlist is request-scoped: Object.keys(sharedState) at request time. Patches to keys not in that set are dropped on the client.
  • Reserved name: update-page-state is in RESERVED_PLATFORM_TOOL_NAMES. User tools with that name throw at build/runtime, they cannot shadow the built-in.
  • Snapshot, not live: The agent sees the snapshot at request time, not subsequent updates from other UI interactions during the agent run.

Developer Pattern

A typical setup: the page has a form, the agent can both ask questions about what's in the form and fill it in.

yaml
- type: AgentChat
  properties:
    agentId: intake_agent
    sharedState:
      legal_name: { _state: legal_name }
      company: { _state: company }
      notes: { _state: notes }

The model now sees those three fields on every turn and can call update-page-state to write them back. Any attempt to write, say, admin: true is silently dropped because admin isn't in the exposed shape.

Hook System

Hooks are server-side lifecycle callbacks that call API endpoints. Configured under agent.hooks:

yaml
hooks:
  onStart: [log_agent_start]
  onFinish: [generate_title, save_conversation]

Fire-and-Forget Hooks

These hooks dispatch endpoint calls without awaiting results:

YAML KeyAI SDK CallbackFires When
onStartexperimental_onStartAgent begins processing
onStepStartexperimental_onStepStartEach tool loop step starts
onToolCallStartexperimental_onToolCallStartModel initiates a tool call
onToolCallFinishexperimental_onToolCallFinishTool execution completes
onStepFinishonStepFinishEach tool loop step finishes

Hook payloads are cleaned of non-serializable fields (abortSignal, functions, messages) via cleanHookEvent.

Awaited Hook: onFinish

onFinish is handled at the stream level, not through the AI SDK callback. After the agent stream completes:

  • Sends a payload with:
    • messages -- full UIMessage array
    • steps -- per-step records collected via the SDK's onStepFinish (each with text, finishReason, usage, toolCalls, toolResults)
    • toolResults -- flattened steps.flatMap(s => s.toolResults) convenience field
    • finishReason -- overall finish reason from the usage accumulator
    • isAborted -- currently always false here
    • usage -- accumulated usage across all steps
    • agentContext fields spread in: pageId, userId, conversationId, urlQuery, sharedState
  • Awaits each endpoint call sequentially
  • If an endpoint returns { dataParts: [...] }, each data part is written to the response stream
  • This enables patterns like title generation: the hook endpoint calls an LLM, returns [{ type: 'data-chat-title', data: { title } }], and the client receives the title as a stream event

Agent Resolvers

Each provider plugin exports an agent type that maps provider-specific config to providerOptions before delegating to handleAgentChat.

ClaudeAgent (@lowdefy/connection-anthropic)

packages/plugins/connections/connection-anthropic/src/connections/Anthropic/ClaudeAgent/ClaudeAgent.js

Maps thinking and effort properties into providerOptions.anthropic:

yaml
properties:
  model: claude-sonnet-4-20250514
  thinking: { type: enabled, budgetTokens: 10000 }
  effort: high

OpenAIAgent (@lowdefy/connection-openai)

packages/plugins/connections/connection-openai/src/connections/OpenAI/OpenAIAgent/OpenAIAgent.js

Maps reasoningEffort and reasoningSummary into providerOptions.openai:

yaml
properties:
  model: o3-mini
  reasoningEffort: medium
  reasoningSummary: auto

GeminiAgent (@lowdefy/connection-google)

packages/plugins/connections/connection-google/src/connections/Google/GeminiAgent/GeminiAgent.js

Maps thinkingConfig and safetySettings into providerOptions.google:

yaml
properties:
  model: gemini-2.5-pro
  thinkingConfig: { thinkingBudget: 5000 }

AIGatewayAgent (@lowdefy/connection-ai-gateway)

packages/plugins/connections/connection-ai-gateway/src/connections/AIGateway/AIGatewayAgent/AIGatewayAgent.js

Wraps the Vercel AI Gateway (@ai-sdk/gateway). Maps gateway routing props into providerOptions.gateway before delegating to handleAgentChat. The model is in creator/model form (e.g. anthropic/claude-sonnet-4.5):

yaml
properties:
  model: anthropic/claude-sonnet-4.5 # creator/model id
  order: [anthropic, vertex] # preferred provider order
  only: [anthropic, vertex] # restrict to these providers
  fallbackModels: # fallback models tried in order
    - openai/gpt-5-mini
  user: '{{ user.id }}' # spend attribution id
  tags: [support, prod] # analytics tags
  zeroDataRetention: true # ZDR-only routing
  providerTimeouts: # per-provider BYOK timeouts (ms)
    byok: { anthropic: 15000 }
  byok: # request-scoped BYOK creds
    anthropic: [{ apiKey: '...' }]

Provider-native options (e.g. Anthropic thinking, OpenAI reasoningEffort) are passed through the gateway by keying providerOptions on the underlying provider slug (providerOptions.anthropic, providerOptions.openai, ...).

Use this when the app needs cross-provider failover, BYOK, or consolidated billing/analytics. Single-provider apps should use the dedicated provider connection (Anthropic, OpenAI, Google) for lower indirection.

AISDKAgent (generic, @lowdefy/ai-utils)

packages/utils/ai-utils/src/AISDKAgent.js

Passes through to handleAgentChat with no provider-specific mapping. Useful for providers that need no special options.

Dynamic Step Configuration

prepareStep rules (packages/utils/ai-utils/src/buildPrepareStep.js) allow per-step config overrides during the tool loop:

yaml
prepareStep:
  - steps: [1]
    toolChoice: required
    activeTools: [search_knowledge_base]
  - from: 2
    toolChoice: auto
    temperature: 0.3

Rules are evaluated in order. The first matching rule wins. Each rule can match by:

  • steps: [1, 3, 5] -- explicit step numbers
  • from: 2, to: 4 -- inclusive range (omit to for open-ended)

Overridable properties: activeTools, toolChoice, maxOutputTokens, temperature.

Message Pruning

The prune config removes older reasoning and tool-call parts to manage context window size. When enabled, handleAgentChat decomposes the standard createAgentUIStream flow to insert pruneMessages from the AI SDK between UI-to-model message conversion and agent execution.

yaml
prune:
  reasoning: before-last-message
  toolCalls: all
  emptyMessages: remove

Reasoning Pruning

  • all -- removes all reasoning parts from all messages
  • before-last-message -- keeps reasoning only in the last assistant message
  • none -- keeps all reasoning

Tool Call Pruning

Global string or per-tool array:

yaml
# Global
prune:
  toolCalls: before-last-message

# Per-tool
prune:
  toolCalls:
    - type: all
      tools: [search_files]
    - type: before-last-message

AgentChat Block

packages/plugins/blocks/blocks-antd-x/src/blocks/AgentChat/AgentChat.js

A composite block built on Ant Design X components that provides the full chat UI.

Transport

LowdefyChatTransport.js creates a DefaultChatTransport from the AI SDK, configured to POST to /api/agent/{pageId}/{agentId}. The conversationId is passed as a query parameter, urlQuery in the body.

State Management

The useChat hook from @ai-sdk/react manages messages, streaming status, and error state. External messages can be synced in via the messages property (for loading saved conversations). Messages are cleared automatically when conversationId changes.

Events

useAgentEvents (packages/plugins/blocks/blocks-antd-x/src/blocks/AgentChat/useAgentEvents.js) bridges AI SDK state changes to Lowdefy's event system using refs to track which events have already fired:

  • onBeforeSend -- before message is sent (can cancel)
  • onUserMessage -- when a user message appears
  • onMessageComplete -- when streaming finishes (includes finishReason, full message parts)
  • onToolCall -- when a tool call is initiated
  • onToolResult -- when a tool call completes
  • onError -- on streaming/transport errors
  • onStop -- when user stops generation
  • onRegenerate -- when user regenerates a response
  • onEditMessage -- when user edits a message (truncates history and resends)
  • onDeleteMessage -- when user deletes a message
  • onFeedback -- when user gives thumbs up/down
  • onSuggestionClick -- when user clicks a suggestion chip
  • onSwitchChange -- when user toggles a sender switch
  • onTitleGenerated -- when a data-chat-title part arrives (from onFinish hook)
  • onDataPart -- when any data part arrives from the stream

Methods

Registered via methods.registerMethod for use with CallMethod actions:

regenerate, setMessages, sendMessage, clearMessages, deleteMessage, stop, clearError, scrollToBottom

Features

  • File attachments: Optional, with direct data URL encoding or S3 upload via a policy request
  • Drawer mode: display: drawer wraps the chat in an Ant Design drawer
  • Welcome screen: Configurable prompts shown when the conversation is empty
  • Tool approval UI: ToolApproval component rendered for tools with needsApproval
  • Suggestions: Static config or dynamic from data-suggestions data parts
  • Sender switches: Toggle controls in the sender footer, state available in onBeforeSend
  • Message display: Configurable roles, avatars, copy/feedback actions, reasoning display, markdown with mermaid/LaTeX/code highlighting

AgentConversations Block

packages/plugins/blocks/blocks-antd-x/src/blocks/AgentConversations/AgentConversations.js

A standalone block for conversation list management, decoupled from the chat UI:

  • items -- conversation list (from state or requests)
  • activeKey -- currently selected conversation
  • menu -- context menu items per conversation
  • creation -- "New Chat" button config
  • groupable -- group conversations with collapsible sections

Events: onSelect, onNew, onMenuClick

Developers wire their own persistence. The block renders the list; the app config connects it to AgentChat via shared state (typically conversationId).

Key Files

FilePurpose
packages/utils/ai-utils/src/handleAgentChat.jsCore orchestration: builds tools, creates ToolLoopAgent, manages streaming
packages/utils/ai-utils/src/buildAgentTools.jsMerges endpoint, MCP, sub-agent, and fileSystem tools
packages/utils/ai-utils/src/buildUpdatePageStateTool.jsBuilds the update-page-state AI SDK tool from the current sharedState
packages/utils/ai-utils/src/reservedToolNames.jsRESERVED_PLATFORM_TOOL_NAMES -- blocks user tools from shadowing platform
packages/utils/ai-utils/src/buildPrepareStep.jsBuilds step-matching function for dynamic per-step config
packages/utils/ai-utils/src/AISDKAgentSchema.jsJSON Schema for agent properties validation
packages/utils/ai-utils/src/AISDKAgent.jsGeneric agent resolver (no provider-specific mapping)
packages/utils/ai-utils/src/fileSystem/resolvePath.jsPath traversal prevention for fileSystem tools
packages/utils/ai-utils/src/fileSystem/readFile.jsFile reading with 512KB truncation
packages/utils/ai-utils/src/fileSystem/searchFiles.jsCase-insensitive search with 200 match limit
packages/api/src/routes/agent/callAgent.jsAPI route handler: loads config, resolves connections, calls resolver
packages/api/src/routes/agent/getAgentConfig.jsReads agent JSON from build artifacts
packages/api/src/routes/agent/getAgentResolver.jsLooks up agent type from plugin registry
packages/build/src/build/buildAgents.jsBuild-time validation, normalization, cycle detection
packages/build/src/build/writeAgents.jsSerializes agent configs to JSON artifacts
packages/build/src/build/writePluginImports/writeAgentImports.jsGenerates agent type import registry
packages/build/src/build/copyAgentFileSystems.jsCopies fileSystem directories to server output
packages/servers/server/pages/api/agent/[...path].jsNext.js API route, SSE streaming
packages/plugins/blocks/blocks-antd-x/src/blocks/AgentChat/AgentChat.jsChat block component
packages/plugins/blocks/blocks-antd-x/src/blocks/AgentChat/LowdefyChatTransport.jsDefaultChatTransport factory
packages/plugins/blocks/blocks-antd-x/src/blocks/AgentChat/useAgentEvents.jsAI SDK to Lowdefy event bridging
packages/plugins/blocks/blocks-antd-x/src/blocks/AgentConversations/AgentConversations.jsConversation list block
packages/plugins/connections/connection-anthropic/src/connections/Anthropic/ClaudeAgent/ClaudeAgent.jsAnthropic resolver
packages/plugins/connections/connection-openai/src/connections/OpenAI/OpenAIAgent/OpenAIAgent.jsOpenAI resolver
packages/plugins/connections/connection-google/src/connections/Google/GeminiAgent/GeminiAgent.jsGoogle resolver
packages/plugins/connections/connection-ai-gateway/src/connections/AIGateway/AIGateway.jsAI Gateway connection (createGateway)
packages/plugins/connections/connection-ai-gateway/src/connections/AIGateway/AIGatewayAgent/AIGatewayAgent.jsAI Gateway resolver (routing opts -> providerOptions.gateway)
packages/plugins/connections/connection-mcp/src/connections/Mcp/Mcp.jsMCP config-container connection

Decision Trace

Why ToolLoopAgent over a manual loop? The AI SDK's ToolLoopAgent handles the tool call loop, step tracking, streaming protocol, and message format conversion. Lowdefy provides tools and configuration; the SDK manages the execution cycle. This avoids reimplementing retry logic, streaming protocol details, and multi-step orchestration.

Why endpoints as tools? Endpoints already have authentication, connection management, operator evaluation, and composable routines. Adding description and payloadSchema to endpoint config makes them callable by models with no new infrastructure. The same endpoint serves page buttons, other endpoints, and agent tools.

Why separate AgentConversations from AgentChat? Conversation management (persistence, search, grouping) varies widely across apps. Decoupling lets developers wire their own storage backend and UI layout. The two blocks connect through shared state, typically a conversationId in Lowdefy state.

Why hooks call endpoints? Hooks follow the same composable pattern as the rest of Lowdefy's server-side architecture. A hook endpoint can write to a database, call an external API, or generate a chat title -- all using existing connection and request infrastructure. No special hook execution engine needed.

Why MCP connections are config containers? Unlike provider connections that create SDK clients, MCP connections just store transport config (URL, command, headers). The actual MCP client is created at runtime by buildAgentTools because MCP clients are stateful (they maintain a session) and must be created fresh per request and cleaned up after.

Why clean build artifact markers before passing schemas to the AI SDK? Build artifacts contain serializer markers (~k, ~r, ~l, ~arr) as non-enumerable properties and wrapper objects. The AI SDK's jsonSchema() function expects clean JSON Schema. cleanBuildArtifact() strips these markers via JSON.stringify/JSON.parse with a key filter.