返回设计文档

CAPABILITY · 07/08

ToolLoop

Dynamic router、fixed batch 和 registered tool execution。

真相源
docs/DESIGN_ENGINES_TOOLLOOP.md
读取方式
构建时本地读取
规模
242

Status: active Scope: nexrur.engines.toolloop, nexrur.prompts.toolloop, app-owned ToolLoop registries Tracking: nexrur GitHub Issues #130, #140

1. Purpose

ToolLoop is a substrate execution engine for bounded, registered callables. It answers two questions only:

  1. which registered callable may run;
  2. how that callable is admitted, executed, audited, timed out, retried, and returned.

It does not decide what a tool means to an App, which source should be queried, which candidate is best, or whether an App workflow should continue.

2. Ownership boundary

The substrate owns:

  • registry loading and ToolSpec construction;
  • callable resolution and invocation;
  • input/output schema gates;
  • timeout, bounded retry, cache, redaction, audit, and evidence hooks;
  • generic dynamic routing among explicitly allowed tools;
  • fixed sequential batches;
  • governed parallel batches over opaque keys;
  • deterministic receipt and result ordering.

The App owns:

  • tool names, descriptions, schemas, and callable registrations;
  • prompts and model profiles for App production;
  • source-specific adapters and result validation;
  • query construction, ranking, filtering, and selection;
  • concurrency choices required by a particular external source;
  • workflow order, terminal rules, diagnosis meaning, and projection policy.

No source family, provider, product, App, field name, ranking rule, or business fallback may be encoded as a ToolLoop Python default.

The active implementation split is explicit:

  • contract.py owns declaration types, explicit YAML loading, mechanical contract validation, and contract compilation;
  • router.py owns the dynamic decision contract, formal Prompt assembly, LLM decisions, observation summarization, and evidence-bounded closeout;
  • toolloop.py owns watchdogs, callable execution, runtime state, contract snapshots, audit/evidence handoff, sequential batches, and parallel batches.

Router code must not execute tools, write contract snapshots, or own a generic execution watchdog. ToolLoop code must not infer App semantics while executing Router decisions.

3. Registry contract

Runtime truth comes from the App registry, normally:

<app_root>/.gemini/skills/_shared/engines/toolloop.yml

A minimal owner block is:

subagents:
  worker-a:
    tools:
      transform:
        callable: app.tools.transform:run
        description: Transform one admitted work item.
        input_schema: {}
        output_schema: {}
        cacheable: false
    runner_defaults:
      allowed_tools: [transform]
      max_calls: 4
      tool_timeout_s: 120

Tool ids must be unique. Callables must resolve to public entrypoints. Allowed tools must be registered. Input and output schemas are mechanical gates, not places for substrate business judgment.

4. Execution modes

4.1 Dynamic router

ToolLoopRunner.run() lets an LLM choose the next action from an explicit allowlist. The caller must provide router_profile and final_profile. ToolLoop never maps friendly tiers, boolean flags, App names, or tool names to a provider/model.

The router receives only:

  • the goal and caller-provided context;
  • registered tool descriptions and schemas;
  • generic observations and budgets;
  • an optional caller-provided final schema.

It may reject an unknown tool, invalid arguments, an exact repeated call, or a budget breach. It may not infer domain-specific identifiers or apply source-specific ordering, query, filtering, or selection rules.

Prompt truth lives in src/nexrur/prompts/toolloop.md. Missing or invalid formal prompts fail closed; Python contains no emergency Prompt copy.

4.2 Sequential batch

run_batch() executes a caller-declared list in order. It is mechanical: the caller chooses the calls and ToolLoop applies registry, schema, timeout, audit, and receipt rules.

4.3 Governed parallel batch

run_parallel_batch() accepts bounded lanes. Each lane contains an opaque item_key and ordered calls identified by opaque call_key values.

lane A: call 1 -> call 2
lane B: call 1 -> call 2

