agent-control-models 7.3.0__tar.gz → 7.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/PKG-INFO +1 -1
  2. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/pyproject.toml +1 -1
  3. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/__init__.py +10 -0
  4. agent_control_models-7.3.2/src/agent_control_models/actions.py +53 -0
  5. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/controls.py +13 -2
  6. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/observability.py +28 -14
  7. agent_control_models-7.3.2/tests/test_actions.py +60 -0
  8. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/.gitignore +0 -0
  9. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/README.md +0 -0
  10. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/agent.py +0 -0
  11. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/base.py +0 -0
  12. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/errors.py +0 -0
  13. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/evaluation.py +0 -0
  14. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/health.py +0 -0
  15. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/policy.py +0 -0
  16. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/py.typed +0 -0
  17. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/server.py +0 -0
  18. {agent_control_models-7.3.0 → agent_control_models-7.3.2}/tests/test_controls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-control-models
3
- Version: 7.3.0
3
+ Version: 7.3.2
4
4
  Summary: Shared data models for Agent Control server and SDK
5
5
  Author: Agent Control Team
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-control-models"
3
- version = "7.3.0"
3
+ version = "7.3.2"
4
4
  description = "Shared data models for Agent Control server and SDK"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -7,6 +7,12 @@ try:
7
7
  except PackageNotFoundError:
8
8
  __version__ = "0.0.0.dev"
9
9
 
