Skip to content

Add agent-factory example: the output gate to SageRoute's run gate - #1

Open
codejunkie99 wants to merge 1 commit into
mainfrom
examples/agent-factory
Open

Add agent-factory example: the output gate to SageRoute's run gate#1
codejunkie99 wants to merge 1 commit into
mainfrom
examples/agent-factory

Conversation

@codejunkie99

@codejunkie99 codejunkie99 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

SageRoute decides whether a run is still worth paying for. This adds the other half as a worked example: a line that turns a job description into a certified agent, and gates every output that agent produces.

Both halves call Sage. The router asks does this run need intervention mid-task; the factory asks is this output right after the answer. Same primitive, two moments.

Six files, 846 lines, standard library only. Runs with no keys at all — deterministic backend, and every run flags to a human and says why.

Worth reviewing

  • broker.py enforces tool grants outside the model. A poisoned ticket reaching for billing:refund dies at the boundary, not in the prompt
  • prove runs with dry=True — an agent under test that can write to the suite it is judged against is not being tested
  • certify refuses without a sealed run, under the pass bar, or if the ABOM changed after the run it was proved against
  • restamp propagates a master fix and revokes every certificate stamped from the old one
  • harvest runs a certified builder agent that writes the next eval cases into proposed.jsonl only; promoting one stays a human edit

Verification

python3 factory.py selfcheck passes with no keys. The seven guards in the README are each a reproducible exit-1.

Includes four SVG diagrams.

🤖 Generated with Claude Code

Note

Add agent-factory example integrating output gating via SageRoute

  • Adds a complete agent factory example in examples/agent-factory/ demonstrating a stamp → prove → certify → run → harvest workflow for managed LLM agents.
  • factory.py implements the CLI pipeline: ABOM-based agent definitions, certification with human sign-off, production runs with cost envelope enforcement, Sage output gating, and recall/selfcheck commands.
  • sage.py provides a fail-open Sage client for yes/no, scale, choice, and batch decisions; output gate in cmd_run uses sage.yesno to approve or suppress agent output.
  • llm.py routes completions through SageRoute when configured, with offline fallback and token-based cost estimation.
  • Two agents are included: agents/triager.py (support ticket triage with broker-gated billing actions) and agents/evalsmith.py (proposes new eval cases from flagged production traces).
📊 Macroscope summarized 765a5c4. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

SageRoute decides whether a run is still worth paying for. This example is
the other half: a line that turns a job description into a certified agent,
and gates every output that agent produces.

Both halves call Sage. The router asks "does this run need intervention"
mid-task; the factory asks "is this output right" after the answer. Same
primitive, two moments.

Six files, 846 lines, standard library only. Runs with no keys at all
(deterministic backend, every run flags to a human and says why).

The parts worth reading:
- broker.py enforces tool grants outside the model, so a poisoned ticket
  reaching for billing:refund dies at the boundary, not in the prompt
- prove runs with dry=True; an agent under test that can write to the suite
  it is judged against is not being tested
- certify refuses without a sealed run, under the pass bar, or if the ABOM
  changed after the run it was proved against
- restamp propagates a master fix and revokes every certificate stamped
  from the old one
- harvest runs a certified builder agent that writes the next eval cases,
  into proposed.jsonl only; promoting one stays a human edit

Includes four SVG diagrams and a README documenting the seven guards, each
a reproducible exit-1.
"use `factory.py harvest <agent>` instead of `run evalsmith`."
)
raw, backend = ctx.llm(PROMPT.replace("{run}", json.dumps(record)), offline=_offline)
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agents/evalsmith.py:48

When the model returns syntactically valid JSON that isn't an object — such as []json.loads succeeds and case.setdefault("expect", {}) throws AttributeError because setdefault is called on a list. The except only catches JSON parsing errors, so this exception aborts the run instead of falling back to _offline. Similarly, {} parses fine and becomes {"expect": {}}, which is sent to evals:propose and returned with an empty input and empty expectations — an unusable proposal silently enters the catalog. Consider validating that case is a dict with the required fields inside the try block, so schema failures fall back to _offline just like parse failures do.

Also found in 1 other location(s)

examples/agent-factory/agents/triager.py:55

The fallback only covers JSON extraction/parsing, not schema validation. If the model returns syntactically valid JSON such as {} or omits label, draft, or escalate, parsing succeeds and the later direct indexing raises KeyError; the triage run aborts instead of using _offline as intended.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/agents/evalsmith.py around line 48:

