Documentation
¶
Overview ¶
Package summarizer is the home of Harbor's production LLM-backed summarisation: the memory-subsystem `memory.Summarizer` and the planner-trajectory `planner.Summariser`.
Two interfaces — do not conflate ¶
The package satisfies TWO unrelated interfaces that happen to share a name root:
- `memory.Summarizer` (Summarizer, `New`) — the memory subsystem's rolling-summary compactor: conversation window (previous summary + evicted turns) in, free-text summary out.
- `planner.Summariser` (TrajectorySummariser, `NewTrajectorySummariser`) — the runtime's trajectory compression producer: a run's Trajectory in, the five-field `planner.TrajectorySummary` out (structured-output JSON-schema mode).; invoked by `planner.CompressionRunner.MaybeCompress` from the steering RunLoop when `Budget.TokenBudget` is exceeded.
Different signatures, different inputs, different consumers — the shared home exists because both are "LLM client + versioned compaction prompt" compositions (the import direction is clean: summarizer → planner/memory → llm; nothing imports back).
The remainder of this doc describes the memory-subsystem Summarizer; see trajectory.go for the TrajectorySummariser.
Before the only `memory.Summarizer` Harbor shipped was `strategy.EchoSummarizer` — a deterministic test stub that concatenated turns into a single string. The runtime's `rolling_summary` strategy required an *injected* Summarizer with no default, so a `harbor dev` boot against `memory.strategy: rolling_summary` would have crashed with `nil Summarizer` (or worse, silently fallen back to a test stub if a future PR wired one).
Harbor closes that seam: `summarizer.New(client)` composes an `llm.LLMClient` with a versioned compaction prompt and returns a `memory.Summarizer` ready to wire into `memory.Open(..., Deps{Summarizer: ...})`. The dev server wires this when an operator configures `memory.strategy: rolling_summary` (the same conditional the spec laid down).
The compaction prompt ¶
The prompt is versioned (`PromptVersion`) so a later prompt-craft improvement bumps the constant and the prior version stays available for golden-test pinning. The template uses an instruction-tuned shape that works against every native provider:
- One system message anchors the summariser persona: "You are a concise meeting-minutes summariser. Compress the conversation turns below into a short, factual rolling summary..."
- One user message stitches the previous summary (if any) + the evicted turns into a single payload. The Summarize implementation uses the request's `PreviousSummary` + `Turns` directly — no reformatting beyond a stable line-prefix scheme so the LLM sees consistent input shape across calls.
Concurrent reuse ¶
`Summarizer` is a compiled artifact: the embedded `llm.LLMClient` and the compaction prompt template are set once at construction (`New`). One Summarizer is safe to share across N concurrent `Summarize` goroutines; the per-call state lives on the function stack and in `ctx`. Concurrent_test.go pins N≥100 under -race.
Identity (CLAUDE.md §6 rule 9) ¶
`Summarize` is identity-mandatory: the request's `identity.Quadruple` is propagated into ctx via `identity.With` before the LLM call so the safety pass sees the same identity the memory subsystem's caller supplied. The LLM-edge audit + governance + retry layers all key on this identity; without it, the safety pass rejects with `ErrIdentityRequired`.
Fail-loud (CLAUDE.md §5) ¶
An LLM call that fails surfaces the wrapped error verbatim — the summariser does NOT silently degrade to an echo or a truncation. Callers handle `Summarize` errors via the `memory.strategy.rolling_summary` health FSM: the strategy transitions from healthy → retry → degraded as failures accumulate.
§13 primitive-with-consumer ¶
the `harbor dev` subcommand is the §13 first consumer of this Summarizer. The dev wiring constructs `summarizer.New(client)` and passes it through `memory.Deps.Summarizer` when the operator configured `memory.strategy: rolling_summary`. Without the summariser would be a dormant primitive; with it is wired end-to-end on the production code path.
trajectory.go — the production LLM-backed planner.Summariser. Distinct from the memory-subsystem Summarizer in summarizer.go: see the package godoc's two-interface disambiguation.
Index ¶
Constants ¶
const PromptVersion = "v1"
PromptVersion is the stable identifier for the compaction prompt template. Tests pin this; a later prompt-craft improvement bumps the constant. Format: `vN` where N is a monotonically-increasing integer.
const TrajectoryPromptVersion = "v1"
TrajectoryPromptVersion is the stable identifier for the trajectory compaction prompt template. Tests pin this; a later prompt-craft improvement bumps the constant. Format: `vN`, monotonically increasing — independent of the memory summarizer's PromptVersion.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Option ¶
type Option func(*Summarizer)
Option configures a Summarizer at construction time.
func WithModel ¶
WithModel pins the model name the Summarizer requests against the LLMClient. Empty model falls back to the client's resolved `ConfigSnapshot.Model` at request time (the snapshot's default model). Pinning at construction is useful for operators who want a cheap model for compaction independent of the planner's model choice.
type Summarizer ¶
type Summarizer struct {
// contains filtered or unexported fields
}
Summarizer is the production LLM-backed `memory.Summarizer` Harbor ships. Construct via `New`; do not construct directly.
`Summarizer` is a compiled artifact: every field is set once at construction and never mutated. One Summarizer is safe to share across N concurrent Summarize goroutines.
func New ¶
func New(client llm.LLMClient, opts ...Option) (*Summarizer, error)
New constructs a production Summarizer. The client is mandatory; a nil client returns an error rather than building a Summarizer that would nil-panic on the first Summarize call.
The returned Summarizer satisfies `memory.Summarizer`. Wire it via `memory.Open(..., Deps{Summarizer: s})` when configuring the `rolling_summary` strategy.
func (*Summarizer) Summarize ¶
func (s *Summarizer) Summarize(ctx context.Context, id identity.Quadruple, req memory.SummarizeRequest) (memory.SummarizeResponse, error)
Summarize implements `memory.Summarizer`. It composes a chat request from the previous summary + the new turns, sends it through the LLM client (which runs through the safety pass + corrections + downgrade + retry + governance chain), and returns the assistant's reply verbatim as the new rolling summary.
Identity is mandatory: `id` is injected into ctx via `identity.With` so the LLM-edge safety pass sees the same identity the memory subsystem's caller supplied. A nil-Quadruple is the caller's responsibility to reject (callers MUST not pass empty identities — the memory subsystem itself enforces).
type TrajectoryOption ¶ added in v1.3.0
type TrajectoryOption func(*TrajectorySummariser)
TrajectoryOption configures a TrajectorySummariser at construction.
func WithTrajectoryHeavyOutputThreshold ¶ added in v1.3.0
func WithTrajectoryHeavyOutputThreshold(threshold int) TrajectoryOption
WithTrajectoryHeavyOutputThreshold threads the operator's resolved heavy-output threshold (`artifacts.heavy_output_threshold_bytes`) so the aggregate compaction-payload budget tracks the SAME limit the LLM-edge safety pass enforces — a summariser built against a non-default threshold must never compose a payload its own Complete call would reject with ErrContextLeak. The budget becomes threshold − trajectoryPayloadHeadroom, floored at one fragment cap. Non-positive is a no-op (the default applies).
func WithTrajectoryMaxSummaryTokens ¶ added in v1.3.0
func WithTrajectoryMaxSummaryTokens(n int) TrajectoryOption
WithTrajectoryMaxSummaryTokens caps the completion's MaxTokens — a guard against a runaway summary. Non-positive is a no-op (the provider default applies).
func WithTrajectoryModel ¶ added in v1.3.0
func WithTrajectoryModel(model string) TrajectoryOption
WithTrajectoryModel pins the model the summariser requests against the LLMClient — operators can route compaction to a cheaper (or stronger) model than the planner's. Empty falls back to the client's resolved default model at request time.
func WithTrajectorySystemPrompt ¶ added in v1.3.0
func WithTrajectorySystemPrompt(prompt string) TrajectoryOption
WithTrajectorySystemPrompt overrides the versioned compaction system prompt. The override is rendered verbatim; the caller owns keeping it aligned with the five-field output schema. Empty is a no-op (the versioned default stays).
type TrajectorySummariser ¶ added in v1.3.0
type TrajectorySummariser struct {
// contains filtered or unexported fields
}
TrajectorySummariser is the production planner.Summariser Harbor ships — the producer half of trajectory compression (RFC §6.2, by design). It composes a compaction prompt over the trajectory's planner-facing projection, calls llm.LLMClient.Complete in structured-output JSON-schema mode, and parses the response into the five-field planner.TrajectorySummary.
Construct via NewTrajectorySummariser; do not construct directly.
TrajectorySummariser is a compiled artifact: every field is set once at construction and never mutated. One instance is safe to share across N concurrent Summarise goroutines; per-call state lives on the function stack and in ctx. trajectory_test.go pins N≥100 under -race.
Fail-loud contract (the planner.Summariser seam's): an LLM error propagates wrapped, never swallowed; a structurally valid but vacuous summary surfaces planner.ErrEmptySummary; a response that does not parse as the five-field object is a loud parse error. There is no silent fall-through to raw history.
func NewTrajectorySummariser ¶ added in v1.3.0
func NewTrajectorySummariser(client llm.LLMClient, opts ...TrajectoryOption) (*TrajectorySummariser, error)
NewTrajectorySummariser constructs the production trajectory summariser. The client is mandatory; a nil client returns an error rather than building a summariser that would nil-panic on the first Summarise call.
The returned summariser satisfies planner.Summariser. Wire it via planner.NewCompressionRunner(s) and set the runner on steering.RunSpec.Compression (plus Base.Budget.TokenBudget > 0) — or let the assembly do it from the `planner.token_budget` config knob.
func (*TrajectorySummariser) Summarise ¶ added in v1.3.0
func (s *TrajectorySummariser) Summarise(ctx context.Context, rc planner.RunContext, tr *planner.Trajectory) (*planner.TrajectorySummary, error)
Summarise implements planner.Summariser. It renders the trajectory's planner-facing projection (query, goal, per-step action + LLM-visible observation), sends it through the LLM client (which runs the safety pass + corrections + structured-output downgrade + retry + governance chain), and parses the assistant's JSON reply into the five-field planner.TrajectorySummary.
The payload deliberately renders Step.LLMObservation (the heavy-content-disciplined projection — what the planner itself saw) over the raw Step.Observation: raw observations may carry heavy content that must never reach the LLM edge (CLAUDE.md §13 / ErrContextLeak). When a step predates the projection split, the raw observation is used, truncated at trajectoryFragmentCap.
Identity is mandatory: rc.Quadruple's identity is propagated into ctx so the LLM-edge layers (safety, governance, audit) see the same identity the compression runner validated.