JianFeeeee 263b651bf0 fix(agent): 并发批次漏记 ToolCalls —— 修上游 400 与 A2A 会话不可用
## 故障现象

生产 A2A 只回一句错误:

    An assistant message with 'tool_calls' must be followed by tool messages
    responding to each 'tool_call_id'. (insufficient tool messages following
    tool_calls message)

所有 provider 全部失败,那条会话彻底不可用。

## 排查方法:读内核自己写的日志

内核每次启动写一份 homed_<时间>.log。在其中看到:

    overflow check #1: ratio=1.21 events=41 (threshold=1.25)

上下文**没有**触发裁剪(1.21 < 1.25)——所以不是裁剪切断配对。

于是查 context.json 的实际数据,按 call_id 配对统计,发现 4 个事件
出现"结果多于调用":

    [17] calls=18 results=20
    [22] calls= 1 results= 3
    [28] calls=14 results=20
    [35] calls= 5 results= 7

## 根因

帧里有两条执行路径,各自记录:

    stepToolBegin → appendToolCallRecord(tc)   ← 记 ToolCalls + ToolsUsed
    stepToolExec  → append ToolResults          (串行路径,两条都记)
    stepToolBatch → append ToolResults          (并发路径,只记结果!)

并发路径(stepToolBatch)**从不调 appendToolCallRecord**。

于是落进 ContextEvent 的数据里 ToolResults 多于 ToolCalls。渲染时
每条 result 都生成一条 role="tool" 消息,而配对的 assistant(tool_calls)
里没有这个 call_id ⇒ 上游 400 ⇒ 所有 provider 失败。

同时还漏了 ToolsUsed(它与 ToolCalls 在 appendToolCallRecord 里同处维护):
并发批次的工具从未被记进"用过的工具",记忆/统计/页化都在少算。

## 为什么不是本次内核升级引入

对比日志:ba8a33d(升级前)今天也有 3 次同样错误。它是既有缺陷,
只是并发批次越多越容易触发。本次排查顺带定位并修掉。

## 修法

在 join 之后的**顺序收尾段**补一行 appendToolCallRecord:

    f.appendToolCallRecord(f.CurTool)
    f.ToolResults = append(...)

为什么放这里而不是 fan-out 里:函数注释已明确 f.Msgs / f.ToolResults
在 goroutine 期间一律不碰。这里是按索引串行的收尾段,与单步路径语义一致。

## 判据 TestBatchContextEventPairsCallsWithResults

三条:调用记录齐全、每条结果有配对、每个调用有结果(双向)。

★ 已有 TestBatchToolCallIDsAllPaired 没抓到,因为它只看 f.Msgs ——
而 Msgs 里的 assistant 消息由 ensureBatchAssistant 单独写入(含全部
PendingTools),不经过 f.ToolCalls。所以 Msgs 侧配平、落盘侧不配平。
判据必须落在**要落盘的那份数据**上。

## 变异验证(两次才对)

第一次:删掉修复行 ⇒ 判据**没红**。原因:测试里的工具没声明
ParallelSafe,batchRunnable 判 false ⇒ 走串行路径(stepToolExec),
根本没测到目标代码。

第二次:给工具加 ParallelSafe + 断言 ConcurrencySafeOf 为真
(确保真的走并发),再删修复行 ⇒

    ★ f.ToolCalls 只有 0 个(map[]),期望 2 个

这才是有效验证。教训:**判据必须断言自己走到了目标路径**,
否则"删掉实现仍绿"会被误读成"实现不必要"。

## 验证

  go build ./...              PASS
  go vet ./...                PASS
  gofmt -l                    干净
  go test ./... -count=1      63 包全过,0 FAIL
  干净检出(git worktree)     build + TestBatch* 全绿
2026-11-04 14:02:58 +08:00

⚠️ AI-Assisted Programming Notice: Parts of this project's code, documentation, and commit history were generated or modified with AI assistance. Key changes have been human-reviewed, but please evaluate and verify before use.

HologramAgent

中文: README.md

Self-hosted agent kernel. Runs on your own hardware; plugins attach as separate processes.

make build build-cli
./build/homed -data /tmp/ha

API keys go in the web UI at http://localhost:8080. Requires Linux.

The main difference from typical agent frameworks is context handling: instead of truncating on overflow or asking the model to summarize, it scores history by relevance and sinks the low-scoring events into cold context, then distills those into a graph. Two other things differ: startup order is resolved at build time, and plugins launch as soon as their facilities are ready.

Architecture

                    homed (kernel)
        LLM orchestration · memory · retrieval
                       │
        ┌──────────────┼──────────────┐
        │              │              │
   stdio RPC       shared mem     event ring
     control       bulk data      notify
        │              │              │
        └──────────────┼──────────────┘
                       │
              plugins (separate processes)
   web UI · QQ · CLI · files · browser

The kernel process handles orchestration, memory and retrieval; HTTP, files and devices belong to plugins (internal/plugin). A crashed plugin is restarted with backoff 1s / 2s / 3s; the 4th crash within 5 minutes stops automatic restarting pending human intervention (cmd/homed/kmod_runtime.go).

Context window

internal/agent/core/context.go

Each event gets two vectors on entry: a dense vector sharing one space with media and cold context, and a TF-IDF word vector as fallback. Pruning scores history against the current input and keeps topK, with the most recent N events exempt from scoring.

