Vega answers the phone — for real

Story-1977 built the functions-side trigger: 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.
Story-1993 · EPIC-1990 · builds on #1953 (panel) · #1955 (frame eval) · #1977 (framer_trigger seam) · pramaan-docs ADR-110 (event stream) · ADR-118/124 (observability) · ADR-121 (secrets) · repo pramaan-agents · branch issue-1993-vega-frame-generation

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.

Why the payload grows an 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.

1 · The seam, end to end

# 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
FileWhat it is
main.pyAgentCore @app.entrypoint: secrets bootstrap (ADR-121 D3) → Logfire lane → fail-closed input → stream JSONL. Mirrors registrar.
runtime.pyhandle_request (parse BEFORE run.started; typed failure mapping) + stream_invoke (the boundary watchdog).
events.pyThe frozen agent_event_stream.v1 builders — Vega's ambient subset, no per-agent dialect.
observability.pyDedicated 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.pyThe one fenced boto3/Secrets Manager read (OPENROUTER_API_KEY / LOGFIRE_TOKEN), fail-safe, only-if-unset.

2 · The typed failure rides the wire

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.

3 · The operational hard rules, now enforced in code

Rule (framer AGENTS.md)Enforcement
1 — every provider call gets a hard timeoutagent._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 loopstream_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-safeA gate that errors is DOUBT → build_frame rebuilds via the panel instead of crashing the run.
Honest cost signalagent.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.

4 · What was deliberately NOT built

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.

5 · Proof

# 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.