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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .yaml-validation-allowlist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,10 @@ allowed_files:
- file: "src/omnibase_core/validators/breaking_schema_change.py"
reason: >-
Breaking-schema-change validator (OMN-12621) — must read adjacent *.topic-schema.yaml declarations
and *.migration.yaml contracts from the filesystem to diff baseline vs current bindings. Every
yaml.safe_load() result is immediately passed to ModelTopicSchemaBinding.model_validate() /
ModelTopicMigrationContract.model_validate(); the raw dict is never used unvalidated. Mirrors the
contract_config_compliance.py validator pattern it is modeled on.
and *.migration.yaml contracts from the filesystem to diff baseline vs current bindings. Every yaml.safe_load()
result is immediately passed to ModelTopicSchemaBinding.model_validate() / ModelTopicMigrationContract.model_validate();
the raw dict is never used unvalidated. Mirrors the contract_config_compliance.py validator pattern
it is modeled on.
added: "2026-06-02"
review: "2026-12-13"

Expand Down
25 changes: 11 additions & 14 deletions contracts/OMN-12663.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ schema_version: "1.0.0"
ticket_id: "OMN-12663"
title: "feat(OMN-12663): widen ModelDelegationRequest.task_type to full 12-value compat set"
summary: >
Parity fix: ModelDelegationRequest.task_type was narrowed to 3 values during graduation
(OMN-12126) when the compat set at sha 4d887307 defines 12 values. This broke omnimarket
validation (3 tests fail; OMN-12659 held at 208/211). This PR widens task_type to the
full 12-value compat set and adds parametrized tests proving all 12 values accept.
Full parity audit conducted across all delegation wire models — only task_type required
widening; all other str fields match compat intentionally.
Parity fix: ModelDelegationRequest.task_type was narrowed to 3 values during graduation (OMN-12126)
when the compat set at sha 4d887307 defines 12 values. This broke omnimarket validation (3 tests fail;
OMN-12659 held at 208/211). This PR widens task_type to the full 12-value compat set and adds parametrized
tests proving all 12 values accept. Full parity audit conducted across all delegation wire models —
only task_type required widening; all other str fields match compat intentionally.
is_seam_ticket: false
interface_change: false
interfaces_touched: []
Expand Down Expand Up @@ -43,20 +42,18 @@ dod_evidence:
check_value: "uv run mypy src/omnibase_core/models/delegation/ --strict"
- id: "dod-parity-audit"
description: >
Full parity audit against compat@4d887307: ModelDelegationRequest.task_type widened
3 to 12. All other task_type fields are str in both compat and core, correct.
ModelDelegationFallbackPolicy.action/on_exhaust and
ModelDelegationBackendConfig.tier are typed Literals in core (stricter than compat
str), intentional improvement per typed-models doctrine.
Full parity audit against compat@4d887307: ModelDelegationRequest.task_type widened 3 to 12. All
other task_type fields are str in both compat and core, correct. ModelDelegationFallbackPolicy.action/on_exhaust
and ModelDelegationBackendConfig.tier are typed Literals in core (stricter than compat str), intentional
improvement per typed-models doctrine.
source: "manual"
checks:
- check_type: "command"
check_value: "echo 'only ModelDelegationRequest.task_type required widening'"
- id: "dod-deploy-gate"
description: >
Deploy-gate evidence: pure Pydantic Literal type widening in a wire DTO.
No Kafka topic schema changes, no new topics, no Docker image rebuild, no runtime
process restart, no database migration required.
Deploy-gate evidence: pure Pydantic Literal type widening in a wire DTO. No Kafka topic schema changes,
no new topics, no Docker image rebuild, no runtime process restart, no database migration required.
source: "manual"
checks:
- check_type: "command"
Expand Down
4 changes: 2 additions & 2 deletions src/omnibase_core/enums/enum_skill_result_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
files to ~/.claude/skill-results/. This enum normalizes the 28+ ad-hoc status
strings observed across 40+ skills into 9 semantic categories. Skills needing
domain-specific granularity should use the ``extra_status`` field on
ModelSkillResult rather than extending this enum.
ModelSkillResultFile rather than extending this enum.

Behavioral definitions
----------------------
Expand Down Expand Up @@ -57,7 +57,7 @@ class EnumSkillResultStatus(UtilStrValueHelper, str, Enum):
Skills write result files to ``~/.claude/skill-results/``. This enum
normalizes the 28+ ad-hoc status strings into 9 semantic categories.
Skills needing domain-specific granularity should use the
``extra_status`` field on ``ModelSkillResult``.
``extra_status`` field on ``ModelSkillResultFile``.