When the model returns syntactically valid JSON that isn't an object — such as `[]` — `json.loads` succeeds and `case.setdefault("expect", {})` throws `AttributeError` because `setdefault` is called on a list. The `except` only catches JSON parsing errors, so this exception aborts the run instead of falling back to `_offline`. Similarly, `{}` parses fine and becomes `{"expect": {}}`, which is sent to `evals:propose` and returned with an empty `input` and empty expectations — an unusable proposal silently enters the catalog. Consider validating that `case` is a dict with the required fields inside the `try` block, so schema failures fall back to `_offline` just like parse failures do.

Also found in 1 other location(s):
- examples/agent-factory/agents/triager.py:55 -- The fallback only covers JSON extraction/parsing, not schema validation. If the model returns syntactically valid JSON such as `{}` or omits `label`, `draft`, or `escalate`, parsing succeeds and the later direct indexing raises `KeyError`; the triage run aborts instead of using `_offline` as intended.

"issues:comment",
"billing:refund"
],
"knowledge": "docs/support-playbook.md",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium masters/triager.json:18

The knowledge field is inert: agents.triager:run constructs its prompt from a hard-coded PROMPT, and no code loads or injects docs/support-playbook.md (or any knowledge value) into the agent context at invocation time. The shipped triager therefore ignores its configured support playbook, and variants created via --set knowledge=... silently inherit the same behavior—producing triage decisions that can contradict the intended playbook. Either wire the knowledge file into the agent context at runtime or document that the field is currently unimplemented.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/masters/triager.json around line 18:

The `knowledge` field is inert: `agents.triager:run` constructs its prompt from a hard-coded `PROMPT`, and no code loads or injects `docs/support-playbook.md` (or any `knowledge` value) into the agent context at invocation time. The shipped triager therefore ignores its configured support playbook, and variants created via `--set knowledge=...` silently inherit the same behavior—producing triage decisions that can contradict the intended playbook. Either wire the `knowledge` file into the agent context at runtime or document that the field is currently unimplemented.