Calls inside one lane remain serial. Different lanes may run concurrently up to the caller's explicit max_concurrency. Before fan-out, ToolLoop rejects the whole plan when keys are duplicated, limits are exceeded, a tool is unknown, or arguments fail schema validation.

Every child receipt carries parallel_batch_id, item_key, and call_key. Completion may be out of order, but convergence and returned lanes preserve input order. ToolLoop treats all three identifiers as opaque strings.

Provider admission and rate limits remain independent hard limits. A local parallelism value never overrides them.

4.4 Registered production router

A registered production callable may dispatch work items to other registered tool ids. The dispatch target must resolve through the same registry; a private helper name or orchestrator step is not a valid target.

5. Generic runtime state

Dynamic routing may retain only generic state:

  • completed tool call count;
  • per-tool call and result counts;
  • exact (tool_id, normalized_args) fingerprints;
  • same-tool streak;
  • observation summaries;
  • token, latency, and deadline accounting;
  • schema, timeout, and execution error counts;
  • explicit final/abort status.

ToolLoop must not inspect argument or result fields to infer source identity, candidate class, App phase, ranking, or business success.

6. Tool input schema

Every callable may declare an args_schema. ToolLoop validates the exact arguments it receives and fails closed on missing, unknown, or invalid fields. It does not rename fields or synthesize aliases.

Trace, cycle, run, asset, and campaign identity belongs to ToolLoop execution context. ToolLoop may use that out-of-band context for audit, evidence, usage, progress, and resume governance, but it must not inject identity fields into a callable payload after schema validation. A callable receives exactly the arguments admitted by its declared schema. Business identity required by the callable must be declared in that schema and supplied explicitly.

Orchestrator maintains a larger internal graph payload. For a closed object schema (type: object, additionalProperties: false), Orchestrator mechanically projects that payload onto the schema's declared top-level properties before deadline resolution and ToolLoop execution. This is a generic contract operation: neither Orchestrator nor ToolLoop recognizes App field names or individual tools. Open/empty schemas receive the full graph payload.

7. Control envelope

When an App tool is consumed by Orchestrator, the App registration declares an explicit generic control_envelope. ToolLoop compiles only the named mappings:

control_envelope:
  version: 1
  role: production
  graph:
    continue_statuses: [success, partial]
    stop_statuses: [failed]
  diagnosis:
    ready_field: diagnosis_ready
    reason_code_field: reason_code
    failure_kind_field: failure_kind
    facts_field: diagnosis_facts
    fields: [errors, warnings]
  handoff_fields: []

The receipt remains producer-owned truth. The compiled envelope is the only control-plane interpretation surface. Missing, ambiguous, or invalid mappings fail closed; ToolLoop does not discover fields by name or directory layout.

8. Failure semantics

ToolLoop distinguishes transport/runtime failure from producer-declared business status. A failed schema gate, unresolved callable, missing profile, missing Prompt, timeout, invalid parallel plan, or conflicting key is never returned as success.

Generic bounded exits may stop a dynamic loop after repeated malformed router output or an exact duplicate action. Such exits report an explicit failure or closeout reason; they do not fabricate App results.

9. Non-goals

ToolLoop does not own:

  • orchestrator graphs or phase dependencies;
  • campaign route, restart, halt, or terminal authorization;
  • diagnosis ontology or root-cause selection;
  • evidence source interpretation;
  • projection layout, binding, archival, or active promotion;
  • LLM/MCP clients inside Apps;
  • provider credentials;
  • compatibility aliases for retired App payloads or callables.

9. Acceptance criteria

The ToolLoop boundary is accepted when:

  1. active substrate code and prompts contain no App/source/provider defaults;
  2. dynamic routing requires explicit model profiles;
  3. exact-call deduplication is schema-agnostic;
  4. parallel lanes use opaque keys and converge deterministically;
  5. invalid plans fail before fan-out;
  6. App registries retain all business semantics and source concurrency policy;
  7. missing Prompt/profile/schema truth fails closed;
  8. focused sequential, parallel, router, schema, audit, and timeout tests pass.