Files
web-official/public/blog/runtime-architecture-upgrade.en.md
Pine e2244a3cae feat: PineAgentsWeb 官网前端应用(Vite+React SPA)
- src/ 页面与组件、public/ 静态资源
- 工程配置:vite/tsconfig/package 依赖
2026-08-23 22:31:31 +08:00

34 KiB
Raw Permalink Blame History

title, date, author, tags, cover, excerpt
title date author tags cover excerpt
QwenPaw Runtime Architecture Upgrade: Design, Implementation & Extensibility 2026-07-07 QwenPaw Team
ArchitectureUpgrade
EightPhaseOrchestration
HookSystem
QwenPaw2.0
/blog/runtime-architecture-upgrade-cover.png QwenPaw Runtime was refactored from a 650-line god method into an 8-phase orchestration engine—enabling one-feature-one-directory extensibility with zero changes to Runtime core code.

QwenPaw Runtime Architecture Upgrade: Design, Implementation & Extensibility


Table of Contents

    1. Background & Motivation — Why a Runtime Architecture Upgrade Was Needed
    1. Overall Architecture
    1. Runtime: The 8-Phase Orchestration Engine
    1. Agent Building & Execution
    1. Hook System — The Core Extensibility Mechanism
    1. Prompt, Tool & Command Registration
    1. Cross-Workspace Services: AppServiceManager
    1. End-to-End Walkthrough: A Request's Complete Journey
    1. Summary

1. Background & Motivation — Why a Runtime Architecture Upgrade Was Needed

Core Pain Points

Before the Runtime architecture upgrade, QwenPaw's request handling was concentrated in a single AgentRunner.query_handler() method — a god method exceeding 650 lines. Every new feature (mission mode, coding mode, plan mode, cron isolation, skill injection, etc.) required invasive modifications to this core function, continuously adding if/else branches and cross-cutting state management.

A concrete example: adding a /mission feature at the time required changes across 8 files, 2 of which were invasive modifications to core files. Each new feature added ~40 lines to runner.py. This development pattern created severe maintenance burden and merge conflicts.

Summary of specific pain points:

# Pain Point Cost
1 Runner is a god method ~40 lines of runner.py changed per new feature
2 Agent construction hardcodes tool dictionary Adding a tool requires ~80 lines of invasive changes to the agent class
3 System prompt assembly is scattered Injection points spread across mixin/runner/env
4 Three duplicate external interfaces Command dispatch + API + CLI, each method written 3 times
5 New features require changes to 48 files /mission changed 8 files, 2 with core invasions

Goal

One feature = one directory. Zero lines of change to the Runtime body.


2. Overall Architecture

Layered Design

The upgraded architecture adopts an OS-style layered design:

Runtime Architecture Upgrade — Layered Design

The actual structure implemented in code:

AppServiceManager (FastAPI lifespan scope, shared across workspaces)
│   ├── TaskTracker (cross-workspace task tracking)
│   ├── ToolCoordinator (cross-workspace tool lifecycle management)
│   └── ApprovalCoordinator (sub-agent approval)
│
└── Workspace (per-agent)
    ├── Runtime (execution engine: 8-phase orchestration)
    ├── ServiceManager (per-agent resource lifecycle)
    │   ├── MemoryManager / CronManager / ChatManager
    │   ├── DriverManager (MCP / Channel / ACP / A2A)
    │   └── SessionManager
    └── WorkspacePlugins (per-workspace registries)
        ├── HookRegistry (8-phase hook orchestration)
        ├── SlashCommandRegistry (unified command dispatch)
        ├── ToolRegistry (declarative tool registration)
        ├── PromptManager (pluggable prompt construction)
        └── modes: list[AgentMode] (behavior mode list)

Core Responsibility Separation

Layer Lifecycle Responsibility
AppServiceManager FastAPI lifespan Three coordinators shared across workspaces
Workspace per-agent (dynamic) Single agent's resources + complete execution unit
Runtime per-request Execution engine: 8-phase hook orchestration + build + execute
ServiceManager per-agent Manages per-agent resource start/stop, dependency ordering, parallel initialization
WorkspacePlugins Registered at startup Hook / Command / Tool / Prompt / Mode registries

