Skip to content

Agent Deep Dive

Someone sends ai: check tomorrow's weather in a group, and a few seconds later the bot replies with a data-backed answer -- what happened in between? This page follows that path to expand on the AI overview runtime: ZhinAgent's turn flow, deferred tools, sub-agents and orchestration, session persistence and compaction, Assistant profile and scheduled tasks.

ZhinAgent turn

After a trigger match, Agent Host calls zhinAgent.process(text, commMessage, tools) to run a turn:

A few facts worth remembering. ZhinAgent, sub-agents, background workers, and AIService.runAgent all go through the same agentLoop, so behavior can be consistently expected. Messages in the same session queue up per ai.agent.inboundQueue (groupMode: supersede | fifo), and concurrent turns do not overwrite each other. When the preferred model fails, it falls back along a candidate chain to other available models from the same provider. maxIterations defaults to 15 (DEFAULT_CONFIG.maxIterations) and can be overridden per provider/model via the model harness (see below). Timeouts have three layers: the trigger-side single turn is bounded by ai.trigger.timeout (default 60000ms), the overall Agent turn defaults to 120000ms (DEFAULT_CONFIG.timeout), and tool pre-execution has a separate 15000ms cap (preExecTimeout).

The tool pool for a turn = plugin-registered tools + ai.mcpServers connected tools + built-in tools + deferred meta tools + schedule_* + bash + Host extension tools (e.g. voice_stt / voice_tts).

Deferred tools (discover / load_tool / load_skill)

When the number of tools is large, stuffing all schemas into the prompt crowds the context. Zhin.js uses lazy loading: a turn keeps only a small set of meta-tools resident, and the model retrieves and loads by name on demand.

Resident tools (default alwaysLoadedTools): ask_user, spawn_task, discover, load_tool, load_skill.