**Transition policy:** This enum defines valid phase names, NOT valid
transitions between them. Consumers must not infer transition semantics
Expand Down
1 change: 1 addition & 0 deletions src/omnibase_core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
__all__ = [
# Domain modules (names only; no runtime import here)
"agent", # Agent status and lifecycle models (OMN-1847)
"artifacts", # Content-addressed artifact references (OMN-13091)
"audit", # Context integrity task dispatch models (OMN-5233)
"cli",
"common",
Expand Down
16 changes: 16 additions & 0 deletions src/omnibase_core/models/artifacts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""Content-addressed artifact models.

Provides ``ModelArtifactRef``, the canonical ``sha256:<hex>`` reference type
for the durable-capture artifact store (OMN-13091).
"""

from __future__ import annotations

from omnibase_core.models.artifacts.model_artifact_ref import ModelArtifactRef

__all__ = [
"ModelArtifactRef",
]
78 changes: 78 additions & 0 deletions src/omnibase_core/models/artifacts/model_artifact_ref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""Content-addressed artifact reference (OMN-13091).

``ModelArtifactRef`` is the single reference type for blobs in the
content-addressed artifact store (skill-output durable capture, OMN-13089).
A ref is always ``sha256:<64 lowercase hex chars>`` — the same prefix
convention as ``gate/diff_hash._sha256_prefixed()`` and omnimemory's
``content_blob_ref``.

Unlike the registry-resolved
:class:`~omnibase_core.models.handlers.model_handler_artifact_ref.ModelHandlerArtifactRef`
(an opaque, registry-dependent identifier), this ref IS the content address:
the bytes it points to can be hash-verified against the ref itself.

.. versionadded:: OMN-13091
"""

from __future__ import annotations

import hashlib
import re

from pydantic import BaseModel, ConfigDict, Field, field_validator

__all__ = ["ModelArtifactRef"]

_SHA256_REF_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
_SHA256_PREFIX = "sha256:"


class ModelArtifactRef(BaseModel):
"""Content-addressed reference to a stored artifact.

The ``ref`` field is the artifact's identity AND its integrity proof:
``sha256:<hex>`` where ``<hex>`` is the lowercase SHA-256 digest of the
artifact bytes. Consumers retrieving the blob re-hash it and compare
against this ref.

Example:
>>> import hashlib
>>> ref = ModelArtifactRef.from_bytes(b"runtime capture log")
>>> ref.ref == f"sha256:{hashlib.sha256(b'runtime capture log').hexdigest()}"
True
"""

model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)

ref: str = Field(
...,
description=(
"Content-addressed artifact reference in the form "
"'sha256:<64 lowercase hex chars>'. The hex digest is the "
"SHA-256 of the artifact bytes."
),
)

@field_validator("ref")
@classmethod
def _validate_sha256_ref(cls, value: str) -> str:
if not _SHA256_REF_RE.match(value):
msg = (
"artifact ref must match 'sha256:<64 lowercase hex chars>', "
f"got: {value!r}"
)
raise ValueError(msg)
return value

@property
def hex_digest(self) -> str:
"""Return the bare 64-char hex digest without the ``sha256:`` prefix."""
return self.ref.removeprefix(_SHA256_PREFIX)