Strict contract: AppServiceManager has exactly three fields — task_tracker, tool_coordinator, approval_coordinator. The code enforces this via __slots__; adding any other field violates the contract. Per-workspace state belongs to WorkspacePlugins / ServiceManager.

Actual constraint in code:

# src/pineagents/app/app_services/app_service_manager.py

class AppServiceManager:
    __slots__ = (
        "task_tracker",
        "tool_coordinator",
        "approval_coordinator",
    )

3. Runtime: The 8-Phase Orchestration Engine

Core: Runtime.run()

The previous pain point was that the runner was a god method. The solution is to split request processing into 8 fixed phases, each serving as a hook mounting point.

The Runtime class (src/pineagents/runtime/runtime.py) is the per-workspace request orchestrator. Each request invokes run(), which produces SSE envelope objects through the 8-phase lifecycle.

class Runtime:
    """Per-workspace request orchestrator.
    One ``Runtime`` instance per ``Workspace``.  ``run()`` is called once
    per ``AgentRequest`` and yields SSE envelope objects."""

The 8 Phases

Defined in src/pineagents/runtime/phases.py:

class Phase(str, Enum):
    PRE_DISPATCH = "pre_dispatch"       # Request normalization, before slash dispatch
    POST_DISPATCH = "post_dispatch"     # After slash dispatch finds no match
    PRE_AGENT_BUILD = "pre_agent_build" # Session loading and other pre-build preparation
    POST_AGENT_BUILD = "post_agent_build"  # Agent built; inject mode context
    PRE_EXECUTE = "pre_execute"         # Bootstrap / prompt refresh
    POST_RESPONSE = "post_response"     # Session save / cron writeback
    ON_ERROR = "on_error"               # Exception normalization
    FINALLY = "finally"                 # Idempotent cleanup

Execution Flow

The actual execution flow of Runtime.run() interleaves hook phases with fixed steps:

Runtime.run() — 8-Phase Orchestration Engine

[Phase 1] PRE_DISPATCH hooks           ← Pluggable
[Fixed 1] SlashCommandRegistry.dispatch ← Fixed: unified command dispatch
[Phase 2] POST_DISPATCH hooks           ← Pluggable
[Phase 3] PRE_AGENT_BUILD hooks         ← Pluggable (session load, etc.)
[Fixed 2] AgentBuilder.build(ctx)       ← Fixed: build agent
[Phase 4] POST_AGENT_BUILD hooks        ← Pluggable
[Phase 5] PRE_EXECUTE hooks             ← Pluggable (bootstrap, etc.)
[Fixed 3] AgentExecutor.run(msgs)       ← Fixed: execute agent
[Phase 6] POST_RESPONSE hooks           ← Pluggable (session save, etc.)
[Phase 7] ON_ERROR hooks                ← Triggered on exception
[Phase 8] FINALLY hooks                 ← Always executed (cleanup)

Key design: Phase positions are fixed; hooks registered at each phase are pluggable. Adding a new feature = adding a new hook, no modification to Runtime.run() needed.

Three Hook Actions

Each hook returns a HookResult containing one of three actions (src/pineagents/runtime/hooks.py):

class HookAction(str, Enum):
    CONTINUE = "continue"           # Continue to next hook / phase
    SHORT_CIRCUIT = "short_circuit" # Return payload immediately, end current request
    SKIP_AGENT = "skip_agent"       # Skip agent build and execution, but subsequent hooks still run

For example, when a slash command is matched, Runtime.run() sets skip_agent = True — agent build and execution steps are skipped, but POST_RESPONSE and FINALLY hooks still execute, ensuring session persistence and cleanup.


4. Agent Building & Execution

The previous pain points were hardcoded tools in the agent class and scattered prompt assembly. The solution is declarative assembly via AgentBuilder and heartbeat-driven execution via AgentExecutor.

AgentBuilder — Per-Request Agent Assembly

AgentBuilder (src/pineagents/runtime/builder.py) is responsible for constructing a complete QwenPawAgent for each request. It integrates:

