sondera-harness 0.6.0__py3-none-any.whl
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.
- sondera/__init__.py +111 -0
- sondera/__main__.py +4 -0
- sondera/adk/__init__.py +3 -0
- sondera/adk/analyze.py +222 -0
- sondera/adk/plugin.py +387 -0
- sondera/cli.py +22 -0
- sondera/exceptions.py +167 -0
- sondera/harness/__init__.py +6 -0
- sondera/harness/abc.py +102 -0
- sondera/harness/cedar/__init__.py +0 -0
- sondera/harness/cedar/harness.py +363 -0
- sondera/harness/cedar/schema.py +225 -0
- sondera/harness/sondera/__init__.py +0 -0
- sondera/harness/sondera/_grpc.py +354 -0
- sondera/harness/sondera/harness.py +890 -0
- sondera/langgraph/__init__.py +15 -0
- sondera/langgraph/analyze.py +543 -0
- sondera/langgraph/exceptions.py +19 -0
- sondera/langgraph/graph.py +210 -0
- sondera/langgraph/middleware.py +454 -0
- sondera/proto/google/protobuf/any_pb2.py +37 -0
- sondera/proto/google/protobuf/any_pb2.pyi +14 -0
- sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/duration_pb2.py +37 -0
- sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
- sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/empty_pb2.py +37 -0
- sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
- sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/struct_pb2.py +47 -0
- sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
- sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
- sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
- sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
- sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
- sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
- sondera/proto/sondera/__init__.py +0 -0
- sondera/proto/sondera/core/__init__.py +0 -0
- sondera/proto/sondera/core/v1/__init__.py +0 -0
- sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
- sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
- sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
- sondera/proto/sondera/harness/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
- sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
- sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
- sondera/py.typed +0 -0
- sondera/settings.py +20 -0
- sondera/strands/__init__.py +5 -0
- sondera/strands/analyze.py +244 -0
- sondera/strands/harness.py +333 -0
- sondera/tui/__init__.py +0 -0
- sondera/tui/app.py +309 -0
- sondera/tui/screens/__init__.py +5 -0
- sondera/tui/screens/adjudication.py +184 -0
- sondera/tui/screens/agent.py +158 -0
- sondera/tui/screens/trajectory.py +158 -0
- sondera/tui/widgets/__init__.py +23 -0
- sondera/tui/widgets/agent_card.py +94 -0
- sondera/tui/widgets/agent_list.py +73 -0
- sondera/tui/widgets/recent_adjudications.py +52 -0
- sondera/tui/widgets/recent_trajectories.py +54 -0
- sondera/tui/widgets/summary.py +57 -0
- sondera/tui/widgets/tool_card.py +33 -0
- sondera/tui/widgets/violation_panel.py +72 -0
- sondera/tui/widgets/violations_list.py +78 -0
- sondera/tui/widgets/violations_summary.py +104 -0
- sondera/types.py +346 -0
- sondera_harness-0.6.0.dist-info/METADATA +323 -0
- sondera_harness-0.6.0.dist-info/RECORD +77 -0
- sondera_harness-0.6.0.dist-info/WHEEL +5 -0
- sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
- sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
- sondera_harness-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.reactive import reactive
|
|
3
|
+
from textual.widget import Widget
|
|
4
|
+
from textual.widgets import DataTable
|
|
5
|
+
|
|
6
|
+
from sondera.types import AdjudicationRecord, Decision
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ViolationsList(Widget):
|
|
10
|
+
"""Widget displaying a list of violations/adjudications in a table format."""
|
|
11
|
+
|
|
12
|
+
BORDER_TITLE = "Adjudications"
|
|
13
|
+
|
|
14
|
+
adjudications: reactive[list[AdjudicationRecord]] = reactive([])
|
|
15
|
+
|
|
16
|
+
HEADERS = ["Decision", "Agent", "Trajectory", "Step", "Reason", "Annotations"]
|
|
17
|
+
|
|
18
|
+
def compose(self) -> ComposeResult:
|
|
19
|
+
yield DataTable()
|
|
20
|
+
|
|
21
|
+
def on_mount(self) -> None:
|
|
22
|
+
table = self.query_one(DataTable)
|
|
23
|
+
table.add_columns(*self.HEADERS)
|
|
24
|
+
table.cursor_type = "row"
|
|
25
|
+
table.zebra_stripes = True
|
|
26
|
+
table.can_focus = True
|
|
27
|
+
|
|
28
|
+
def watch_adjudications(
|
|
29
|
+
self,
|
|
30
|
+
_old_adjudications: list[AdjudicationRecord],
|
|
31
|
+
new_adjudications: list[AdjudicationRecord],
|
|
32
|
+
) -> None:
|
|
33
|
+
table = self.query_one(DataTable)
|
|
34
|
+
table.clear()
|
|
35
|
+
for record in new_adjudications:
|
|
36
|
+
decision = record.adjudication.decision
|
|
37
|
+
if decision == Decision.DENY:
|
|
38
|
+
decision_display = "❌ DENY"
|
|
39
|
+
elif decision == Decision.ESCALATE:
|
|
40
|
+
decision_display = "⚠️ ESCALATE"
|
|
41
|
+
else:
|
|
42
|
+
decision_display = "✅ ALLOW"
|
|
43
|
+
|
|
44
|
+
reason = record.adjudication.reason
|
|
45
|
+
reason_display = reason[:40] + "..." if len(reason) > 43 else reason
|
|
46
|
+
|
|
47
|
+
# Format annotations as comma-separated policy IDs
|
|
48
|
+
annotations = record.adjudication.annotations
|
|
49
|
+
if annotations:
|
|
50
|
+
ann_ids = [ann.id for ann in annotations]
|
|
51
|
+
annotations_display = ", ".join(ann_ids)
|
|
52
|
+
if len(annotations_display) > 30:
|
|
53
|
+
annotations_display = annotations_display[:27] + "..."
|
|
54
|
+
else:
|
|
55
|
+
annotations_display = "-"
|
|
56
|
+
|
|
57
|
+
table.add_row(
|
|
58
|
+
decision_display,
|
|
59
|
+
record.agent_id[:12] + "..."
|
|
60
|
+
if len(record.agent_id) > 15
|
|
61
|
+
else record.agent_id,
|
|
62
|
+
record.trajectory_id[:8] + "..."
|
|
63
|
+
if len(record.trajectory_id) > 11
|
|
64
|
+
else record.trajectory_id,
|
|
65
|
+
record.step_id[:8] + "..."
|
|
66
|
+
if len(record.step_id) > 11
|
|
67
|
+
else record.step_id,
|
|
68
|
+
reason_display,
|
|
69
|
+
annotations_display,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def get_selected_adjudication(self) -> AdjudicationRecord | None:
|
|
73
|
+
"""Get the currently selected adjudication record from the table."""
|
|
74
|
+
table = self.query_one(DataTable)
|
|
75
|
+
cursor_row = table.cursor_row
|
|
76
|
+
if cursor_row is not None and 0 <= cursor_row < len(self.adjudications):
|
|
77
|
+
return self.adjudications[cursor_row]
|
|
78
|
+
return None
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Container, Horizontal
|
|
5
|
+
from textual.reactive import reactive
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import Digits, Static
|
|
8
|
+
|
|
9
|
+
from sondera.types import AdjudicationRecord, Decision
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ViolationsSummary(Widget):
|
|
13
|
+
"""Widget displaying violations statistics.
|
|
14
|
+
|
|
15
|
+
Shows:
|
|
16
|
+
- Total violations count
|
|
17
|
+
- Violations grouped by agent
|
|
18
|
+
- Violations grouped by policy
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
BORDER_TITLE = "Violations Summary"
|
|
22
|
+
|
|
23
|
+
adjudications: reactive[list[AdjudicationRecord]] = reactive([], recompose=True)
|
|
24
|
+
|
|
25
|
+
def _get_violations(self) -> list[AdjudicationRecord]:
|
|
26
|
+
"""Filter adjudications to only include violations (DENY decisions)."""
|
|
27
|
+
return [
|
|
28
|
+
adj
|
|
29
|
+
for adj in self.adjudications
|
|
30
|
+
if adj.adjudication.decision == Decision.DENY
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def _get_escalations(self) -> list[AdjudicationRecord]:
|
|
34
|
+
"""Filter adjudications to include escalations."""
|
|
35
|
+
return [
|
|
36
|
+
adj
|
|
37
|
+
for adj in self.adjudications
|
|
38
|
+
if adj.adjudication.decision == Decision.ESCALATE
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def _count_by_agent(self, violations: list[AdjudicationRecord]) -> Counter:
|
|
42
|
+
"""Count violations by agent ID."""
|
|
43
|
+
return Counter(v.agent_id for v in violations)
|
|
44
|
+
|
|
45
|
+
def _count_by_policy(self, violations: list[AdjudicationRecord]) -> Counter:
|
|
46
|
+
"""Count violations by policy (from reason field)."""
|
|
47
|
+
# Extract policy identifier from reason - typically first word or phrase
|
|
48
|
+
return Counter(v.adjudication.reason.split(":")[0].strip() for v in violations)
|
|
49
|
+
|
|
50
|
+
def compose(self) -> ComposeResult:
|
|
51
|
+
violations = self._get_violations()
|
|
52
|
+
escalations = self._get_escalations()
|
|
53
|
+
total_adjudications = len(self.adjudications)
|
|
54
|
+
allowed = total_adjudications - len(violations) - len(escalations)
|
|
55
|
+
|
|
56
|
+
# Overall counts
|
|
57
|
+
yield Static("[bold]Overview[/bold]", classes="section-header")
|
|
58
|
+
with Horizontal(classes="summary-row"):
|
|
59
|
+
with Container(classes="summary-item"):
|
|
60
|
+
yield Static("Total Adjudications")
|
|
61
|
+
yield Digits(str(total_adjudications), classes="summary-digit")
|
|
62
|
+
with Container(classes="summary-item"):
|
|
63
|
+
yield Static("Allowed", classes="stat-allowed")
|
|
64
|
+
yield Digits(str(allowed), classes="summary-digit digit-allowed")
|
|
65
|
+
with Container(classes="summary-item"):
|
|
66
|
+
yield Static("Violations", classes="stat-violations")
|
|
67
|
+
yield Digits(
|
|
68
|
+
str(len(violations)), classes="summary-digit digit-violations"
|
|
69
|
+
)
|
|
70
|
+
with Container(classes="summary-item"):
|
|
71
|
+
yield Static("Escalated", classes="stat-escalated")
|
|
72
|
+
yield Digits(
|
|
73
|
+
str(len(escalations)), classes="summary-digit digit-escalated"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Violations by Agent
|
|
77
|
+
yield Static("[bold]Violations by Agent[/bold]", classes="section-header")
|
|
78
|
+
by_agent = self._count_by_agent(violations)
|
|
79
|
+
if by_agent:
|
|
80
|
+
with Horizontal(classes="summary-row"):
|
|
81
|
+
for agent_id, count in by_agent.most_common(5):
|
|
82
|
+
with Container(classes="summary-item"):
|
|
83
|
+
display_id = (
|
|
84
|
+
agent_id[:12] + "..." if len(agent_id) > 15 else agent_id
|
|
85
|
+
)
|
|
86
|
+
yield Static(display_id, classes="agent-label")
|
|
87
|
+
yield Digits(str(count), classes="summary-digit")
|
|
88
|
+
else:
|
|
89
|
+
yield Static("No violations by agent", classes="empty-message")
|
|
90
|
+
|
|
91
|
+
# Violations by Policy/Reason
|
|
92
|
+
yield Static("[bold]Violations by Policy[/bold]", classes="section-header")
|
|
93
|
+
by_policy = self._count_by_policy(violations)
|
|
94
|
+
if by_policy:
|
|
95
|
+
with Horizontal(classes="summary-row"):
|
|
96
|
+
for policy, count in by_policy.most_common(5):
|
|
97
|
+
with Container(classes="summary-item"):
|
|
98
|
+
display_policy = (
|
|
99
|
+
policy[:15] + "..." if len(policy) > 18 else policy
|
|
100
|
+
)
|
|
101
|
+
yield Static(display_policy, classes="policy-label")
|
|
102
|
+
yield Digits(str(count), classes="summary-digit")
|
|
103
|
+
else:
|
|
104
|
+
yield Static("No violations by policy", classes="empty-message")
|
sondera/types.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Sondera SDK type definitions for agent interoperability and policy evaluation."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from pydantic.alias_generators import to_camel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Model(BaseModel):
|
|
13
|
+
"""Base model for all Sondera SDK types."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
alias_generator=to_camel,
|
|
17
|
+
populate_by_name=True,
|
|
18
|
+
from_attributes=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Parameter(Model):
|
|
23
|
+
"""
|
|
24
|
+
Parameter object allows the definition of input and output data types.
|
|
25
|
+
|
|
26
|
+
Simple parameter type theory for now.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
""" Name of the parameter. For human-readable display or metadata.
|
|
31
|
+
"""
|
|
32
|
+
description: str
|
|
33
|
+
""" Description of the parameter. For human-readable display or metadata.
|
|
34
|
+
"""
|
|
35
|
+
type: str
|
|
36
|
+
""" Type of the parameter.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SourceCode(Model):
|
|
41
|
+
language: str
|
|
42
|
+
code: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Tool(Model):
|
|
46
|
+
id: str | None = None
|
|
47
|
+
""" Optional unique identifier for the tool. Auto-generated if not provided.
|
|
48
|
+
"""
|
|
49
|
+
name: str
|
|
50
|
+
""" Name of the tool. For human-readable display or metadata.
|
|
51
|
+
"""
|
|
52
|
+
description: str
|
|
53
|
+
""" Description of the tool. For human-readable display or metadata.
|
|
54
|
+
"""
|
|
55
|
+
parameters: list[Parameter]
|
|
56
|
+
""" The parameters that are used by the tool.
|
|
57
|
+
"""
|
|
58
|
+
parameters_json_schema: str | None = None
|
|
59
|
+
""" JSON schema for the tool parameters.
|
|
60
|
+
"""
|
|
61
|
+
response: str | None = None
|
|
62
|
+
""" The type that is returned by the tool.
|
|
63
|
+
"""
|
|
64
|
+
response_json_schema: str | None = None
|
|
65
|
+
""" JSON schema for the tool response.
|
|
66
|
+
"""
|
|
67
|
+
source: SourceCode | None = None
|
|
68
|
+
""" The source of the tool if available.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Agent(Model):
|
|
73
|
+
id: str
|
|
74
|
+
""" Unique identifier for the agent.
|
|
75
|
+
"""
|
|
76
|
+
provider_id: str
|
|
77
|
+
""" Identifier for the provider of the agent.
|
|
78
|
+
"""
|
|
79
|
+
name: str
|
|
80
|
+
""" Name of the agent. For human-readable display or metadata.
|
|
81
|
+
"""
|
|
82
|
+
description: str
|
|
83
|
+
""" Description of the agent. For human-readable display or metadata.
|
|
84
|
+
"""
|
|
85
|
+
instruction: str
|
|
86
|
+
""" Instruction or goal of the agent.
|
|
87
|
+
"""
|
|
88
|
+
tools: list[Tool]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TrajectoryStatus(Enum):
|
|
92
|
+
"""Status of the trajectory."""
|
|
93
|
+
|
|
94
|
+
UNKNOWN = "unknown"
|
|
95
|
+
PENDING = "pending"
|
|
96
|
+
RUNNING = "running"
|
|
97
|
+
COMPLETED = "completed"
|
|
98
|
+
SUSPENDED = "suspended"
|
|
99
|
+
FAILED = "failed"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Stage(Enum):
|
|
103
|
+
"""Lifecycle stage of the step."""
|
|
104
|
+
|
|
105
|
+
PRE_RUN = "pre_run"
|
|
106
|
+
PRE_MODEL = "pre_model"
|
|
107
|
+
POST_MODEL = "post_model"
|
|
108
|
+
PRE_TOOL = "pre_tool"
|
|
109
|
+
POST_TOOL = "post_tool"
|
|
110
|
+
POST_RUN = "post_run"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Role(Enum):
|
|
114
|
+
"""Role of the step."""
|
|
115
|
+
|
|
116
|
+
USER = "user"
|
|
117
|
+
MODEL = "model"
|
|
118
|
+
TOOL = "tool"
|
|
119
|
+
SYSTEM = "system"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TrajectoryStep(Model):
|
|
123
|
+
role: Role
|
|
124
|
+
""" Role of the step.
|
|
125
|
+
"""
|
|
126
|
+
state: dict[str, Any] = Field(default_factory=dict)
|
|
127
|
+
""" State of the step.
|
|
128
|
+
"""
|
|
129
|
+
stage: Stage
|
|
130
|
+
""" Stage of the step.
|
|
131
|
+
"""
|
|
132
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
133
|
+
""" Created at timestamp.
|
|
134
|
+
"""
|
|
135
|
+
context: Any | None = None
|
|
136
|
+
""" Context of the step.
|
|
137
|
+
"""
|
|
138
|
+
content: Any
|
|
139
|
+
""" Content of the step.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Trajectory(Model):
|
|
144
|
+
id: str = Field(default_factory=lambda: f"traj-{uuid.uuid4()}")
|
|
145
|
+
""" Unique identifier for the trajectory.
|
|
146
|
+
"""
|
|
147
|
+
agent_id: str = Field(default_factory=lambda: "agent-1")
|
|
148
|
+
""" Identifier for the agent that created the trajectory.
|
|
149
|
+
"""
|
|
150
|
+
status: TrajectoryStatus = Field(default=TrajectoryStatus.PENDING)
|
|
151
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
152
|
+
""" Metadata of the trajectory.
|
|
153
|
+
"""
|
|
154
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
155
|
+
""" Created at timestamp.
|
|
156
|
+
"""
|
|
157
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
158
|
+
""" Updated at timestamp.
|
|
159
|
+
"""
|
|
160
|
+
started_at: datetime | None = Field(default=None)
|
|
161
|
+
""" Started at timestamp.
|
|
162
|
+
"""
|
|
163
|
+
ended_at: datetime | None = Field(default=None)
|
|
164
|
+
""" Ended at timestamp.
|
|
165
|
+
"""
|
|
166
|
+
steps: list[TrajectoryStep] = Field(default_factory=list)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def duration(self) -> float | None:
|
|
170
|
+
"""Calculate trajectory duration in seconds."""
|
|
171
|
+
if self.started_at and self.ended_at:
|
|
172
|
+
return (self.ended_at - self.started_at).total_seconds()
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def step_count(self) -> int:
|
|
177
|
+
"""Get the total number of steps."""
|
|
178
|
+
return len(self.steps)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def is_completed(self) -> bool:
|
|
182
|
+
"""Check if trajectory is in a terminal state."""
|
|
183
|
+
return self.status in [TrajectoryStatus.COMPLETED, TrajectoryStatus.FAILED]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def is_active(self) -> bool:
|
|
187
|
+
"""Check if trajectory is currently running."""
|
|
188
|
+
return self.status == TrajectoryStatus.RUNNING
|
|
189
|
+
|
|
190
|
+
def get_steps_by_role(self, role: Role) -> list[TrajectoryStep]:
|
|
191
|
+
"""Get all steps with a specific role."""
|
|
192
|
+
return [step for step in self.steps if step.role == role]
|
|
193
|
+
|
|
194
|
+
def get_steps_by_stage(self, stage: Stage) -> list[TrajectoryStep]:
|
|
195
|
+
"""Get all steps at a specific stage."""
|
|
196
|
+
return [step for step in self.steps if step.stage == stage]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class PolicyEngineMode(Enum):
|
|
200
|
+
"""Policy engine mode."""
|
|
201
|
+
|
|
202
|
+
MONITOR = "monitor"
|
|
203
|
+
"""Monitor policy. Run policies but allow all actions."""
|
|
204
|
+
GOVERN = "govern"
|
|
205
|
+
"""Govern policy on all actions."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Decision(Enum):
|
|
209
|
+
"""Decision of the adjudication."""
|
|
210
|
+
|
|
211
|
+
ALLOW = "allow"
|
|
212
|
+
DENY = "deny"
|
|
213
|
+
ESCALATE = "escalate"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class PolicyAnnotation(Model):
|
|
217
|
+
"""Annotation from a policy evaluation."""
|
|
218
|
+
|
|
219
|
+
id: str
|
|
220
|
+
"""Unique identifier of the policy that produced this annotation."""
|
|
221
|
+
description: str
|
|
222
|
+
"""Human-readable description of why this annotation was added."""
|
|
223
|
+
custom: dict[str, str] = Field(default_factory=dict)
|
|
224
|
+
"""Custom key-value metadata from the policy."""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class Adjudication(Model):
|
|
228
|
+
"""Result of the adjudication."""
|
|
229
|
+
|
|
230
|
+
decision: Decision
|
|
231
|
+
"""Whether the input is allowed."""
|
|
232
|
+
reason: str
|
|
233
|
+
"""Reason for the adjudication decision."""
|
|
234
|
+
annotations: list[PolicyAnnotation] = Field(default_factory=list)
|
|
235
|
+
"""Annotations from policy evaluations."""
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def is_denied(self) -> bool:
|
|
239
|
+
"""Check if is denied."""
|
|
240
|
+
return self.decision == Decision.DENY
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def is_allowed(self) -> bool:
|
|
244
|
+
"""Check if allowed."""
|
|
245
|
+
return self.decision == Decision.ALLOW
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def is_escalated(self) -> bool:
|
|
249
|
+
"""Check if result requires escalation."""
|
|
250
|
+
return self.decision == Decision.ESCALATE
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class AdjudicatedStep(Model):
|
|
254
|
+
"""Result of the adjudicated input."""
|
|
255
|
+
|
|
256
|
+
mode: PolicyEngineMode
|
|
257
|
+
"""Mode of the adjudication."""
|
|
258
|
+
adjudication: Adjudication
|
|
259
|
+
"""Adjudication of the input."""
|
|
260
|
+
step: TrajectoryStep
|
|
261
|
+
"""Step of the adjudication."""
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def is_denied(self) -> bool:
|
|
265
|
+
"""Check if result is denied."""
|
|
266
|
+
return (
|
|
267
|
+
self.adjudication.decision == Decision.DENY
|
|
268
|
+
and self.mode == PolicyEngineMode.GOVERN
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def is_allowed(self) -> bool:
|
|
273
|
+
"""Check if result is allowed."""
|
|
274
|
+
return self.adjudication.decision == Decision.ALLOW
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def is_escalated(self) -> bool:
|
|
278
|
+
"""Check if result requires escalation."""
|
|
279
|
+
return (
|
|
280
|
+
self.adjudication.decision == Decision.ESCALATE
|
|
281
|
+
and self.mode == PolicyEngineMode.GOVERN
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def message(self) -> str:
|
|
286
|
+
"""Get the adjudication reason in a friendly format."""
|
|
287
|
+
decision = self.adjudication.decision.value.capitalize()
|
|
288
|
+
return f"{decision}: {self.adjudication.reason}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class AdjudicatedTrajectory(Trajectory):
|
|
292
|
+
"""Adjudicated trajectory with annotated steps."""
|
|
293
|
+
|
|
294
|
+
steps: list[AdjudicatedStep] = Field(default_factory=list) # type: ignore[assignment]
|
|
295
|
+
"""Steps of the adjudicated trajectory."""
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class PromptContent(Model):
|
|
299
|
+
"""Prompt content type for trajectory steps."""
|
|
300
|
+
|
|
301
|
+
content_type: Literal["prompt"] = "prompt"
|
|
302
|
+
text: str
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class ToolRequestContent(Model):
|
|
306
|
+
"""Tool request content type for trajectory steps."""
|
|
307
|
+
|
|
308
|
+
content_type: Literal["tool_request"] = "tool_request"
|
|
309
|
+
tool_id: str
|
|
310
|
+
args: dict[str, Any]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ToolResponseContent(Model):
|
|
314
|
+
"""Tool response content type for trajectory steps."""
|
|
315
|
+
|
|
316
|
+
content_type: Literal["tool_response"] = "tool_response"
|
|
317
|
+
tool_id: str
|
|
318
|
+
response: Any
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
Content = PromptContent | ToolRequestContent | ToolResponseContent
|
|
322
|
+
"""Union type representing the content of a trajectory step.
|
|
323
|
+
|
|
324
|
+
Corresponds to the Content protobuf message which uses a oneof field
|
|
325
|
+
to represent different types of step content:
|
|
326
|
+
- PromptContent: Text prompts or messages
|
|
327
|
+
- ToolRequestContent: Requests to execute tools
|
|
328
|
+
- ToolResponseContent: Results from tool execution
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class AdjudicationRecord(Model):
|
|
333
|
+
"""Record of an adjudication event from the harness service.
|
|
334
|
+
|
|
335
|
+
Represents a single adjudication (policy decision) that occurred during
|
|
336
|
+
agent execution, linking the decision to its agent, trajectory, and step.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
agent_id: str = Field(description="ID of the agent that triggered the adjudication")
|
|
340
|
+
trajectory_id: str = Field(
|
|
341
|
+
description="ID of the trajectory containing the adjudicated step"
|
|
342
|
+
)
|
|
343
|
+
step_id: str = Field(description="ID of the step that was adjudicated")
|
|
344
|
+
adjudication: Adjudication = Field(
|
|
345
|
+
description="The adjudication decision and reason"
|
|
346
|
+
)
|