Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/api/src/cora/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
RUN_SUPERVISOR_AGENT_ID,
seed_run_supervisor_agent,
)
from cora.agent.seed_run_translator import (
RUN_TRANSLATOR_AGENT_ID,
seed_run_translator_agent,
)
from cora.agent.seed_run_witness import (
RUN_WITNESS_AGENT_ID,
seed_run_witness_agent,
Expand Down Expand Up @@ -134,6 +138,7 @@
"RATIFICATION_ENFORCER_AGENT_ID",
"RUN_INITIATOR_AGENT_ID",
"RUN_SUPERVISOR_AGENT_ID",
"RUN_TRANSLATOR_AGENT_ID",
"RUN_WITNESS_AGENT_ID",
"STATUS_PUBLISHER_AGENT_ID",
"AgentHandlers",
Expand Down Expand Up @@ -172,6 +177,7 @@
"seed_run_debriefer_local_agent",
"seed_run_initiator_agent",
"seed_run_supervisor_agent",
"seed_run_translator_agent",
"seed_run_witness_agent",
"seed_status_publisher_agent",
"wire_agent",
Expand Down
16 changes: 14 additions & 2 deletions apps/api/src/cora/agent/_seeded_fleet.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""The set of Agents CORA ships with itself.

Every deployment gets these twenty-three at boot, seeded by the
Every deployment gets these twenty-four at boot, seeded by the
`seed_*_agent` functions. Until this file existed the set existed only
as sequential calls in `cora.api.main`, which was enough to create them
and not enough to ask a question ABOUT them.

Four of the twenty-three are two "kind, split by which brain serves it"
Four of the twenty-four are two "kind, split by which brain serves it"
pairs: `RunDebriefer`/`RunDebriefer (External)` and
`CautionDrafter`/`CautionDrafter (External)`. The bare name is the
local/in-house arm, `(External)` marks the vendor-API arm; both members
Expand All @@ -18,6 +18,13 @@
per-id FOREVER-STABLE means they are retired via `deprecate_agent` on a
deployment, never removed from source.

One further identity, `RunWitness`, was renamed outright to
`RunTranslator` (`seed_run_translator.py`): not a split, a straight
rename, since `witness` named the modeling axis this runtime implements
rather than what it does. `RUN_WITNESS_AGENT_ID` (from `seed_run_witness.py`)
stays in this tuple unchanged for the same FOREVER-STABLE reason as the
pair above.

The question that needed asking: which of these can actually act? A
seeded Agent lands `Versioned` on a fresh bootstrap, but a deployment
seeded before that change carries a fleet stuck at `Defined`, and the
Expand Down Expand Up @@ -124,6 +131,10 @@
RUN_SUPERVISOR_AGENT_ID,
RUN_SUPERVISOR_AGENT_NAME,
)
from cora.agent.seed_run_translator import (
RUN_TRANSLATOR_AGENT_ID,
RUN_TRANSLATOR_AGENT_NAME,
)
from cora.agent.seed_run_witness import (
RUN_WITNESS_AGENT_ID,
RUN_WITNESS_AGENT_NAME,
Expand Down Expand Up @@ -167,6 +178,7 @@ class SeededAgent:
SeededAgent(RUN_DEBRIEFER_LOCAL_AGENT_ID, RUN_DEBRIEFER_LOCAL_AGENT_NAME),
SeededAgent(RUN_INITIATOR_AGENT_ID, RUN_INITIATOR_AGENT_NAME),
SeededAgent(RUN_SUPERVISOR_AGENT_ID, RUN_SUPERVISOR_AGENT_NAME),
SeededAgent(RUN_TRANSLATOR_AGENT_ID, RUN_TRANSLATOR_AGENT_NAME),
SeededAgent(RUN_WITNESS_AGENT_ID, RUN_WITNESS_AGENT_NAME),
SeededAgent(STATUS_PUBLISHER_AGENT_ID, STATUS_PUBLISHER_AGENT_NAME),
)
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/cora/agent/fleet_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class FleetReadiness:
"""Fleet members by whether they can act, named rather than counted.

Names and not ids: this is read by an operator deciding whether to
run a promotion, and `RunWitness` answers that question where
run a promotion, and `RunTranslator` answers that question where
`01900000-0000-7000-8000-0000aaaa0010` does not. The ids stay
reachable through `SEEDED_FLEET` for anything that needs to act on a
member.
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/cora/agent/seed_capture_baseline_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
(`provider="deterministic"`). Never used to build an LLM: the
runtime is a one-shot read-and-append at promotion, not an LLM
subscriber.
- A SEPARATE principal from RunWitness AND from CaptureProgressFeeder,
- A SEPARATE principal from RunTranslator AND from CaptureProgressFeeder,
deliberately, for the same reason CaptureProgressFeeder already got
its own: an operator can revoke baseline-writing (this grant)
without blinding either the witness or the progress feeder, and
without blinding either the translator or the progress feeder, and
`Observation.actor_id` tells all three runtimes' rows apart in the
record even though `sampling_procedure` already discriminates
baseline from monitor rows. The ONLY lever that actually revokes it
Expand All @@ -43,10 +43,10 @@
accepts any Running-or-Held Run, driven ones included, the same as
every other operator-facing entry writer. This principal's safety
rests entirely on `CaptureBaselineReader.read` only ever
being called by `RunWitnessRecorder._promote` with the `run_id` it
being called by `RunTranslator._promote` with the `run_id` it
JUST minted via `record_witnessed_run`, never a run_id sourced any
other way. Same structural residual already documented for
RunWitness's own `TruncateRun` grant in `seed_run_witness.py` and
RunTranslator's own `TruncateRun` grant in `seed_run_translator.py` and
for CaptureProgressFeeder's `AppendObservations` grant above it.
"""