Component Source Description
Toolkit local_workspace.list_tools() Internally uses ToolRegistry for declarative filtering by mode/skill/feature
System Prompt PromptManager.build_sync() Pluggable prompt contributors assembled by priority
Model ProviderManager + model factory LLM model + formatter
Middlewares ToolCoordinatorMiddleware + plugin middlewares Onion-model middleware
Governor ResourceGovernor Governance policy layer (sandbox, permissions)
Session State ctx.session_state (from SessionLoadHook) Session state restoration

Key improvement: Tools are no longer hardcoded in the agent class. They are obtained via the workspace's list_tools(), which internally uses ToolRegistry (tools registered via the @tool_descriptor decorator) to filter per-request based on active modes, skills, and features.

AgentExecutor & Envelope

AgentExecutor (src/pineagents/runtime/executor.py) drives the agent's reply stream and wraps a heartbeat mechanism — ensuring that during long idle periods (e.g., tool-guard approval waits), keep-alive envelopes are emitted instead of letting the SSE connection drop.

Envelope (src/pineagents/runtime/envelope.py) is an SSE state machine that translates agentscope EventType events into the frontend's streaming envelope protocol, tracking per-request state (text blocks, reasoning blocks, tool calls, data blocks) and producing the correct event sequence.


5. Hook System — The Core Extensibility Mechanism

This is the core extensibility mechanism of this architecture upgrade. Previously, adding a new feature required invasive changes to the runner; now you only need to register a hook.

Hook System Class Hierarchy & Execution Mechanism

Two Types of Hooks

The upgraded architecture provides two hook base classes for different scenarios:

Base Class Definition Location Execution Timing Purpose
LifecycleHook hooks/base.py Every request reaching that phase Infrastructure: session load/save, bootstrap, skill env, error handling
ModeGatedHook modes/base.py Only when the owning mode is active Behavior modes: mission state, coding mode context, goal persistence

Both inherit from HookBase in runtime/hooks.py. ModeGatedHook automatically checks self.owner_mode.is_active(ctx) in run(), skipping execution if the condition is not met. This design eliminates a recurring bug in the old code: every hook forgetting to add a mode activation check.

HookRegistry — Topological Sorting

HookRegistry (src/pineagents/runtime/hooks.py) manages hook registration and execution:

  • Hooks are grouped by Phase
  • Within the same phase, hooks are topologically sorted via before / after constraints, with priority as tiebreaker
  • Cycle detection at startup throws HookCycleError
  • Sort results are cached and automatically invalidated on new registration
class HookBase:
    phase: Phase
    name: str
    priority: int = 100
    before: tuple[str, ...] = ()  # "run before these hooks"
    after: tuple[str, ...] = ()   # "run after these hooks"

For example, SessionLoadHook declares priority = 10 (executes earlier in PRE_AGENT_BUILD), while MissionStateLoadHook declares after = ("session_load",), ensuring task state loads after session state.

Built-in LifecycleHooks

The project currently includes the following built-in LifecycleHooks (many appear as setup/cleanup pairs across different phases):

Hook Phase Priority Responsibility
ContextVarsSetupHook PRE_DISPATCH 10 Inject per-request ContextVars (workspace_dir, session_id, etc.)
CronContextHook PRE_DISPATCH 5 Mark cron-originated requests
SessionLoadHook PRE_AGENT_BUILD 10 Load session state from persistent storage
MediaProcessHook PRE_EXECUTE 5 Process file/media blocks in input messages
CronMemoryIsolateHook PRE_EXECUTE 10 Snapshot and clear agent context (cron isolation)
LangfuseTraceHook PRE_EXECUTE 12 Open Langfuse root trace span
BootstrapHook PRE_EXECUTE 20 Inject BOOTSTRAP.md guidance
SkillEnvHook PRE_EXECUTE 40 Push skill-declared environment variables
CronMemoryRestoreHook POST_RESPONSE 80 Restore cron snapshot and append new messages
SessionSaveHook POST_RESPONSE 90 Write back agent state to session storage
ErrorNormalizeHook ON_ERROR 10 Normalize provider exceptions to user-readable messages
CancelCleanupHook ON_ERROR 20 Clean up approvals and interrupt agent on cancellation
SkillEnvCleanupHook FINALLY 40 Pop skill environment variable stack
LangfuseTraceCleanupHook FINALLY 50 Close Langfuse root trace span