Meta-toolPurpose
discoverSearch for tools/skills by query (`kind: tool
load_toolLoad a tool schema into the current session by name; callable thereafter
load_skillLoad a skill's description text and its declared tools

Tuning (ai.agent.deferredTools):

yaml
ai:
  agent:
    deferredTools:
      maxLoadedPerSession: 12   # Max tools loaded per session (default 12)
      discoverTopK: 5           # Number of discover results (default 5)
      alwaysLoadedTools: [ask_user, spawn_task, discover, load_tool, load_skill]
      mcpServers:
        icqq: { alwaysLoaded: [send_msg] }   # Resident tools for a specific MCP server

Reserved/built-in tool names (bash, read_file, spawn_task, etc.) cannot be overridden by plugins. For non-reserved tools, later registrations override earlier ones with the same name, and conflicts are logged as warnings.

Sub-agents and spawn_task

The main Agent uses spawn_task to delegate complex/time-consuming tasks to background sub-agents without blocking the main conversation:

yaml
ai:
  agent:
    maxParallelSubagents: 5        # Hard cap on parallel sub-agents (default 5)
    toolExecution: tiered          # parallel | sequential | tiered (default)
    subagentAutoContinue: true     # Wake main Agent to continue after async completion (default true)
    subagentDirectImDelivery: false # Also send sub-task summary directly to IM (default false)
    subagentTools: []              # Additional tools allowlisted for sub-agents

Key spawn_task parameters:

ParameterDescription
taskTask description (goal, scope, expected output)
agentSub-agent name (must exist in ai.agents or agents/*.agent.md presets)
waitWhen true, waits synchronously; result returns to the current turn via tool result
contextfork (inject recent messages from parent session) / fresh (empty context)
tools / skillsDeclare tools and skills needed for the sub-task

Several behavioral constraints apply: multiple spawn_task calls can be initiated in a single turn -- independent sub-tasks should run in parallel. In tiered mode, read-only tools and spawns run in parallel while write/bash operations execute sequentially. Sub-agents use a restricted tool set by default (read_file / write_file / edit_file / list_dir / glob / grep / web_search / web_fetch / bash + deferred meta) and do not automatically inherit all tools from the main session; use ai.agent.subagentTools to explicitly add more. The sub-agent types visible to the main Agent are constrained by ai.agents.<name>.permission.task (glob -> allow/deny). After async completion, results are returned to the main Agent first (written to the main session and auto-continued); the user-visible reply is composed and sent by the main Agent. Additionally, sub-agent presets can be declared as files using agents/<name>.agent.md (YAML frontmatter + description), auto-discovered and registered at startup.

Orchestration

OrchestrationService (Kernel) maintains a Run / Task state machine: a single user request can be split into multiple tasks with dependency relationships (dependsOn), dispatched for execution by executor (local / remote). Built-in orchestration tools:

ToolPurpose
orchestration_startStart an orchestration Run
orchestration_add_taskAdd a task to a Run (role / goal / dependsOn / priority)
orchestration_statusQuery Run / Task status
orchestration_completeEnd a Run
orchestration_retry_task / orchestration_skip_taskRetry / skip failed tasks

Run states: open -> running / waiting -> completed. After configuring ai.remoteAgents (id + cardUrl + token), tasks can be dispatched to remote A2A agents for execution:

yaml
ai:
  remoteAgents:
    - id: local
      cardUrl: http://127.0.0.1:8069/a2a/zhin/.well-known/agent-card.json
      token: ${HTTP_TOKEN}

Orchestration runs can be viewed on the Console's Orchestration page (GET /api/agent/orchestration/runs); see Console.

Session persistence and session tree

When a database is available, Agent Host persists three tables (automatically falls back to in-memory mode without a database):

TableContents
agent_sessionsAgent session metadata (including session tree parent_id / active_leaf)
agent_messagesAgent turn messages (context store)
im_transcriptsIM inbound/outbound ledger (data source for the chat_history tool)

Set ai.sessions.useDatabase: false to force in-memory mode.

Within the same IM conversation, sessions can also be branched into a session tree, managed via IM commands:

CommandPurpose
/compactManually compact the current session context
/tree / /tree NView / switch session branches
/resetReset the session
Send clear / resetArchive and clear the current session's AI multi-turn context

Other management commands (master / authorized users): /models, /health, /cmd, /endpoints, /bindings, /tools, /mcp.

Compaction

Context is automatically compressed when approaching the window limit. Configure with ai.agent.compaction:

yaml
ai:
  agent:
    compaction:
      enabled: true
      auto: true
      keepRecentTokens: 20000   # Token budget for keeping recent messages (default 20000)
      minKeepCount: 2           # Minimum number of messages to keep (default 2)

Compression triggers when estimated tokens exceed contextWindow x 0.6. Compression has two stages: first a micro-compact (trimming redundant tool results, etc.), then the LLM generates a [Previous conversation summary] to replace old history. If consecutive auto-compressions fail up to the limit, auto-compaction stops to avoid repeated consumption.

Model harness

Override execution loop parameters per provider / model (currently consuming maxIterations). Merge order: TS default table -> providerPatterns (supports * wildcards) -> models exact keys:

yaml
ai:
  agent:
    modelHarness:
      providerPatterns:
        "open*": { maxIterations: 7 }
      models:
        "gpt-4o": { maxIterations: 8 }
        "openai:gpt-4o": { maxIterations: 9 }

Built-in tools

CategoryTools
Executionbash, run_deferred_task
Fileread_file, write_file, edit_file, list_dir, glob, grep
Networkweb_search, web_fetch
Interactionask_user
Taskspawn_task, todo_read, todo_write, orchestration_*
Memory/Retrievalmemory_search, memory_upsert, knowledge_search, chat_history
Mediagenerate_image, analyze_media
Metadiscover, load_tool, load_skill, install_skill
Schedulingschedule_list, schedule_add, schedule_remove, schedule_pause, schedule_resume, schedule_preview

Assistant profile and scheduled tasks

The Assistant runtime productizes "doing things on a schedule": persistent tasks are stored in data/schedule-jobs.json and executed by ScheduleJobEngine at the designated time, with results pushed back to IM.

yaml
assistant:
  enabled: true
  profile:
    enabled: true
    file: assistant.profile.yml   # Relative to project root, this is the default name
  events:
    enabled: true                 # Enable POST /api/assistant/events external event endpoint
  defaults:
    notifyOnFailure: false

assistant.profile.yml declares the persona and routines, which are synced to scheduled tasks at startup:

yaml
version: 1
persona:
  soul: You are a thoughtful life assistant.
routines:
  heartbeat:
    enabled: true
    everyMs: 1800000
    prompt: Check to-dos and report.
  morningBrief:
    enabled: true
    scheduleKind: solar      # solar | lunar | workday | freeDay | holiday
    cron: "0 0 8 * * *"
    tz: Asia/Shanghai
    prompt: Generate today's morning briefing.

Built-in routines include heartbeat (interval execution), morningBrief (default 08:00), bedtimeCheck (default 22:00), and weatherReport; custom keys are also supported. Note that for mainland China "workday" scenarios, use workday (which accounts for make-up workdays) instead of solar's 1-5.

Users can also manage tasks directly in IM by having the Agent use schedule_add / schedule_list / schedule_preview and other tools. External systems can inject events via POST /api/assistant/events and query tasks via GET /api/assistant/jobs.