Expand Down Expand Up @@ -79,7 +79,7 @@
"genesis-baseline PVs exactly once, at the instant a capture promotes "
"to a witnessed Run, and appends them as AppendObservations rows with "
'sampling_procedure="baseline", scoped exclusively to the Run '
"RunWitness itself just promoted. Not a control path: it never drives "
"RunTranslator itself just promoted. Not a control path: it never drives "
"the substrate, only records what it read at that moment."
)

Expand Down
20 changes: 10 additions & 10 deletions apps/api/src/cora/agent/seed_capture_progress_feeder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
(`prompt_template_id=None`) and a sentinel `ModelRef`
(`provider="deterministic"`). Never used to build an LLM: the
runtime is a buffer-and-flush loop, not an LLM subscriber.
- A SEPARATE principal from RunWitness, deliberately: an operator can
revoke progress-writing (this grant) without blinding the witness
(RunWitness's own four grants), and `Observation.actor_id` tells
- A SEPARATE principal from RunTranslator, deliberately: an operator can
revoke progress-writing (this grant) without blinding the translator
(RunTranslator's own four grants), and `Observation.actor_id` tells
the two runtimes' rows apart in the record. The ONLY lever that
actually revokes it is removing this principal from the Policy's
`permitted_principal_ids` (a Policy edit): `AppendObservations` is
liveness-exempt (`cora.shared.liveness`), so deactivating this
Agent's Actor (`ActorDeactivated`) has NO effect on it, unlike
RunWitness, where three of its four grants ARE liveness-gated.
RunTranslator, where three of its four grants ARE liveness-gated.
- Authorization: the runtime issues exactly one command,
`AppendObservations`, through the Authorize port like any
principal. Under the default AllowAllAuthorize it is permitted;
Expand All @@ -42,16 +42,16 @@
accepts any Running-or-Held Run, driven ones included, the same as
every other operator-facing entry writer. This principal's safety
therefore rests entirely on `CaptureProgressFeeder` only ever
sourcing a `run_id` from `RunWitnessRecorder.open_captures`, which
is populated exclusively from RunWitness's own promotions -- this
sourcing a `run_id` from `RunTranslator.open_captures`, which
is populated exclusively from RunTranslator's own promotions -- this
process's, or a prior one's via the boot-time
`rebuild_open_captures`, itself scoped to `conduct_mode="Witnessed"`
Runs only -- so it can only ever name a Run RunWitness created. A
Runs only -- so it can only ever name a Run RunTranslator created. A
future change to `_capture_progress_feeder.py` that sources a
`run_id` for this call from anywhere else would lose that guarantee
with no decider-level backstop to catch it. Same structural
residual as the one already documented for RunWitness's own
`TruncateRun` grant in `seed_run_witness.py`.
residual as the one already documented for RunTranslator's own
`TruncateRun` grant in `seed_run_translator.py`.
"""

from __future__ import annotations
Expand Down Expand Up @@ -83,7 +83,7 @@
"Deterministic in-process runtime: buffers a witnessed Run's "
"capture-progress readings (images saved, images collected) and "
"flushes them as AppendObservations batches plus a feed heartbeat, "
"scoped exclusively to Runs RunWitness itself promoted. Not a "
"scoped exclusively to Runs RunTranslator itself promoted. Not a "
"control path: it never drives the substrate, only records what a "
"promoted capture reported."
)
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/cora/agent/seed_capture_scan_ingestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
(`prompt_template_id=None`) and a sentinel `ModelRef`
(`provider="deterministic"`). The sweep is a poll-and-append loop,
not an LLM subscriber.
- A SEPARATE principal from RunWitness, CaptureProgressFeeder, and
- A SEPARATE principal from RunTranslator, CaptureProgressFeeder, and
CaptureBaselineReader, deliberately: an operator can revoke
scan-ingest (this grant) without blinding any of the other three,
and `Dataset.registered_by` / `Acquisition.recorded_by` tell this
Expand Down Expand Up @@ -63,7 +63,7 @@
"Deterministic in-process runtime: sweeps terminated witnessed Runs "
"whose observed capture path resolved and which have no Dataset yet, "
"and ingests each as a Dataset + Distribution + Acquisition via "
"IngestScan. Never triggered off the witness terminal itself; a "
"IngestScan. Never triggered off the translator terminal itself; a "
"periodic reconciliation sweep against the read model. Not a control "
"path: it never drives the substrate, only records the file a "
"promoted capture already produced."
Expand Down
138 changes: 138 additions & 0 deletions apps/api/src/cora/agent/seed_run_translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Bootstrap-time seed for the RunTranslator Agent.

Renamed from `RunWitness` (`seed_run_witness.py`, kept forever-stable for
identity continuity of its already-recorded Runs; see that module's
docstring). Same runtime, same job, new name: `witness` named the modeling
axis this runtime implements (`ConductMode.WITNESSED`, "who drives the
act"), not what the runtime itself does. What it does is translate an
external tool's own vocabulary (TomoScan's free-text `ScanStatus` values)
into CORA's canonical `CapturePhase`, then issue the matching Run command
-- translation, not witnessing, is this agent's own verb, per the R5
doer-naming convention. The `ConductMode.WITNESSED` domain term is
untouched by this rename; only the agent's identity and its implementing
runtime (`cora.api._run_translator`, formerly `cora.api._run_witness`)
changed name.

The RunTranslator runtime needs an Agent record (and its co-registered
Actor) to exist at the pinned `RUN_TRANSLATOR_AGENT_ID` so it can issue
`RecordWitnessedRun` as an agent-kind principal when it promotes a BEGUN
capture observation to a real witnessed Run. Mirrors
`cora.agent.seed_run_supervisor.seed_run_supervisor_agent` verbatim except
for the per-agent constants below; the shared scaffolding lives in
`cora.agent._agent_seed`.

- Pinned UUID continues the numeric-mnemonic range at `5555` (next
unclaimed block after `1111`/`2222`/`3333`/`4444`); deployment-stable
forever.
- DETERMINISTIC agent (rule-based, NOT LLM): no prompt template
(`prompt_template_id=None`) and a sentinel `ModelRef`
(`provider="deterministic"`). Never used to build an LLM: the
runtime is a substrate-observation loop, not an LLM subscriber.
- Authorization: the runtime issues four distinct commands through the
Authorize port like any principal. Under the default AllowAllAuthorize
all four are permitted; under TrustAuthorize the operator's single
configured Policy must include this principal + {RecordWitnessedRun,
RecordWitnessedRunOutcome, TruncateRun, ListRuns}. ListRuns is the
restart-rebuild read: without it a restart cannot rediscover which
captures are already open, and would re-promote them. Without the
RecordWitnessedRun grant, a real BEGUN observation logs
`run_translator.promotion_unauthorized` and stays IDLE (retried on
the next BEGUN). Without RecordWitnessedRunOutcome, a real terminal
logs `run_translator.outcome_unauthorized` and leaves the Run open
(retried on the next BEGUN via truncation). Without TruncateRun, a
missed terminal cannot be recovered and logs
`run_translator.truncate_unauthorized`, but the new capture still
promotes regardless.

UNLIKE the RecordWitnessedRunOutcome grant, TruncateRun's decider
carries no `conduct_mode` gate (it accepts any Running-or-Held Run,
same as every other operator-facing terminal). This principal's
safety therefore rests on `_run_translator.py`'s own bookkeeping
discipline: `_truncate_stale` only ever supplies a `run_id` it
popped from its own `_open_captures` dict, which is populated
exclusively by this same runtime's own promotions, so it can only
ever name a Run it created. A future change to `_run_translator.py`
that sources a `run_id` for this call from anywhere else would lose
that guarantee with no decider-level backstop to catch it.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from uuid import UUID

from cora.agent._agent_seed import AgentSeedIdentity, seed_agent
from cora.agent.aggregates.agent import ModelRef

if TYPE_CHECKING:
from cora.infrastructure.kernel import Kernel


# ---------------------------------------------------------------------------
# RunTranslator agent identity (deployment-stable constants)
# ---------------------------------------------------------------------------

# Treat as FOREVER-STABLE. Same change-cost rationale as
# `RUN_SUPERVISOR_AGENT_ID` / `RUN_INITIATOR_AGENT_ID`: changing this
# orphans every prior RunTranslator-authored Run's principal_id pointer.
# UUID continues the numeric-mnemonic range at `5555` (next unclaimed
# block).
RUN_TRANSLATOR_AGENT_ID = UUID("01900000-0000-7000-8000-000055550010")
RUN_TRANSLATOR_AGENT_NAME = "RunTranslator"
RUN_TRANSLATOR_AGENT_KIND = "RunTranslator"
RUN_TRANSLATOR_AGENT_VERSION = "1.0.0"
RUN_TRANSLATOR_AGENT_DESCRIPTION = (
"Deterministic in-process runtime: promotes a real Witnessed "
"Run via record_witnessed_run when it observes an external tool "
"(TomoScan) begin a capture, with per-capture-code dedup so a single "
"in-progress capture is never promoted twice. Not a control path: it "
"never drives the substrate, only translates a capture already "
"begun into CORA's own record."
)


# Sentinel model ref: RunTranslator is rule-based, not an LLM agent. The
# Agent aggregate requires a ModelRef; this value is never used to build
# an LLM (no subscriber / no build_llm call for this agent).
_DETERMINISTIC_MODEL_REF = ModelRef(
provider="deterministic",
model="agent:RunTranslator:v1",
snapshot_pin=None,
)


# ---------------------------------------------------------------------------
# Deterministic IDs for the bootstrap write envelope
# ---------------------------------------------------------------------------

_AGENT_EVENT_ID = UUID("01900000-0000-7000-8000-000055550012")
_ACTOR_EVENT_ID = UUID("01900000-0000-7000-8000-000055550013")
_BOOTSTRAP_CORRELATION_ID = UUID("01900000-0000-7000-8000-000055550014")


async def seed_run_translator_agent(kernel: Kernel) -> None:
"""Seed the RunTranslator Agent + co-registered Actor (idempotent)."""
identity = AgentSeedIdentity(
agent_id=RUN_TRANSLATOR_AGENT_ID,
name=RUN_TRANSLATOR_AGENT_NAME,
kind=RUN_TRANSLATOR_AGENT_KIND,
version=RUN_TRANSLATOR_AGENT_VERSION,
description=RUN_TRANSLATOR_AGENT_DESCRIPTION,
model_ref=_DETERMINISTIC_MODEL_REF,
prompt_template_id=None,
agent_event_id=_AGENT_EVENT_ID,
actor_event_id=_ACTOR_EVENT_ID,
correlation_id=_BOOTSTRAP_CORRELATION_ID,
command_name="SeedRunTranslatorAgent",
)
await seed_agent(kernel, identity)


__all__ = [
"RUN_TRANSLATOR_AGENT_DESCRIPTION",
"RUN_TRANSLATOR_AGENT_ID",
"RUN_TRANSLATOR_AGENT_KIND",
"RUN_TRANSLATOR_AGENT_NAME",
"RUN_TRANSLATOR_AGENT_VERSION",
"seed_run_translator_agent",
]
11 changes: 10 additions & 1 deletion apps/api/src/cora/agent/seed_run_witness.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Bootstrap-time seed for the RunWitness Agent.
"""Bootstrap-time seed for the RunWitness Agent (retired).

Superseded by `seed_run_translator.py`'s `RunTranslator`: same runtime,
same job, renamed because `witness` named the modeling axis this runtime
implements, not what the runtime itself does. This module stays
source-tracked and its boot-time seed call stays in `main.py` forever,
per this identity's own FOREVER-STABLE rule below -- 2-BM's already-
recorded Runs keep `RUN_WITNESS_AGENT_ID` as their `principal_id`
pointer permanently, and a deployment retires this Agent via
`deprecate_agent` rather than losing it from source.

The RunWitness runtime (`cora.api._run_witness`) needs an Agent record
(and its co-registered Actor) to exist at the pinned `RUN_WITNESS_AGENT_ID`
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/cora/api/_capture_baseline_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

Unlike `CaptureProgressFeeder` (buffer + periodic flush over the whole
capture's lifetime), this is a single read-every-PV-once-and-append call,
invoked exactly once per promotion by `RunWitnessRecorder._promote`
invoked exactly once per promotion by `RunTranslator._promote`
immediately after `record_witnessed_run` returns a `run_id`. There is no
buffer, no tick, and no ongoing liveness claim: a baseline reading is a
snapshot of genesis, not a trail, so there is nothing for a heartbeat to
Expand All @@ -30,7 +30,7 @@
way `CaptureProgressFeeder._flush_observations` catches them: a baseline
read that fails must never prevent or unwind the promotion that
triggered it, because by the time this runs the promotion has already
succeeded and `RunWitnessRecorder._open_captures` already reflects it.
succeeded and `RunTranslator._open_captures` already reflects it.

## Numeric and categorical readings, one entry kind

Expand Down Expand Up @@ -153,7 +153,7 @@ async def read(self, capture_code: str, run_id: UUID) -> None:
`AppendObservations` batch against `run_id`.

Concurrent, not sequential: this runs inline inside
`RunWitnessRecorder._promote`, itself on `run_witness_loop`'s
`RunTranslator._promote`, itself on `run_translator_loop`'s
single consumer path, so a slow or partially-unreachable
control system must not block that loop from reacting to the
NEXT lifecycle observation (for a different capture_code, or a
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/cora/api/_capture_experiment_identity_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Slice 14a. Mirrors `_capture_baseline_reader.py`'s ONE-READ-NOT-A-FEED
shape exactly: invoked exactly once per promotion by
`RunWitnessRecorder._promote`, right after `record_witnessed_run`
`RunTranslator._promote`, right after `record_witnessed_run`
returns a `run_id`, alongside (not instead of) the genesis-baseline
read. There is no buffer, no tick, and no ongoing liveness claim.

Expand Down Expand Up @@ -46,7 +46,7 @@
`capture_watch_preflight.py`'s own per-PV independence), and the vault
write's own failure must never unwind or retry the promotion that
already committed (mirroring `_read_baseline`'s exact posture in
`_run_witness.py`).
`_run_translator.py`).

Unlike `CaptureBaselineReader`, none of these three values is personal
data, so a write failure's exception text is logged in full (no
Expand Down Expand Up @@ -147,7 +147,7 @@ async def read(self, capture_code: str, run_id: UUID) -> None:

Concurrent, not sequential: mirrors `CaptureBaselineReader.read`'s
reasoning exactly -- this runs inline inside
`RunWitnessRecorder._promote`, on `run_witness_loop`'s single
`RunTranslator._promote`, on `run_translator_loop`'s single
consumer path, so a slow or partially-unreachable control system
must not block the loop from reacting to the next observation.

Expand Down
Loading
Loading