A notable pattern: setup/cleanup hook pairs (e.g., SkillEnvHook in PRE_EXECUTE / SkillEnvCleanupHook in FINALLY) pass context manager handles between phases via the ctx.extras dictionary.

AgentMode — Feature Bundle

AgentMode (src/pineagents/modes/base.py) is a behavior bundle: commands, tools, hooks, and prompt contributors packaged together.

class AgentMode:
    name: str

    def setup(self, workspace):
        for spec in self.commands():
            workspace.plugins.slash_command_registry.register(spec)
        for desc in self.tools():
            workspace.plugins.tool_registry.register(desc)
        for hook in self.hooks():
            workspace.plugins.hook_registry.register(hook)
        for contributor in self.prompt_contributors():
            workspace.plugins.prompt_manager.register(contributor)

    def commands(self) -> list[CommandSpec]: ...
    def tools(self) -> list[ToolDescriptor]: ...
    def hooks(self) -> list[HookBase]: ...
    def prompt_contributors(self) -> list[PromptContributor]: ...
    def is_active(self, ctx: HookContext) -> bool: ...

setup(workspace) is the sole entry point for registration. "Which mode owns what" can be easily derived from the four content methods.

WorkspacePlugins — Registry Container

All per-workspace registries reside in WorkspacePlugins (src/pineagents/app/workspace/workspace_plugins.py):

@dataclass
class WorkspacePlugins:
    slash_command_registry: SlashCommandRegistry
    hook_registry: HookRegistry
    tool_registry: ToolRegistry
    prompt_manager: PromptManager
    modes: list[AgentMode]

    def register_mode(self, mode: AgentMode, workspace):
        # Actual code includes duplicate name detection; duplicate registration raises ValueError
        self.modes.append(mode)
        mode.setup(workspace)  # one call registers everything

    def active_mode_names(self, ctx) -> set[str]:
        return {m.name for m in self.modes if m.is_active(ctx)}

6. Prompt, Tool & Command Registration

PromptManager — Composable System Prompt

Previously, system prompt construction was scattered across mixin, runner, and env modules; adding a new prompt fragment required finding the correct injection point. Now, PromptManager (src/pineagents/runtime/prompt_manager.py) declaratively assembles the system prompt from ordered PromptContributor instances:

class PromptContributor:
    name: str
    priority: int = 100  # lower = appears earlier

    async def contribute(self, ctx) -> str | None:
        raise NotImplementedError

class PromptManager:
    async def build(self, ctx) -> str:
        parts = []
        for c in self._contributors:  # sorted by priority
            fragment = await c.contribute(ctx)
            if fragment:
                parts.append(fragment.strip())
        return "\n\n".join(parts)

The project includes 9 built-in contributors:

Contributor Priority Content
AgentIdentityContributor 5 Agent ID identity
AgentsMdContributor 10 AGENTS.md (including heartbeat / memory handling)
SoulMdContributor 20 SOUL.md personality file
ProfileMdContributor 30 PROFILE.md configuration file
MultimodalHintContributor 80 Multimodal capability hints
CodingModeContributor 85 Coding Mode personality block
ScrollContextContributor 86 Scroll context strategy hints
DriverPolicyHintContributor 88 Driver policy guidance
EnvContextContributor 90 Environment context (time / session / OS)

Adding a new prompt fragment = writing a PromptContributor and registering it. No changes to other files needed. Notably, PromptManager.build() includes built-in fault tolerance — if a single contributor throws an exception, it is logged and skipped without causing the entire prompt build to fail.

ToolRegistry — Declarative Tool Registration

The previous pain point was that tools were hardcoded in the agent class; adding a tool required ~80 lines of invasive changes to the agent class. ToolRegistry (src/pineagents/runtime/tool_registry.py) replaces the old hardcoded tool dictionary with a declarative, filterable registry.