@classmethod
def from_bytes(cls, data: bytes) -> ModelArtifactRef:
"""Compute the content address of ``data`` and return its ref."""
return cls(ref=f"{_SHA256_PREFIX}{hashlib.sha256(data).hexdigest()}")
6 changes: 6 additions & 0 deletions src/omnibase_core/models/dispatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
ModelLifecycleChain,
)
from omnibase_core.models.dispatch.model_model_call_record import ModelCallRecord
from omnibase_core.models.dispatch.model_skill_result import (
SKILL_RESULT_SCHEMA_VERSION,
ModelSkillResult,
)
from omnibase_core.models.dispatch.model_topic_parser import (
EnumTopicStandard,
ModelParsedTopic,
Expand All @@ -139,6 +143,7 @@
# Constants
"DEFAULT_HEARTBEAT_REQUIRED_SECONDS",
"HEARTBEAT_REQUIRED_ENV_VAR",
"SKILL_RESULT_SCHEMA_VERSION",
# Enums
"EnumDispatchLifecycleEmitter",
"EnumDispatchLifecycleState",
Expand All @@ -162,6 +167,7 @@
"ModelHandlerOutput",
"ModelHandlerRegistration",
"ModelParsedTopic",
"ModelSkillResult",
"ModelTopicParser",
# Functions
"compute_blocker_id",
Expand Down
162 changes: 162 additions & 0 deletions src/omnibase_core/models/dispatch/model_skill_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""Typed skill dispatch receipt envelope (OMN-13091).

``ModelSkillResult[T]`` is the skill-surface analogue of
:class:`~omnibase_core.models.dispatch.model_handler_output.ModelHandlerOutput`:
the single typed JSON object a receipt-mode dispatch (``onex node``/``onex run``
``--output receipt``) prints to stdout. It carries the FULL handler result —
never truncated, never size-limited — plus content-addressed
:class:`~omnibase_core.models.artifacts.model_artifact_ref.ModelArtifactRef`
handles for the captured intermediate context (runtime logs, envelopes,
progress) that is hidden from the dispatching agent.

Schema identity travels in the receipt: ``result_model`` is the fully
qualified name of the concrete ``T`` so consumers validate the CONCRETE
result type, never just the envelope structure. Every skill command declares
its concrete result model and the CLI validates against it before printing.

This is distinct from the legacy
:class:`~omnibase_core.models.skill.model_skill_result_file.ModelSkillResultFile`
(the ``skill-results/`` file contract from OMN-3867), which models a different
surface and is unrelated to receipt-mode dispatch.

See ``docs/plans/2026-06-12-skill-output-suppression-plan.md`` (Phase 0,
items 2-3) and epic OMN-13089.

.. versionadded:: OMN-13091
"""

from __future__ import annotations

import re
from typing import Generic, TypeVar
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, field_validator

from omnibase_core.enums.enum_skill_result_status import EnumSkillResultStatus
from omnibase_core.models.artifacts.model_artifact_ref import ModelArtifactRef
from omnibase_core.models.primitives.model_semver import ModelSemVer

__all__ = ["SKILL_RESULT_SCHEMA_VERSION", "ModelSkillResult"]

T = TypeVar("T")

# Current schema version for skill dispatch receipts. The hook backstop
# (Layer C) sniffs this field to pass receipt-mode output through untouched.
SKILL_RESULT_SCHEMA_VERSION = ModelSemVer(major=1, minor=0, patch=0)

# Fully qualified dotted path: at least one dot, each segment a valid
# Python identifier (e.g. "omnimarket.models.ModelDelegateSkillResponse").
_RESULT_MODEL_FQN_RE = re.compile(
r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+$"
)


class ModelSkillResult(BaseModel, Generic[T]):
"""Typed result envelope for one skill dispatch.

Exactly one of these is printed to stdout per receipt-mode dispatch.
The ``result`` field carries the skill's full typed result — the result
is the result; intermediate context is captured behind ``artifact_refs``
instead of flooding the dispatching agent.

Example:
>>> from uuid import uuid4
>>> envelope = ModelSkillResult[dict[str, str]](
... skill_name="delegate",
... node_name="node_delegate_skill_orchestrator",
... status=EnumSkillResultStatus.SUCCESS,
... correlation_id=uuid4(),
... run_id=uuid4(),
... exit_code=0,
... duration_ms=1250,
... result={"answer": "42"},
... result_model="builtins.dict",
... )
>>> envelope.status.is_success_like
True
"""

model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)

skill_name: str = Field(
...,
min_length=1,
description="Name of the dispatched skill (e.g. 'delegate').",
)
node_name: str = Field(
...,
min_length=1,
description="Name of the backing node the skill dispatched to.",
)
status: EnumSkillResultStatus = Field(
...,
description="Canonical execution outcome of the dispatch.",
)
correlation_id: UUID = Field(
...,
description="Correlation ID propagated from the dispatch envelope.",
)
run_id: UUID = Field(
...,
description="Unique ID for this skill run.",
)
exit_code: int = Field(
...,
description=(
"Process exit code of the dispatch. 0 on success; negative "
"values indicate signal termination (subprocess convention)."
),
)
duration_ms: int = Field(
...,
ge=0,
description="Wall-clock duration of the dispatch in milliseconds.",
)
result: T = Field(
...,
description=(
"The skill's FULL typed result. No size limits, no truncation — "
"the result is the result. On failure this carries the full "
"error output inline (errors are never hidden)."
),
)
result_model: str = Field(
...,
description=(
"Fully qualified name of the concrete result type T (e.g. "
"'omnimarket.models.ModelDelegateSkillResponse'). Schema "
"identity travels in the receipt so consumers validate the "
"concrete type, never just the envelope structure."
),
)
metrics: dict[str, float] = Field(
default_factory=dict,
description="Dispatch-level metrics (e.g. token counts, retries).",
)
artifact_refs: list[ModelArtifactRef] = Field(
default_factory=list,
description=(
"Content-addressed handles for captured intermediate context "
"(runtime logs, full handler result, envelopes). Retrievable "
"and hash-verified via the artifact store."
),
)
schema_version: ModelSemVer = Field(
default_factory=lambda: SKILL_RESULT_SCHEMA_VERSION,
description="Receipt schema version for format evolution.",
)

@field_validator("result_model")
@classmethod
def _validate_result_model_fqn(cls, value: str) -> str:
if not _RESULT_MODEL_FQN_RE.match(value):
msg = (
"result_model must be a fully qualified dotted name "
f"(e.g. 'pkg.module.ModelName'), got: {value!r}"
)
raise ValueError(msg)
return value
Loading
Loading