8 KiB
AgentInstance and the plugin-based workflow
This document explains how an agentInstance invokes a handler and how logic is composed via plugins to enable strategy-like processing. It covers message persistence, streaming updates, tool calling, and second-round handoff.
Overview
- Entry:
IAgentInstanceService.sendMsgToAgentreceives user input. - Orchestrator:
basicPromptConcatHandlerdrives prompt concatenation, AI calls, and plugin hooks. - Plugins:
createHooksWithPluginsattaches plugins to unified hooks with shared context, enabling decoupled, replaceable strategies. - Data: message model
AgentInstanceMessage, status modelAgentInstanceLatestStatus.
Handler selection and registration
- Source of handlerID: prefer the instance’s handlerID, fallback to the agent definition’s handlerID (see
src/pages/Agent/store/agentChatStore/actions/agentActions.ts#getHandlerIdand the preferences hookuseHandlerConfigManagement.ts). - Backend registration: in
AgentInstanceService.initialize(),registerBuiltinHandlers()registersbasicPromptConcatHandlerunder the IDbasicPromptConcatHandler;initializePluginSystem()registers built-in plugins. - Runtime selection: inside
sendMsgToAgent(), the handler is fetched fromthis.agentHandlersby agentDef.handlerID and started as an async generatorconst generator = handler(handlerContext), then iterated withfor await (const result of generator).
Related code:
- index.ts:
initialize(),registerBuiltinHandlers(),sendMsgToAgent() - basicPromptConcatHandler.ts
Sequence
sequenceDiagram
autonumber
participant User as User
participant AISvc as IAgentInstanceService
participant Handler as basicPromptConcatHandler
participant Hooks as Plugins(Hooks)
participant API as External API
User->>AISvc: sendMsgToAgent(text,file)
AISvc-->>Handler: append to agent.messages
Handler->>Hooks: userMessageReceived
Hooks-->>AISvc: saveUserMessage / debounceUpdateMessage
Handler->>Hooks: agentStatusChanged(working)
loop generation and streaming updates
Handler->>AISvc: concatPrompt(handlerConfig, messages)
AISvc-->>Handler: flatPrompts
Handler->>API: generateFromAI(flatPrompts)
API-->>Handler: update(content)
Handler->>Hooks: responseUpdate(update)
Hooks-->>AISvc: debounceUpdateMessage
end
API-->>Handler: done(final content)
Handler->>Hooks: responseComplete(done)
alt plugin requests next round
Hooks-->>Handler: actions.yieldNextRoundTo = self
Handler->>Handler: append messages and continue flow
else return to user
Handler-->>AISvc: completed(final)
end
Key design points
1. Event-driven strategy composition
– createHooksWithPlugins exposes unified hooks: processPrompts, userMessageReceived, agentStatusChanged, responseUpdate, responseComplete, toolExecuted.
– Plugins subscribe as needed and compose different strategies without changing the main flow.
Plugin registration and wiring:
- At app init,
initializePluginSystem()registers built-in plugins to a global registry. - For each round,
createHooksWithPlugins(handlerConfig)creates a fresh hooks instance and attaches plugins per config. responseConcat()andpromptConcatalso look upbuiltInPluginsand run plugin logic (e.g.,postProcess) with a dedicated context.
Stateless plugins requirement:
- Plugins must be stateless. Do not persist cross-round or cross-session state inside closures.
- All state must travel through
context(e.g.,handlerContext.agent.messages,metadata). - Plugins may be registered to multiple hooks across conversations and then discarded; internal mutable state risks races and contamination.
2. Messages as the source of truth
– User, assistant, and tool result messages are all AgentInstanceMessage.
– duration limits how many subsequent rounds include a message in context.
– UI and persistence coordinate via saveUserMessage and debounceUpdateMessage.
Persistence and UI updates:
– User messages: messageManagementPlugin.userMessageReceived persists via IAgentInstanceService.saveUserMessage, pushes into handlerContext.agent.messages, and calls debounceUpdateMessage to notify UI.
– Streaming updates: responseUpdate maintains an in-progress assistant message (metadata.isComplete=false) with debounced UI updates.
– Finalization: responseComplete persists the final assistant message and updates UI once more.
– Tool results: toolExecuted persists messages with metadata.isToolResult and sets metadata.isPersisted to avoid duplicates.
3. Second-round handoff and control
– Plugins may set actions.yieldNextRoundTo = 'self' in responseComplete to trigger another LLM round immediately.
– The handler stops after reaching retry limits and returns the final result.
concatPrompt and prompt delivery:
– AgentInstanceService.concatPrompt exposes an observable stream for prompt assembly. The handler uses getFinalPromptResult to obtain final prompts before calling the external API.
Example plugins
messageManagementPlugin
Responsibilities:
– Persist user messages in userMessageReceived and sync UI.
– Manage streaming assistant message in responseUpdate; persist final content in responseComplete.
– Update status in agentStatusChanged.
– Persist tool results in toolExecuted and mark as persisted.
Notes:
– Update handlerContext.agent.messages in place for immediate UI rendering.
– Use debounced updates to reduce re-renders.
– Mark streaming messages with metadata.isComplete.
wikiSearchPlugin
Responsibilities:
– Inject available wiki workspaces and tool list in processPrompts.
– On responseComplete, detect tool calls, execute, produce isToolResult message with duration=1.
– Set actions.yieldNextRoundTo = 'self' to continue immediately with tool outputs.
Notes:
– Validate parameters with zod.
– Use messages as the carrier for tool I/O.
– Set duration=1 for tool-call assistant messages to economize context.
Tool calling details:
– Parse: detect tool-call patterns via matchToolCalling in responseComplete.
– Validate & execute: validate with zod, then executeWikiSearchTool uses workspace and wiki services to fetch results.
– History: create an isToolResult message (role: 'user', duration=1) for the next round; report via hooks.toolExecuted.promise(...) so messageManagementPlugin persists and notifies UI.
– Loop: set actions.yieldNextRoundTo='self' to continue another round using tool outputs.
Flow
flowchart TD
A[User input] --> B[sendMsgToAgent]
B --> C[Message enqueued to agent.messages]
C --> D[userMessageReceived persist + UI]
D --> E[agentStatusChanged = working]
E --> F[concatPrompt generate prompts]
F --> G[generateFromAI streaming]
G --> H[responseUpdate update UI]
H --> I{responseComplete}
I -->|tool call| J[Execute tool and write tool result message]
J --> K[actions.yieldNextRoundTo=self]
K --> F
I -->|plain reply| L[Complete and return to UI]
Related code
Benefits
– Loose coupling: the main flow stays unchanged while capabilities are pluggable. – Testability: plugins can be unit-tested and integration-tested with the handler. – Evolvability: new capabilities land as new plugins and hook subscriptions.
Notes
– Avoid double persistence; use metadata flags for dedup.
– Ensure idempotency and robust error handling; prefer UI updates over persistence when degrading.
– Control retry limits and exit conditions to avoid infinite loops.