Each tool is described by a ToolDescriptor (core gating fields shown below; the full definition also includes async_execution, description, metadata, etc.):

@dataclass(frozen=True)
class ToolDescriptor:
    name: str
    func: Callable
    enabled_by_default: bool = True
    requires_modes: tuple[str, ...] = ()    # Required active modes (any match)
    requires_skills: tuple[str, ...] = ()   # Required active skills (any match)
    requires_features: tuple[str, ...] = () # Required feature flags (all must match)
    requires_sandbox: tuple[str, ...] = ()  # Sandbox resource requirements
    # Also includes: async_execution, description, metadata, etc.

Built-in tools use the @tool_descriptor decorator for automatic collection at import time:

@tool_descriptor(
    requires_modes=("goal",),
    description="Get the current goal status.",
)
async def get_goal(...):
    ...

At build time, ToolRegistry.filter() selects the correct tools based on the current request's mode, skill, and feature — no if/else needed.

SlashCommandRegistry — Unified Command Dispatch

Previously, QwenPaw had four parallel command mechanisms: conversation, control, daemon, and skill, each method written 3 times. SlashCommandRegistry (src/pineagents/runtime/slash_command_registry.py) unifies them into a single dispatch point.

@dataclass(frozen=True)
class CommandSpec:
    name: str
    handler: CommandHandler   # async (ctx, args) -> Msg | None
    aliases: tuple[str, ...]
    category: str             # "builtin" / "daemon" / "control" / "skill" / "user"
    help_text: str

@api_action — Triple Auto-Generation

Addressing the previously listed pain point of "three duplicate external interfaces," @api_action (src/pineagents/api_action.py) fundamentally solves this problem: one decorator simultaneously generates HTTP API, CLI subcommands, and slash commands.

@api_action — Triple Auto-Generation

Manager classes inherit from ManagerBase and mark methods with @api_action:

class CronManager(ManagerBase):
    endpoint_prefix = "crons"

    @api_action(
        methods={"http", "cli", "slash"},
        http_method="GET",
        http_path="/crons/jobs",
        slash_command="cron-list",
    )
    async def list_jobs(self) -> list[CronJobSpec]:
        return await self._repo.list_jobs()

The triple generation implementation is distributed across three auto-registrars:

Generation Path Implementation File Mechanism
HTTP Route app/_api_action_routes.py register_http_routes() scans ManagerRegistry, auto-mounts FastAPI endpoints
CLI Subcommand cli/auto.py _LazyAutoGroup lazy-loads; generated CLI commands call the server as HTTP clients
Slash Command app/_api_action_routes.py collect_slash_specs_from_api_actions() generates CommandSpec registered to per-workspace registry

Adding a new management interface = writing one @api_action method. Triple registration completes automatically, zero duplicate code.


7. Cross-Workspace Services: AppServiceManager

AppServiceManager holds exactly three cross-workspace coordinators, enforced via __slots__ — adding any other field violates the contract. Per-workspace state belongs to Workspace.plugins / Workspace.service_manager.

TaskTracker — Cross-Agent Session Tracking

TaskTracker tracks in-progress streaming requests across all workspaces. It supports graceful shutdown (waiting for active sessions to complete) and provides visibility into agents currently processing requests.

ToolCoordinator — Tool Call Lifecycle

ToolCoordinator (src/pineagents/tool_calls/) provides single tool call granularity control. In the old system, /stop could only kill the entire agent. Now individual tool calls can be tracked, cancelled, or backgrounded.

Key components include ToolCallEntry (per-call state), ToolCallStatus (PENDING → RUNNING → DONE / CANCELLED state machine), ToolCoordinatorMiddleware (injected into agent middleware stack), ToolResultPruningMiddleware (tool result pruning), ToolStream (streaming tool output), ToolCallContext / ToolHookRegistry (HITL lifecycle hooks), etc.

ApprovalCoordinator — Cross-Agent Approval

ApprovalCoordinator handles sub-agent approval chains. Because it resides in AppServiceManager (FastAPI lifespan scope), it naturally spans all workspaces — sub-agent approval requests are visible to the parent agent.

