Skip to content

refactor(runtime): make the tool-result archive capability indivisible #2026

Closed

Description

@Astro-Han

Problem

The tool-result archive capability is currently three independent optional fields spread across two different configuration surfaces:

  • archiveToolResult (writer) → backend options
  • readToolResultArchive (replay hydration reader) → backend options
  • archiveResources (ref-addressed reader powering ArchiveRead) → tool options

Nothing structurally ties them together, so "writer enabled, reader tool absent" is a representable state. It has already bitten twice: once on the child-agent path (patched locally via CHILD_RECOVERY_TOOL_NAMES in packages/runtime/src/subagent-tools.ts:35) and once at the headless top-level surface (#2025).

Desktop happens to be correct only because both sides were wired by hand in two separate places (apps/desktop/src/main/tool-assembly.ts:328,398 and apps/desktop/src/main/session-stream.ts:333).

Proposal

Add a factory in packages/runtime that returns one indivisible bundle:

buildToolResultArchiveCapability({ recorder, resourceReader }) => {
  archiveToolResult,      // → backend input
  readToolResultArchive,  // → replay hydration
  tools: [ArchiveRead],   // → host tool surface
}

Hosts supply only the host-specific storage pieces (desktop: artifactStore; Harbor: local archive dir). A host either has the whole capability or none of it. CLI keeps "none", which stays a valid state.

This makes the illegal state unrepresentable by construction rather than caught by an after-the-fact assertion.

Explicitly out of scope

  • No generic capability → tool derivation framework. Archiving is currently the only real instance of this "writer implies reader tool" coupling. runtimeResources and snapshotImage look similar but differ in kind: when missing they fail at the call site rather than leaving a dangling reference. Generalize when a second real case appears.
  • No bidirectional catalog invariant. The catalog is the full product vocabulary and host bindings are legitimately a subset (economy policy narrows surfaces, desktop-only tools are absent on headless, disabledSurfaceIds: ['agent'] is intentional). catalog ⊆ bound is never true; the correct invariant is the conditional implication archiveToolResult present ⟹ ArchiveRead ∈ bound.
  • No consolidation of CHILD_RECOVERY_TOOL_NAMES and DEEP_RESEARCH_ALLOWED_TOOL_NAMES. They are consumer-side allowlists with distinct responsibilities (surface-narrowing recovery rule vs. mode-level product policy), not duplicate registration state. Extracting an exported ARCHIVE_READ_TOOL_NAME constant to remove string drift is sufficient.
  • No activation of the catalog's effects field. It cannot express same-authority coupling between writer and reader.

Dependencies

Blocked by #2025. That one fixes behavior; this one is a behavior-neutral consolidation. They must not be combined — mixing a refactor with a behavior change makes benchmark deltas unattributable.

Activity

  1. Astro-Han commented on Aug 5, 2026

    @Astro-Han
    ContributorAuthor

    Claiming this — starting work now.

    Two corrections to the premises in the issue body, both verified against current main:

    1. CHILD_RECOVERY_TOOL_NAMES is not a working patch. It adds ArchiveRead to the candidate pool returned by buildChildAgentTools, but every child surface is then re-narrowed by the strict allowlist buildToolsForAgentDefinition (packages/runtime/src/agent-catalog.ts:293) against definition.tools, and none of the three builtin agent definitions lists ArchiveRead. The durable snapshot records [...definition.tools] (session-manager.ts:2490,2549,3077), so resume behaves the same. Children cannot call ArchiveRead today. The child path is not "patched locally" — it is the same defect as #2025, still live, and the only test covering it (subagent-tools.test.ts:457) asserts the pool rather than the child surface, sharing the same one-directional blind spot the implementation has.

    2. Four capability assembly authorities, not three: embedded desktop, runtime-host server, CLI, and headless/Harbor. Desktop's runtime-host client path is protocol-only and assembles no backend capability. CLI supplies none of the three fields — the legitimate "no archive at all" state, which stays expressible with no escape hatch.

    Adjustment to the proposal

    The factory as originally specified returns tools: [ArchiveRead] for the host to bind. That still leaves the capability with two delivery addresses — backend options and the tool surface — constructed at different layers (desktop: session-stream.ts:332 vs tool-assembly.ts:189). A host can hold a complete bundle and deliver it to one address only, which reproduces exactly the state we are trying to eliminate. The factory makes a half-built capability unconstructible; it does not make a half-delivered one unrepresentable.

    So the capability carries the tool, and the backend binds it:

    export interface ToolResultArchiveServices {
      archiveToolResult: ToolResultArchiveRecorder;
      readToolResultArchive: ToolResultArchiveReader;
      readArchivedToolResultResource:
        ToolResultArchiveResourceReader['readArchivedToolResultResource'];
    }
    
    export interface ToolResultArchiveCapability {
      readonly services: ToolResultArchiveServices;
      readonly archiveReadTool: MakaTool & { readonly name: 'ArchiveRead' };
    }
    
    export function createToolResultArchiveCapability(
      services: ToolResultArchiveServices,
    ): ToolResultArchiveCapability;

    AiSdkBackendInput drops archiveToolResult (ai-sdk-compaction-contract.ts:160) and readToolResultArchive (ai-sdk-backend.ts:604) for a single optional toolResultArchive, and resolves its own effective tool set from it. archiveResources disappears from builtin-tools.ts:147,301 and headless/src/tools.ts:44,113.

    One address, so nothing to forget; no invariant assertion needed, because there is no longer a second surface to check against. The reasoning is that the placeholder is a runtime-generated protocol value and ArchiveRead is its decoder — which host binds it was never a legitimate configuration question.

    This also removes the need for any AgentDefinition or durable-snapshot change: ArchiveRead never enters definition.tools, so child sessions inherit it from the same backend seam and CHILD_RECOVERY_TOOL_NAMES is deleted rather than fixed.

    What this changes on purpose, and must not slip through review

    BackendFactoryContext.tools (session-manager.ts:705) is currently documented as a hard ceiling: a host "may remove tools for stricter local policy, but must never append, substitute, or otherwise expose a tool outside this exact set." Binding a runtime protocol decoder inside the backend needs that contract rewritten explicitly to carve out runtime recovery tools as not-an-agent-permission, rather than quietly leaning on the fact that the constraint addresses hosts.

    Product-surface identity is computed before the backend, so it will not list the bound ArchiveRead unless it is either derived from the backend's effective tools or given an explicit protocolCapabilities field. Benchmark execution identity should not silently under-report the real tool surface.

    Expected non-behavioral churn: request-shape hash baselines, and writer-only unit fixtures that currently construct the illegal state and will need a whole capability.

    Delivery

    One draft PR. This is an atomic type migration across runtime, runtime-host, desktop and headless; splitting it produces intermediate states where the new capability exists but the old split entry points still accept a half-wired archive. The child-surface fix is not a separable behavior change here — it falls out of the same seam.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions