Chapter 8 — The Critic Agent
# Chapter 8 — The Critic Agent
Last chapter the contract promised an agent. This chapter you write it —
and you write it the way the framework’s authors write paradigms: against
the runtime surface, with the governance bar stated in the type system,
the phases structured as prompts, and every claim pinned by a test driven
by the fake model you built in Chapter 2. When this chapter is done,
cognitionzoo/critic contains a complete paradigm: contract, agent, and
prompt file. What it will not yet have is a runtime that hands it work —
that is the Chapter 9 wiring, and knowing exactly what is missing is part
of this chapter’s lesson.
# Where you are
Twenty-six tests green in the package; the contract registered; the
ensemble runnable. This chapter adds the package’s third production file
(critic.go), its first workspace artifact (a prompt file under
relurpify_cfg/), and six tests. Nothing else in the repository changes.
#
paradigm.Deps, the curated subset, field by field
Deps (cognitionzoo/paradigm/deps.go) carries fifteen fields; the
composition root adapts workspace state into this one value, and every
paradigm receives it whole. Here is the curated subset this book teaches
field by field (D12) — the fields a paradigm author actually reasons
about — with the full fifteen carded in Appendix C:
| Field | Type | What it hands you |
|---|---|---|
Model |
model.LanguageModel |
The workspace’s model, behind the Chapter 2 contract. Your critique phase is a Chat on this. |
Registry |
*registry.CapabilityRegistry |
The governed capability registry — the only door to tools. The Chapter 4 governed invoker forwards here. |
PermissionChecker |
permissions.CapabilityChecker |
The capability gate shared by governed paradigms. One method: CheckCapability(ctx, agentID, capability) error. The critic refuses to run without one. |
CommandRunner, CommandPolicy |
sandbox.CommandRunner, sandbox.CommandPolicy |
Sandboxed command execution under its policy — how a paradigm runs anything, never bare. |
WorkingMemory |
*memory.WorkingMemoryStore |
Per-task working memory beyond the envelope’s own store. |
IndexManager, SearchEngine |
*ast.IndexManager, *search.SearchEngine |
The AST index and search — the retrieval seams RetrievalNode and your own delegate’s tools sit on. |
StreamTrigger |
*contextstream.Trigger |
The compiler trigger StreamTriggerNode resolves from context. |
Grounder |
*knowledge.GroundingService |
The capture-as-bridge write path the epoch barrier flushes. |
PromptRegistry |
prompt.Registry |
The workspace prompt corpus — the registry your prompt file loads into at boot. |
Config |
*execution.Config |
Run configuration: model ID, iteration caps, the telemetry sink. |
The four fields this table omits — EpochDrain, AgentLifecycle,
Telemetry, and the rest of Config’s surface — are runtime-plumbing
concerns you receive rather than reason about at this layer; Appendix C
cards them. Curated is not censored: it is the difference between
learning a surface and memorizing one.
Read one field comment again, because it is the governance bar in the
authors’ own words (deps.go:29-31):
PermissionChecker is the capability gate shared by governed paradigms (ReWOO refuses to execute without one).
The critic joins that sentence. Its Execute begins:
if a.permissionChecker == nil {
return nil, ErrNoPermissionChecker
}Fail closed — an error, never a silent downgrade. The critique phase itself is model-only (data, not action, per D14), but the critic drives a delegate, and the delegate is the governed action surface. A paradigm that cannot answer “who says this agent may act” does not act.
# The agent, mirroring reflection
CriticAgent (cognitionzoo/critic/critic.go) follows reflection’s
shape closely enough that the two files read as siblings — deliberately,
because Chapter 7 declared reflection the template:
-
Constructor:
New(deps *paradigm.Deps, delegate graph.WorkflowExecutor, opts …Option)— and when the delegate is nil, it defaults toreactpkg.New(deps). TheComposes: ["react"]declaration from the contract, as a line of code. -
Options lower authored directives:
WithDraftCriteria,WithCritiqueCriteria(thecritiquedirective),WithReviseCycle(therevise when:block). The zero value preserves the library critique report — FR-9’s restraint, in the critic’s own grammar. -
Execute branches on
directiveMode(). The library mode builds a three-node graph — delegate run → critique report → done — where the critique node carries a typed contract (SideEffectNone,replay-safe). The directive mode runs the loop:draft (delegate, state-isolated: clone → execute → merge) → critique (one Chat, verdict JSON → scratch.critic.*) → (predicate → revise body → critique)ⁿ, n ≤ MaxRevisionCycles -
The separation is structural. The draft phase runs through the delegate with state isolation (
Clone→Execute→Merge); the critique phase’s model call carries the task, the authored criterion, and a compact rendering of the result — never the drafter’s reasoning.MaxRevisionCyclesis a contract constant (2), and hitting the cap ends the loop normally with the last verdict standing — a cap is a bound, not a malfunction — after emittingcritic.revision_capped. -
Telemetry follows the shipped vocabulary pattern:
critic.iteration,critic.reviewed,critic.revision_capped,critic.completed, as package-local constants. Upstreaming named constants into the telemetry package’s vocabulary file is part of the Chapter 13 PR checklist — the event strings follow the house convention today, and the vocabulary owner will adopt the names when the contribution lands. -
Verdict vocabulary is
{pass, fail, issues}, strictly decoded from JSON (with react’sExtractJSONused to find it in the response) — anything else isErrInvalidCritique. Strict decode is decision repair applied at the paradigm layer: malformed output fails loudly and the profile’s repair strategy gets a chance, instead of a garbage verdict silently routing the loop.
# The prompt file
Book 1 taught you that prompts are workspace files with frontmatter. The critic ships one, at the convention’s path:
relurpify_cfg/prompts/agents/agent.critic.default.prompt---
schema framework.prompt/v2
id agent.critic.default
tag "system"
tag "agent"
tag "critic"
---
You are an independent critique agent. You did not write the work under
review, and you never edit it: you judge it.
…The naming convention is agent.<agent-name>.default — react, chainer,
and pipeline ship siblings (and ReWOO breaks the pattern with
rewoo.plan.v1, a reminder that conventions have documented exceptions).
The file’s body says the two things that make a critic a critic: you did
not write this work, and you never edit it. The agent’s library prompt
carries the same stance in code; the file is the operator-facing override
point, loaded into the PromptRegistry at boot. The lab proves the file
is honest: it loads through the prompt registry from a fstest.MapFS —
the loader’s own test pattern — and validates clean.
# Hands-on
The overlay adds critic.go, the prompt file, and
critic_agent_test.go — six tests, driven by the Chapter 2 fake model
standing in for the critique model and a recording delegate standing in
for react (a scripted delegate is a test double for another agent, not
a bypass: react’s own governed paths are react’s tests to pin).
./verify.sh build ch08== tier 1 (build): ch08 ==
ok codeburg.org/lexbit/relurpify/cognitionzoo/critic 0.061s
[PASS] build: ch08 compiles clean and its tests passThe six, in the order they matter:
TestCriticFailsClosedWithoutPermissionChecker— deps without the checker:ErrNoPermissionChecker, asserted witherrors.Is. The chapter’s first test is the governance bar.TestCritiqueWritesScratch— delegate ran once, critique ran once, the verdict landed inscratch.critic.verdict/scratch.critic.issues, the result fields carry it, and — via the fake model’sLastInput— the prompt carried the authored criteria.TestReviseFiresOnPredicateAndRecritiques— body runs exactly once, re-critique returnspass, two critique calls total.TestRevisionCapEndsLoopNormally— always-issues script: body runs exactlyMaxRevisionCyclestimes,critic.revision_cappedis in the recorded telemetry, and the run ends successfully.TestLibraryModeReportsWithoutRevision— zero value runs the graph: draft → critique → done.TestCriticPromptFileLoadsAndValidates— the workspace artifact loads and validates through the registry.
$ go test ./cognitionzoo/critic/... -count=1
ok codeburg.org/lexbit/relurpify/cognitionzoo/critic 0.061sThirty-two tests. One debugging story from this chapter’s build is worth
retelling, because you will hit it: the telemetry helper initially took
*recordingSink and stored it in Config.Telemetry — a non-nil
interface holding a nil pointer, so the nil check passed and the first
Emit panicked. The fix is the same shape as reflection’s helper: take
the interface. Typed nils are Go’s most polite trap; the mirror tests
caught it in seconds.
# Gotchas
- Fail closed is an error at Execute time, not a panic at construction
time.
Newcannot return an error (and reflection’s doesn’t try); the bar lives on the first call that would do work. Mirror it exactly — a construction-time panic would take the whole process down in production for a misconfiguration a warning could have caught. - The scratch namespace is never grounded. Verdicts are ephemeral
task-scoped state (
MemoryClassTask), like reflection’s — they route the loop, they do not become knowledge. - A cap is a bound, not an error. If your revise loop exits non-zero when the cap bites, you have turned a design decision into a failure mode.
- Prompt file frontmatter opens with
---. The loader refuses files without the opening fence, and the error message will not remind you. The lab’s test pins the file byte-for-byte, so this cannot regress. - The registry override path is boot-time.
agent.critic.defaultloads when the workspace composes; unit tests feed it viaLoadFS(fstest.MapFS{…}). There is no mid-run prompt swap.
# Summary
paradigm.Depsis the governed runtime surface; the curated subset is ten fields you reason about, with the plumbing carded in Appendix C. The permission checker is the line between a paradigm and a liability.CriticAgentmirrors reflection structurally — constructor, options, directive mode versus library mode, state-isolated delegation, scratch verdicts, bounded revisions, cap-as-bound — and differs where the thesis demands: the critique phase never sees the drafter’s reasoning, and the fail-closed bar is unconditional.- Prompts are three-layered: the workspace file (operator-facing, convention-named), the library prompt in code, and the authored criteria through options. Strict verdict decoding keeps the loop honest about malformed model output.
- The paradigm is complete: contract (Ch 7), agent (here), prompt file
(here) — with
paradigm.Registeralready done and the runtime composition still to come.
# Check yourself
- Name the four fields of the curated
Depssubset that are not in this chapter’s table, and say what kind of concern each is. - Why does the critic fail closed at
Executerather than panicking inNew— and what would you lose if it panicked? - The draft phase wraps the delegate in
Clone→Execute→Merge. What does the isolation buy the critique phase, and what does the merge preserve for the final result? - Your critique model returns
verdict: "OK". What happens, which error surfaces, and what would happen without the strict decode? - What can the runner binary’s process do now that it could not last chapter — and what is still Chapter 9’s work?
Answers. 1. EpochDrain (invalidation drain at the epoch barrier),
AgentLifecycle (workflow/run records), Telemetry (the emit surface —
the critic reaches telemetry through Config), and Config’s extended
surface; runtime-plumbing concerns received, not reasoned about. 2.
Because New cannot return an error, and a panic would crash the whole
process for a misconfiguration; failing at Execute turns the bar into
a routable, testable, telemetry-visible refusal. 3. The critique phase
sees only merged, successful delegate state — never the delegate’s
in-flight partials — and the merge preserves the delegate’s results in
the parent envelope so the final result and provenance stay intact. 4.
decodeCritiqueVerdict returns ErrInvalidCritique, the run fails with
it, and the model profile’s repair strategy can retry; without the strict
decode, “OK” would silently fail the vocabulary switch downstream or —
worse — be treated as a non-issues verdict, ending the loop without a
revision. 5. The process now links the complete paradigm — contract,
agent, prompt file — and paradigm.Registry carries it; what cannot
happen yet is the relurpish runtime composing critic for a recipe,
because the wiring, lint parity, and goldens are Chapter 9’s.
# Try it yourself
- A second checker. Write a
deniedCheckerwhoseCheckCapabilityreturns an error, and add a test asserting the critic’s delegate work cannot proceed through a governed path — then notice what actually happens: the critic’s ownExecutenever checks capabilities directly, because it invokes none. Write down where in the stack the denial would land (the registry’s pipeline, Chapter 4) and why the checker’s presence is still the right bar for the paradigm. - Cap semantics, strengthened. Change
TestRevisionCapEndsLoopNormallyto also assertcritic.revise_cyclein the envelope equalsMaxRevisionCycles. Then re-read the contract guarantee (“bounded revision iterations”) and confirm every word of it is now pinned by an assertion.
# Where next
Everything exists; nothing is addressable. Chapter 9 makes the framework
believe: the runtime composition wiring that hands critic its Deps
(the [NEEDS CONFIRMATION] item — the constructor’s callers, pinned by
reading), lint parity so relurplint knows the new paradigm, the DSL
goldens re-baselined deliberately, and the payoff — agent fixer uses critic loading and running offline against stringkit, with telemetry as
the proof.
The lab lives at devdocs/agent-construction-relurpify/labs/ —
verify.sh gates ch08 re-runs everything this chapter proved, and
transcripts/ch08-critic-agent.md holds the captured run and the build
notes.