ServiceManager — Per-Agent Resource Lifecycle

ServiceManager (src/pineagents/app/workspace/service_manager.py) manages per-agent services, supporting:

  • Declarative descriptors — Each service has a ServiceDescriptor with priority, dependencies, start/stop methods
  • Priority-based parallel startup — Same-priority services start concurrently
  • Reusable services — Marked services survive workspace reload (e.g., MemoryManager)
  • Optional services — Optional service failures are logged only, without aborting the workspace
@dataclass
class ServiceDescriptor:
    name: str
    service_class: type | Callable[[Workspace], type]  # Supports dynamic class factory
    init_args: Callable[[Workspace], dict]
    post_init: Callable[[Workspace, Any], None]  # Optional post-creation hook
    start_method: str
    stop_method: str
    reusable: bool = False
    reload_func: Callable[[Workspace, Any], None]  # Reusable service reload callback
    dependencies: List[str] = []
    priority: int = 100
    concurrent_init: bool = True
    optional: bool = False

8. End-to-End Walkthrough: A Request's Complete Journey

Having introduced each module separately, we now tie them all together with a concrete scenario — watching a user message travel from entering the system to returning a response, seeing what role each module plays.

Scenario: User Sends a Conversation Message in Mission Mode

Assume the user previously started a task via /mission start "Deploy an ECS instance", and Mission Mode is now active. The user sends a follow-up message:

"First, help me check what ECS specifications are currently available"

This message is not a slash command, so it follows the complete agent execution path. Here is its full journey through the system:

Stop 1: Workspace Receives the Request

Workspace is the per-agent resource and execution unit. It holds Runtime, ServiceManager, and WorkspacePlugins.

Upon receiving the request, Workspace calls Runtime.run(), beginning the 8-phase orchestration.

Stop 2: Runtime.run() — 8 Phases Executed Sequentially

Phase 1 + Fixed 1 — Request Normalization & Command Dispatch

LifecycleHooks inject per-request context variables (workspace_dir, session_id, etc.). Then SlashCommandRegistry attempts to match the user message — "First, help me check..." is not a slash command, so skip_agent remains False, continuing into the agent build flow.

Phase 3 · PRE_AGENT_BUILD — ModeGatedHook + Topological Sorting in Action

This demonstrates two core designs of the new architecture:

  • SessionLoadHook (LifecycleHook) loads session state and conversation history first
  • MissionStateLoadHook (ModeGatedHook, after=("session_load",)) then loads task state

Key point: MissionStateLoadHook declares after=("session_load",), and HookRegistry's topological sorting ensures it always executes after SessionLoadHook. Meanwhile, because it is a ModeGatedHook, it is automatically skipped when Mission Mode is inactive — developers need not write any if/else.

Fixed 2 · AgentBuilder — Three Registries Working Together

AgentBuilder declaratively assembles the agent for this request: ToolRegistry.filter() filters the correct tool set based on currently active modes; PromptManager.build_sync() assembles the complete system prompt by priority; ToolCoordinatorMiddleware is injected to make every tool call trackable and cancellable.

→ Tools are not hardcoded, prompts are not hand-assembled — everything is declarative and filtered per-request.

Phase 5 — Pre-Execution Preparation

Multiple LifecycleHooks collaborate to prepare the environment: media processing, Langfuse trace opening, Bootstrap guidance injection, Skill environment variable pushing, etc.

Fixed 3 · AgentExecutor — ToolCoordinator Cross-Layer Control

The agent begins execution and decides to call alicloud_cli to query ECS specifications. This tool call passes through ToolCoordinatorMiddleware, creating a ToolCallEntry and tracking its status. If the user sends /stop at this point, ToolCoordinator (at the AppServiceManager layer) can precisely cancel this one tool call instead of killing the entire agent. Envelope translates output events into the SSE envelope protocol, and the heartbeat mechanism ensures the connection stays alive during long executions.

Phase 6 + Phase 8 — Persistence & Cleanup

