diff --git a/buf.gen.yaml b/buf.gen.yaml index c2101f2446..0fcf4e0cd8 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -13,6 +13,8 @@ inputs: exclude_paths: - proto/trogonai/agents - proto/trogonai/catalog + - proto/trogonai/evaluation + - proto/trogonai/library - proto/trogonai/policy - proto/trogonai/projects - proto/trogonai/session diff --git a/docs/adr/0031-agent-implementation-and-session-plan.md b/docs/adr/0031-agent-implementation-and-session-plan.md index da96af134b..cd10ce7c78 100644 --- a/docs/adr/0031-agent-implementation-and-session-plan.md +++ b/docs/adr/0031-agent-implementation-and-session-plan.md @@ -217,6 +217,7 @@ Before a Session becomes runnable, create one SessionExecutionPlan: SessionExecutionPlan session_id agent_revision_ref + candidate_ref, when the Session runs a recorded candidate ([ADR#0072](0072-agents-improve-through-recorded-candidates.md)) agent_configuration_ref + configuration_digest agent_implementation_version_ref + definition_digest runtime_settings_type + runtime_settings_digest @@ -816,6 +817,7 @@ configuration does not establish support for every execution mode. | Runtime model settings and derived managed-model selections | No within a revision | Proposal and new revision | | AgentImplementationVersion definition | No | Publish another version | | SessionExecutionPlan | No | Start another Session | +| Candidate | No | Record another candidate | | Implementation availability or revocation | Yes | Live policy with no fallback | | Provider credential behind a stable SecretRef | Yes | Rotate or revoke under [ADR#0023](./0023-secret-management-and-key-custody-direction.md) | | Authorization, grants, and budgets | Yes | Evaluate live without changing pins | diff --git a/docs/adr/0072-agents-improve-through-recorded-candidates.md b/docs/adr/0072-agents-improve-through-recorded-candidates.md new file mode 100644 index 0000000000..c0d727428a --- /dev/null +++ b/docs/adr/0072-agents-improve-through-recorded-candidates.md @@ -0,0 +1,279 @@ +--- +number: "0072" +slug: agents-improve-through-recorded-candidates +status: draft +date: 2026-10-21 +--- + +# ADR#0072: Agents Improve by Running Recorded Candidates, Scored Blind, and Promoted Only Through Proposals + +## Context + +[ADR#0025](./0025-agent-definition-data-ownership.md) settles that an Agent's +configuration is immutable per revision and that change arrives as a proposal +that mints a new revision. [ADR#0031](./0031-agent-implementation-and-session-plan.md) +adds that a Session runs one immutable plan and that V1 has no Session override +of admitted values. Together they leave an agent that wants to improve with +only one move: propose a change and hope. There is no way to try a change on +the same work, measure it, and carry the measurement into the judgment. + +The platform needs that loop, and it needs the loop to stay honest: + +- **A trial must be recorded, not improvised.** If the edit that was tried is + not a durable fact, a score cannot be tied to it and a promotion cannot point + back at what was measured. +- **The improver must not see the yardstick.** An agent that can read its own + rubric, its scores, or the identity of the candidate behind a run can + optimize the measurement instead of the work. +- **A trial must not be able to reach the yardstick over the network, and + nothing may widen what an agent may reach merely by being declared.** +- **Who may judge a change is a policy question.** The proposal verdict has a + single immutable decision, and nothing says whether an agent may record it. + +The product principle behind every decision here is event sourcing: immutable +facts in events, typed pointers pinned by digest everywhere, and temporality +reconstructed from the event history, never by mutating a record. What was true +when is answered by folding a stream up to a position, so nothing here is an +edited row. + +## Decision + +### 1. A candidate is its own immutable stream + +`agents/candidates` holds one stream per candidate. `RecordCandidate` creates it +under a NoStream precondition and emits `CandidateRecorded`; there is no edit +command. A different edit is a different candidate. + +The recorded content is a `Candidate`: a pinned `base_revision_ref` +(`AgentRevisionRef`), an optional `SettingsEdit`, `PlatformEdit`s, and +`StagedSkill`s. Staged skill bytes are not inline: each carries a `ClaimCheck +content_ref`, a locator plus the digest the bytes are verified against. Every +edit must fall inside the `EditableSurface` of the base revision's +implementation, committed by its definition digest, and within its caps; an +edit outside it is refused when the candidate is recorded. + +A candidate is never a revision and is never activated. A revision is minted +only by a proposal and its activation. + +Everything else points at a candidate through `CandidateRef { candidate_id, +content_digest }`. The digest pins the exact recorded bytes, so a reference can +never name different content than the one that was measured. The agents package +owns the full type. The session package keeps its own `CandidateRef` and +`CandidateId` carrying the same values, because the session package must not +import the agents package. + +### 2. Sessions, workflows, and proposals point at the candidate + +- The Session execution plan pins `candidate_ref` alongside `agent_revision_ref` + ([ADR#0031](./0031-agent-implementation-and-session-plan.md) Decision 2). The + plan is not defined in this change; the contract records only that the plan + is where the pin lives. +- `SessionStarted`, `ForkSession`, and `SessionForked` carry `candidate_ref` as + the projection surface, as `checkout_ref` is for the working directory: the + plan stays authoritative, and absent means the registered revision ran + unchanged. `FORK_REASON_PROBE` runs a different plan over the same context + prefix. +- A workflow `AgentStep` takes `candidate_ref` as a `ValueReference` whose + registered contract is `CandidateRef`, so an upstream step's output can feed + the next step. +- `ProposalDefinition.origin_candidate_ref` points a promotion back at the + candidate that was measured. A library skill promotion carries the same + pointer next to its `ProposalId`. + +### 3. Network egress is a policy binding, and absence is deny + +`policy/egress_permissions` mirrors `policy/delegation_permissions`: initialize, +set, and clear commands and events, attached to a `PolicySubject`, with permits +accumulating downward through ancestor nodes +([ADR#0071](./0071-policy-bindings-on-a-subject-entitlements-ceilings-and-holds.md) +Decision 2). A Session with no permission on its Agent or any ancestor node has +no network egress at all, and a connection declaration grants nothing +([ADR#0063](./0063-agent-connection-declarations.md) Decision 5). + +An `EgressDestination` is an exact identity, never a pattern: no wildcard, no +suffix, no range. No declared-connection identifier exists in the agents +contracts today, so the only arm is an exact `EgressHost { host, port }`; an +arm naming a declared connection is an additive change when one exists. +Evaluation storage and rubric storage are never permitted destinations for a +Session under evaluation, whatever the effective set says. + +Egress is evaluated live on each attempt, as the authorization row of +[ADR#0031](./0031-agent-implementation-and-session-plan.md) Decision 8 already +says. What was permitted when is the policy stream's history, not a field on +the Session. + +### 4. Verdict authority is a policy binding, and absence means a human + +`policy/verdict_authorities` mirrors the same layout. Per subject, per +`AgentChangeClass`, a rule names the authority required to record a proposal +verdict: `HUMAN_MEMBER` or `HUMAN_MEMBER_OR_REGISTERED_AGENT`. The nearest +binding on the chain that lists a class decides it, starting at the proposal's +Agent and walking up through its ancestor nodes. No binding means human member +for every class, so the default fails closed. + +The charter class may never be relaxed below human member. The policy message +states this as a CEL constraint. The issuer of a verdict is read from the event +envelope ([ADR#0069](./0069-event-envelope-records-issuer-and-times.md)); no +verdict field names its own author. + +### 5. Scoring lives in evaluation streams, and rubrics are versioned + +- `evaluation/rubrics`: `RegisterRubric` fixes an identity and display name; + `AddRubricVersion` appends an immutable version whose criteria are a + `ClaimCheck criteria_ref`, plus an optional `ClaimCheck + withheld_vocabulary_ref` naming terms that would reveal how a run is scored, + and rejects a repeated exact version string; + `RetireRubric` stops new versions and bindings. A pinned reference is + `RubricVersionRef { rubric_id, version }`. +- `evaluation/bindings`: an `EvaluationBinding` pins a `RubricVersionRef` and is + immutable once bound. Rebinding is an unbind followed by a new binding under + a new `EvaluationBindingId`. +- `RecordEvaluationResult` records an `EvaluationResultId`, the binding, an + `evaluated_ref`, an optional `candidate_ref`, the `Score` inline (the score is + the fact), an optional `ClaimCheck rationale_ref`, and a content digest. The + evaluated run is a temporal pointer, `SessionPositionRef { session_id, + through_ordinal }`: that session's log as of that ordinal. +- `evaluation/comparisons`: `OpenComparison` fixes a base revision and a task by + `ClaimCheck task_ref`; `EnrollComparisonRun` assigns a `CandidateLabel` to a + session and optionally a `CandidateRef`; `CloseComparison` ends enrollment. A + label is assigned once, is immutable, and is unique within its comparison. + The label-to-session mapping lives only in this stream and is never exposed + to the improver. `screening_rubric_refs` pins the rubric versions whose + withheld vocabulary screens the task and every rendered run item: admission + refuses a matching task, and a matching item renders as + `ELISION_REASON_WITHHELD_VOCABULARY`, which never says which term or rubric + matched. +- The session run-history query takes a `ComparisonId` and a `CandidateLabel`, + defined locally in the queries package so the session package imports no + evaluation type. It carries no score, rubric, verdict, candidate identity, or + session id, and the server resolves labels through the comparison stream. + +Scores, rubrics, bindings, and comparisons never enter agent, proposal, or +session streams and are never readable through their queries +([ADR#0024](./0024-agent-platform-stream-topology.md), +[ADR#0025](./0025-agent-definition-data-ownership.md)). A verdict cites +`EvaluationResultRef { evaluation_result_id, content_digest }`; the score +itself never leaves the evaluation stream. + +### 6. Pointers are typed, and references are value objects + +Under [ADR#0068](./0068-reference-fields-say-they-are-references.md), every +identifier in the new packages is a single-field id message, never a bare +string: `CandidateId`, `RubricId`, `EvaluationBindingId`, `EvaluationResultId`, +`ComparisonId`, `CandidateLabel`, `SkillId`, `MemoryId`, `ProposalId`, and +`SessionId`. Out-of-line bytes are a `ClaimCheck` under a `*_ref` name. Existing +`session_id` and `proposal_id` strings on existing messages are not migrated; +only new fields use the new types. + +Two existing fields change type in place, keeping their field numbers. These +contracts are excluded from codegen and have no deployed readers, so the break +is taken directly rather than reserving the old numbers: + +- `EvaluationResultRef.evaluation_result_id` becomes an `EvaluationResultId`. +- `SkillVersionRef.skill_id` becomes a `SkillId`. + +`SkillId` lives in a leaf package that imports nothing from agents or from the +skills module, because the agents `SkillVersionRef` uses it while the skills +module imports agents, and a package used from both directions is an import +cycle. + +### 7. Temporality is the event history + +Nothing records "current" state on a fact. A candidate, a rubric version, a +binding, a result, and an enrollment are each immutable once recorded; a change +of mind is a new fact. The envelope owns issuer and time +([ADR#0069](./0069-event-envelope-records-issuer-and-times.md)), so none of +these messages carries an actor or a timestamp. A reader reconstructs what was +true at a moment by folding the relevant stream, or a session's log through a +`SessionPositionRef`, up to that point. + +### 8. The library records provenance, not copies + +Library skills and memories hold content by `ClaimCheck content_ref`. A skill +version event carries the agents `SkillVersionRef` as `version_ref`. Skill +provenance is either a promotion, naming the `ProposalId` that promoted it and, +when the content was measured as a candidate, the `CandidateRef`, or an +authoring Agent. A memory's provenance names the author, the +`AgentRevisionRef produced_by_revision_ref`, and the `ProposalId`. +`SupersedeMemory` carries `memory_id` and `superseded_by_id`. + +### 9. Improver and selector instructions are ordinary agent configuration + +The agents that propose and select candidates are ordinary Agents. Their +instructions are `PlatformHarnessConfiguration.instructions`, so changing them +is an Agent revision like any other, and they reach the platform only through +`RecordCandidate`, `OpenProposal`, and `RecordProposalVerdict` under policy. +Instruction text adapted from third-party work carries `InstructionSource { +source_uri, spdx_license_id, attribution_notice }` in the same configuration, so +the license obligation is pinned by the same digest as the text it covers. + +## Alternatives Rejected + +### A Session-scoped override of admitted values + +Rejected. An override that exists only inside a Session's plan is not a durable +fact, so a score cannot point at it and a promotion cannot point back to it. It +also contradicts the rule that a Session runs one immutable plan. + +### Minting an Agent revision per candidate + +Rejected. A revision is the unit that an operator activates and reverts, and +minting one per trial would fill the revision history with configurations +nobody chose and make activation ambiguous. A candidate is a trial; a revision +is a decision. + +### Scores on the proposal or session streams + +Rejected. A score readable from a stream the improver can query is a yardstick +the improver can optimize. Keeping scores, rubrics, bindings, and the label +mapping in evaluation streams lets the run-history query stay score-free by +construction. + +### Network egress open by default + +Rejected. A declaration grants nothing, and an open default would let a +candidate under evaluation reach the stores that hold its own rubric and scores. +Absence is deny, as for delegation. + +### Letting an agent judge every class of change + +Rejected. Absence of a verdict authority binding means a human member, and the +charter class cannot be relaxed, so an Agent cannot approve a change to its own +charter. + +## Consequences + +- Candidates, rubrics, comparisons, egress permissions, and verdict authorities + are new streams. Until enforcement lands, each is a reviewable record without + effect. +- An Agent with no egress permission on its chain has no network egress, and an + Agent with no verdict authority binding on its chain has its proposals judged + by a human member. +- Dispatch of `RecordCandidate` must resolve the base revision's editable + surface and verify every edit against it and its caps. +- Dispatch of `BindRubric` verifies the rubric version exists on a rubric that + is not retired and that the judge differs from the subject. +- Rendering a run history must resolve the comparison's screening rubric + versions and elide matching items before they reach the reader, and + `OpenComparison` dispatch must screen the task. +- Library skill and memory content move out of line. Resolution fetches the + bytes through the claim check and verifies the digest before use. +- `buf breaking` against `main` reports the type changes on + `EvaluationResultRef` and `SkillVersionRef`. This is expected while + `proto/trogonai/agents`, `proto/trogonai/evaluation`, and + `proto/trogonai/library` remain excluded from codegen in `buf.gen.yaml`; no + compiled caller observes the break. +- The Session execution plan is defined separately and must pin `candidate_ref` + alongside `agent_revision_ref`. +- `SessionId` and `ProposalId` coexist with older messages that carry the same + identifier as a bare string. A later migration can retire the strings. + +## References + +- [ADR#0024: Agent Platform Stream Topology](./0024-agent-platform-stream-topology.md) +- [ADR#0025: Agent Definition Data Ownership](./0025-agent-definition-data-ownership.md) +- [ADR#0031: Agent Implementation and Session Plan](./0031-agent-implementation-and-session-plan.md) +- [ADR#0063: An Agent Declares the Connections It Needs](./0063-agent-connection-declarations.md) +- [ADR#0068: A Reference Field Says It Is a Reference in Both Its Name and Its Type](./0068-reference-fields-say-they-are-references.md) +- [ADR#0069: Every Event Envelope Records Who Caused It and When Each Step Happened](./0069-event-envelope-records-issuer-and-times.md) +- [ADR#0071: What an Agent May Reach, How Far It May Go, and Whether It May Run at All Are Policy Bindings on a Subject](./0071-policy-bindings-on-a-subject-entitlements-ceilings-and-holds.md) diff --git a/docs/adr/index.md b/docs/adr/index.md index f3cbf0f36b..cfb1dc5116 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -77,3 +77,4 @@ future implementation work. - [ADR#0069: Every Event Envelope Records Who Caused It and When Each Step Happened (Draft)](./0069-event-envelope-records-issuer-and-times.md) - [ADR#0070: An Agent Declares What It Offers and Whose Authority It Carries; Throughput Ceilings Stay on the Policy Plane (Draft)](./0070-agent-offered-interface-and-throughput-limits.md) - [ADR#0071: What an Agent May Reach, How Far It May Go, and Whether It May Run at All Are Policy Bindings on a Subject (Draft)](./0071-policy-bindings-on-a-subject-entitlements-ceilings-and-holds.md) +- [ADR#0072: Agents Improve by Running Recorded Candidates, Scored Blind, and Promoted Only Through Proposals (Draft)](./0072-agents-improve-through-recorded-candidates.md) diff --git a/proto/trogonai/agents/agents/v1alpha1/dependencies.proto b/proto/trogonai/agents/agents/v1alpha1/dependencies.proto index f4f48268fb..0dcbd97337 100644 --- a/proto/trogonai/agents/agents/v1alpha1/dependencies.proto +++ b/proto/trogonai/agents/agents/v1alpha1/dependencies.proto @@ -4,6 +4,7 @@ package trogonai.agents.agents.v1alpha1; import "buf/validate/validate.proto"; import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/library/skills/ids/v1alpha1/skill_id.proto"; // These declarations are revision-owned. Session admission resolves them under // live hierarchy and policy, then records exact references and digests. A @@ -54,10 +55,7 @@ message SkillDeclaration { } message SkillVersionRef { - string skill_id = 1 [ - features.field_presence = LEGACY_REQUIRED, - (buf.validate.field).string.min_len = 1 - ]; + trogonai.library.skills.ids.v1alpha1.SkillId skill_id = 1 [features.field_presence = LEGACY_REQUIRED]; // Exact immutable version identifier. Empty values and moving aliases are invalid. string version = 2 [ features.field_presence = LEGACY_REQUIRED, diff --git a/proto/trogonai/agents/agents/v1alpha1/editable_surface.proto b/proto/trogonai/agents/agents/v1alpha1/editable_surface.proto new file mode 100644 index 0000000000..439a461455 --- /dev/null +++ b/proto/trogonai/agents/agents/v1alpha1/editable_surface.proto @@ -0,0 +1,53 @@ +edition = "2024"; + +package trogonai.agents.agents.v1alpha1; + +import "buf/validate/validate.proto"; + +// What a candidate may change for one implementation version. +// Anything not listed is not editable. The set is committed by the +// implementation definition digest, so the surface cannot change under a +// pinned revision. Absent on an implementation means it opts out of candidates. +message EditableSurface { + option (buf.validate.message).cel = { + id: "editable_surface.settings_paths_unique" + message: "each settings path may be listed at most once" + expression: "this.settings_paths.map(p, p.path).unique()" + }; + repeated EditableSettingsPath settings_paths = 1; + // Each PlatformEdit change case of a candidate corresponds to one PlatformComponent value. + repeated PlatformComponent platform_components = 2 [(buf.validate.field).repeated = { + unique: true + items: { + enum: { + defined_only: true + not_in: [0] + } + } + }]; + EditCaps caps = 3 [features.field_presence = LEGACY_REQUIRED]; +} + +// Path is a field path in the runtime settings message. A path covers its +// whole subtree. Only learned-layer paths are listable; model selections, +// runtime identity, required declarations, connections, variables schema and +// grant or budget data never are. +message EditableSettingsPath { + string path = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + uint32 max_bytes = 2 [features.field_presence = LEGACY_REQUIRED]; +} + +enum PlatformComponent { + PLATFORM_COMPONENT_UNSPECIFIED = 0; + PLATFORM_COMPONENT_SKILL_SET = 1; + PLATFORM_COMPONENT_MEMORY_KINDS = 2; +} + +message EditCaps { + uint32 max_staged_skills = 1 [features.field_presence = LEGACY_REQUIRED]; + uint32 max_staged_skill_bytes = 2 [features.field_presence = LEGACY_REQUIRED]; + uint32 max_candidate_bytes = 3 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/agents/v1alpha1/implementation.proto b/proto/trogonai/agents/agents/v1alpha1/implementation.proto index 641bff1fea..17daed0778 100644 --- a/proto/trogonai/agents/agents/v1alpha1/implementation.proto +++ b/proto/trogonai/agents/agents/v1alpha1/implementation.proto @@ -5,6 +5,7 @@ package trogonai.agents.agents.v1alpha1; import "buf/validate/validate.proto"; import "google/protobuf/any.proto"; import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/agents/v1alpha1/editable_surface.proto"; import "trogonai/agents/agents/v1alpha1/model_selection.proto"; message PlatformHarnessVersionRef { @@ -52,6 +53,29 @@ message PlatformHarnessConfiguration { // interactions must not be independently configured again as tool dependencies. // Capability changes are charter-class, including when instructions also change. repeated google.protobuf.Any capabilities = 3 [(buf.validate.field).repeated.items.any.not_in = ""]; + // Where adapted parts of `instructions` came from, so the license obligations + // travel with the exact text they cover. Behavior-neutral: the harness never + // reads them, and they change only with the instructions they describe. + repeated InstructionSource instruction_sources = 4; +} + +// InstructionSource attributes adapted third-party instruction text. +message InstructionSource { + // A stable locator of the upstream work, pinned to a release or commit. + string source_uri = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.uri = true + ]; + // SPDX license identifier of the upstream work, such as Apache-2.0. + string spdx_license_id = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + // The attribution or NOTICE text the license requires to be retained. + string attribution_notice = 3 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; } // The definition commits transitively to every behavior-significant artifact, @@ -70,4 +94,7 @@ message AgentImplementation { RegisteredExtensionVersionRef registered_extension = 2; } Digest definition_digest = 3 [features.field_presence = LEGACY_REQUIRED]; + // Absent means nothing is editable by a candidate. Committed by + // definition_digest. + EditableSurface editable_surface = 4; } diff --git a/proto/trogonai/agents/candidates/v1alpha1/candidate.proto b/proto/trogonai/agents/candidates/v1alpha1/candidate.proto new file mode 100644 index 0000000000..da2ef3ffcf --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/candidate.proto @@ -0,0 +1,93 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/field_mask.proto"; +import "trogonai/agents/agents/v1alpha1/agent_revision.proto"; +import "trogonai/agents/candidates/v1alpha1/claim_check.proto"; + +// The content of a candidate: a delta over a base revision, recorded as an +// immutable fact and never stored as a revision or activated. A session that +// runs it pins a CandidateRef in its admitted plan. Every edit must fall inside +// the EditableSurface of the base revision's implementation; edits outside it +// are rejected when the candidate is recorded. Omitted means inherited from the +// base revision. +message Candidate { + option (buf.validate.message).cel = { + id: "candidate.has_edit" + message: "a candidate must carry at least one edit" + expression: "has(this.settings_edit) || size(this.platform_edits) > 0 || size(this.staged_skills) > 0" + }; + option (buf.validate.message).cel = { + id: "candidate.platform_components_unique" + message: "each platform component may be edited at most once" + expression: "this.platform_edits.filter(e, has(e.skill_set)).size() <= 1 && this.platform_edits.filter(e, has(e.memory_kinds)).size() <= 1" + }; + option (buf.validate.message).cel = { + id: "candidate.staged_skill_bindings_unique" + message: "each skill binding name may be staged at most once" + expression: "this.staged_skills.map(s, s.binding_name).unique()" + }; + + trogonai.agents.agents.v1alpha1.AgentRevisionRef base_revision_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + SettingsEdit settings_edit = 2; + repeated PlatformEdit platform_edits = 3; + repeated StagedSkill staged_skills = 4; +} + +// Partial runtime settings of the same message type as the base settings. +// Only the paths in `paths` are replaced, wholesale per path. Repeated and map +// fields are replaced as a unit, never merged element by element. +message SettingsEdit { + option (buf.validate.message).cel = { + id: "settings_edit.has_paths" + message: "a settings edit must replace at least one path" + expression: "size(this.paths.paths) > 0" + }; + google.protobuf.Any partial_settings = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).any.not_in = "" + ]; + google.protobuf.FieldMask paths = 2 [features.field_presence = LEGACY_REQUIRED]; +} + +// The change case is the component being edited. +message PlatformEdit { + oneof change { + option (buf.validate.oneof).required = true; + + SkillSetReplacement skill_set = 1; + MemoryKindReplacement memory_kinds = 2; + } +} + +message SkillSetReplacement { + repeated string binding_names = 1 [(buf.validate.field).repeated = { + unique: true + items: { + string: {min_len: 1} + } + }]; +} + +message MemoryKindReplacement { + repeated string kinds = 1 [(buf.validate.field).repeated = { + unique: true + items: { + string: {min_len: 1} + } + }]; +} + +// Skill content staged for this candidate only. It has no stream identity until +// a promotion registers it in the library. The bytes are held out of line; the +// claim check's digest is what a later SkillDeclaration must match. +message StagedSkill { + string binding_name = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + ClaimCheck content_ref = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/candidate_id.proto b/proto/trogonai/agents/candidates/v1alpha1/candidate_id.proto new file mode 100644 index 0000000000..3f88938f69 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/candidate_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "buf/validate/validate.proto"; + +// CandidateId is the opaque identity of one recorded candidate stream. +message CandidateId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/candidate_recorded.proto b/proto/trogonai/agents/candidates/v1alpha1/candidate_recorded.proto new file mode 100644 index 0000000000..09f3c9c6a6 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/candidate_recorded.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_id.proto"; + +// The creation fact for a candidate stream. The content and its digest are +// immutable; the digest is what every CandidateRef pins. The envelope owns who +// recorded it and when (ADR#0069). +message CandidateRecorded { + CandidateId candidate_id = 1 [features.field_presence = LEGACY_REQUIRED]; + Candidate candidate = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest content_digest = 3 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/candidate_ref.proto b/proto/trogonai/agents/candidates/v1alpha1/candidate_ref.proto new file mode 100644 index 0000000000..7e63b45342 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/candidate_ref.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_id.proto"; + +// A typed pointer to one recorded candidate, pinned by the digest of its +// content. A candidate is immutable, so the pin can never name different bytes +// than the ones that were measured. +message CandidateRef { + CandidateId candidate_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest content_digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/claim_check.proto b/proto/trogonai/agents/candidates/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..1b4dd99114 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/events.proto b/proto/trogonai/agents/candidates/v1alpha1/events.proto new file mode 100644 index 0000000000..077935b281 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/events.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_recorded.proto"; + +// An unset or unknown event rejects replay; skipping a recording could let a +// pinned CandidateRef resolve to nothing. +message CandidateEvent { + oneof event { + option (buf.validate.oneof).required = true; + + CandidateRecorded candidate_recorded = 1; + } +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/faults.proto b/proto/trogonai/agents/candidates/v1alpha1/faults.proto new file mode 100644 index 0000000000..2f5194368e --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/faults.proto @@ -0,0 +1,48 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the agents.candidates module's DeciderService +// refusals. These messages are never encoded. + +message CandidateCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "agents.candidates" + reason: "CANDIDATE_COMMAND_INVALID" + message: "The command violates the candidate contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message CandidateAlreadyRecordedError { + option (trogon.error.v1alpha1.message).template = { + domain: "agents.candidates" + reason: "CANDIDATE_ALREADY_RECORDED" + message: "The candidate has already been recorded." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message CandidateEditOutsideEditableSurfaceError { + option (trogon.error.v1alpha1.message).template = { + domain: "agents.candidates" + reason: "CANDIDATE_EDIT_OUTSIDE_EDITABLE_SURFACE" + message: "The candidate edits something the base revision's editable surface does not list." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message CandidateBaseRevisionNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "agents.candidates" + reason: "CANDIDATE_BASE_REVISION_NOT_FOUND" + message: "The base revision named by the candidate does not exist." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/agents/candidates/v1alpha1/record_candidate.proto b/proto/trogonai/agents/candidates/v1alpha1/record_candidate.proto new file mode 100644 index 0000000000..31c151ab49 --- /dev/null +++ b/proto/trogonai/agents/candidates/v1alpha1/record_candidate.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.agents.candidates.v1alpha1; + +import "trogonai/agents/candidates/v1alpha1/candidate.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_id.proto"; + +// Records one candidate as an immutable fact. NoStream creation requires a +// nonempty Decider command_id and no expected_revision; retries preserve the id. +// An already-recorded candidate_id is rejected, never replaced: a different edit +// is a new candidate. Dispatch verifies the base revision exists, that every +// edit falls inside the EditableSurface of the base revision's implementation +// and within its caps, and computes the content digest over the exact encoded +// Candidate bytes. +message RecordCandidate { + CandidateId candidate_id = 1 [features.field_presence = LEGACY_REQUIRED]; + Candidate candidate = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/agents/proposals/v1alpha1/proposal.proto b/proto/trogonai/agents/proposals/v1alpha1/proposal.proto index 29b3866693..0dbb19b26c 100644 --- a/proto/trogonai/agents/proposals/v1alpha1/proposal.proto +++ b/proto/trogonai/agents/proposals/v1alpha1/proposal.proto @@ -5,7 +5,9 @@ package trogonai.agents.proposals.v1alpha1; import "buf/validate/validate.proto"; import "trogonai/agents/agents/v1alpha1/agent_revision.proto"; import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; import "trogonai/agents/proposals/v1alpha1/configuration_difference.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto"; message ProposalEvidenceRef { string evidence_id = 1 [ @@ -15,11 +17,10 @@ message ProposalEvidenceRef { trogonai.agents.agents.v1alpha1.Digest content_digest = 2 [features.field_presence = LEGACY_REQUIRED]; } +// Points at one immutable evaluation result. The score itself never leaves the +// evaluation stream. message EvaluationResultRef { - string evaluation_result_id = 1 [ - features.field_presence = LEGACY_REQUIRED, - (buf.validate.field).string.min_len = 1 - ]; + trogonai.evaluation.bindings.v1alpha1.EvaluationResultId evaluation_result_id = 1 [features.field_presence = LEGACY_REQUIRED]; trogonai.agents.agents.v1alpha1.Digest content_digest = 2 [features.field_presence = LEGACY_REQUIRED]; } @@ -37,6 +38,9 @@ message ProposalDefinition { repeated ProposalEvidenceRef evidence = 4; // Supersession does not close another stream; its author withdraws it separately. string supersedes_proposal_id = 5 [(buf.validate.field).string.min_len = 1]; + // The recorded candidate whose measured run this proposal promotes, so the + // promotion points back at what was measured. + trogonai.agents.candidates.v1alpha1.CandidateRef origin_candidate_ref = 6; } message ProposalJudgment { diff --git a/proto/trogonai/agents/proposals/v1alpha1/proposal_id.proto b/proto/trogonai/agents/proposals/v1alpha1/proposal_id.proto new file mode 100644 index 0000000000..9c8a89848c --- /dev/null +++ b/proto/trogonai/agents/proposals/v1alpha1/proposal_id.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.agents.proposals.v1alpha1; + +import "buf/validate/validate.proto"; + +// ProposalId is the identity of one proposal stream. New fields that point at a +// proposal use it; existing proposal_id strings are not migrated. +message ProposalId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/bind_rubric.proto b/proto/trogonai/evaluation/bindings/v1alpha1/bind_rubric.proto new file mode 100644 index 0000000000..347a51a605 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/bind_rubric.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/evaluation/bindings/v1alpha1/binding.proto"; + +// Dispatch verifies the rubric version exists on a rubric that is not retired, +// and that the judge differs from the subject. NoStream creation requires a nonempty Decider command_id and no +// expected_revision; retries preserve the id. An already-bound binding_id is +// rejected, never replaced, and an unbound binding_id is never reused. +message BindRubric { + EvaluationBinding binding = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/binding.proto b/proto/trogonai/evaluation/bindings/v1alpha1/binding.proto new file mode 100644 index 0000000000..0d13cc4ae0 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/binding.proto @@ -0,0 +1,23 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/identity.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Attaches one exact rubric version to a subject for evaluation purposes. The +// binding is immutable once bound: it pins the rubric version, never the rubric, +// so what a result was measured against never changes. Rebinding is an unbind +// followed by a new binding under a new binding_id. The judge is a distinct +// principal from every agent it evaluates: a judge that is the subject Agent, or +// an Agent under the subject node, is rejected (ADR#0024). Bindings never enter +// agent or proposal streams, and are never readable through agent or session +// queries. +message EvaluationBinding { + EvaluationBindingId binding_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.evaluation.rubrics.v1alpha1.RubricVersionRef rubric_version_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 3 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.AgentId judge = 4 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/claim_check.proto b/proto/trogonai/evaluation/bindings/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..850416bc62 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto new file mode 100644 index 0000000000..14e4ecae8b --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "buf/validate/validate.proto"; + +// EvaluationBindingId is the identity of one evaluation binding stream. +message EvaluationBindingId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto new file mode 100644 index 0000000000..c869886744 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "buf/validate/validate.proto"; + +// EvaluationResultId is the identity of one recorded evaluation result. A proposal verdict cites it through EvaluationResultRef. +message EvaluationResultId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_recorded.proto b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_recorded.proto new file mode 100644 index 0000000000..148c31822e --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/evaluation_result_recorded.proto @@ -0,0 +1,23 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; +import "trogonai/evaluation/bindings/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto"; +import "trogonai/evaluation/bindings/v1alpha1/score.proto"; +import "trogonai/session/sessions/v1alpha1/session_position_ref.proto"; + +// Immutable measurement evidence for one binding. The envelope owns the time of +// measurement and who judged. +message EvaluationResultRecorded { + EvaluationResultId evaluation_result_id = 1 [features.field_presence = LEGACY_REQUIRED]; + EvaluationBindingId binding_id = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.session.sessions.v1alpha1.SessionPositionRef evaluated_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.candidates.v1alpha1.CandidateRef candidate_ref = 4; + Score score = 5 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck rationale_ref = 6; + trogonai.agents.agents.v1alpha1.Digest content_digest = 7 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/events.proto b/proto/trogonai/evaluation/bindings/v1alpha1/events.proto new file mode 100644 index 0000000000..4c99cc70f1 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_result_recorded.proto"; +import "trogonai/evaluation/bindings/v1alpha1/rubric_bound.proto"; +import "trogonai/evaluation/bindings/v1alpha1/rubric_unbound.proto"; + +// An unset or unknown event rejects replay; skipping a result could change which +// candidate a later verdict is judged to have beaten. +message EvaluationBindingEvent { + oneof event { + option (buf.validate.oneof).required = true; + + RubricBound rubric_bound = 1; + RubricUnbound rubric_unbound = 2; + EvaluationResultRecorded evaluation_result_recorded = 3; + } +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/faults.proto b/proto/trogonai/evaluation/bindings/v1alpha1/faults.proto new file mode 100644 index 0000000000..7b2ac01960 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/faults.proto @@ -0,0 +1,68 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the evaluation.bindings module's DeciderService +// refusals. These messages are never encoded. + +message EvaluationBindingCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_BINDING_COMMAND_INVALID" + message: "The command violates the evaluation binding contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EvaluationBindingAlreadyExistsError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_BINDING_ALREADY_EXISTS" + message: "The evaluation binding already exists." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EvaluationBindingNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_BINDING_NOT_FOUND" + message: "The evaluation binding does not exist or is unbound." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EvaluationBindingRubricVersionNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_BINDING_RUBRIC_VERSION_NOT_FOUND" + message: "The rubric version named by the binding does not exist or its rubric is retired." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EvaluationBindingJudgeMatchesSubjectError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_BINDING_JUDGE_MATCHES_SUBJECT" + message: "The judge must be distinct from every agent it evaluates." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EvaluationResultAlreadyRecordedError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.bindings" + reason: "EVALUATION_RESULT_ALREADY_RECORDED" + message: "The evaluation result has already been recorded." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/record_evaluation_result.proto b/proto/trogonai/evaluation/bindings/v1alpha1/record_evaluation_result.proto new file mode 100644 index 0000000000..b8cde47dac --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/record_evaluation_result.proto @@ -0,0 +1,30 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/digest.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; +import "trogonai/evaluation/bindings/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto"; +import "trogonai/evaluation/bindings/v1alpha1/evaluation_result_id.proto"; +import "trogonai/evaluation/bindings/v1alpha1/score.proto"; +import "trogonai/session/sessions/v1alpha1/session_position_ref.proto"; + +// Records one judged result against a live binding. evaluated_ref is a temporal +// pointer to the run that was measured: that session's log as of that ordinal. +// candidate_ref, when present, ties the score to the recorded candidate that +// run executed. The evaluation_result_id and content_digest are what a proposal +// verdict later cites through its EvaluationResultRef; the score itself never +// leaves this stream. A repeated evaluation_result_id is rejected, never +// overwritten. +message RecordEvaluationResult { + EvaluationResultId evaluation_result_id = 1 [features.field_presence = LEGACY_REQUIRED]; + EvaluationBindingId binding_id = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.session.sessions.v1alpha1.SessionPositionRef evaluated_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.candidates.v1alpha1.CandidateRef candidate_ref = 4; + // The score is the fact and is recorded inline. + Score score = 5 [features.field_presence = LEGACY_REQUIRED]; + // The judge's reasoning, held out of line. + ClaimCheck rationale_ref = 6; + trogonai.agents.agents.v1alpha1.Digest content_digest = 7 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/rubric_bound.proto b/proto/trogonai/evaluation/bindings/v1alpha1/rubric_bound.proto new file mode 100644 index 0000000000..c75b61ba69 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/rubric_bound.proto @@ -0,0 +1,10 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/evaluation/bindings/v1alpha1/binding.proto"; + +// Genesis fixes the rubric version, subject, and judge for this history. +message RubricBound { + EvaluationBinding binding = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/rubric_unbound.proto b/proto/trogonai/evaluation/bindings/v1alpha1/rubric_unbound.proto new file mode 100644 index 0000000000..17422909bb --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/rubric_unbound.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto"; + +// Results recorded before the unbind remain valid evidence. An already unbound +// history emits no second unbind. +message RubricUnbound { + EvaluationBindingId binding_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/score.proto b/proto/trogonai/evaluation/bindings/v1alpha1/score.proto new file mode 100644 index 0000000000..ed29f246a7 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/score.proto @@ -0,0 +1,39 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "buf/validate/validate.proto"; + +// A per-criterion value. The criterion name is meaningful only against the +// bound rubric. +message CriterionScore { + string criterion = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + double value = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).double.finite = true + ]; +} + +message ScoreVector { + option (buf.validate.message).cel = { + id: "score_vector.criteria_unique" + message: "each criterion may appear at most once" + expression: "this.criteria.map(c, c.criterion).unique()" + }; + + repeated CriterionScore criteria = 1 [(buf.validate.field).repeated.min_items = 1]; +} + +// A measurement. Scores exist only in the evaluation streams and must never be +// readable via agent, proposal, or session queries (ADR#0024). +message Score { + oneof value { + option (buf.validate.oneof).required = true; + + double scalar = 1 [(buf.validate.field).double.finite = true]; + ScoreVector vector = 2; + } +} diff --git a/proto/trogonai/evaluation/bindings/v1alpha1/unbind_rubric.proto b/proto/trogonai/evaluation/bindings/v1alpha1/unbind_rubric.proto new file mode 100644 index 0000000000..e3b2c23bf5 --- /dev/null +++ b/proto/trogonai/evaluation/bindings/v1alpha1/unbind_rubric.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.evaluation.bindings.v1alpha1; + +import "trogonai/evaluation/bindings/v1alpha1/evaluation_binding_id.proto"; + +// Ends the binding without erasing recorded results. Dispatch requires the +// original positive expected_revision; an already unbound binding succeeds +// without an event after that concurrency check. +message UnbindRubric { + EvaluationBindingId binding_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/candidate_label.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/candidate_label.proto new file mode 100644 index 0000000000..b2da56ea38 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/candidate_label.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; + +// CandidateLabel is the blind label a run carries within one comparison. It is a value object assigned once, and it reveals nothing about candidate identity or origin. +message CandidateLabel { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/claim_check.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..9a96eede79 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/close_comparison.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/close_comparison.proto new file mode 100644 index 0000000000..6b1a0a9c7b --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/close_comparison.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; + +// Closing stops further enrollment and keeps every enrolled run. Dispatch +// requires the original positive expected_revision; an already closed +// comparison succeeds without an event after that concurrency check. +message CloseComparison { + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_closed.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_closed.proto new file mode 100644 index 0000000000..b04ef6b668 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_closed.proto @@ -0,0 +1,10 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; + +// An already closed history emits no second close. +message ComparisonClosed { + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto new file mode 100644 index 0000000000..319b437347 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; + +// ComparisonId is the identity of one comparison stream. +message ComparisonId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_opened.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_opened.proto new file mode 100644 index 0000000000..bff8810c03 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_opened.proto @@ -0,0 +1,28 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/agent_revision.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto"; + +// Genesis fixes the base revision and the task for this history. +message ComparisonOpened { + option (buf.validate.message).cel = { + id: "comparison_opened.screening_rubric_refs_unique" + message: "each rubric version may screen a comparison at most once" + expression: "this.screening_rubric_refs.all(a, this.screening_rubric_refs.filter(b, b.rubric_id.value == a.rubric_id.value && b.version == a.version).size() == 1)" + }; + + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.AgentRevisionRef base_revision_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck task_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + // Rubric versions whose withheld vocabulary the task and every rendered run + // item are screened against. Admission refuses a task that matches; a run + // item that matches is rendered as ELISION_REASON_WITHHELD_VOCABULARY. Pinned + // by exact version, so the screen a comparison applied stays reproducible. + // Evaluation-side only: never readable through a session or run query. + repeated trogonai.evaluation.rubrics.v1alpha1.RubricVersionRef screening_rubric_refs = 4; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_run_enrolled.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_run_enrolled.proto new file mode 100644 index 0000000000..d69c8311a6 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/comparison_run_enrolled.proto @@ -0,0 +1,17 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/candidate_label.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; +import "trogonai/session/sessions/v1alpha1/session_id.proto"; + +// The label, session, and candidate are immutable once enrolled. The mapping +// recorded here is never exposed to the improver. +message ComparisonRunEnrolled { + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; + CandidateLabel label = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.session.sessions.v1alpha1.SessionId session_id = 3 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.candidates.v1alpha1.CandidateRef candidate_ref = 4; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/enroll_comparison_run.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/enroll_comparison_run.proto new file mode 100644 index 0000000000..9e13711107 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/enroll_comparison_run.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/candidate_label.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; +import "trogonai/session/sessions/v1alpha1/session_id.proto"; + +// Enrolls one session as a run of the comparison under a label. The label is +// assigned once and is immutable, and it is unique within the comparison. This +// stream is the only place the label-to-session mapping lives; it is never +// exposed to the improver, and run-history queries resolve labels through it. +// candidate_ref, when present, names the recorded candidate the session ran; +// absent means the registered base revision ran unchanged. Dispatch requires the +// original positive expected_revision and an open comparison. +message EnrollComparisonRun { + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; + CandidateLabel label = 2 [features.field_presence = LEGACY_REQUIRED]; + trogonai.session.sessions.v1alpha1.SessionId session_id = 3 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.candidates.v1alpha1.CandidateRef candidate_ref = 4; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/events.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/events.proto new file mode 100644 index 0000000000..256139433b --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_closed.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_opened.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_run_enrolled.proto"; + +// An unset or unknown event rejects replay; skipping an enrollment could make a +// run history resolve a label to the wrong session. +message ComparisonEvent { + oneof event { + option (buf.validate.oneof).required = true; + + ComparisonOpened comparison_opened = 1; + ComparisonRunEnrolled comparison_run_enrolled = 2; + ComparisonClosed comparison_closed = 3; + } +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/faults.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/faults.proto new file mode 100644 index 0000000000..a9216badb6 --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/faults.proto @@ -0,0 +1,78 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the evaluation.comparisons module's DeciderService +// refusals. These messages are never encoded. + +message ComparisonCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_COMMAND_INVALID" + message: "The command violates the comparison contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonAlreadyOpenedError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_ALREADY_OPENED" + message: "The comparison has already been opened." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_NOT_FOUND" + message: "The comparison has not been opened." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonClosedError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_CLOSED" + message: "The comparison is closed and accepts no new runs." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonLabelAlreadyAssignedError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_LABEL_ALREADY_ASSIGNED" + message: "The label is already assigned within this comparison." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonTaskWithheldVocabularyError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_TASK_WITHHELD_VOCABULARY" + message: "The task contains vocabulary a screening rubric withholds." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ComparisonSessionAlreadyEnrolledError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.comparisons" + reason: "COMPARISON_SESSION_ALREADY_ENROLLED" + message: "The session is already enrolled in this comparison." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/evaluation/comparisons/v1alpha1/open_comparison.proto b/proto/trogonai/evaluation/comparisons/v1alpha1/open_comparison.proto new file mode 100644 index 0000000000..c335d6e87e --- /dev/null +++ b/proto/trogonai/evaluation/comparisons/v1alpha1/open_comparison.proto @@ -0,0 +1,32 @@ +edition = "2024"; + +package trogonai.evaluation.comparisons.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/agent_revision.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/comparisons/v1alpha1/comparison_id.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto"; + +// Opens a comparison: a group of runs over the same task against the same base +// revision. NoStream creation requires a nonempty Decider command_id and no +// expected_revision; retries preserve the id. An already-opened comparison is +// rejected, never replaced. +message OpenComparison { + option (buf.validate.message).cel = { + id: "open_comparison.screening_rubric_refs_unique" + message: "each rubric version may screen a comparison at most once" + expression: "this.screening_rubric_refs.all(a, this.screening_rubric_refs.filter(b, b.rubric_id.value == a.rubric_id.value && b.version == a.version).size() == 1)" + }; + + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.AgentRevisionRef base_revision_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + // The shared task every run in the comparison was given, held out of line. + ClaimCheck task_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + // Rubric versions whose withheld vocabulary the task and every rendered run + // item are screened against. Admission refuses a task that matches; a run + // item that matches is rendered as ELISION_REASON_WITHHELD_VOCABULARY. Pinned + // by exact version, so the screen a comparison applied stays reproducible. + // Evaluation-side only: never readable through a session or run query. + repeated trogonai.evaluation.rubrics.v1alpha1.RubricVersionRef screening_rubric_refs = 4; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/add_rubric_version.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/add_rubric_version.proto new file mode 100644 index 0000000000..fd1490b281 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/add_rubric_version.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "trogonai/evaluation/rubrics/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto"; + +// Appends an immutable version; a version is never edited. Criteria are opaque +// to the agent under evaluation and are held out of line by claim check, whose +// digest is what a score is later read against. A repeated version is rejected. +// Dispatch requires the original positive expected_revision and a rubric that +// is registered and not retired. +message AddRubricVersion { + RubricVersionRef version_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck criteria_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + // Terms the judged must never see: words that would reveal how a run is + // scored. Held out of line like the criteria and never readable outside the + // evaluation streams. Absent means the version withholds no vocabulary. + ClaimCheck withheld_vocabulary_ref = 3; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/claim_check.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..f6715a882d --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/events.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/events.proto new file mode 100644 index 0000000000..c00f2dc438 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_registered.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_retired.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_added.proto"; + +// An unset or unknown event rejects replay; skipping a version could let a +// binding resolve to criteria the stream never held. +message RubricEvent { + oneof event { + option (buf.validate.oneof).required = true; + + RubricRegistered rubric_registered = 1; + RubricVersionAdded rubric_version_added = 2; + RubricRetired rubric_retired = 3; + } +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/faults.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/faults.proto new file mode 100644 index 0000000000..0791759876 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/faults.proto @@ -0,0 +1,58 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the evaluation.rubrics module's DeciderService +// refusals. These messages are never encoded. + +message RubricCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.rubrics" + reason: "RUBRIC_COMMAND_INVALID" + message: "The command violates the rubric contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message RubricAlreadyRegisteredError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.rubrics" + reason: "RUBRIC_ALREADY_REGISTERED" + message: "The rubric has already been registered." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message RubricNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.rubrics" + reason: "RUBRIC_NOT_FOUND" + message: "The rubric has not been registered." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message RubricVersionAlreadyExistsError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.rubrics" + reason: "RUBRIC_VERSION_ALREADY_EXISTS" + message: "The rubric already has this version." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message RubricRetiredError { + option (trogon.error.v1alpha1.message).template = { + domain: "evaluation.rubrics" + reason: "RUBRIC_RETIRED" + message: "The rubric is retired and accepts no new versions." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/register_rubric.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/register_rubric.proto new file mode 100644 index 0000000000..d2b1e27723 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/register_rubric.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto"; + +// Registers a rubric identity with no criteria; criteria arrive only through +// AddRubricVersion. NoStream creation requires a nonempty Decider command_id and +// no expected_revision; retries preserve the id. An already-registered rubric is +// rejected, never overwritten. +message RegisterRubric { + RubricId rubric_id = 1 [features.field_presence = LEGACY_REQUIRED]; + string display_name = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/retire_rubric.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/retire_rubric.proto new file mode 100644 index 0000000000..11fa8f9b98 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/retire_rubric.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto"; + +// Retiring stops new versions and new bindings; bindings and results that +// already pin a version keep resolving it. Dispatch requires the original +// positive expected_revision; an already retired rubric succeeds without an +// event after that concurrency check. +message RetireRubric { + RubricId rubric_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto new file mode 100644 index 0000000000..f1be0d5973 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; + +// RubricId is the identity of one rubric stream. +message RubricId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_registered.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_registered.proto new file mode 100644 index 0000000000..58e7171791 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_registered.proto @@ -0,0 +1,15 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto"; + +// Genesis fixes the identity and display name for this history. +message RubricRegistered { + RubricId rubric_id = 1 [features.field_presence = LEGACY_REQUIRED]; + string display_name = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_retired.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_retired.proto new file mode 100644 index 0000000000..ef8d14e825 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_retired.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto"; + +// Retiring never removes versions. An already retired history emits no second +// retirement. +message RubricRetired { + RubricId rubric_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_added.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_added.proto new file mode 100644 index 0000000000..b5ae9cac51 --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_added.proto @@ -0,0 +1,19 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "trogonai/evaluation/rubrics/v1alpha1/claim_check.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto"; + +// The version and the criteria it points at are immutable once recorded. Neither +// the rubric nor its criteria may be readable through any agent, proposal, or +// session query; they exist only in the evaluation streams (ADR#0024, +// ADR#0025). +message RubricVersionAdded { + RubricVersionRef version_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck criteria_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + // Terms the judged must never see: words that would reveal how a run is + // scored. Held out of line like the criteria and never readable outside the + // evaluation streams. Absent means the version withholds no vocabulary. + ClaimCheck withheld_vocabulary_ref = 3; +} diff --git a/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto new file mode 100644 index 0000000000..b44440fb2c --- /dev/null +++ b/proto/trogonai/evaluation/rubrics/v1alpha1/rubric_version_ref.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.evaluation.rubrics.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/evaluation/rubrics/v1alpha1/rubric_id.proto"; + +// A typed pointer to one immutable version of a rubric. The version is an exact +// identifier: empty values and moving aliases are invalid. +message RubricVersionRef { + RubricId rubric_id = 1 [features.field_presence = LEGACY_REQUIRED]; + string version = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/claim_check.proto b/proto/trogonai/library/memories/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..cfbca4f879 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/events.proto b/proto/trogonai/library/memories/v1alpha1/events.proto new file mode 100644 index 0000000000..56b94662c9 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/events.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/library/memories/v1alpha1/memory_superseded.proto"; +import "trogonai/library/memories/v1alpha1/memory_written.proto"; + +// An unset or unknown event rejects replay; skipping a supersession could +// resurface a memory the library has already replaced. +message MemoryEvent { + oneof event { + option (buf.validate.oneof).required = true; + + MemoryWritten memory_written = 1; + MemorySuperseded memory_superseded = 2; + } +} diff --git a/proto/trogonai/library/memories/v1alpha1/faults.proto b/proto/trogonai/library/memories/v1alpha1/faults.proto new file mode 100644 index 0000000000..a00d8bf19f --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/faults.proto @@ -0,0 +1,48 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the library.memories module's DeciderService +// refusals. These messages are never encoded. + +message MemoryCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.memories" + reason: "MEMORY_COMMAND_INVALID" + message: "The command violates the memory contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message MemoryNotFoundError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.memories" + reason: "MEMORY_NOT_FOUND" + message: "The memory entry has not been written." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message MemoryAlreadyWrittenError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.memories" + reason: "MEMORY_ALREADY_WRITTEN" + message: "The memory entry has already been written." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message MemorySuccessorMismatchError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.memories" + reason: "MEMORY_SUCCESSOR_MISMATCH" + message: "The successor must exist with the same scope and kind." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/library/memories/v1alpha1/memory_id.proto b/proto/trogonai/library/memories/v1alpha1/memory_id.proto new file mode 100644 index 0000000000..6657f1bd10 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/memory_id.proto @@ -0,0 +1,13 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "buf/validate/validate.proto"; + +// MemoryId is the identity of one memory entry stream. +message MemoryId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/memory_kind.proto b/proto/trogonai/library/memories/v1alpha1/memory_kind.proto new file mode 100644 index 0000000000..422f82bdb2 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/memory_kind.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "buf/validate/validate.proto"; + +// Names a category of memory; an agents MemoryKindSelector selects entries by +// this value. +message MemoryKind { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/memory_provenance.proto b/proto/trogonai/library/memories/v1alpha1/memory_provenance.proto new file mode 100644 index 0000000000..bd1fda6df0 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/memory_provenance.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/agent_revision.proto"; +import "trogonai/agents/agents/v1alpha1/identity.proto"; +import "trogonai/agents/proposals/v1alpha1/proposal_id.proto"; + +// Origin fields are references, never copied configuration. +// produced_by_revision_ref is the revision that produced the entry; the +// revision already holds its configuration immutably. +message MemoryProvenance { + trogonai.agents.agents.v1alpha1.AgentId author = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.AgentRevisionRef produced_by_revision_ref = 2; + trogonai.agents.proposals.v1alpha1.ProposalId promoted_by_proposal_id = 3; +} diff --git a/proto/trogonai/library/memories/v1alpha1/memory_superseded.proto b/proto/trogonai/library/memories/v1alpha1/memory_superseded.proto new file mode 100644 index 0000000000..9a345f78be --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/memory_superseded.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "trogonai/library/memories/v1alpha1/memory_id.proto"; + +// The superseded entry stays in history; resolution simply prefers its +// successor. An already superseded entry emits no second supersession. +message MemorySuperseded { + MemoryId memory_id = 1 [features.field_presence = LEGACY_REQUIRED]; + MemoryId superseded_by_id = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/memory_written.proto b/proto/trogonai/library/memories/v1alpha1/memory_written.proto new file mode 100644 index 0000000000..c8fedb2501 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/memory_written.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "trogon/hierarchy/v1alpha1/node.proto"; +import "trogonai/library/memories/v1alpha1/claim_check.proto"; +import "trogonai/library/memories/v1alpha1/memory_id.proto"; +import "trogonai/library/memories/v1alpha1/memory_kind.proto"; +import "trogonai/library/memories/v1alpha1/memory_provenance.proto"; + +// Genesis fixes scope, kind, content, and provenance for this entry. +message MemoryWritten { + MemoryId memory_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogon.hierarchy.v1alpha1.NodeId scope = 2 [features.field_presence = LEGACY_REQUIRED]; + MemoryKind kind = 3 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck content_ref = 4 [features.field_presence = LEGACY_REQUIRED]; + MemoryProvenance provenance = 5 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/supersede_memory.proto b/proto/trogonai/library/memories/v1alpha1/supersede_memory.proto new file mode 100644 index 0000000000..8ee75de527 --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/supersede_memory.proto @@ -0,0 +1,21 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/library/memories/v1alpha1/memory_id.proto"; + +// Marks an entry as replaced by another entry of the same scope and kind. +// Dispatch requires the original positive expected_revision; an entry already +// superseded by the same successor succeeds without an event after that +// concurrency check. +message SupersedeMemory { + option (buf.validate.message).cel = { + id: "supersede_memory.successor_distinct" + message: "an entry cannot supersede itself" + expression: "this.memory_id != this.superseded_by_id" + }; + + MemoryId memory_id = 1 [features.field_presence = LEGACY_REQUIRED]; + MemoryId superseded_by_id = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/memories/v1alpha1/write_memory.proto b/proto/trogonai/library/memories/v1alpha1/write_memory.proto new file mode 100644 index 0000000000..6ad44c5bce --- /dev/null +++ b/proto/trogonai/library/memories/v1alpha1/write_memory.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.library.memories.v1alpha1; + +import "trogon/hierarchy/v1alpha1/node.proto"; +import "trogonai/library/memories/v1alpha1/claim_check.proto"; +import "trogonai/library/memories/v1alpha1/memory_id.proto"; +import "trogonai/library/memories/v1alpha1/memory_kind.proto"; +import "trogonai/library/memories/v1alpha1/memory_provenance.proto"; + +// Writes never mint agent revisions (ADR#0025). Entries are appended and +// superseded, never edited. Entries are keyed by scope node plus kind. +// Resolution order across an agent's node and its ancestors is a runtime +// decision, not defined here. NoStream creation requires a nonempty Decider +// command_id and no expected_revision; retries preserve the id. +message WriteMemory { + MemoryId memory_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogon.hierarchy.v1alpha1.NodeId scope = 2 [features.field_presence = LEGACY_REQUIRED]; + MemoryKind kind = 3 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck content_ref = 4 [features.field_presence = LEGACY_REQUIRED]; + MemoryProvenance provenance = 5 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/skills/ids/v1alpha1/skill_id.proto b/proto/trogonai/library/skills/ids/v1alpha1/skill_id.proto new file mode 100644 index 0000000000..ee333f2dc0 --- /dev/null +++ b/proto/trogonai/library/skills/ids/v1alpha1/skill_id.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.library.skills.ids.v1alpha1; + +import "buf/validate/validate.proto"; + +// SkillId is the identity of one skill stream. It lives in a leaf package that +// imports nothing from agents or from the skills module, because agents +// SkillVersionRef uses it while the skills module imports agents, and a package +// shared by both directions would be an import cycle. +message SkillId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/add_skill_version.proto b/proto/trogonai/library/skills/v1alpha1/add_skill_version.proto new file mode 100644 index 0000000000..539d30f098 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/add_skill_version.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/dependencies.proto"; +import "trogonai/library/skills/v1alpha1/claim_check.proto"; +import "trogonai/library/skills/v1alpha1/skill_provenance.proto"; + +// Appends an immutable version; a version is never edited. version_ref is what a +// revision's SkillDeclaration pins. Its version is an exact identifier: empty +// values and moving aliases are invalid, and a repeated version is rejected. +// Content is held by claim check, and its digest is what +// SkillDeclaration.content_digest must match. +message AddSkillVersion { + trogonai.agents.agents.v1alpha1.SkillVersionRef version_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck content_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + SkillProvenance provenance = 3 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/archive_skill.proto b/proto/trogonai/library/skills/v1alpha1/archive_skill.proto new file mode 100644 index 0000000000..cb06b1bee6 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/archive_skill.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "trogonai/library/skills/ids/v1alpha1/skill_id.proto"; + +// Archiving stops new pins; revisions that already pin a version keep resolving +// it. Dispatch requires the original positive expected_revision; an already +// archived skill succeeds without an event after that concurrency check. +message ArchiveSkill { + trogonai.library.skills.ids.v1alpha1.SkillId skill_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/claim_check.proto b/proto/trogonai/library/skills/v1alpha1/claim_check.proto new file mode 100644 index 0000000000..3ec2d24200 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/claim_check.proto @@ -0,0 +1,22 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/digest.proto"; + +// StorageLocator is an opaque external location of stored bytes, for example +// an Object Store key. It names where bytes live, never what they are. +message StorageLocator { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} + +// ClaimCheck is bytes stored out of line: a locator to fetch them plus the +// digest they are verified against before use. +message ClaimCheck { + StorageLocator storage_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.agents.v1alpha1.Digest digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/events.proto b/proto/trogonai/library/skills/v1alpha1/events.proto new file mode 100644 index 0000000000..af936529b2 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/library/skills/v1alpha1/skill_archived.proto"; +import "trogonai/library/skills/v1alpha1/skill_registered.proto"; +import "trogonai/library/skills/v1alpha1/skill_version_added.proto"; + +// An unset or unknown event rejects replay; skipping a version could resolve a +// pinned SkillDeclaration to the wrong content. +message SkillEvent { + oneof event { + option (buf.validate.oneof).required = true; + + SkillRegistered skill_registered = 1; + SkillVersionAdded skill_version_added = 2; + SkillArchived skill_archived = 3; + } +} diff --git a/proto/trogonai/library/skills/v1alpha1/faults.proto b/proto/trogonai/library/skills/v1alpha1/faults.proto new file mode 100644 index 0000000000..e6a566d5e7 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/faults.proto @@ -0,0 +1,58 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the library.skills module's DeciderService +// refusals. These messages are never encoded. + +message SkillCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.skills" + reason: "SKILL_COMMAND_INVALID" + message: "The command violates the skill contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message SkillNotRegisteredError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.skills" + reason: "SKILL_NOT_REGISTERED" + message: "The skill must be registered before this command." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message SkillAlreadyRegisteredError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.skills" + reason: "SKILL_ALREADY_REGISTERED" + message: "The skill has already been registered." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message SkillVersionAlreadyExistsError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.skills" + reason: "SKILL_VERSION_ALREADY_EXISTS" + message: "The skill already has this version." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message SkillArchivedError { + option (trogon.error.v1alpha1.message).template = { + domain: "library.skills" + reason: "SKILL_ARCHIVED" + message: "The skill is archived and accepts no new versions." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/library/skills/v1alpha1/register_skill.proto b/proto/trogonai/library/skills/v1alpha1/register_skill.proto new file mode 100644 index 0000000000..422a4053b9 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/register_skill.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogon/hierarchy/v1alpha1/node.proto"; +import "trogonai/library/skills/ids/v1alpha1/skill_id.proto"; + +// Registers a skill under a hierarchy node. Resolution order across an agent's +// node and its ancestors is a runtime decision, not defined here. NoStream +// creation requires a nonempty Decider command_id and no expected_revision; +// retries preserve the id. +message RegisterSkill { + trogonai.library.skills.ids.v1alpha1.SkillId skill_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogon.hierarchy.v1alpha1.NodeId scope = 2 [features.field_presence = LEGACY_REQUIRED]; + string display_name = 3 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/skill_archived.proto b/proto/trogonai/library/skills/v1alpha1/skill_archived.proto new file mode 100644 index 0000000000..98d2b1dc3f --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/skill_archived.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "trogonai/library/skills/ids/v1alpha1/skill_id.proto"; + +// Archiving never removes versions. An already archived history emits no second +// archive. +message SkillArchived { + trogonai.library.skills.ids.v1alpha1.SkillId skill_id = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/skill_provenance.proto b/proto/trogonai/library/skills/v1alpha1/skill_provenance.proto new file mode 100644 index 0000000000..94c67c943c --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/skill_provenance.proto @@ -0,0 +1,27 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/agents/v1alpha1/identity.proto"; +import "trogonai/agents/candidates/v1alpha1/candidate_ref.proto"; +import "trogonai/agents/proposals/v1alpha1/proposal_id.proto"; + +// Origin fields are references, never copied configuration. +message SkillProvenance { + oneof origin { + option (buf.validate.oneof).required = true; + + // Proposal that promoted this version. + SkillPromotion promoted = 1; + // Agent that authored this version directly. + trogonai.agents.agents.v1alpha1.AgentId authored_by = 2; + } +} + +// A promotion points back at the proposal that judged it and, when the content +// was measured as a candidate, at that exact candidate. +message SkillPromotion { + trogonai.agents.proposals.v1alpha1.ProposalId promoted_by_proposal_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogonai.agents.candidates.v1alpha1.CandidateRef origin_candidate_ref = 2; +} diff --git a/proto/trogonai/library/skills/v1alpha1/skill_registered.proto b/proto/trogonai/library/skills/v1alpha1/skill_registered.proto new file mode 100644 index 0000000000..66998e6209 --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/skill_registered.proto @@ -0,0 +1,17 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogon/hierarchy/v1alpha1/node.proto"; +import "trogonai/library/skills/ids/v1alpha1/skill_id.proto"; + +// Genesis fixes the scope for this history; a skill never moves between nodes. +message SkillRegistered { + trogonai.library.skills.ids.v1alpha1.SkillId skill_id = 1 [features.field_presence = LEGACY_REQUIRED]; + trogon.hierarchy.v1alpha1.NodeId scope = 2 [features.field_presence = LEGACY_REQUIRED]; + string display_name = 3 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/library/skills/v1alpha1/skill_version_added.proto b/proto/trogonai/library/skills/v1alpha1/skill_version_added.proto new file mode 100644 index 0000000000..e38586ebcc --- /dev/null +++ b/proto/trogonai/library/skills/v1alpha1/skill_version_added.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.library.skills.v1alpha1; + +import "trogonai/agents/agents/v1alpha1/dependencies.proto"; +import "trogonai/library/skills/v1alpha1/claim_check.proto"; +import "trogonai/library/skills/v1alpha1/skill_provenance.proto"; + +// The version, content digest, and provenance are immutable once recorded. +message SkillVersionAdded { + trogonai.agents.agents.v1alpha1.SkillVersionRef version_ref = 1 [features.field_presence = LEGACY_REQUIRED]; + ClaimCheck content_ref = 2 [features.field_presence = LEGACY_REQUIRED]; + SkillProvenance provenance = 3 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/clear_egress_permission.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/clear_egress_permission.proto new file mode 100644 index 0000000000..9d0f1110f4 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/clear_egress_permission.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Clears only this subject's permit, which returns it to deny unless an +// ancestor node still permits the destination. The initialized identity remains. +// Dispatch requires live authority and a nonempty Decider command_id plus the +// original positive expected_revision; retries preserve both. An already cleared +// permission succeeds without an event after that concurrency check. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, +// so requested provisioning setup cannot disappear before activation. +message ClearEgressPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_cleared.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_cleared.proto new file mode 100644 index 0000000000..0b30aedea1 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_cleared.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Removes the permission, never the identity. An already cleared history emits +// no second clear. +message EgressPermissionCleared { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_initialized.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_initialized.proto new file mode 100644 index 0000000000..1a24d1e5be --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_initialized.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Genesis fixes the subject for this history. The admitted origin and exact +// initial policy remain immutable receipt evidence after replacements and +// clears, independent of the current policy projection. +message EgressPermissionInitialized { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + EgressPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "egress_permission_initialized.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_set.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_set.proto new file mode 100644 index 0000000000..5dcf42bb6c --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/egress_permission_set.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Replaces the permission in force without replacing the history's identity. +message EgressPermissionSet { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + EgressPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/events.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/events.proto new file mode 100644 index 0000000000..345d8d3a56 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/egress_permissions/v1alpha1/egress_permission_cleared.proto"; +import "trogonai/policy/egress_permissions/v1alpha1/egress_permission_initialized.proto"; +import "trogonai/policy/egress_permissions/v1alpha1/egress_permission_set.proto"; + +// An unset or unknown event rejects replay; skipping a permission change could +// admit a connection the current permission forbids, or refuse one it allows. +message EgressPermissionEvent { + oneof event { + option (buf.validate.oneof).required = true; + + EgressPermissionInitialized egress_permission_initialized = 1; + EgressPermissionSet egress_permission_set = 2; + EgressPermissionCleared egress_permission_cleared = 3; + } +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/faults.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/faults.proto new file mode 100644 index 0000000000..eeeafc4759 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/faults.proto @@ -0,0 +1,49 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the policy.egress_permissions module's +// DeciderService refusals. These messages are never encoded. Administration +// authorization and egress admission keep their own failure +// contracts; this fold only owns permission mutations. +message EgressPermissionCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.egress_permissions" + reason: "EGRESS_PERMISSION_COMMAND_INVALID" + message: "The command violates the egress permission policy contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EgressPermissionNotInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.egress_permissions" + reason: "EGRESS_PERMISSION_NOT_INITIALIZED" + message: "The egress permission history must be initialized before this command." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EgressPermissionAlreadyInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.egress_permissions" + reason: "EGRESS_PERMISSION_ALREADY_INITIALIZED" + message: "The egress permission history has already been initialized." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message EgressPermissionSubjectMismatchError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.egress_permissions" + reason: "EGRESS_PERMISSION_SUBJECT_MISMATCH" + message: "The command names a different subject from this egress permission history." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/initialize_egress_permission.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/initialize_egress_permission.proto new file mode 100644 index 0000000000..057f76c3fd --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/initialize_egress_permission.proto @@ -0,0 +1,28 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Exists for the same reason as budget initialization: with absence meaning +// deny, an Agent provisioned with a REQUIRED connection declaration would fail its +// first Session unless the provisioning operation can install its permission. +// provisioning_id is valid only for an Agent subject. Dispatch verifies the +// subject exists and live administration authority. During PROVISIONING only the +// owning operation may initialize, and dispatch verifies registry origin rather +// than trusting caller-supplied provenance; PROVISIONING_CANCELLED and unknown +// lifecycle reject. NoStream creation requires a nonempty Decider command_id and +// no expected_revision; retries preserve the id. An already-initialized +// rejection is not success; recovery verifies the durable genesis receipt's +// subject, provisioning origin, and exact policy. +message InitializeEgressPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + EgressPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "initialize_egress_permission.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto new file mode 100644 index 0000000000..2411bdec20 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto @@ -0,0 +1,68 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "buf/validate/validate.proto"; + +// EgressHost is one exact network destination: a host name or address and a +// port. It is an identity, never a pattern, so a wildcard, a suffix, or a CIDR +// range is invalid and a relabel or a new subdomain cannot widen a permission. +message EgressHost { + option (buf.validate.message).cel = { + id: "egress_host.host_is_exact" + message: "a host must be an exact name or address, never a pattern" + expression: "!this.host.contains('*') && !this.host.contains('/') && !this.host.contains(' ')" + }; + + string host = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + uint32 port = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).uint32 = { + gte: 1 + lte: 65535 + } + ]; +} + +// One thing a Session of the subject may open a network connection to. No +// declared-connection identifier exists in the agents contracts today (a +// ConnectionDeclaration names a requirement class, ADR#0063), so the exact host +// is the only arm; an arm naming a declared connection is an additive change +// when one exists. +message EgressDestination { + oneof destination { + option (buf.validate.oneof).required = true; + + EgressHost host = 1; + } +} + +// A permission widens; it is the grant that lets a Session of the subject open +// a network connection to a destination. Absence is deny: a Session whose Agent +// has no permission on itself or any ancestor node has no network egress, and a +// ConnectionDeclaration in its configuration grants nothing (ADR#0025, +// ADR#0063 Decision 5). +// +// Permits accumulate downward: the effective set for an Agent is the union of +// its own permission and every ancestor node's. A connection is admitted when +// its destination equals a permitted destination exactly. Evaluation runs live +// on each connection attempt (ADR#0031 §8 authorization row); the history of +// what was permitted when is the stream. +// +// Evaluation storage and rubric storage are never permitted destinations for a +// Session under evaluation, whatever the effective set says, so a candidate run +// cannot reach the yardstick it is measured against (ADR#0072). +// +// An empty policy is invalid; clear the permission to withdraw it. +message EgressPermissionPolicy { + option (buf.validate.message).cel = { + id: "egress_permission_policy.destinations_unique" + message: "each egress destination may appear at most once" + expression: "this.permitted.map(d, d.host.host + ':' + string(d.host.port)).unique()" + }; + + repeated EgressDestination permitted = 1 [(buf.validate.field).repeated.min_items = 1]; +} diff --git a/proto/trogonai/policy/egress_permissions/v1alpha1/set_egress_permission.proto b/proto/trogonai/policy/egress_permissions/v1alpha1/set_egress_permission.proto new file mode 100644 index 0000000000..35d3eff008 --- /dev/null +++ b/proto/trogonai/policy/egress_permissions/v1alpha1/set_egress_permission.proto @@ -0,0 +1,19 @@ +edition = "2024"; + +package trogonai.policy.egress_permissions.v1alpha1; + +import "trogonai/policy/egress_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Replaces an initialized subject's permission, including after a clear. +// Dispatch requires live administration authority and a nonempty Decider +// command_id plus original positive expected_revision. Retries preserve both; a +// refreshed revision is a new intent. Equal destinations succeed without an event +// after the concurrency check; reordering destinations alone is not a change. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, so +// requested provisioning setup cannot be replaced before activation. Genesis +// provisioning origin survives every replacement. +message SetEgressPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + EgressPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto new file mode 100644 index 0000000000..c3918394dd --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto @@ -0,0 +1,64 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/agents/proposals/v1alpha1/configuration_difference.proto"; + +// Who may record a proposal verdict. The issuer is read from the event envelope +// (ADR#0069); nothing in a verdict names its own author. +enum VerdictAuthority { + VERDICT_AUTHORITY_UNSPECIFIED = 0; + // Only a human member of the project may record the verdict. + VERDICT_AUTHORITY_HUMAN_MEMBER = 1; + // A human member, or an Agent registered in the registry, may record it. + VERDICT_AUTHORITY_HUMAN_MEMBER_OR_REGISTERED_AGENT = 2; +} + +message ClassVerdictAuthority { + trogonai.agents.proposals.v1alpha1.AgentChangeClass change_class = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).enum = { + defined_only: true + not_in: [0] + } + ]; + VerdictAuthority required_authority = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).enum = { + defined_only: true + not_in: [0] + } + ]; +} + +// Per change class, the authority required to record a verdict on a proposal +// for the subject. Absence means human member for every class, so an Agent with +// no policy on itself or any ancestor node can never have its change judged by +// an agent (fail closed). A rule can only relax a class; a policy never +// removes the requirement that a verdict exists. +// +// For a proposal, the effective requirement for a class is the one named by the +// nearest binding that lists that class: the proposal's Agent first, then each +// ancestor node toward the root. No binding listing the class means human +// member. Evaluation runs live when the verdict is dispatched (ADR#0031 §8 +// authorization row); the history of what was required when is the stream. +// +// The charter class may never be relaxed below human member. The enum values +// are AGENT_CHANGE_CLASS_CHARTER = 2 and VERDICT_AUTHORITY_HUMAN_MEMBER = 1. +// +// An empty policy is invalid; clear the authority to withdraw it. +message VerdictAuthorityPolicy { + option (buf.validate.message).cel = { + id: "verdict_authority_policy.classes_unique" + message: "each change class may appear at most once" + expression: "this.rules.map(r, r.change_class).unique()" + }; + option (buf.validate.message).cel = { + id: "verdict_authority_policy.charter_stays_human" + message: "the charter class may never be relaxed below human member" + expression: "this.rules.all(r, r.change_class != 2 || r.required_authority == 1)" + }; + + repeated ClassVerdictAuthority rules = 1 [(buf.validate.field).repeated.min_items = 1]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/clear_verdict_authority.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/clear_verdict_authority.proto new file mode 100644 index 0000000000..6ca28430b6 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/clear_verdict_authority.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Clears only this subject's rules, which returns its classes to the nearest +// ancestor node that decides them, or to human member when none does. The initialized identity remains. +// Dispatch requires live authority and a nonempty Decider command_id plus the +// original positive expected_revision; retries preserve both. An already cleared +// authority succeeds without an event after that concurrency check. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, +// so requested provisioning setup cannot disappear before activation. +message ClearVerdictAuthority { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/events.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/events.proto new file mode 100644 index 0000000000..906d801ae6 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_cleared.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_initialized.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_set.proto"; + +// An unset or unknown event rejects replay; skipping an authority change could +// accept a verdict from an issuer the current authority forbids, or refuse one it allows. +message VerdictAuthorityEvent { + oneof event { + option (buf.validate.oneof).required = true; + + VerdictAuthorityInitialized verdict_authority_initialized = 1; + VerdictAuthoritySet verdict_authority_set = 2; + VerdictAuthorityCleared verdict_authority_cleared = 3; + } +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/faults.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/faults.proto new file mode 100644 index 0000000000..d319f1343c --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/faults.proto @@ -0,0 +1,49 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the policy.verdict_authorities module's +// DeciderService refusals. These messages are never encoded. Administration +// authorization and verdict admission keep their own failure +// contracts; this fold only owns authority mutations. +message VerdictAuthorityCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.verdict_authorities" + reason: "VERDICT_AUTHORITY_COMMAND_INVALID" + message: "The command violates the verdict authority policy contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message VerdictAuthorityNotInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.verdict_authorities" + reason: "VERDICT_AUTHORITY_NOT_INITIALIZED" + message: "The verdict authority history must be initialized before this command." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message VerdictAuthorityAlreadyInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.verdict_authorities" + reason: "VERDICT_AUTHORITY_ALREADY_INITIALIZED" + message: "The verdict authority history has already been initialized." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message VerdictAuthoritySubjectMismatchError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.verdict_authorities" + reason: "VERDICT_AUTHORITY_SUBJECT_MISMATCH" + message: "The command names a different subject from this verdict authority history." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/initialize_verdict_authority.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/initialize_verdict_authority.proto new file mode 100644 index 0000000000..ae3c802733 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/initialize_verdict_authority.proto @@ -0,0 +1,28 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto"; + +// Exists for the same reason as budget initialization: with absence meaning +// human member, an Agent provisioned for agent-authored review would have no way +// to relax a class unless the provisioning operation can install the policy. +// provisioning_id is valid only for an Agent subject. Dispatch verifies the +// subject exists and live administration authority. During PROVISIONING only the +// owning operation may initialize, and dispatch verifies registry origin rather +// than trusting caller-supplied provenance; PROVISIONING_CANCELLED and unknown +// lifecycle reject. NoStream creation requires a nonempty Decider command_id and +// no expected_revision; retries preserve the id. An already-initialized +// rejection is not success; recovery verifies the durable genesis receipt's +// subject, provisioning origin, and exact policy. +message InitializeVerdictAuthority { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + VerdictAuthorityPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "initialize_verdict_authority.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/set_verdict_authority.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/set_verdict_authority.proto new file mode 100644 index 0000000000..3bcd817f80 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/set_verdict_authority.proto @@ -0,0 +1,19 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto"; + +// Replaces an initialized subject's authority policy, including after a clear. +// Dispatch requires live administration authority and a nonempty Decider +// command_id plus original positive expected_revision. Retries preserve both; a +// refreshed revision is a new intent. Equal policies succeed without an event +// after the concurrency check; reordering rules alone is not a change. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, so +// requested provisioning setup cannot be replaced before activation. Genesis +// provisioning origin survives every replacement. +message SetVerdictAuthority { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + VerdictAuthorityPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_cleared.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_cleared.proto new file mode 100644 index 0000000000..0aa7752e51 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_cleared.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Removes the rules, never the identity. An already cleared history emits +// no second clear. +message VerdictAuthorityCleared { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_initialized.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_initialized.proto new file mode 100644 index 0000000000..356fad5637 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_initialized.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto"; + +// Genesis fixes the subject for this history. The admitted origin and exact +// initial policy remain immutable receipt evidence after replacements and +// clears, independent of the current policy projection. +message VerdictAuthorityInitialized { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + VerdictAuthorityPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "verdict_authority_initialized.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_set.proto b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_set.proto new file mode 100644 index 0000000000..e782b7dcb9 --- /dev/null +++ b/proto/trogonai/policy/verdict_authorities/v1alpha1/verdict_authority_set.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.policy.verdict_authorities.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; +import "trogonai/policy/verdict_authorities/v1alpha1/authority_policy.proto"; + +// Replaces the authority policy in force without replacing the history's identity. +message VerdictAuthoritySet { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + VerdictAuthorityPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/candidate_label.proto b/proto/trogonai/session/sessions/queries/v1alpha1/candidate_label.proto new file mode 100644 index 0000000000..9e94725b58 --- /dev/null +++ b/proto/trogonai/session/sessions/queries/v1alpha1/candidate_label.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.session.sessions.queries.v1alpha1; + +import "buf/validate/validate.proto"; + +// CandidateLabel is the blind label a run carries within one comparison. It +// reveals nothing about candidate identity or origin. +message CandidateLabel { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/comparison_id.proto b/proto/trogonai/session/sessions/queries/v1alpha1/comparison_id.proto new file mode 100644 index 0000000000..4e24e6191d --- /dev/null +++ b/proto/trogonai/session/sessions/queries/v1alpha1/comparison_id.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.session.sessions.queries.v1alpha1; + +import "buf/validate/validate.proto"; + +// ComparisonId is the queries package's own copy of the comparison identity, so +// the queries package never imports the evaluation package. +message ComparisonId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/get_run_history.proto b/proto/trogonai/session/sessions/queries/v1alpha1/get_run_history.proto new file mode 100644 index 0000000000..50236934c3 --- /dev/null +++ b/proto/trogonai/session/sessions/queries/v1alpha1/get_run_history.proto @@ -0,0 +1,62 @@ +edition = "2024"; + +package trogonai.session.sessions.queries.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/session/sessions/queries/v1alpha1/candidate_label.proto"; +import "trogonai/session/sessions/queries/v1alpha1/comparison_id.proto"; +import "trogonai/session/sessions/queries/v1alpha1/contract_version.proto"; +import "trogonai/session/sessions/queries/v1alpha1/projection_freshness.proto"; +import "trogonai/session/sessions/queries/v1alpha1/read_consistency.proto"; +import "trogonai/session/sessions/queries/v1alpha1/run_history_item.proto"; + +// GetRunHistoryRequest reads one page of the run histories of a comparison. +// +// A comparison groups candidate runs for one reader. Labels are assigned per +// comparison, so the same candidate carries different labels in different +// comparisons and a label reveals nothing about candidate identity or origin. +// The label-to-session mapping lives only in the comparison stream and is not +// exposed by this query; the server resolves labels through it. The query is +// score-free. +message GetRunHistoryRequest { + // An unknown comparison is QUERY_ERROR_CODE_COMPARISON_NOT_FOUND. + ComparisonId comparison_id = 1 [features.field_presence = LEGACY_REQUIRED]; + ContractVersion accepted_contract = 2 [features.field_presence = LEGACY_REQUIRED]; + // Unset reads every candidate in the comparison. A label the comparison does + // not assign is QUERY_ERROR_CODE_CANDIDATE_LABEL_NOT_FOUND. + CandidateLabel candidate_label = 3; + uint32 page_size = 4 [features.field_presence = LEGACY_REQUIRED]; + // Opaque continuation from a prior response, a serialized CursorEnvelope + // whose PageCursor holds the run_history arm. Unset starts a new scan. + bytes page_token = 5; + // Freshness the caller requires. Honored when the scan opens and ignored on + // a continuation. + ReadConsistency consistency = 6; +} + +// GetRunHistoryResponse is one page of run history items. +message GetRunHistoryResponse { + ContractNegotiation contract = 1 [features.field_presence = LEGACY_REQUIRED]; + repeated RunHistoryItem items = 2; + // Unset when the scan is exhausted. Presence, not an empty `items` list, is + // the end signal. + bytes next_page_token = 3; + // Items rendered as RUN_HISTORY_ITEM_KIND_ELIDED because they need a variant + // newer than the caller's contract minor. + uint32 contract_clamped_count = 4 [features.field_presence = LEGACY_REQUIRED]; + // Items whose content was withheld by redaction or authorization. + uint32 withheld_count = 5 [features.field_presence = LEGACY_REQUIRED]; + // How current the read model was when the scan opened. Equal on every page. + ProjectionFreshness freshness = 6 [features.field_presence = LEGACY_REQUIRED]; + // One scan anchor per run in the scan, equal on every page. Turns appended + // after the scan opened are above the anchor and are not in it; should a + // run's effective boundary move, the continuation fails with STALE_CURSOR. + repeated RunScanAnchor anchors = 7; +} + +// RunScanAnchor is the effective history boundary of one run, after rewind +// masking, as a SessionOrdinal. +message RunScanAnchor { + CandidateLabel candidate_label = 1 [features.field_presence = LEGACY_REQUIRED]; + uint64 effective_through = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/history_item.proto b/proto/trogonai/session/sessions/queries/v1alpha1/history_item.proto index 41f25ae6a1..b82529f7a1 100644 --- a/proto/trogonai/session/sessions/queries/v1alpha1/history_item.proto +++ b/proto/trogonai/session/sessions/queries/v1alpha1/history_item.proto @@ -78,6 +78,9 @@ enum ElisionReason { ELISION_REASON_REDACTED = 2; // The caller is not authorized to see this item's content. ELISION_REASON_NOT_AUTHORIZED = 3; + // The item contains vocabulary a screening rubric withholds from the judged. + // Says only that a screen matched, never which term or which rubric. + ELISION_REASON_WITHHELD_VOCABULARY = 4; } // UserMessageItem is a message from the user. diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/page_cursor.proto b/proto/trogonai/session/sessions/queries/v1alpha1/page_cursor.proto index c0224ad1b9..3d401264c3 100644 --- a/proto/trogonai/session/sessions/queries/v1alpha1/page_cursor.proto +++ b/proto/trogonai/session/sessions/queries/v1alpha1/page_cursor.proto @@ -4,6 +4,8 @@ package trogonai.session.sessions.queries.v1alpha1; import "buf/validate/validate.proto"; import "google/protobuf/timestamp.proto"; +import "trogonai/session/sessions/queries/v1alpha1/candidate_label.proto"; +import "trogonai/session/sessions/queries/v1alpha1/comparison_id.proto"; import "trogonai/session/sessions/queries/v1alpha1/contract_version.proto"; import "trogonai/session/sessions/queries/v1alpha1/get_session_history.proto"; import "trogonai/session/sessions/queries/v1alpha1/list_sessions.proto"; @@ -45,6 +47,7 @@ message PageCursor { oneof scan { HistoryScanCursor history = 1; SessionListScanCursor session_list = 2; + RunHistoryScanCursor run_history = 4; } // When the token stops being honored regardless of validity. // @@ -90,6 +93,50 @@ message HistoryScanCursor { CursorValidity validity = 6 [features.field_presence = LEGACY_REQUIRED]; } +// RunHistoryScanCursor is a position within a run-history scan across every +// run of one comparison. +// +// Runs are delivered one at a time in ascending label order, and within a run +// by SessionOrdinal ascending, so a position is a label plus the last ordinal +// delivered under it. Runs are named by label and never by session id: the MAC +// stops tampering but does not hide the payload, and the label-to-session +// mapping must never reach the reader. The server resolves each label through +// the comparison stream on every continuation. +message RunHistoryScanCursor { + option (buf.validate.message).cel = { + id: "run_history_scan_cursor.runs_unique" + message: "each run may appear at most once in a scan" + expression: "this.runs.map(r, r.candidate_label.value).unique()" + }; + option (buf.validate.message).cel = { + id: "run_history_scan_cursor.current_label_in_runs" + message: "the current label must be one of the scan's runs" + expression: "this.runs.exists(r, r.candidate_label.value == this.current_label.value)" + }; + ContractVersion minted_contract = 1 [features.field_presence = LEGACY_REQUIRED]; + // The comparison and optional label filter the scan was opened with. A + // continuation whose request disagrees is MALFORMED_CURSOR. + ComparisonId comparison_id = 2 [features.field_presence = LEGACY_REQUIRED]; + CandidateLabel candidate_label = 3; + // Every run in the scan with the boundary it was pinned to, fixed when the + // scan opened. A run enrolled after the scan opened is not in it. + repeated RunScanPin runs = 4 [(buf.validate.field).repeated.min_items = 1]; + // The run being delivered and the last SessionOrdinal delivered under it. + // The next page resumes strictly past it, then moves to the next label. + CandidateLabel current_label = 5 [features.field_presence = LEGACY_REQUIRED]; + uint64 last_delivered_ordinal = 6 [features.field_presence = LEGACY_REQUIRED]; +} + +// RunScanPin is one run's pinned boundary and validity within a run-history +// scan. A change in its validity is STALE_CURSOR, as for a history scan. +message RunScanPin { + CandidateLabel candidate_label = 1 [features.field_presence = LEGACY_REQUIRED]; + // The run's effective-history head when the scan opened, as a + // SessionOrdinal. The scan never looks past it. + uint64 anchor_ordinal = 2 [features.field_presence = LEGACY_REQUIRED]; + CursorValidity validity = 3 [features.field_presence = LEGACY_REQUIRED]; +} + // SessionListScanCursor is a position within a list scan. message SessionListScanCursor { ContractVersion minted_contract = 1 [features.field_presence = LEGACY_REQUIRED]; diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/query_error.proto b/proto/trogonai/session/sessions/queries/v1alpha1/query_error.proto index d731edf8ea..ae3b32c839 100644 --- a/proto/trogonai/session/sessions/queries/v1alpha1/query_error.proto +++ b/proto/trogonai/session/sessions/queries/v1alpha1/query_error.proto @@ -72,6 +72,11 @@ enum QueryErrorCode { QUERY_ERROR_CODE_RESOURCE_EXHAUSTED = 8; // An unexpected server-side failure. Carries no detail on purpose. QUERY_ERROR_CODE_INTERNAL = 9; + // The comparison id resolves to no stream. Like SESSION_NOT_FOUND, a + // comparison the caller may not see is QUERY_ERROR_CODE_PERMISSION_DENIED. + QUERY_ERROR_CODE_COMPARISON_NOT_FOUND = 10; + // The comparison exists but assigns no run to the requested candidate label. + QUERY_ERROR_CODE_CANDIDATE_LABEL_NOT_FOUND = 11; } // UnsupportedContractVersionDetail tells the caller what it would have to diff --git a/proto/trogonai/session/sessions/queries/v1alpha1/run_history_item.proto b/proto/trogonai/session/sessions/queries/v1alpha1/run_history_item.proto new file mode 100644 index 0000000000..2cae538002 --- /dev/null +++ b/proto/trogonai/session/sessions/queries/v1alpha1/run_history_item.proto @@ -0,0 +1,119 @@ +edition = "2024"; + +package trogonai.session.sessions.queries.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/session/sessions/queries/v1alpha1/candidate_label.proto"; +import "trogonai/session/sessions/queries/v1alpha1/history_item.proto"; + +// RunHistoryItem is one entry of a run history, redefined rather than shared +// with HistoryItem. It carries no score, rubric, verdict, evaluation result, +// candidate identity or session id. Artifacts are referenced by id so large +// traces are fetched on demand. +message RunHistoryItem { + // The label assigned within one comparison. + CandidateLabel candidate_label = 1 [features.field_presence = LEGACY_REQUIRED]; + // The run's own 1-indexed position for this item. + uint64 ordinal = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).uint64.gt = 0 + ]; + // Set even when the matching `item` arm is absent, so an unknown or withheld + // item is still identifiable. + RunHistoryItemKind kind = 3 [features.field_presence = LEGACY_REQUIRED]; + oneof item { + RunMessage message = 4; + RunToolCall tool_call = 5; + RunArtifact artifact = 6; + RunElided elided = 7; + } +} + +// RunHistoryItemKind is what an item is, readable without decoding the union. +enum RunHistoryItemKind { + RUN_HISTORY_ITEM_KIND_UNSPECIFIED = 0; + RUN_HISTORY_ITEM_KIND_MESSAGE = 1; + RUN_HISTORY_ITEM_KIND_TOOL_CALL = 2; + RUN_HISTORY_ITEM_KIND_ARTIFACT = 3; + // The item exists but was not rendered. `elided` says why. + RUN_HISTORY_ITEM_KIND_ELIDED = 4; +} + +// RunMessage is a message exchanged during the run. +message RunMessage { + option (buf.validate.message).cel = { + id: "run_message.generation_outcome_iff_assistant" + message: "generation_outcome must be set for assistant messages and only for them" + expression: "(this.role == 2) == has(this.generation_outcome)" + }; + string turn_id = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + RunMessageRole role = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).enum = { + defined_only: true + not_in: [0] + } + ]; + string text = 3 [features.field_presence = LEGACY_REQUIRED]; + // The text was shortened for transport. The full content is not recoverable + // from this response. + bool truncated = 4 [features.field_presence = LEGACY_REQUIRED]; + // How the assistant generation ended, so interrupted or in-flight work never + // reads as finished output. Unset for user messages. + AssistantMessageOutcome generation_outcome = 5 [(buf.validate.field).enum = { + defined_only: true + not_in: [0] + }]; +} + +enum RunMessageRole { + RUN_MESSAGE_ROLE_UNSPECIFIED = 0; + RUN_MESSAGE_ROLE_USER = 1; + RUN_MESSAGE_ROLE_ASSISTANT = 2; +} + +// RunToolCall is one tool call and where it stopped. +message RunToolCall { + string turn_id = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + string tool_call_id = 2 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + string tool_name = 3 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + ToolCallOutcome outcome = 4 [features.field_presence = LEGACY_REQUIRED]; + // Behavior is read from what the run sent and received, so both are carried. + string input_json = 5 [features.field_presence = LEGACY_REQUIRED]; + // Unset while the call has not produced a result. + string result_text = 6; + // Input or result was shortened for transport. The full content is not + // recoverable from this response. + bool truncated = 7 [features.field_presence = LEGACY_REQUIRED]; + // True when the call started and no terminal outcome was ever recorded, as on + // ToolCallItem. + bool unreconciled = 8 [features.field_presence = LEGACY_REQUIRED]; +} + +// RunArtifact names an artifact produced during the run. Its content is not +// inlined. +message RunArtifact { + string artifact_id = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; + string media_type = 2 [features.field_presence = LEGACY_REQUIRED]; +} + +// RunElided is an item the server chose not to render, stated affirmatively so +// a caller never mistakes a withheld item for a missing one. +message RunElided { + ElisionReason reason = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/session/sessions/v1alpha1/candidate_id.proto b/proto/trogonai/session/sessions/v1alpha1/candidate_id.proto new file mode 100644 index 0000000000..9f74fdca32 --- /dev/null +++ b/proto/trogonai/session/sessions/v1alpha1/candidate_id.proto @@ -0,0 +1,15 @@ +edition = "2024"; + +package trogonai.session.sessions.v1alpha1; + +import "buf/validate/validate.proto"; + +// CandidateId is the session package's own copy of the candidate identity, so +// the session package never imports the agents package. It carries the same +// value as trogonai.agents.candidates.v1alpha1.CandidateId. +message CandidateId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/session/sessions/v1alpha1/candidate_ref.proto b/proto/trogonai/session/sessions/v1alpha1/candidate_ref.proto new file mode 100644 index 0000000000..acce856179 --- /dev/null +++ b/proto/trogonai/session/sessions/v1alpha1/candidate_ref.proto @@ -0,0 +1,15 @@ +edition = "2024"; + +package trogonai.session.sessions.v1alpha1; + +import "trogonai/session/sessions/v1alpha1/candidate_id.proto"; +import "trogonai/session/sessions/v1alpha1/digest.proto"; + +// Projection of the candidate pinned in the stored plan bytes, so the session +// package never depends on the candidate type. Sessions are comparable only +// when their candidate_ref values name the same content digest or both are +// absent. +message CandidateRef { + CandidateId candidate_id = 1 [features.field_presence = LEGACY_REQUIRED]; + Digest content_digest = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/session/sessions/v1alpha1/create_child_session.proto b/proto/trogonai/session/sessions/v1alpha1/create_child_session.proto index cdf487167f..ddf9dd91c1 100644 --- a/proto/trogonai/session/sessions/v1alpha1/create_child_session.proto +++ b/proto/trogonai/session/sessions/v1alpha1/create_child_session.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/cascade_policy.proto"; import "trogonai/session/sessions/v1alpha1/checkout.proto"; import "trogonai/session/sessions/v1alpha1/execution_plan.proto"; @@ -38,4 +39,8 @@ message CreateChildSession { features.field_presence = LEGACY_REQUIRED, (buf.validate.field).string.min_len = 1 ]; + // Projection surface of the candidate pinned in the plan, as on + // SessionStarted; the plan stays authoritative. Absent means the registered + // revision ran unchanged. + CandidateRef candidate_ref = 8; } diff --git a/proto/trogonai/session/sessions/v1alpha1/create_session.proto b/proto/trogonai/session/sessions/v1alpha1/create_session.proto index 659c53c3e5..cd54c32116 100644 --- a/proto/trogonai/session/sessions/v1alpha1/create_session.proto +++ b/proto/trogonai/session/sessions/v1alpha1/create_session.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/checkout.proto"; import "trogonai/session/sessions/v1alpha1/execution_plan.proto"; @@ -23,4 +24,8 @@ message CreateSession { ]; StoredSessionExecutionPlan execution_plan = 2 [features.field_presence = LEGACY_REQUIRED]; CheckoutRef checkout_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + // Projection surface of the candidate pinned in the plan, as on + // SessionStarted; the plan stays authoritative. Absent means the registered + // revision ran unchanged. + CandidateRef candidate_ref = 4; } diff --git a/proto/trogonai/session/sessions/v1alpha1/fork_session.proto b/proto/trogonai/session/sessions/v1alpha1/fork_session.proto index 764cd51b62..e568f63b57 100644 --- a/proto/trogonai/session/sessions/v1alpha1/fork_session.proto +++ b/proto/trogonai/session/sessions/v1alpha1/fork_session.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/checkout.proto"; import "trogonai/session/sessions/v1alpha1/execution_plan.proto"; import "trogonai/session/sessions/v1alpha1/session_forked.proto"; @@ -33,4 +34,8 @@ message ForkSession { // The source's own ordinal the inherited prefix ends at. SessionOrdinal context_prefix_boundary = 5 [features.field_presence = LEGACY_REQUIRED]; ForkReason reason = 6 [features.field_presence = LEGACY_REQUIRED]; + // Projection surface of the candidate pinned in the plan, as on + // SessionStarted; the plan stays authoritative. Absent means the registered + // revision ran unchanged. + CandidateRef candidate_ref = 7; } diff --git a/proto/trogonai/session/sessions/v1alpha1/recover_session.proto b/proto/trogonai/session/sessions/v1alpha1/recover_session.proto index 20d02c1331..f1481f2832 100644 --- a/proto/trogonai/session/sessions/v1alpha1/recover_session.proto +++ b/proto/trogonai/session/sessions/v1alpha1/recover_session.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/checkout.proto"; import "trogonai/session/sessions/v1alpha1/digest.proto"; import "trogonai/session/sessions/v1alpha1/execution_plan.proto"; @@ -40,4 +41,8 @@ message RecoverSession { string salvage_key = 7 [features.field_presence = LEGACY_REQUIRED]; RecoveryCompleteness completeness = 8 [features.field_presence = LEGACY_REQUIRED]; uint32 omitted_count = 9 [features.field_presence = LEGACY_REQUIRED]; + // Projection surface of the candidate pinned in the plan, as on + // SessionStarted; the plan stays authoritative. Absent means the registered + // revision ran unchanged. + CandidateRef candidate_ref = 10; } diff --git a/proto/trogonai/session/sessions/v1alpha1/session_forked.proto b/proto/trogonai/session/sessions/v1alpha1/session_forked.proto index 6a75562cfb..481f6f943a 100644 --- a/proto/trogonai/session/sessions/v1alpha1/session_forked.proto +++ b/proto/trogonai/session/sessions/v1alpha1/session_forked.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/session_ordinal.proto"; // SessionForked is the second event in the atomic [SessionStarted, SessionForked] @@ -31,6 +32,10 @@ message SessionForked { // Why the fork happened; a command-time input not derivable from the rest of // the log. ForkReason reason = 4 [features.field_presence = LEGACY_REQUIRED]; + // Projection surface of the candidate pinned in the plan, as on + // SessionStarted; the plan stays authoritative. Absent means the registered + // revision ran unchanged. + CandidateRef candidate_ref = 5; } // ForkReason is why a session was forked. @@ -42,4 +47,7 @@ enum ForkReason { FORK_REASON_COMPACTION_CONTINUATION = 2; // A retry of a prior run from a shared prefix. FORK_REASON_RETRY = 3; + // A branch that runs a different plan from the parent over the same context + // prefix, to observe behavior without continuing the parent. + FORK_REASON_PROBE = 4; } diff --git a/proto/trogonai/session/sessions/v1alpha1/session_id.proto b/proto/trogonai/session/sessions/v1alpha1/session_id.proto new file mode 100644 index 0000000000..ab67b3612d --- /dev/null +++ b/proto/trogonai/session/sessions/v1alpha1/session_id.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.session.sessions.v1alpha1; + +import "buf/validate/validate.proto"; + +// SessionId is the identity of one session's logical stream. New fields that +// point at a session use it; existing session_id strings are not migrated. +message SessionId { + string value = 1 [ + features.field_presence = LEGACY_REQUIRED, + (buf.validate.field).string.min_len = 1 + ]; +} diff --git a/proto/trogonai/session/sessions/v1alpha1/session_position_ref.proto b/proto/trogonai/session/sessions/v1alpha1/session_position_ref.proto new file mode 100644 index 0000000000..19e872290f --- /dev/null +++ b/proto/trogonai/session/sessions/v1alpha1/session_position_ref.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.session.sessions.v1alpha1; + +import "trogonai/session/sessions/v1alpha1/session_id.proto"; +import "trogonai/session/sessions/v1alpha1/session_ordinal.proto"; + +// A temporal pointer: this session's log as of that ordinal, inclusive. The log +// is append-only, so the pointed-to prefix never changes and what was true at +// that point is always reconstructable by folding up to it. +message SessionPositionRef { + SessionId session_id = 1 [features.field_presence = LEGACY_REQUIRED]; + SessionOrdinal through_ordinal = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/session/sessions/v1alpha1/session_started.proto b/proto/trogonai/session/sessions/v1alpha1/session_started.proto index 872f01649f..82a2233feb 100644 --- a/proto/trogonai/session/sessions/v1alpha1/session_started.proto +++ b/proto/trogonai/session/sessions/v1alpha1/session_started.proto @@ -3,6 +3,7 @@ edition = "2024"; package trogonai.session.sessions.v1alpha1; import "buf/validate/validate.proto"; +import "trogonai/session/sessions/v1alpha1/candidate_ref.proto"; import "trogonai/session/sessions/v1alpha1/checkout.proto"; import "trogonai/session/sessions/v1alpha1/execution_plan.proto"; @@ -25,4 +26,9 @@ message SessionStarted { // directory; this field is the projection surface, the plan stays // authoritative. CheckoutRef checkout_ref = 3 [features.field_presence = LEGACY_REQUIRED]; + // Candidate this session ran, carried inline so candidate-scoped reads never + // decode plan_bytes. It must agree with the candidate pinned in the plan; + // this field is the projection surface, the plan stays authoritative. Absent + // means the registered revision ran unchanged. + CandidateRef candidate_ref = 4; } diff --git a/proto/trogonai/workflows/definitions/v1alpha1/workflow_definition.proto b/proto/trogonai/workflows/definitions/v1alpha1/workflow_definition.proto index fcb4d8209a..b3aa682285 100644 --- a/proto/trogonai/workflows/definitions/v1alpha1/workflow_definition.proto +++ b/proto/trogonai/workflows/definitions/v1alpha1/workflow_definition.proto @@ -166,6 +166,10 @@ message AgentStep { RetryPolicy retry = 3 [features.field_presence = LEGACY_REQUIRED]; RegisteredValue result_capture = 4; Condition acceptance = 5; + // Present means the step runs the base revision with this candidate applied. + // The reference must resolve to a registered value whose contract is + // CandidateRef, and the candidate's base_revision_ref must equal agent_revision. + ValueReference candidate_ref = 6; } // Children start in declaration order after the previous child is accepted.