Deep-dive companion to
../architecture.md. The Agent subsystem is an AI-agent layer that runs on top of the five existing runtime contexts (service worker, content, inject, offscreen, sandbox) — it is not a sixth context. Its code lives undersrc/app/service/agent/, split into a context-agnosticcore/and aservice_worker/composition layer, plus a content-side API surface and offscreen/sandbox delegation described below.
AgentService is constructed once in
ServiceWorkerManager (new AgentService(this.api.group("agent"), this.offscreenSend, resource)) and composes the narrower services below rather than being one large class.
Each sub-service takes only the dependencies it needs — not a single Group/IMessageQueue/DAO triple applied
uniformly:
| Service | File | Responsibility |
|---|---|---|
ChatService |
chat_service.ts |
Chat request lifecycle: builds the system prompt, wires a per-request SessionToolRegistry, delegates the tool loop to the orchestrator. |
AgentTaskService |
task_service.ts |
CRUD + scheduling for AgentTask (cron-like triggers), runs tasks through the same tool-loop orchestrator. |
SkillService |
skill_service.ts |
Skill install/update/list from .md or .zip sources (parseSkillMd/parseSkillZip), backed by SkillRepo. |
AgentModelService |
model_service.ts |
Model config CRUD and default/summary-model selection, backed by AgentModelRepo. |
MCPService |
mcp.ts |
Manages MCPClient connections per configured server and registers/unregisters their tools on the shared ToolRegistry. |
BackgroundSessionManager |
background_session_manager.ts |
Tracks running background conversations (streaming state, listeners, pending ask_user prompts) so a UI can reattach to an in-flight session. |
SubAgentService |
sub_agent_service.ts |
Runs a sub-agent conversation through the shared tool loop with a type-scoped tool exclusion list. |
CompactService |
compact_service.ts |
Summarizes/compacts long conversation history via a dedicated compact prompt. |
AgentDomService |
dom.ts (+ dom_cdp.ts helpers) |
Page automation — see below for the default-vs-trusted split. |
AgentOPFSService |
opfs_service.ts |
Serves CAT.agent.opfs requests from both content scripts (no Blob support) and offscreen (Blob support), dispatched on whether the caller has a sender. |
Current set: git grep -n "export class" -- src/app/service/agent/service_worker/.
Message actions are namespaced under the agent group (this.api.group("agent")), the same RPC pattern
architecture.md describes for other services — the difference here is internal composition, not the wiring
into Group/Server.
ToolRegistry is the global registry: tools it holds
persist for the process lifetime and are classified by ToolSource — builtin (permanent, e.g.
web_fetch/web_search/opfs_*/the tab tools such as list_tabs/open_tab), mcp (from an
MCPService-managed server), skill (skill meta-tools: load_skill, execute_skill_script,
read_reference), session (registered per conversation: task tools, ask_user, agent, execute_script),
and script (user-script-supplied tools passed through conv.chat, dispatched via a callback rather than
stored in the map).
Tool names don't always match their source file — the sub-agent tool defined in sub_agent.ts registers as
agent, and tab_tools.ts registers get_tab_content/list_tabs/open_tab/close_tab/activate_tab with
no shared prefix. Read the name: field, not the filename.
SessionToolRegistry wraps a read-only reference
to the global ToolRegistry with its own per-session Map. This exists because registering a same-named
builtin tool (task tools, ask_user, agent) directly on the global registry would let concurrent
sessions clobber each other's closures — a session-scoped tool needs to stay bound to its own
conversationId/sendEvent. getDefinitions() merges session tools over parent tools (session shadows
parent); execute() builds the merged map and delegates to parent.executeTools() so shared behavior (e.g.
attachment persistence) isn't duplicated. A session's tools are reclaimed by garbage collection when the
session ends — no explicit unregister loop is required.
ToolLoopOrchestrator drives one
conversation turn: call the model, execute any tool calls the model requested, feed results back, and repeat
until the model stops calling tools (or the user stops it via Loop Guard / cancellation). It depends on injected callLLM and
autoCompact functions (rather than importing a concrete client) so tests can substitute spies.
retry_utils.ts's isRetryableError matches an
error message containing 429, a 5xx code, or a network-ish signal (network/fetch/ECONNRESET), then
excludes it if the message also matches 400, 401, 403, or 404 — those four codes specifically, not
every 4xx. withRetry then applies exponential backoff, aborting immediately if the caller's AbortSignal
fires. Context-window overflow triggers auto-compaction
(compact_service.ts / core/compact_prompt.ts) before the loop continues. Provider-specific request/response
shaping lives under core/providers/ (anthropic.ts, openai.ts, registry.ts), keeping the orchestrator
provider-agnostic.
- Background session —
BackgroundSessionManagerkeeps aRunningConversation(streaming buffer, tool calls so far, pendingask_userstate, abort controller) alive independent of whether a UI is currently listening, so a popup/options page can attach, detach, and reattach to the same in-flight run. - Sub-agent —
SubAgentServiceruns a nested conversation through the samecallLLMWithToolLoopcontract as the top-level chat, but resolves an exclusion list viaresolveSubAgentType/getExcludeToolsForType(core/sub_agent_types.ts) so a sub-agent type doesn't get tools it shouldn't (e.g. spawning further sub-agents). - Scheduled task —
AgentTaskServicepersistsAgentTaskdefinitions (AgentTaskRepo) and run records (AgentTaskRunRepo), computing next-fire times viacore/task_scheduler.tsandpkg/utils/cron; the service worker'schrome.alarmshandler (agentTaskScheduler, wired insrc/app/service/service_worker/index.ts) callsagent.onSchedulerTick()to drive due tasks through the same tool loop as interactive chat.
The Agent subsystem does not use one persistence pattern; pick by data shape, matching
architecture-data.md:
Repo<T>(chrome.storage.local) —AgentModelRepo(small config objects),AgentTaskRepo(task definitions).OPFSRepo(Origin Private File System) —AgentChatRepo(conversation history, can grow large and holds attachments),AgentTaskRunRepo(task run history),SkillRepo(skill.md/script bundles).MCPServerRepo(Repo<T>) — MCP server configs.
-
Content (
src/app/service/content/gm_api/cat_agent.ts) exposes theCAT.agent.*API to user scripts —ConversationInstancewraps a conversation and dispatches tool-call handlers registered by the calling script. It registers through the same@GMContext.API/@PermissionVerify.API/@grantpath as the traditional GM API, with dotted grant names andconnect()-based chat streaming — seearchitecture-gm-api.mdfor the differences that matter when adding one. -
DOM automation runs from the service worker through a single
AgentDomService(dom.ts), which handles every action (navigate,readPage,screenshot,click,fill,scroll,waitFor,executeScript, tab monitoring) and delegates to CDP helpers imported fromdom_cdp.ts(cdpClick,cdpFill,cdpScreenshot,cdpStartMonitor/cdpStopMonitor/cdpPeekMonitor) where it needschrome.debugger.dom_cdp.tsis a helper moduledom.tscalls into, not an independent service with its own request path. The default-vs-trusted split is not uniform across actions — check each one:- Navigation and tab bookkeeping (
navigate,update,create,query) always go throughchrome.tabs, never CDP. click/fillgenuinely branch on the caller'strustedoption: default mode drives them viachrome.scripting.executeScript, "trusted" mode delegates to CDP for real synthetic input (isTrusted: true), falling back to the non-trusted path if the CDP call fails.screenshothas its own logic independent of anytrustedflag: aselector-scoped capture always uses CDP; a background (non-active) tab tries CDP first and falls back tochrome.tabs.captureVisibleTabon failure; an active tab with no selector useschrome.tabs.captureVisibleTabdirectly.- Tab monitoring (
startMonitor/stopMonitor/peekMonitor) is unconditionally CDP-based — there is no non-CDP path for it at all.
CDP attaches the debugger to a tab and carries the extra permission/user-visible-banner implications that come with
chrome.debugger; how often that applies depends on which action you're looking at, not a single binary "default vs. trusted mode" switch. - Navigation and tab bookkeeping (
-
OPFS access is dispatched by caller:
AgentOPFSService.handleOPFSApichecks whether the request has asender(content script, no Blob support) or came overpostMessage(offscreen, Blob support) and adjusts behavior accordingly, rather than assuming one execution context. -
Skill scripts execute through
core/skill_script_executor.ts, delegating to the Sandbox the same way regular background/scheduled scripts do (seearchitecture-execution.md) — the Agent subsystem doesn't introduce a parallel script-execution path.
Test file names in service_worker/ don't all mirror their source 1:1 — some group by behavior instead
(background.test.ts covers background_session_manager.ts, retry.test.ts covers retry_utils.ts,
autocompact.test.ts covers the compaction trigger path), so a missing <source>.test.ts isn't proof of a
coverage gap either way. Current inventory:
git ls-tree --name-only HEAD src/app/service/agent/service_worker/ | grep test. core/ follows the same
co-located *.test.ts convention. Vitest conventions generally:
develop-testing.md.
- New tool — add it under
core/tools/, register it with the appropriateToolSource(builtinat startup,sessioninside the relevant service's session setup), and give it aToolExecutor. Don't register session-scoped tools on the globalToolRegistry— useSessionToolRegistryso sessions can't clobber each other. - New MCP-backed tool — goes through
MCPService, not manual registration; it already handles connecting, naming (mcp_<server>_<tool>), and cleanup. - New sub-agent type — extend
core/sub_agent_types.tswith its exclusion list rather than special-casing it inSubAgentService.