After execution completes, MissionStateSaveHook (ModeGatedHook) saves task state, and SessionSaveHook writes back the session — symmetric with the load in Phase 3. Finally, the FINALLY phase always executes cleanup (environment variable stack pop, trace closing), regardless of whether the request succeeded or failed.

Comparison: If the User Sends a Slash Command

If the user sends /mission status instead of a regular message:

  1. Phase 1 — Same as above, ContextVars injection
  2. Fixed 1SlashCommandRegistry.dispatch() matches the mission command → calls _mission_handler → returns status Msg → skip_agent = True
  3. Phase 2 — Command matched successfully, POST_DISPATCH branch not entered
  4. Phase 3 ~ Fixed 3 — Due to skip_agent = True, agent build and execution are skipped
  5. Phase 6SessionSaveHook still executes, ensuring state persistence
  6. Phase 8 — Cleanup hooks still execute

Key point: skip_agent skips Phase 3 ~ Fixed 3 (build and execution), but POST_RESPONSE and FINALLY phase hooks always execute. This is why the phase design is safer than a simple "early return" — cleanup and persistence are never missed.

Comparison: Developer Wants to Add a New Mode

Suppose you are a developer wanting to add a new Mode to QwenPaw. You do not need to modify a single line of Runtime code.

Taking the existing GoalMode in the project as an example, its directory structure is:

modes/goal/
├── __init__.py      # Imports
├── goal_mode.py     # GoalMode(AgentMode) + setup()
├── tools.py         # Goal-specific tools: get_goal, create_goal, update_goal, etc.
└── contributor.py   # GoalPromptContributor(PromptContributor)

One call to GoalMode.setup(workspace) registers commands / tools / prompt into the WorkspacePlugins registries.

Worth noting: Not every Mode needs to use all extension points. The four content methods of AgentMode (commands(), tools(), hooks(), prompt_contributors()) are all optional:

  • GoalMode uses tools + prompt contributor, no hooks — it controls iteration loops via StopHandler's Gate mechanism (GoalIterationGate, GoalBudgetGate)
  • MissionMode uses command + hooks + prompt contributor, managing task state load/save via ModeGatedHook
  • Future new Modes can freely combine these extension points as needed

When GoalMode is active:

  • ToolRegistry automatically filters tools with requires_modes=("goal",)
  • PromptManager automatically includes GoalMode's prompt contributor
  • Runtime code changes: zero lines

9. Summary

Before vs. After Comparison

Runtime Architecture Upgrade — Before vs. After

Aspect Before After
Adding a mode Modify runner.py ~40 lines + 3 wiring files 1 directory, setup() registers once
Adding a tool ~80 lines invasive change to agent class @tool_descriptor decorator
Adding a command Write 3 dispatch implementations 1 CommandSpec, or @api_action triple auto-generation
Adding a management interface Write HTTP + CLI + slash 3 times One @api_action decoration, auto-generates all three
Adding a prompt fragment Find insertion point in runner 1 PromptContributor
Runtime code changes Every feature modifies runtime Zero lines

What This Architecture Upgrade Achieved

  1. OS-style LayeringAppServiceManager manages cross-workspace shared services; Workspace per-agent; Runtime per-request pure execution
  2. One Feature, One Module — AgentMode bundles commands + tools + hooks + prompt; LifecycleHook handles cross-mode infrastructure
  3. Zero Runtime Invasion — 8-phase hook mechanism for extension; Runtime has only three fixed steps (dispatch, build, execute)
  4. Declarative Agent Construction — Tools via ToolRegistry + @tool_descriptor; Prompts via PromptManager + PromptContributor
  5. Unified Command DispatchSlashCommandRegistry replaces four parallel mechanisms
  6. @api_action Triple Auto-Generation — One decorator simultaneously generates HTTP API + CLI + slash command, completely eliminating interface duplication
  7. Single Tool Call Granularity ControlToolCoordinator supports tracking, cancellation, and backgrounding of individual tool calls
  8. Topologically Sorted Hooksbefore / after constraints + cycle detection, replacing fragile manual ordering
  9. Lightweight IsolationServiceManager handles per-agent service lifecycle, supporting dependency-ordered startup and reusable services