Two related AgentCore tactical fixes from the 2026-05-12 audit. #24 — Assistant text blocks alongside tool_use were dropped from message history. Opus 4.7 commonly narrates ("Let me check the time for you...") before calling a tool. On the next round-trip the appended assistant message contained only the .toolUse block, so the model had no record of the narration — a user follow-up like "what did you say before checking the time?" would deny it. Fix: introduce `assistantTextSinceLastFlush` parallel to `assistantTextSoFar` (the latter stays cumulative for D-02 vision routing). On every toolUseRequested, flush the per-flush buffer into a .text(...) block bundled into the same assistant message as the .toolUse, trim whitespace-only text to avoid Anthropic 400s on empty text blocks, and reset the flush buffer so subsequent tool_uses in the same round get only the text emitted between them. D-02 untouched. #23 — CacheHints documentation flagged that the eligibility threshold (4096 chars ≈ 1024 tokens) is structurally false on healthy boot (preamble ~700 chars, empty initial context). Added a paragraph explicitly documenting the dormancy, the intentional cost-acceptance rationale, and the natural recovery once M-4 (v0.8) lands searchFacts-driven hydration. No code change. Tests: 3 new tests in AssistantTextWithToolUseTests cover text+toolUse bundling, toolUse-alone (no spurious empty text block), and whitespace-only-text rejection. Full AgentCore suite: 248 tests, 0 failures, 1 skipped. Closes #23, #24
AgentCore
The agent loop, LLM provider protocol, and concrete providers. Four library products: shared types (AgentCore), the orchestrator (AgentOrchestrator), and two providers (AnthropicProvider, OllamaProvider).
Key public types
| Type | Purpose |
|---|---|
LLMProvider (protocol) |
Provider-agnostic streaming contract — returns AsyncThrowingStream<LLMEvent, Error> |
LLMEvent |
Stream event union — tokenDelta, thinkingDelta, toolUseRequested, usage, messageStop |
LLMMessage |
Provider-neutral chat message (role + content blocks, including ImageBlock) |
ToolSchema / ToolChoice |
Tool advertising + tool-choice discipline (auto, none, required) |
ImageBlock |
Multimodal extension (Plan 07-05 / D-18); base64 + media type |
ModelID |
Strong-typed model identifier (claude-opus-4-7, qwen2.5-coder:32b) |
TurnID / TurnNonce |
Turn identity (TurnID on bus + replay; TurnNonce for SEC-06, never on bus) |
CacheHints |
eligibleForSystemPrompt (≥1024 tokens), extended-cache-ttl beta header guard |
UntrustedWrapper |
Prompt-injection isolation around tool results |
ToolResultPacker |
8 KB cap on model-facing tool result content (Opus 4.7 footgun) |
BoundedAsyncChannel |
dropOldest-policy bounded channel for orch → replay events |
AnthropicProvider |
Streaming SSE Opus 4.7 via Anthropic Messages API |
OllamaProvider |
Native NDJSON /api/chat + OpenAI-compat /v1/chat/completions |
AgentOrchestrator |
Actor — turn-lifecycle owner, tool dispatch, cap-recovery, retry |
OrchestratorEvent |
One-consumer event stream — state changes, tokens, tool starts/ends |
Depends on
Keychain, Replay, Vision, Logging, Config. External: swift-log.
Used by
App/AppDelegate, App/Voice/VoiceOrchestratorAdapter.swift, App/MCPBusGatewayAdapter.swift, packages/Memory, packages/Vision, packages/Harness, packages/DevOverlay, packages/MCP.
Key invariants / contracts
LLMProvidercancellation throughTask.isCancelled— provider task closes the underlying transport on consumer cancel.- Tool-choice mandatory. Cap-recovery turns pass
.none(Anthropic{"type":"none"}; Ollama dropstoolsarray entirely). turnNoncenever on the bus or in anyOrchestratorEvent(SEC-06).- Tool result content capped at 8 KB before model. Full bytes still go to replay.
extended-cache-ttlrequires the beta header ANDttl: "1h"oncache_control. Without the header, 1h is silently ignored.- Cache hints gated on prompts ≥1024 tokens (
CacheHints.eligibleForSystemPrompt). - Anthropic SSE: close on
message_stop, notmessage_delta. Swallowping. Routethinking_deltato its own case. Treatstop_reason: "refusal"first-class. Emitpartial_tool_use_at_disconnecton mid-delta termination. - Ollama NDJSON:
tool_callsarrive on the chunk precedingdone: true— decoder readstool_callswhenever seen, never gates ondone. - One-consumer invariant on
OrchestratorEventstream —scripts/check-orchestrator-events-single-consumer.sh.
Tests
19 swift-testing + 72 XCTest. Includes SSE fixture corpus (packages/Harness/Sources/Harness/Corpus/SSEFixtureCorpus.swift), NDJSON fixture corpus, tool-cap-recovery scenarios, retry-budget tests. Live-network tests gated by JARVIS_REAL_MODELS=1.
Notable files
Sources/AgentCore/LLMProvider.swift— the core protocol + multimodal overloadSources/AgentOrchestrator/AgentOrchestrator.swift— the turn-lifecycle actorSources/AnthropicProvider/SSEDecoder.swift— SSE parser with all the footgun casesSources/OllamaProvider/NDJSONDecoder.swift—/api/chatdecoder; tool_calls-before-doneSources/AgentCore/CacheHints.swift— Track A streamTruncated fix