10
+ from .actions import (
11
+ ActionDecision,
12
+ expand_action_filter,
13
+ normalize_action,
14
+ normalize_action_list,
15
+ )
10
16
  from .agent import (
11
17
  BUILTIN_STEP_TYPES,
12
18
  STEP_TYPE_LLM,
@@ -91,6 +97,7 @@ __all__ = [
91
97
  "STEP_TYPE_TOOL",
92
98
  "STEP_TYPE_LLM",
93
99
  "BUILTIN_STEP_TYPES",
100
+ "ActionDecision",
94
101
  # Policy
95
102
  "Policy",
96
103
  # Evaluation
@@ -107,6 +114,9 @@ __all__ = [
107
114
  "EvaluatorSpec",
108
115
  "EvaluatorResult",
109
116
  "SteeringContext",
117
+ "normalize_action",
118
+ "normalize_action_list",
119
+ "expand_action_filter",
110
120
  # Error models
111
121
  "ProblemDetail",
112
122
  "ErrorCode",
@@ -0,0 +1,53 @@
1
+ """Shared control-action types and normalization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Literal, cast
7
+
8
+ type ActionDecision = Literal["deny", "steer", "observe"]
9
+
10
+ _OBSERVE_ACTION_ALIASES = frozenset({"allow", "observe", "warn", "log"})
11
+ _ACTION_QUERY_EXPANSION: dict[ActionDecision, tuple[str, ...]] = {
12
+ "deny": ("deny",),
13
+ "steer": ("steer",),
14
+ "observe": ("observe", "allow", "warn", "log"),
15
+ }
16
+
17
+
18
+ def normalize_action(action: str) -> ActionDecision:
19
+ """Normalize a public or legacy action name to the canonical action."""
20
+ if action in _OBSERVE_ACTION_ALIASES:
21
+ return "observe"
22
+ if action in ("deny", "steer"):
23
+ return cast(ActionDecision, action)
24
+ raise ValueError(
25
+ "Invalid action. Expected one of: deny, steer, observe "
26
+ "(legacy aliases allow/warn/log are also accepted temporarily)."
27
+ )
28
+
29
+
30
+ def normalize_action_list(actions: Sequence[str]) -> list[ActionDecision]:
31
+ """Normalize a list of actions while preserving order and removing duplicates."""
32
+ normalized: list[ActionDecision] = []
33
+ seen: set[ActionDecision] = set()
34
+ for action in actions:
35
+ canonical = normalize_action(action)
36
+ if canonical in seen:
37
+ continue
38
+ seen.add(canonical)
39
+ normalized.append(canonical)
40
+ return normalized
41
+
42
+
43
+ def expand_action_filter(actions: Sequence[ActionDecision]) -> list[str]:
44
+ """Expand canonical action filters to include legacy stored event values."""
45
+ expanded: list[str] = []
46
+ seen: set[str] = set()
47
+ for action in actions:
48
+ for candidate in _ACTION_QUERY_EXPANSION[action]:
49
+ if candidate in seen:
50
+ continue
51
+ seen.add(candidate)
52
+ expanded.append(candidate)
53
+ return expanded
@@ -10,6 +10,7 @@ from uuid import uuid4
10
10
  import re2
11
11
  from pydantic import ConfigDict, Field, ValidationInfo, field_validator, model_validator
12
12
 
13
+ from .actions import ActionDecision, normalize_action
13
14
  from .base import BaseModel
14
15
 
15
16
 
@@ -265,7 +266,7 @@ class SteeringContext(BaseModel):
265
266
  class ControlAction(BaseModel):
266
267
  """What to do when control matches."""
267
268
 
268
- decision: Literal["allow", "deny", "steer", "warn", "log"] = Field(
269
+ decision: ActionDecision = Field(
269
270
  ..., description="Action to take when control is triggered"
270
271
  )
271
272
  steering_context: SteeringContext | None = Field(
@@ -277,6 +278,11 @@ class ControlAction(BaseModel):
277
278
  )
278
279
  )
279
280
 
281
+ @field_validator("decision", mode="before")
282
+ @classmethod
283
+ def normalize_decision(cls, value: str) -> ActionDecision:
284
+ return normalize_action(value)
285
+
280
286
 
281
287
  MAX_CONDITION_DEPTH = 6
282
288
 
@@ -649,7 +655,7 @@ class ControlMatch(BaseModel):
649
655
  )
650
656
  control_id: int = Field(..., description="Database ID of the control")
651
657
  control_name: str = Field(..., description="Name of the control")
652
- action: Literal["allow", "deny", "steer", "warn", "log"] = Field(
658
+ action: ActionDecision = Field(
653
659
  ..., description="Action configured for this control"
654
660
  )
655
661
  result: EvaluatorResult = Field(
@@ -659,3 +665,8 @@ class ControlMatch(BaseModel):
659
665
  None,
660
666
  description="Steering context for steer actions if configured"
661
667
  )
668
+
669
+ @field_validator("action", mode="before")
670
+ @classmethod
671
+ def normalize_action_value(cls, value: str) -> ActionDecision:
672
+ return normalize_action(value)
@@ -14,6 +14,7 @@ from uuid import uuid4
14
14
 
15
15
  from pydantic import Field, field_validator
16
16
 
17
+ from .actions import ActionDecision, normalize_action, normalize_action_list
17
18
  from .agent import AGENT_NAME_MIN_LENGTH, AGENT_NAME_PATTERN, normalize_agent_name
18
19
  from .base import BaseModel
19
20
 
@@ -42,7 +43,7 @@ class ControlExecutionEvent(BaseModel):
42
43
  control_name: Name of the control (denormalized for queries)
43
44
  check_stage: "pre" (before execution) or "post" (after execution)
44
45
  applies_to: "llm_call" or "tool_call"
45
- action: The action taken (allow, deny, warn, log)
46
+ action: The action taken (deny, steer, observe)
46
47
  matched: Whether the control evaluator matched
47
48
  confidence: Confidence score from the evaluator (0.0-1.0)
48
49
  timestamp: When the control was executed (UTC)
@@ -90,7 +91,7 @@ class ControlExecutionEvent(BaseModel):
90
91
  )
91
92
 
92
93
  # Result
93
- action: Literal["allow", "deny", "steer", "warn", "log"] = Field(
94
+ action: ActionDecision = Field(
94
95
  ..., description="Action taken by the control"
95
96
  )
96
97
  matched: bool = Field(
@@ -160,6 +161,11 @@ class ControlExecutionEvent(BaseModel):
160
161
  def validate_and_normalize_agent_name(cls, value: str) -> str:
161
162
  return normalize_agent_name(str(value))
162
163
 
164
+ @field_validator("action", mode="before")
165
+ @classmethod
166
+ def normalize_event_action(cls, value: str) -> ActionDecision:
167
+ return normalize_action(value)
168
+
163
169
  model_config = {
164
170
  "json_schema_extra": {
165
171
  "examples": [
@@ -265,7 +271,7 @@ class EventQueryRequest(BaseModel):
265
271
  control_execution_id: Filter by specific event ID
266
272
  agent_name: Filter by agent identifier
267
273
  control_ids: Filter by control IDs
268
- actions: Filter by actions (allow, deny, steer, warn, log)
274
+ actions: Filter by actions (deny, steer, observe)
269
275
  matched: Filter by matched status
270
276
  check_stages: Filter by check stages (pre, post)
271
277
  applies_to: Filter by call type (llm_call, tool_call)
@@ -293,7 +299,7 @@ class EventQueryRequest(BaseModel):
293
299
  control_ids: list[int] | None = Field(
294
300
  default=None, description="Filter by control IDs"
295
301
  )
296
- actions: list[Literal["allow", "deny", "steer", "warn", "log"]] | None = Field(
302
+ actions: list[ActionDecision] | None = Field(
297
303
  default=None, description="Filter by actions"
298
304
  )
299
305
  matched: bool | None = Field(default=None, description="Filter by matched status")
@@ -318,7 +324,7 @@ class EventQueryRequest(BaseModel):
318
324
  {"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"},
319
325
  {
320
326
  "agent_name": "my-agent",
321
- "actions": ["deny", "warn"],
327
+ "actions": ["deny", "observe"],
322
328
  "start_time": "2025-01-09T00:00:00Z",
323
329
  "limit": 50,
324
330
  },
@@ -335,6 +341,15 @@ class EventQueryRequest(BaseModel):
335
341
  return None
336
342
  return normalize_agent_name(str(value))
337
343
 
344
+ @field_validator("actions", mode="before")
345
+ @classmethod
346
+ def normalize_actions_filter(
347
+ cls, value: list[str] | None
348
+ ) -> list[ActionDecision] | None:
349
+ if value is None:
350
+ return None
351
+ return normalize_action_list(value)
352
+
338
353
 
339
354
  class EventQueryResponse(BaseModel):
340
355
  """
@@ -368,14 +383,15 @@ class ControlStats(BaseModel):
368
383
  execution_count: Total number of executions
369
384
  match_count: Number of times the control matched
370
385
  non_match_count: Number of times the control did not match
371
- allow_count: Number of allow actions
372
386
  deny_count: Number of deny actions
373
387
  steer_count: Number of steer actions
374
- warn_count: Number of warn actions
375
- log_count: Number of log actions
388
+ observe_count: Number of observe actions
376
389
  error_count: Number of errors during evaluation
377
390
  avg_confidence: Average confidence score
378
391
  avg_duration_ms: Average execution duration in milliseconds
392
+
393
+ Invariant:
394
+ deny_count + steer_count + observe_count == match_count
379
395
  """
380
396
 
381
397
  control_id: int = Field(..., description="Control ID")
@@ -383,11 +399,9 @@ class ControlStats(BaseModel):
383
399
  execution_count: int = Field(..., ge=0, description="Total executions")
384
400
  match_count: int = Field(..., ge=0, description="Total matches")
385
401
  non_match_count: int = Field(..., ge=0, description="Total non-matches")
386
- allow_count: int = Field(..., ge=0, description="Allow actions")
387
402
  deny_count: int = Field(..., ge=0, description="Deny actions")
388
403
  steer_count: int = Field(..., ge=0, description="Steer actions")
389
- warn_count: int = Field(..., ge=0, description="Warn actions")
390
- log_count: int = Field(..., ge=0, description="Log actions")
404
+ observe_count: int = Field(..., ge=0, description="Observe actions")
391
405
  error_count: int = Field(..., ge=0, description="Evaluation errors")
392
406
  avg_confidence: float = Field(..., ge=0.0, le=1.0, description="Average confidence")
393
407
  avg_duration_ms: float | None = Field(
@@ -460,7 +474,7 @@ class TimeseriesBucket(BaseModel):
460
474
  error_count: int = Field(..., ge=0, description="Errors in bucket")
461
475
  action_counts: dict[str, int] = Field(
462
476
  default_factory=dict,
463
- description="Action breakdown: {allow, deny, steer, warn, log}",
477
+ description="Action breakdown: {deny, steer, observe}",
464
478
  )
465
479
  avg_confidence: float | None = Field(
466
480
  default=None, ge=0.0, le=1.0, description="Average confidence score"
@@ -476,7 +490,7 @@ class StatsTotals(BaseModel):
476
490
 
477
491
  Invariant: execution_count = match_count + non_match_count + error_count
478
492
 
479
- Matches have actions (allow, deny, steer, warn, log) tracked in action_counts.
493
+ Matches have actions (deny, steer, observe) tracked in action_counts.
480
494
  sum(action_counts.values()) == match_count
481
495
 
482
496
  Attributes:
@@ -494,7 +508,7 @@ class StatsTotals(BaseModel):
494
508
  error_count: int = Field(default=0, ge=0, description="Total errors")
495
509
  action_counts: dict[str, int] = Field(
496
510
  default_factory=dict,
497
- description="Action breakdown for matches: {allow, deny, steer, warn, log}",
511
+ description="Action breakdown for matches: {deny, steer, observe}",
498
512
  )
499
513
  timeseries: list[TimeseriesBucket] | None = Field(
500
514
  default=None,
@@ -0,0 +1,60 @@
1
+ """Tests for shared control-action compatibility behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from agent_control_models import (
7
+ ControlAction,
8
+ ControlExecutionEvent,
9
+ ControlMatch,
10
+ EventQueryRequest,
11
+ EvaluatorResult,
12
+ expand_action_filter,
13
+ )
14
+ from pydantic import ValidationError
15
+
16
+
17
+ def test_event_query_actions_normalize_and_expand_for_legacy_observability() -> None:
18
+ # Given: a query that mixes canonical and legacy advisory action names
19
+ query = EventQueryRequest(
20
+ actions=["warn", "observe", "deny", "log", "deny", "steer", "allow", "steer"]
21
+ )
22
+
23
+ # When: expanding the normalized public action filter for stored event rows
24
+ expanded = expand_action_filter(query.actions or [])
25
+
26
+ # Then: the public filter is canonicalized, deduped, and expanded for legacy rows
27
+ assert query.actions == ["observe", "deny", "steer"]
28
+ assert expanded == ["observe", "allow", "warn", "log", "deny", "steer"]
29
+
30
+
31
+ def test_invalid_action_is_rejected_across_public_model_boundaries() -> None:
32
+ # Given: the same invalid action at each public model boundary
33
+ invalid_action = "block"
34
+ invalid_builders = [
35
+ lambda: ControlAction.model_validate({"decision": invalid_action}),
36
+ lambda: ControlMatch(
37
+ control_id=123,
38
+ control_name="pii-check",
39
+ action=invalid_action,
40
+ result=EvaluatorResult(matched=True, confidence=0.9),
41
+ ),
42
+ lambda: ControlExecutionEvent(
43
+ trace_id="trace-123",
44
+ span_id="span-123",
45
+ agent_name="test-agent",
46
+ control_id=123,
47
+ control_name="pii-check",
48
+ check_stage="pre",
49
+ applies_to="llm_call",
50
+ action=invalid_action,
51
+ matched=True,
52
+ confidence=0.9,
53
+ ),
54
+ lambda: EventQueryRequest(actions=[invalid_action]),
55
+ ]
56
+
57
+ for build_invalid_model in invalid_builders:
58
+ # When / Then: validation fails before the invalid action can enter the system
59
+ with pytest.raises(ValidationError, match="Invalid action"):
60
+ build_invalid_model()