Repository navigation
AgentCore 0.7.0
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 throughspill_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. -
KeepLastNToolResultsCompactornow resolves an existing recovery handle with
spill_refstaking precedence overresult_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
-
TieredCompactortakesmanifest_max_pathsandmanifest_max_chars, either
of which may beNoneto 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-Nonebounds must be large enough to retain at least one
entry; invalid zero or header-only bounds fail at construction time. -
ToolResult.host_metadatacarries whateverToolExecutionHooks.result_metadata
returned, verbatim, through toAgentLoopHooks.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 ApodexHarnessworkflows/agent_team/publication.pyso every consumer
shares one definition of "delivered" instead of inferring it fromstopped_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_byalone 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 onno_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";touchand 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/outputsreally is — core has no mount-resolver fallback,
because a guessed root reconciles against the wrong tree) andignore_matcher
(the host's.outputsignorepolicy). Every filesystem entry point has an
_asynctwin, 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/报告.pdfas no claim at all and turned
/outputs/résumé.pdfinto/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 underdocs/versioning.md. A consumer asserting equality against
OMITTED_TOOL_RESULT_PLACEHOLDERmust switch tostartswith; 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_idorerror_kindin its own loop copy must port that note into
render_tool_resultbefore adoptingrun_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.mdrecords why, and
scripts/check_unconsumed_fields.pynow fails CI on a new field in that state.
Documented
docs/deliverables-boundary.mdstates 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,.outputsignorematching,
which stop reasons complete a run, quarantine of anything refused).docs/agent-loop-boundary.mdnow records thatToolResult.repeat_count,
repeat_recovery_id,result_idanderror_kindhave no consumer inside
AgentCore, that this is by construction, and that a product moving onto
run_agent_looptherefore loses any note it words from them silently. The
two ways to close it are stated, withhost_metadataas the chosen route.scripts/check_unconsumed_fields.pyruns in CI: a field on a watched model with
no attribute read anywhere inagent_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.