Skip to content

v1.4.0 — Agent Core: sub-agents, compaction, budget stops, loop hooks

Choose a tag to compare

@U-C4N U-C4N released this 04 Jul 12:38
· 80 commits to main since this release

Minor Changes

  • Loop hooks — prepareStep + activeTools (on CommonCallOptions; work in both generateText and streamChat whenever tools is present): prepareStep(ctx) runs before every model step, after automatic compaction, and may return { messages, activeTools, toolChoice, model }messages becomes the base history for this and all following steps (doubling as a user-controlled compaction/rewrite hook, including system-prompt edits via the system-role message), while activeTools/toolChoice/model apply to that step only. A thrown prepareStep fails the call like any caller code — it is never swallowed. Static activeTools restricts which tools are sent every step (unknown names warn and are dropped; matching nothing fails open to the full list); prepareStep's activeTools overrides it. prepareStep is a plain call option on the free functions (no agent class to instantiate) and composes with the automatic compaction below — compaction runs first, prepareStep sees the result.
  • Budget stop conditions — totalTokensExceed / costExceeds (StopCondition factories, exported from root + /edge alongside stepCountIs / hasToolCall, now also exported): stop the loop once cumulative REAL usage or cost — all steps and sub-agents included — crosses a bound, OR-ed into stopWhen like any condition and evaluated at the step boundary (never mid-tool-batch). A budget stop never changes finishReason (the locked union is untouched); instead it sets providerMetadata.deuz.stoppedBy: 'totalTokensExceed' | 'costExceeds' on the result / finish part (GenerateTextResult.providerMetadata is a new additive field). costExceeds needs deps.priceProvider — without one it warns once and never fires. Token- and cost-budget bounds are first-class StopCondition factories here (alongside stepCountIs/hasToolCall), and stoppedBy reports which bound ended the loop.
  • Automatic layered compaction (compaction?: 'auto' | CompactionPolicy on CommonCallOptions; opt-in, off by default, active only inside the agentic loop): three cheapest-first layers — prune old tool results into [pruned N chars] stubs, prune old reasoning parts, summarize the oldest unprotected slice into one message — run before a step once estimated context fill crosses a threshold (default 92%), always leaving every system message, the first user message, the last message, and the last keepRecentSteps assistant turns untouched. History stays immutable and prefix-stable for prompt-cache hits; a failed summarize logs a warning and skips the layer instead of ending the call; token counts are a calibrated heuristic, not a real tokenizer. Streaming emits a new compaction StreamPart/UI part per layer that ran; buffered calls log it. Anthropic's native providerOptions.anthropic.context_management still works verbatim alongside this. This is automatic, layered, cache-aware context management from one opt-in flag — the alternative is to estimate tokens and prune inside a per-step hook yourself.
  • Sub-agents — agentTool (exported from root + /edge; AgentToolDef exported type): wraps a { model, tools, system, maxSteps, maxDepth, ... } definition into a Tool that runs a nested agentic loop and returns its final text — no new runtime. When the parent streams, the sub-agent's entire canonical stream forwards live as agentPath-tagged sub-agent parts, rather than surfacing only the final text. The parent's server-mode approveToolCall is inherited to every nesting depth as first-class behavior, so a sub-agent's own tool calls stay gated with no extra wiring. Usage folds into the parent total and is tagged with meta.agentPath; maxDepth (default 2) guards against runaway nesting; the parent signal propagates down. Client-mode approval inside a sub-agent isn't supported yet (needs durable suspend/resume — lands in 1.5); a gated sub-agent call with no inherited approver returns a clear is_error instead.