N defaults to 10, bounded by budget: min(configured, ContextTokens / avg tokens per event).

That bound came from a measurement. At a 50k window with 5800 tokens per event and a 40000 budget, pinning 10 events needed 58k, more than the budget itself. Every prune met its target without freeing space; the budget was gone after two rounds and the task terminated, with recall falling from 44% to 22%. Small windows now tighten automatically; a 1M window uses the configured value.

Pruned events move to the cold-context layer together with the media blocks they hold. Block IDs stay the same; only the holding layer changes. Retrieval uses coldcontext_query: Peek to confirm, then Remove the source copy. There is no fetch-and-delete path, since deletion follows confirmation.

Three layers

internal/agent/core/context.go · internal/memory/coldcontext/ · internal/memory/graph.go

Layer Holds Reaches the model Cooling
L1 Context events + blocks directly, in the system prompt low relevance sinks
L2 Cold Context events + blocks (same shape as L1) inserted back into L1 order on recall ≤2 accesses and 72h idle, then distilled
L3 Stable triples, nodes are blocks on recall never leaves

A block has exactly one holder at any layer, with no reference counting. Images and audio are blocks in L2/L3 with their own vectors, searched in the same space as text.

Scenes

internal/memory/scene_emerge.go

A scene is built from five weighted features: channel 1.0, conversation partner 1.0, tools 0.8, topic 0.4, time of day 0.2. A scene is created on the second occurrence of the same fingerprint (minSceneEvidence = 2); one-off interactions don't create one. Join threshold 0.5, recall 0.35. Recurrence adds 1 to strength; scenes that stop recurring decay by half-life.

Two entry points run in parallel: a plugin declares the scene at an injection point (e.g. chan:qq/peer:group_1), or fingerprints cluster on their own. A declared scene derives features only from its own key and does not absorb the full fingerprint. Absorbing it would make the declared scene match nearly every later turn of that kind at close to 1.0 similarity, and the clustering path could never grow narrower scenes.

Seven stages

internal/agent/core/stages.go

Stage When Plugins can
on_input message arrives blocklist, rate limit, short-circuit reply
pre_action context ready, before LLM inject external data
post_action LLM returns text and tool calls filter, force redirect
before_toolcall before one tool runs audit, deny, rewrite args
after_toolcall after one tool runs redact, sort results
before_output final text ready format adaptation
after_output after sending statistics

The inner loop is post_action → [before_toolcall → run → after_toolcall] → post_action. It exits when the model stops calling tools, all calls are denied, or the limit is reached.

Everything else

  • Channels: web UI, CLI, QQ groups and DMs, A2A, ACP in your IDE
  • Tools: files, shell, code, browser (real Chrome), calendar, notes, RSS, music
  • Modalities: images, video, voice, screenshots, web pages

28 built-in plugins, 89 model-callable tools. External plugins live one per repo under github.com/HologramAgent.

Downloads

Releases ship three variants:

Variant Contents For
full homed + waiter + desktop GUI + systemd unit Single-machine, everything
server homed + waiter + systemd unit Servers (no desktop environment)
client waiter + desktop GUI Connecting to a remote HologramAgent
  • Linux: .deb (amd64/arm64), .rpm (x86_64), .tar.gz
  • Windows: HologramAgent_v1.2.0_{Full,Server,Client}_win64.exe (NSIS installer, includes the AGPL license page). Since v1.2.0 homed no longer supports native Windows (it relies on fd inheritance and in-segment offset dereferencing), so the installer bootstraps WSL2 and installs the Linux packages inside the distribution the same way a Linux host would.
  • Portable: homeagent-bin-<os>_<arch>.tar.gz (homed/waiter/initconfig)
  • Verification: SHA256SUMS

The macOS homed requires a native macOS build (CGO + sqlite3), so release packages ship only waiter/initconfig.

Build

make build build-cli    # Build daemon + CLI
make test               # go test ./...
make install            # Install to system

Dependencies: Go 1.25+, CGo (go-sqlite3), Linux.

homed requires Linux (it relies on fd inheritance and intra-segment offset dereferencing of the shared memory region; see cmd/homed/platform_windows.go). On Windows only waiter.exe is built and homed runs under WSL2 (see Downloads). macOS can build waiter/initconfig; homed must be built on native macOS.

License

AGPL-3.0-only. See LICENSE.

Bundled third-party components keep their own licenses; full texts ship in /usr/share/doc/hologramagent/licenses/.

Third-party components shipped with the packages

Component License Location
Chinese-CLIP ViT-B/16 (ONNX artifacts) Apache-2.0 /usr/lib/hologram/models/chinese-clip-vit-b16-onnx/
ONNX Runtime (libonnxruntime.so) MIT /usr/lib/hologram/onnxruntime/
jieba dictionary (embedded in the binary) MIT internal/memory/jiebadict/
Go dependencies (go-sqlite3, gojieba, bubbletea, …) MIT / BSD-3 / Apache-2.0 permissive, AGPL-3.0-compatible

The package metadata declares this package as AGPL-3.0-only.

Description
No description provided
Readme AGPL-3.0 190 MiB
Languages
Go 80.7%
JavaScript 9.1%
Python 2.9%
HTML 1.8%
CSS 1.4%
Other 4%