{run}"""


def _offline(prompt):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agents/evalsmith.py:22

_offline always sets expect.label to the rejected output's label and flips escalate, but a gate can flag a run because the label was wrong while escalation was already correct. For such a trace, the generated eval case canonizes the incorrect label and reverses a correct escalation expectation — promoting it would teach the suite the wrong behavior. The fallback assumes every flag means both fields are wrong. Consider only inverting the field the gate actually flagged, or document why blanket inversion is intended.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/agents/evalsmith.py around line 22:

`_offline` always sets `expect.label` to the rejected output's label and flips `escalate`, but a gate can flag a run because the label was wrong while escalation was already correct. For such a trace, the generated eval case canonizes the incorrect label and reverses a correct escalation expectation — promoting it would teach the suite the wrong behavior. The fallback assumes every flag means both fields are wrong. Consider only inverting the field the gate actually flagged, or document why blanket inversion is intended.

return out


def cmd_restamp(args):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agent-factory/factory.py:132

cmd_restamp fails to propagate master fixes to existing variants: values that a variant inherited unchanged from the old master become treated as overrides as soon as the master is edited, so dict(master, **overrides, ...) writes those old values back over the fix. The restamped variant ends up identical to what it was before the master was changed.

The overrides dict on line 142 is computed by comparing each variant field against the already-updated master (old[k] != master.get(k)). Any field the variant inherited from the old master that the master edit changed will match this condition and be carried forward as an override, overwriting the new master value. To fix this, overrides must be identified explicitly (e.g., recorded at stamp time) or compared against the previous master, not the updated one.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/factory.py around line 132:

`cmd_restamp` fails to propagate master fixes to existing variants: values that a variant inherited unchanged from the old master become treated as overrides as soon as the master is edited, so `dict(master, **overrides, ...)` writes those old values back over the fix. The restamped variant ends up identical to what it was before the master was changed.

The `overrides` dict on line 142 is computed by comparing each variant field against the *already-updated* master (`old[k] != master.get(k)`). Any field the variant inherited from the old master that the master edit changed will match this condition and be carried forward as an override, overwriting the new master value. To fix this, overrides must be identified explicitly (e.g., recorded at stamp time) or compared against the *previous* master, not the updated one.

Comment on lines +212 to +220
for i, (case, got) in enumerate(results):
missed = {k: {"want": v, "got": got.get(k)}
for k, v in case["expect"].items() if got.get(k) != v}
if i in scores:
drafts.append(scores[i])
if scores[i] < rubric["min"]:
missed["draft"] = {"want": f">={rubric['min']}", "got": round(scores[i], 2)}
if missed:
failures.append({"id": case["id"], "input": case["input"], "missed": missed})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agent-factory/factory.py:212

cmd_prove does not treat missing draft rubric scores as failures. When sage.batch returns [] (no key or outage) or a partial response, no scores[i] entry is created for that case, so the case is judged only on exact-match expect fields and can pass. cmd_certify then accepts the resulting sealed score and certifies an agent whose required draft safety rubric was never evaluated. Printing UNSCORED to the console does not prevent certification because the record itself contains no signal that scoring was incomplete.

The issue is that a case with a draft but no score is neither added to failures nor counted as a draft failure. Consider treating an unscored draft as a missed draft expectation (e.g., recording missed["draft"] when rubric is set, the output contains a draft, and no score was returned), so the score drops below the pass bar and cmd_certify refuses to sign.

    for i, (case, got) in enumerate(results):
        missed = {k: {"want": v, "got": got.get(k)}
                  for k, v in case["expect"].items() if got.get(k) != v}
-        if i in scores:
+        if rubric and isinstance(got.get("draft"), str) and i not in scores:
+            missed["draft"] = {"want": f">={rubric['min']}", "got": None}
+        elif i in scores:
            drafts.append(scores[i])
            if scores[i] < rubric["min"]:
                missed["draft"] = {"want": f">={rubric['min']}", "got": round(scores[i], 2)}
        if missed:
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/factory.py around lines 212-220:

`cmd_prove` does not treat missing draft rubric scores as failures. When `sage.batch` returns `[]` (no key or outage) or a partial response, no `scores[i]` entry is created for that case, so the case is judged only on exact-match `expect` fields and can pass. `cmd_certify` then accepts the resulting sealed score and certifies an agent whose required draft safety rubric was never evaluated. Printing `UNSCORED` to the console does not prevent certification because the `record` itself contains no signal that scoring was incomplete.

The issue is that a case with a draft but no score is neither added to `failures` nor counted as a draft failure. Consider treating an unscored draft as a missed `draft` expectation (e.g., recording `missed["draft"]` when `rubric` is set, the output contains a draft, and no score was returned), so the score drops below the pass bar and `cmd_certify` refuses to sign.

f.write(json.dumps(case) + "\n")


def invoke(abom, task, agent, target=None, dry=False):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agent-factory/factory.py:95

invoke returns llm.LAST["cost_usd"] as the total cost, but llm.LAST is reset by llm.complete on every call — so when an agent's entrypoint calls ctx.llm more than once, only the final call's cost is reported and earlier calls' costs are silently discarded. cmd_run then compares this understated cost against cost_envelope_usd, so a multi-call run that exceeds the envelope can be allowed to act instead of being forced to a human. Consider accumulating cost per invocation (e.g., a callback that sums each complete result) rather than reading the global after the fact.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/factory.py around line 95:

`invoke` returns `llm.LAST["cost_usd"]` as the total cost, but `llm.LAST` is reset by `llm.complete` on every call — so when an agent's entrypoint calls `ctx.llm` more than once, only the final call's cost is reported and earlier calls' costs are silently discarded. `cmd_run` then compares this understated cost against `cost_envelope_usd`, so a multi-call run that exceeds the envelope can be allowed to act instead of being forced to a human. Consider accumulating cost per invocation (e.g., a callback that sums each `complete` result) rather than reading the global after the fact.

Comment on lines +364 to +365
if not (REGISTRY / "evalsmith.card.json").exists():
sys.exit("evalsmith is not certified. the line only hires from the registry.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agent-factory/factory.py:364

cmd_harvest checks only that evalsmith.card.json exists, but never rejects a recalled card or verifies that the card's abom_digest matches digest("evalsmith"). A recalled evalsmith, or one whose ABOM changed after certification, still runs and writes proposals — bypassing the revocation and tamper checks that cmd_run enforces. Consider applying the same recalled and abom_digest guards used in cmd_run before invoking the builder.

Suggested change
if not (REGISTRY / "evalsmith.card.json").exists():
sys.exit("evalsmith is not certified. the line only hires from the registry.")
if not (REGISTRY / "evalsmith.card.json").exists():
sys.exit("evalsmith is not certified. the line only hires from the registry.")
card = json.loads((REGISTRY / "evalsmith.card.json").read_text())
if card.get("recalled"):
sys.exit(f"evalsmith recalled: {card['recalled']}. re-certify before harvesting.")
if card["abom_digest"] != digest("evalsmith"):
sys.exit("evalsmith's ABOM changed since certification. re-prove and re-certify.")
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/factory.py around lines 364-365:

`cmd_harvest` checks only that `evalsmith.card.json` exists, but never rejects a recalled card or verifies that the card's `abom_digest` matches `digest("evalsmith")`. A recalled `evalsmith`, or one whose ABOM changed after certification, still runs and writes proposals — bypassing the revocation and tamper checks that `cmd_run` enforces. Consider applying the same `recalled` and `abom_digest` guards used in `cmd_run` before invoking the builder.

if card["abom_digest"] != digest(args.agent):
sys.exit("the ABOM changed since certification. re-prove and re-certify.")

try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agent-factory/factory.py:297

cmd_run invokes the agent with live tools before checking the output gate or certificate tier, so an agent that fails the gate (or a C0/C1 agent) still executes every granted tool call with real side effects. The acted flag is set to false afterward, but the side effects have already happened — the gate and tier cannot prevent them, only label the run after the fact.

The invoke call on line 298 runs the agent with live tools (no dry=True), so tool calls like billing:refund or drafts:write take effect before the gate is evaluated. To prevent side effects from agents that should only observe or draft, either run the agent dry first and only re-invoke with live tools when the gate and tier permit action, or route tool calls through the broker based on the certificate tier.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @examples/agent-factory/factory.py around line 297:

`cmd_run` invokes the agent with live tools before checking the output gate or certificate tier, so an agent that fails the gate (or a `C0`/`C1` agent) still executes every granted tool call with real side effects. The `acted` flag is set to `false` afterward, but the side effects have already happened — the gate and tier cannot prevent them, only label the run after the fact.

The `invoke` call on line 298 runs the agent with live tools (no `dry=True`), so tool calls like `billing:refund` or `drafts:write` take effect before the gate is evaluated. To prevent side effects from agents that should only observe or draft, either run the agent dry first and only re-invoke with live tools when the gate and tier permit action, or route tool calls through the broker based on the certificate tier.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 765a5c4c8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +142 to +145
overrides = {k: v for k, v in old.items()
if k in master and old[k] != master.get(k) and k not in keep}
new = dict(master, **keep, **overrides, master_digest=fresh)
(MASTERS / f"{name}.json").write_text(json.dumps(new, indent=2) + "\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve only explicit overrides during restamping

When a master field is changed, every existing variant differs from the new master, so this comprehension misclassifies the variant's stale inherited value as an override and reapplies it over the updated master. For example, changing the master's gate_question and running restamp leaves every variant on the old question, defeating propagation of the fix; explicit overrides need to be recorded separately or compared with the version of the master from which the variant was stamped.

Useful? React with 👍 / 👎.

Comment on lines +297 to +298
try:
output, trace, cost, route = invoke(abom, args.input, args.agent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage tool side effects until after the output gate

When a broker implementation has real side effects, invoke executes those tools before the Sage result, cost envelope, or certificate tier is evaluated below. Consequently a failed gate, an over-budget run, or even a C0 "observe" certificate can already have performed its writes; evals:propose is an included persistent example. Tool calls need to be staged or otherwise constrained by the tier and committed only after the gate passes.

Useful? React with 👍 / 👎.

failures.append({"id": case["id"], "input": case["input"], "missed": missed})

scored = len(scores)
score = (len(cases) - len(failures)) / len(cases)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject sealed proofs with missing rubric scores

When SAGE_API_KEY is absent or batch scoring fails, scores is empty and no draft is added to missed, so exact-match outputs produce a score of 1.0 despite every draft being explicitly reported as UNSCORED. Because certification checks only this score, an agent can be certified without the prose-safety evaluation that the rubric is intended to enforce; missing scores for rubric-bearing cases must make the proof incomplete or failing.

Useful? React with 👍 / 👎.

Comment on lines +364 to +365
if not (REGISTRY / "evalsmith.card.json").exists():
sys.exit("evalsmith is not certified. the line only hires from the registry.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate the evalsmith certificate before harvesting

If evalsmith's card has been recalled or its ABOM has changed since certification, this existence-only check still accepts the card and harvest invokes the builder directly, bypassing the digest and recall checks in cmd_run. Thus a recalled or stale builder can continue writing proposals; harvest should apply the same certificate validation before invocation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant