matter.created wakes a consumer that commits a run row and invokes a stub. Story-1993 replaces the far end of that call with the real thing: an AgentCore runtime that takes the invoke, runs the crowned Propose → Critique → Synthesize panel, and answers with a validated FrameOutput on the frozen event stream — or a typed, routable failure. Nothing about the panel changed; what shipped is the shell that makes it callable, observable, and impossible to hang.The panel (Story-1953) and its acceptance evals (Story-1955 — frame_status honesty via FrameCoreMatchesLabel.status_honest, the NeverFabricatesAClaim L3 disqualifier) were already merged and crowned. What did not exist: a main.py for the functions invoke to land on, the wire language, runtime Logfire identity, and the operational hard rules (timeouts) applied to the money path. That is this PR.
input field: the #1977 stub payload is IDs-only — {firm_id, matter_id, agent_run_id, traceparent, tracestate}. Vega's brain is stateless and holds no DB credentials (ADR-110 §6 — CI greps for it), so IDs alone can never become documents. The functions-side ingest boundary (EPIC-1990 V4) assembles the FramerInput and rides it in input; the entrypoint fails closed without it. No demo fallback exists, so a stub-shaped invoke against the real runtime errors loudly instead of fabricating an empty build.# functions framer_trigger (already live) # THIS PR — agents/framer/ matter.created → gates → run row COMMITTED → invoke ───────► main.py (fail-closed 'input') └► runtime.stream_invoke # Bug-1370 watchdog └► handle_request └► agent.build_frame # the crowned panel # response: agent_event_stream.v1 JSONL (ADR-110 §3.2) run.started → model.usage → turn.completed → run.completed | run.failed
| File | What it is |
|---|---|
| main.py | AgentCore @app.entrypoint: secrets bootstrap (ADR-121 D3) → Logfire lane → fail-closed input → stream JSONL. Mirrors registrar. |
| runtime.py | handle_request (parse BEFORE run.started; typed failure mapping) + stream_invoke (the boundary watchdog). |
| events.py | The frozen agent_event_stream.v1 builders — Vega's ambient subset, no per-agent dialect. |
| observability.py | Dedicated Logfire trace provider (Bug-1294 two-lane rule) + AmbientIdentitySpanProcessor stamping gen_ai.agent.name=Vega / agent.key=framer / run_id/firm_id/model_id. Traceparent from the invoke payload is extracted so Vega's spans join the producer→consumer trace (EB-04). No baggage, no content on telemetry. |
| secrets_bootstrap.py | The one fenced boto3/Secrets Manager read (OPENROUTER_API_KEY / LOGFIRE_TOKEN), fail-safe, only-if-unset. |
FrameBuildFailed (the panel's fail-closed envelope) maps 1:1 onto run.failed: failure_stage = propose/critic/synthesize, failure_kind = the closed reason enum (insufficient_drafts, role_error, unresolved_critique, unknown_locked_field, …). The broker can route retryable role errors differently from permanent input defects without parsing prose.
| Rule (framer AGENTS.md) | Enforcement |
|---|---|
| 1 — every provider call gets a hard timeout | agent._bounded: asyncio.wait_for(…, VEGA_CALL_TIMEOUT_S=150) on triage, every seat, critic, synthesizer. A hung seat is cancelled → skip + strike; a hung critic/synth → typed role_error. |
| Bug-1370 — a wedged brain must not hang the worker loop | stream_invoke runs the brain on a private thread/loop; the deadline (VEGA_INVOKE_BUDGET_S=780 < the 900s functions job lease) ticks on the AgentCore loop and ships a typed invoke_watchdog terminal. |
| Triage gate fail-safe | A gate that errors is DOUBT → build_frame rebuilds via the panel instead of crashing the run. |
| Honest cost signal | agent.usage_bucket (ContextVar, same pattern as the eval cost bucket) totals real input/output tokens across all panel calls → the model.usage event the broker's cost cap reads. Never a hardcoded zero on a run that spent. |
No manual (T3) spans — Vega has no registry-listed domain operations yet; Pydantic AI auto-instrumentation (T1) + ambient identity (T2) is the ADR-124 floor and covers the panel. No run-control events — Vega is ambient; pause/resume/stop is a broker concept for interactive agents. No generation-CAS write — the brain returns the frame; the functions completion path writes through release_frame_claim (that is its contract, EB-18). No content capture on telemetry, ever.
# offline, no key — the whole suite + the CI gates cd agents/framer && pytest tests # 92 passed (incl. 12 new runtime/entrypoint tests) python scripts/check_brain_egress.py # clean — no DB/S3/SQS, no rogue tracer, no baggage python scripts/check_agent_name_guard.py # 3 agent units guarantee gen_ai.agent.name (framer now counted) ruff check agents/ # clean # the acceptance mechanism (NO UAT for ambient agents) — the existing harness uv run python agents/framer/evals/run.py frame # golden + adversarial; frame_status honesty + no-fabrication gates
New tests pin: the frozen event order · fail-closed parse before run.started · the typed run.failed for FrameBuildFailed · the watchdog terminal on a wedged brain · a hung seat dropping instead of hanging · the triage fail-safe · real usage totals (5 tracked calls on a 3-seat happy path) · the entrypoint's input contract.