ExAthena.Conclusions (ExAthena v0.17.0)

Copy Markdown View Source

Per-iteration conclusion extraction.

Every loop iteration should leave a one-line record of what was concluded — the host Overview renders these as a progress timeline, and the rolling ledger is recited back to the model (Manus-style) to keep long runs on track. The lineage is ReAct's Thought: prefixes → Magentic-One's progress ledger → Manus's todo recitation, scaled down to ZERO extra LLM calls (mandatory on 1-3-slot local GPUs).

The contract is lenient by design (small local models drift on strict formats), with a three-stage fallback chain:

  1. :stated — the model ended its turn with CONCLUSION: <text> (the prompt contract; last marker wins).
  2. :tail — no marker: the final paragraph of the turn's text, truncated. For terminal answers the final paragraph ≈ the conclusion.
  3. :derived — no usable text: synthesized from the turn's tool calls ("ran read, grep").

A turn with neither text nor tool calls yields :none — nothing happened worth recording (e.g. an empty provider response).

Summary

Functions

Extract the conclusion for one turn from its assistant text, its thinking (reasoning channel), and the names of the tool calls it made.

Whether text reads as a negative finding ("X does not exist", "no blog directory", "couldn't find …"). Used by the research rail to detect when local investigation has come up empty. Returns false for nil/blank.

Types

source()

@type source() :: :stated | :tail | :thinking | :derived

t()

@type t() :: %{text: String.t(), source: source()}

Functions

from_turn(text, thinking \\ nil, tool_names)

@spec from_turn(String.t() | nil, String.t() | nil, [String.t()]) ::
  {:ok, t()} | :none

Extract the conclusion for one turn from its assistant text, its thinking (reasoning channel), and the names of the tool calls it made.

Thinking models (Qwen3.5 et al.) put everything — including the CONCLUSION line the contract asks for — inside the reasoning channel, leaving the text channel of tool-call turns blank. Without thinking as a source, every conclusion degraded to the "ran bash" fallback.

negative_finding?(text)

@spec negative_finding?(String.t() | nil) :: boolean()

Whether text reads as a negative finding ("X does not exist", "no blog directory", "couldn't find …"). Used by the research rail to detect when local investigation has come up empty. Returns false for nil/blank.