Skip to content

AgentCore 0.7.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 09:45
· 175 commits to main since this release

Changed

  • Tier 1 deterministic compaction (KeepLastNToolResultsCompactor) no longer
    leaves a bare placeholder where it drops a tool body. It now appends a bounded
    card naming the call (tool name + a single-line, 120-char arguments preview)
    and up to three source URLs found in the discarded body, above the existing
    recovery pointer. Both fields already existed in the history and in the body,
    so this adds no LLM call, no storage, and no second model-visible recovery
    route. Measured cost is ~+220 characters per elided result, hard-capped at 400.

    Rationale: Tier 2's summary preserves arguments and URLs, but Tier 2 only fires
    when Tier 1 did not free enough, so on a Tier1-only turn the model lost exactly
    the two things it needs in order not to re-issue a query it already ran.

    A card that would not be shorter than the body it replaces is skipped and the
    body kept verbatim — but only when no spill callback is configured. With a
    recovery pointer the body is always replaced even if the card is longer: the
    pointer reaches the model only through spill_refs → the Tier 2 recovery
    index, and such a body is itself already an upstream-truncated preview, so
    keeping it would strand the spilled full text as unrecoverable.

  • KeepLastNToolResultsCompactor now resolves an existing recovery handle with
    spill_refs taking precedence over result_store_ref. A ref pinned by an
    earlier compaction pass describes the content still on the message, while the
    loop-cap handle describes the pre-truncation body upstream shed; reading the
    latter first re-spilled a body that was already stored and pinned the wrong
    handle into the recovery index.

Added

  • TieredCompactor takes manifest_max_paths and manifest_max_chars, either
    of which may be None to remove that bound. The defaults are unchanged (20 /
    3,000) and are sized for a handle rendered as a filesystem path. A product
    whose handles are short content-addressed ids pays a fraction of that per entry
    and should raise or remove the cap: when a cap binds, the OLDEST handles are
    dropped, and a product measured decisive early evidence becoming unrecoverable
    after a long unrelated detour for exactly that reason. The cap is charged
    against rendered characters, which is the only quantity the two handle shapes
    share — cap and handle shape are therefore not independent choices. The
    character cap covers the complete rendered index, including its header and
    list syntax. Non-None bounds must be large enough to retain at least one
    entry; invalid zero or header-only bounds fail at construction time.

  • ToolResult.host_metadata carries whatever ToolExecutionHooks.result_metadata
    returned, verbatim, through to AgentLoopHooks.render_tool_result. This is the
    seam for a product that words its own note about a repeated call: whether a call
    counts as a repeat is per-tool product policy (repeat_count), while whether
    the body came back byte-identical is a separate observed fact with no typed
    field, and both are needed to avoid asserting "identical output" for a body that
    differs. The pass-through is verbatim rather than filtered to unrecognised keys,
    so adopting a new reserved key here cannot silently remove something a product
    already reads.

  • agent_core.components.deliverables: deliverable reconciliation, extracted
    from ApodexHarness workflows/agent_team/publication.py so every consumer
    shares one definition of "delivered" instead of inferring it from stopped_by.
    A run is complete only if the loop ended on a stop reason the caller counts as
    completing AND the answer made good on every deliverable it claimed. Judging on
    stopped_by alone mis-reported in both directions: a publisher that emitted
    its write as a fenced bash block never ran the tool yet reported the deliverable
    as shipped, while a model that answered in plain text after writing its file was
    reported incomplete because it stopped on no_tool.

    Delivery is decided on content identity — a path holding non-empty bytes whose
    (relpath, size, sha256) is new relative to this round's baseline. /outputs
    persists across rounds, so the baseline is what separates "delivered this
    round" from "a previous round's file is still there"; touch and symlinks
    deliver nothing. Both host-specific concerns are injected rather than imported,
    which is what lets the module live in the portable library: host_root (where
    the sandbox's /outputs really is — core has no mount-resolver fallback,
    because a guessed root reconciles against the wrong tree) and ignore_matcher
    (the host's .outputsignore policy). Every filesystem entry point has an
    _async twin, since hashing a large tree on the event loop stalls every
    concurrent sub-agent while the caller holds the publication lock.

    Claim scanning treats paths as non-whitespace runs rather than an ASCII
    allowlist: an allowlist read /outputs/报告.pdf as no claim at all and turned
    /outputs/résumé.pdf into /outputs/r, so a non-ASCII deliverable was both
    waved through when missing and flagged as unverified when correctly delivered.
    Full-width punctuation terminates a match because Chinese prose leaves no space
    after it. Space-bearing paths are recognised only where the prose delimits them
    unambiguously (backtick span or markdown link target), and where the scan still
    truncates at a space a declared manifest entry outranks the guess.

    New surface only — no existing behaviour changes. See
    docs/deliverables-boundary.md.

Consumer action

  • This changes text the model reads and is therefore a compaction-decision
    change under docs/versioning.md. A consumer asserting equality against
    OMITTED_TOOL_RESULT_PLACEHOLDER must switch to startswith; the placeholder
    remains the first line precisely so that check keeps working.
  • No API change for the card. No configuration flag for it either: it has no
    failure mode of its own, and a switch would be one more configuration dimension
    to maintain.
  • A product that words a note from repeat_count, repeat_recovery_id,
    result_id or error_kind in its own loop copy must port that note into
    render_tool_result before adopting run_agent_loop
    . AgentCore reads none of
    those fields, so nothing fails if the note is forgotten — it just stops reaching
    the model. docs/agent-loop-boundary.md records why, and
    scripts/check_unconsumed_fields.py now fails CI on a new field in that state.

Documented

  • docs/deliverables-boundary.md states the deliverable-reconciliation split:
    what core decides (the three verdicts, baseline semantics, what it refuses to
    count) and what the host still owns (host_root, .outputsignore matching,
    which stop reasons complete a run, quarantine of anything refused).
  • docs/agent-loop-boundary.md now records that ToolResult.repeat_count,
    repeat_recovery_id, result_id and error_kind have no consumer inside
    AgentCore, that this is by construction, and that a product moving onto
    run_agent_loop therefore loses any note it words from them silently. The
    two ways to close it are stated, with host_metadata as the chosen route.
  • scripts/check_unconsumed_fields.py runs in CI: a field on a watched model with
    no attribute read anywhere in agent_core/ must be named in a boundary
    document. Deciding not to consume a field is a boundary decision, and an
    undocumented one is indistinguishable from an oversight — which is how four
    fields reached 0.4.0 with no consumer and no note.