Files
Teknium 919322f6c3 feat(delegation): pluggable delegation providers
delegate_task asks the active delegation provider (delegation.backend, a new
provider category registered with ctx.register_delegation_provider) for each
task's route: model/provider/endpoint/reasoning resolved by the existing
delegation credential resolver, or a Hermes profile the child runs as. A
provider may add enum-checked per-task schema parameters built once with the
tool definitions. Core keeps the whole lifecycle.

- agent/delegation_provider.py + agent/delegation_registry.py: ABC + registry;
  the built-in 'default' strategy is today's behaviour and the fallback for an
  unregistered/unavailable backend.
- tools/delegate_tool_routes.py: per-task routes, parameter validation, and the
  profile scope binding (home override + secret scope + multiplex semantics +
  terminal policy) around a profile child's construction and conversation.
- Host isolation: HOST_OBJECT_BASES row, so providers run under
  plugins.isolation: host.
- delegation.backend in DEFAULT_CONFIG; registrar table moved to
  hermes_cli/plugins_registrars.py to keep plugins.py under its cap.
- Default config: delegate_task schema and every hermes-cli tool definition
  are byte-identical to main.
2026-10-22 14:37:10 -07:00

4.4 KiB

title, sidebar_label
title sidebar_label
Public Subagent Lifecycle API Subagent lifecycle API

Public Subagent Lifecycle API

Plugins can launch and supervise fresh Hermes child sessions without importing tools.delegate_tool, gateway internals, TUI state, or AIAgent fields. The service resolves its parent from the current agent turn, so it works in CLI, gateway, non-interactive, and kanban-worker sessions. Launching outside an active agent turn fails closed with No active Hermes parent session. The facade also works when a plugin runs under plugins.isolation: host.

Requests accept goal, context, role, model, provider, allowed_toolsets, blocked_tools, working_directory, parent_session_id, correlation_id, metadata, and timeout_seconds, subject to the restrictions below.

from agent.subagent_lifecycle import SubagentLaunchRequest

def launch_review(ctx):
    # Call from a plugin tool or hook while an agent turn is active.
    service = ctx.subagent_lifecycle
    handle = service.launch(SubagentLaunchRequest(
        goal="Review this change for regressions.",
        context="Only inspect the supplied repository.",
        role="leaf",
        correlation_id="review-42",
        allowed_toolsets=("file",),
    ))
    # Persist handle.to_dict() if desired.
    if service.wait(handle, timeout_seconds=2).timed_out:
        return handle.to_dict()
    return service.result(handle)

SubagentHandle is serializable and carries a versioned, opaque capability. Requests and handles may also arrive as mappings, including plugin-host wire records. Pass a handle back to status, wait, cancel, steer, result, or reconnect; malformed or forged handles return UNKNOWN/UNKNOWN_HANDLE and cannot access a child.

steer(handle, text) queues text for the child's next iteration boundary and returns True while the child accepts steering. It returns False for empty text, unknown or forged handles, handles owned by another parent session, or a child that has finished or stopped accepting steering.

The stable states are PENDING, STARTING, RUNNING, SUCCEEDED, FAILED, INTERRUPTED, CANCEL_REQUESTED, CANCELLED, and UNKNOWN.

cancel(handle, reason=...) is cooperative: it asks the child agent to interrupt at its next safe boundary and returns CANCEL_REQUESTED; it never claims completion until wait or result observes a terminal state. Terminal results are immutable, idempotent, bounded to 32k characters, omit transcripts and hidden reasoning, and include a stable result hash.

This API is lifecycle-managed asynchronous execution. Child construction and completion use the same host-owned path as delegate_task, including parent tool-resolution restoration, memory notification, serialized subagent_stop hooks, resource cleanup, and child-cost rollup. It does not change the synchronous delegate_task tool, batch delegation, or its gateway/TUI display. The initial implementation retains metadata and terminal results in-process for one hour. After a process restart, reconnect returns RECONNECT_UNAVAILABLE and never starts a replacement child. Running Python threads also cannot survive process exit; callers must treat those handles as interrupted by process exit.

Requests are fail-closed: goal/context/metadata sizes are capped, unknown or parent-broadening toolsets are rejected (plugin-registered toolsets count as known), and per-tool blocks, working-directory overrides, and per-launch timeouts are explicitly rejected until Hermes can support them without weakening isolation. Use allowed_toolsets to narrow a child; Hermes's existing unsafe-tool block remains enforced.

Routing

Children follow the delegation config (provider, model, base_url, api_key, api_mode, request_overrides) exactly as delegate_task children do. The request's model overrides the configured model. Its optional provider names a configured provider for this launch, with precedence request > delegation config > parent; the selected provider supplies its own credential bundle. An unresolvable provider raises SubagentLifecycleError before launch. Core does not expose provider to the model: the plugin chooses it, for example from operator config.

The lifecycle API does not consult delegation.backend: the plugin that launches a child already chooses its route. To change how delegate_task routes the model's subagents, write a delegation provider plugin.