Coverage for node / src / stigmem_node / models / intents.py: 96%
85 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Intent envelope models."""
3from __future__ import annotations
5from pydantic import BaseModel, ConfigDict, Field, field_validator
7from .constants import VALID_SCOPES
8from .facts import FactValue
10VALID_ESCALATION_PRIORITIES = {"low", "medium", "high", "critical"}
11VALID_ESCALATION_CHANNELS = {"stigmem", "email", "slack"}
14class Constraint(BaseModel):
15 """Hard limit on an intent (Spec-X8-Intent-Envelope)."""
17 kind: str = Field(..., min_length=1)
18 limit: FactValue
19 unit: str | None = None
22class Preference(BaseModel):
23 """Soft preference on an intent (Spec-X8-Intent-Envelope)."""
25 kind: str = Field(..., min_length=1)
26 value: FactValue
27 weight: float = Field(1.0, ge=0.0, le=1.0)
30class DeferenceRule(BaseModel):
31 """When to defer to another entity before proceeding (Spec-X8-Intent-Envelope)."""
33 condition: str = Field(..., min_length=1)
34 defer_to: str = Field(..., min_length=1)
35 timeout_s: int | None = Field(None, ge=0)
38class EscalationPolicy(BaseModel):
39 """Who to notify and how when escalation is required (Spec-X8-Intent-Envelope)."""
41 escalate_to: str = Field(..., min_length=1)
42 channel: str = Field("stigmem")
43 priority: str = Field("medium")
44 include_context: bool = True
46 @field_validator("priority")
47 @classmethod
48 def check_priority(cls, p: str) -> str:
49 if p not in VALID_ESCALATION_PRIORITIES:
50 raise ValueError(f"priority must be one of {VALID_ESCALATION_PRIORITIES}")
51 return p
53 @field_validator("channel")
54 @classmethod
55 def check_channel(cls, c: str) -> str:
56 if c not in VALID_ESCALATION_CHANNELS:
57 raise ValueError(f"channel must be one of {VALID_ESCALATION_CHANNELS}")
58 return c
61class HandoffArtifact(BaseModel):
62 """Named artifact reference passed along with a handoff (Spec-X8-Intent-Envelope)."""
64 name: str = Field(..., min_length=1)
65 ref: str = Field(..., min_length=1, description="URI of the artifact fact")
68class HandoffPayload(BaseModel):
69 """Structured context transfer for agent handoffs (Spec-X8-Intent-Envelope).
71 fact_refs MUST include any facts the receiver needs to reconstitute context.
72 """
74 summary: str = Field(..., min_length=1)
75 fact_refs: list[str] = Field(default_factory=list)
76 continuation: str | None = None
77 artifacts: list[HandoffArtifact] = Field(default_factory=list)
80class IntentEnvelopeRequest(BaseModel):
81 """POST /v1/intents request body (Spec-X8-Intent-Envelope).
83 The ``from`` field is aliased as ``from_uri`` in Python to avoid the keyword clash.
84 Clients MUST send the JSON key ``"from"``.
85 """
87 model_config = ConfigDict(populate_by_name=True)
89 id: str | None = Field(
90 None,
91 description="Optional client-generated intent ID (UUID). Auto-assigned when omitted.",
92 )
93 from_uri: str = Field(..., alias="from", min_length=1, description="Sender entity URI")
94 to: list[str] = Field(
95 ..., min_length=1, description="Target entity URIs (at least one required)"
96 )
97 goal: str = Field(..., min_length=1, max_length=2048)
98 scope: str = Field("company")
99 expires_at: str | None = Field(None, description="ISO 8601 UTC expiry; null = non-expiring")
100 constraint: list[Constraint] = Field(default_factory=list)
101 preference: list[Preference] = Field(default_factory=list)
102 deference: list[DeferenceRule] = Field(default_factory=list)
103 escalation: EscalationPolicy | None = None
104 handoff: HandoffPayload | None = None
106 @field_validator("scope")
107 @classmethod
108 def check_scope(cls, s: str) -> str:
109 if s not in VALID_SCOPES:
110 raise ValueError(f"scope must be one of {VALID_SCOPES}")
111 return s
113 @field_validator("to")
114 @classmethod
115 def check_to_non_empty(cls, to: list[str]) -> list[str]:
116 if not to: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 raise ValueError("to must contain at least one target URI")
118 if any(not t for t in to): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 raise ValueError("to entries must be non-empty strings")
120 return to
123class IntentEnvelopeRecord(BaseModel):
124 """Response for POST /v1/intents and GET /v1/intents/:id (Spec-X8-Intent-Envelope)."""
126 model_config = ConfigDict(populate_by_name=True)
128 id: str
129 from_uri: str = Field(..., alias="from", description="Sender entity URI")
130 to: list[str]
131 goal: str
132 scope: str
133 created_at: str
134 expires_at: str | None
135 constraint: list[Constraint]
136 preference: list[Preference]
137 deference: list[DeferenceRule]
138 escalation: EscalationPolicy | None
139 handoff: HandoffPayload | None
140 fact_ids: list[str] = Field(
141 description="IDs of all facts written into the fabric for this envelope"
142 )