Skip to main content
Zuko’s AI agent runs in-process inside the NestJS backend (apps/backend/src/agent/). It is built on DeepAgents, which extends LangGraph with a hierarchical sub-agent model. There is no separate agent service to start — the agent activates when a user sends a chat message.

Architecture

Chat message (SSE)


ChatController  ─── AgentService.stream()


                    buildChatGraph()

                    DeepAgent (root)
                    ├── Persistent context middleware
                    ├── CRM tools (company / contact / deal / context)
                    ├── Filesystem tools (read / write / bash / glob / grep / …)
                    ├── Web tool (web_fetch)
                    └── Sub-agents
                         ├── Contacts Agent
                         ├── Companies Agent
                         ├── Deals Agent
                         └── Meetings Agent
The root agent handles the conversation turn. When a task falls within a specialist’s responsibility, the root agent delegates to a sub-agent. Each sub-agent has its own system prompt and a scoped set of tools.

Sub-agents

Sub-agentResponsibilities
Contacts AgentFetch, query, create, and update contacts
Companies AgentFetch, query, create, and update companies
Deals AgentFetch, query, create, and update deals
Meetings AgentRetrieve meeting records from the backend
Sub-agents share the same authentication context (org ID, user ID) as the root agent and always operate within the active organization.

Tool reference

CRM tools

These call /api/agents/* endpoints on the backend and write audit log entries with source AI.

Contacts

ToolDescription
get_contact_detailsFetch a single contact by ID
get_contact_ownerLook up the owner/assignee of a contact
query_contactsFilter contacts by name, email, company, owner, date range
create_contactCreate a new contact
update_contactUpdate contact fields (name, email, phone, linkedinId, notes)

Companies

ToolDescription
get_company_detailsFetch a single company by ID
query_companiesFilter companies by name, domain, owner, etc.
create_companyCreate a new company
update_companyUpdate company fields

Deals

ToolDescription
get_deal_detailsFetch a single deal by ID
query_dealsFilter deals by stage, contact, company, owner, date range
create_dealCreate a new deal
update_dealUpdate deal fields

Context

ToolDescription
get_conversation_contextReturns the contact/company/deal IDs currently attached to the chat. The agent always calls this first.

Filesystem tools

These run inside the chat’s sandbox — an isolated execution environment. In production, sandboxes are remote Sprites machines; in development they run locally.
ToolDescription
bashRun a shell command in the sandbox. 120 s timeout.
readRead a file
writeWrite a file
editApply a targeted edit to a file
statGet file metadata (size, mtime, type)
mkdirCreate a directory
readdirList directory contents
globMatch files by glob pattern
grepSearch file contents by regex
bash and read have a needsApproval check. Commands that match rm -rf or reference .env files are blocked until the user explicitly approves them.

Web tool

ToolDescription
web_fetchGET a URL and return its text body. Capped at 2 MB.

Interactive tools

ToolDescription
ask_user_questionPause the agent and ask the user a question. Supports optional predefined choices. The agent waits for the reply before continuing.
todo_writeWrite a structured to-do list back to the conversation.

Tool approval (human-in-the-loop)

Tools can declare a needsApproval function. When it returns true, the tool returns { pending: true } instead of executing, and the frontend surfaces an approval prompt to the user. Execution continues only after the user approves. Approval is currently required for:
  • bash commands that include rm -rf
  • bash commands or read calls that reference .env files

Persistent context

Context entities (contacts, companies, deals) attached to a chat thread are stored in LangGraph checkpoint state and persist across turns. The get_conversation_context tool always returns the current set, even after a page reload. This is handled by PersistentContextMiddleware (apps/backend/src/agent/middleware/persistent-context.middleware.ts), which merges context entity arrays via a LangGraph state reducer.

Model selection

The model is selected via the AGENT_MODEL environment variable using the format provider/model:
AGENT_MODEL=openai/gpt-4.1          # OpenAI (default)
AGENT_MODEL=openai/gpt-4o           # GPT-4o
AGENT_MODEL=anthropic/claude-sonnet-4-5  # Anthropic Claude
Supported providers: openai, anthropic. An invalid format throws at startup.

Streaming

The backend streams agent output to the frontend as SSE. The response includes:
  • X-Thread-Id — LangGraph thread ID for this run
  • X-Chat-Id — Zuko chat ID
The raw LangGraph stream (modes: values + messages) is converted to the AI SDK UIMessageStream format via @ai-sdk/langchain, then piped to the browser. To stop a running agent turn, call POST /api/v1/chat/stop. The backend signals cancellation via AbortSignal, which is passed through to all tool execute calls.