jharness-tools 0.2.1__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.
@@ -0,0 +1,20 @@
1
+ """Ready-to-use tool implementations for JHarness agents."""
2
+
3
+ from jharness.tools.agent import AgentCancelTool, AgentGetTool, AgentTool, AgentWaitTool
4
+ from jharness.tools.filesystem import EditTool, GlobTool, GrepTool, ReadTool, WriteTool
5
+ from jharness.tools.interaction import AskQuestionTool
6
+ from jharness.tools.shell import BashTool
7
+
8
+ __all__ = [
9
+ "AgentCancelTool",
10
+ "AgentGetTool",
11
+ "AgentTool",
12
+ "AgentWaitTool",
13
+ "AskQuestionTool",
14
+ "BashTool",
15
+ "EditTool",
16
+ "GlobTool",
17
+ "GrepTool",
18
+ "ReadTool",
19
+ "WriteTool",
20
+ ]
@@ -0,0 +1,32 @@
1
+ """Host-mediated Child Agent tools and durable parent-resume helpers."""
2
+
3
+ from jharness.tools.agent.backend import AgentBackend
4
+ from jharness.tools.agent.models import (
5
+ AgentBackendError,
6
+ AgentRequest,
7
+ AgentSnapshot,
8
+ AgentStatus,
9
+ )
10
+ from jharness.tools.agent.response import (
11
+ AgentWaitRequest,
12
+ agent_completion_message,
13
+ extract_agent_wait,
14
+ resume_agent,
15
+ )
16
+ from jharness.tools.agent.tools import AgentCancelTool, AgentGetTool, AgentTool, AgentWaitTool
17
+
18
+ __all__ = [
19
+ "AgentBackend",
20
+ "AgentBackendError",
21
+ "AgentCancelTool",
22
+ "AgentGetTool",
23
+ "AgentRequest",
24
+ "AgentSnapshot",
25
+ "AgentStatus",
26
+ "AgentTool",
27
+ "AgentWaitRequest",
28
+ "AgentWaitTool",
29
+ "agent_completion_message",
30
+ "extract_agent_wait",
31
+ "resume_agent",
32
+ ]
@@ -0,0 +1,411 @@
1
+ """Strict schemas and defensive normalization for the Agent preset tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any, cast
7
+
8
+ from jharness.kernel import JsonValue
9
+ from jharness.tools.agent.models import AgentRequest, AgentSnapshot, AgentStatus
10
+
11
+ SCHEMA_VERSION = 1
12
+ DEFAULT_MAX_AGENT_ID_CHARS = 512
13
+ DEFAULT_MAX_DESCRIPTION_CHARS = 200
14
+ DEFAULT_MAX_PROMPT_CHARS = 100_000
15
+ DEFAULT_MAX_RESULT_CHARS = 65_536
16
+ DEFAULT_MAX_ERROR_CODE_CHARS = 128
17
+ DEFAULT_MAX_ERROR_MESSAGE_CHARS = 4_096
18
+
19
+ _DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema"
20
+ _TERMINAL_STATUSES = frozenset[AgentStatus]({"completed", "failed", "cancelled"})
21
+
22
+
23
+ class AgentContractError(ValueError):
24
+ """A recoverable validation error at an Agent tool contract boundary."""
25
+
26
+
27
+ def positive_int(value: object, label: str) -> int:
28
+ """Validate one positive Host-configured integer limit."""
29
+
30
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
31
+ raise ValueError(f"{label} must be a positive integer")
32
+ return value
33
+
34
+
35
+ def build_wait_id(run_id: str, tool_call_id: str) -> str:
36
+ """Build an unambiguous stable identifier for one Agent wait."""
37
+
38
+ run_id = _contract_non_empty_string(run_id, "run_id")
39
+ tool_call_id = _contract_non_empty_string(tool_call_id, "tool_call_id")
40
+ return f"agent-wait:{len(run_id)}:{run_id}:{len(tool_call_id)}:{tool_call_id}"
41
+
42
+
43
+ def agent_input_schema(
44
+ *,
45
+ max_description_chars: int = DEFAULT_MAX_DESCRIPTION_CHARS,
46
+ max_prompt_chars: int = DEFAULT_MAX_PROMPT_CHARS,
47
+ ) -> dict[str, Any]:
48
+ """Build the strict Draft 2020-12 input schema for ``Agent``."""
49
+
50
+ max_description_chars = positive_int(max_description_chars, "max_description_chars")
51
+ max_prompt_chars = positive_int(max_prompt_chars, "max_prompt_chars")
52
+ return {
53
+ "$schema": _DRAFT_2020_12,
54
+ "type": "object",
55
+ "required": ["description", "prompt"],
56
+ "properties": {
57
+ "description": {
58
+ "type": "string",
59
+ "minLength": 1,
60
+ "maxLength": max_description_chars,
61
+ },
62
+ "prompt": {
63
+ "type": "string",
64
+ "minLength": 1,
65
+ "maxLength": max_prompt_chars,
66
+ },
67
+ "background": {
68
+ "type": "boolean",
69
+ "default": False,
70
+ },
71
+ },
72
+ "additionalProperties": False,
73
+ }
74
+
75
+
76
+ def agent_id_input_schema(
77
+ *,
78
+ max_agent_id_chars: int = DEFAULT_MAX_AGENT_ID_CHARS,
79
+ ) -> dict[str, Any]:
80
+ """Build the strict shared input schema for Agent id operations."""
81
+
82
+ max_agent_id_chars = positive_int(max_agent_id_chars, "max_agent_id_chars")
83
+ return {
84
+ "$schema": _DRAFT_2020_12,
85
+ "type": "object",
86
+ "required": ["agent_id"],
87
+ "properties": {
88
+ "agent_id": {
89
+ "type": "string",
90
+ "minLength": 1,
91
+ "maxLength": max_agent_id_chars,
92
+ }
93
+ },
94
+ "additionalProperties": False,
95
+ }
96
+
97
+
98
+ def snapshot_output_schema(
99
+ *,
100
+ max_agent_id_chars: int = DEFAULT_MAX_AGENT_ID_CHARS,
101
+ max_description_chars: int = DEFAULT_MAX_DESCRIPTION_CHARS,
102
+ max_result_chars: int = DEFAULT_MAX_RESULT_CHARS,
103
+ max_error_code_chars: int = DEFAULT_MAX_ERROR_CODE_CHARS,
104
+ max_error_message_chars: int = DEFAULT_MAX_ERROR_MESSAGE_CHARS,
105
+ ) -> dict[str, Any]:
106
+ """Build the strict shared Agent snapshot-or-null output schema."""
107
+
108
+ max_agent_id_chars = positive_int(max_agent_id_chars, "max_agent_id_chars")
109
+ max_description_chars = positive_int(max_description_chars, "max_description_chars")
110
+ max_result_chars = positive_int(max_result_chars, "max_result_chars")
111
+ max_error_code_chars = positive_int(max_error_code_chars, "max_error_code_chars")
112
+ max_error_message_chars = positive_int(
113
+ max_error_message_chars,
114
+ "max_error_message_chars",
115
+ )
116
+ branches = [
117
+ _snapshot_branch(
118
+ status,
119
+ max_agent_id_chars=max_agent_id_chars,
120
+ max_description_chars=max_description_chars,
121
+ max_result_chars=max_result_chars,
122
+ max_error_code_chars=max_error_code_chars,
123
+ max_error_message_chars=max_error_message_chars,
124
+ )
125
+ for status in ("queued", "running", "completed", "failed", "cancelled")
126
+ ]
127
+ return {
128
+ "$schema": _DRAFT_2020_12,
129
+ "oneOf": [*branches, {"type": "null"}],
130
+ }
131
+
132
+
133
+ def normalize_agent_request(
134
+ arguments: object,
135
+ *,
136
+ max_description_chars: int = DEFAULT_MAX_DESCRIPTION_CHARS,
137
+ max_prompt_chars: int = DEFAULT_MAX_PROMPT_CHARS,
138
+ ) -> AgentRequest:
139
+ """Validate and normalize one complete ``Agent`` argument object."""
140
+
141
+ max_description_chars = _contract_positive_int(
142
+ max_description_chars,
143
+ "max_description_chars",
144
+ )
145
+ max_prompt_chars = _contract_positive_int(max_prompt_chars, "max_prompt_chars")
146
+ root = _contract_mapping(arguments, "Agent arguments")
147
+ _require_exact_keys(
148
+ root,
149
+ "Agent arguments",
150
+ required=frozenset({"description", "prompt"}),
151
+ allowed=frozenset({"description", "prompt", "background"}),
152
+ )
153
+ description = _bounded_non_empty_string(
154
+ root["description"],
155
+ "description",
156
+ max_description_chars,
157
+ )
158
+ prompt = _bounded_non_empty_string(root["prompt"], "prompt", max_prompt_chars)
159
+ background = root.get("background", False)
160
+ if not isinstance(background, bool):
161
+ raise AgentContractError("background must be bool")
162
+ return AgentRequest(description, prompt, background)
163
+
164
+
165
+ def normalize_agent_id(
166
+ arguments: object,
167
+ *,
168
+ max_agent_id_chars: int = DEFAULT_MAX_AGENT_ID_CHARS,
169
+ ) -> str:
170
+ """Validate one complete Agent id operation argument object."""
171
+
172
+ max_agent_id_chars = _contract_positive_int(max_agent_id_chars, "max_agent_id_chars")
173
+ root = _contract_mapping(arguments, "Agent id arguments")
174
+ _require_exact_keys(
175
+ root,
176
+ "Agent id arguments",
177
+ required=frozenset({"agent_id"}),
178
+ allowed=frozenset({"agent_id"}),
179
+ )
180
+ return _bounded_non_empty_string(root["agent_id"], "agent_id", max_agent_id_chars)
181
+
182
+
183
+ def snapshot_payload(
184
+ snapshot: object,
185
+ *,
186
+ max_agent_id_chars: int = DEFAULT_MAX_AGENT_ID_CHARS,
187
+ max_description_chars: int = DEFAULT_MAX_DESCRIPTION_CHARS,
188
+ max_result_chars: int = DEFAULT_MAX_RESULT_CHARS,
189
+ max_error_code_chars: int = DEFAULT_MAX_ERROR_CODE_CHARS,
190
+ max_error_message_chars: int = DEFAULT_MAX_ERROR_MESSAGE_CHARS,
191
+ ) -> dict[str, JsonValue]:
192
+ """Return a bounded JSON payload for one validated Agent snapshot."""
193
+
194
+ max_agent_id_chars = _contract_positive_int(max_agent_id_chars, "max_agent_id_chars")
195
+ max_description_chars = _contract_positive_int(
196
+ max_description_chars,
197
+ "max_description_chars",
198
+ )
199
+ max_result_chars = _contract_positive_int(max_result_chars, "max_result_chars")
200
+ max_error_code_chars = _contract_positive_int(
201
+ max_error_code_chars,
202
+ "max_error_code_chars",
203
+ )
204
+ max_error_message_chars = _contract_positive_int(
205
+ max_error_message_chars,
206
+ "max_error_message_chars",
207
+ )
208
+ snapshot = _validated_snapshot(snapshot)
209
+
210
+ agent_id = _bounded_non_empty_string(
211
+ snapshot.agent_id,
212
+ "snapshot.agent_id",
213
+ max_agent_id_chars,
214
+ )
215
+ description = _bounded_non_empty_string(
216
+ snapshot.description,
217
+ "snapshot.description",
218
+ max_description_chars,
219
+ )
220
+ payload: dict[str, JsonValue] = {
221
+ "agent_id": agent_id,
222
+ "status": snapshot.status,
223
+ "description": description,
224
+ "background": snapshot.background,
225
+ "cancellation_requested": snapshot.cancellation_requested,
226
+ }
227
+ if snapshot.status == "completed":
228
+ result = snapshot.result
229
+ if result is None: # pragma: no cover - revalidated above
230
+ raise AgentContractError("completed snapshot must contain a result")
231
+ payload["result"] = _bounded_string(
232
+ result,
233
+ "snapshot.result",
234
+ max_result_chars,
235
+ )
236
+ elif snapshot.status == "failed":
237
+ error = snapshot.error
238
+ if error is None: # pragma: no cover - revalidated above
239
+ raise AgentContractError("failed snapshot must contain an error")
240
+ error_payload: dict[str, JsonValue] = {
241
+ "code": _bounded_non_empty_string(
242
+ error.code,
243
+ "snapshot.error.code",
244
+ max_error_code_chars,
245
+ ),
246
+ "message": _bounded_non_empty_string(
247
+ error.message,
248
+ "snapshot.error.message",
249
+ max_error_message_chars,
250
+ ),
251
+ }
252
+ payload["error"] = error_payload
253
+ return payload
254
+
255
+
256
+ def _snapshot_branch(
257
+ status: AgentStatus,
258
+ *,
259
+ max_agent_id_chars: int,
260
+ max_description_chars: int,
261
+ max_result_chars: int,
262
+ max_error_code_chars: int,
263
+ max_error_message_chars: int,
264
+ ) -> dict[str, Any]:
265
+ properties: dict[str, Any] = {
266
+ "agent_id": {
267
+ "type": "string",
268
+ "minLength": 1,
269
+ "maxLength": max_agent_id_chars,
270
+ },
271
+ "status": {"const": status},
272
+ "description": {
273
+ "type": "string",
274
+ "minLength": 1,
275
+ "maxLength": max_description_chars,
276
+ },
277
+ "background": {"type": "boolean"},
278
+ "cancellation_requested": (
279
+ {"const": status == "cancelled"}
280
+ if status in _TERMINAL_STATUSES
281
+ else {"type": "boolean"}
282
+ ),
283
+ }
284
+ required = [
285
+ "agent_id",
286
+ "status",
287
+ "description",
288
+ "background",
289
+ "cancellation_requested",
290
+ ]
291
+ if status == "completed":
292
+ properties["result"] = {
293
+ "type": "string",
294
+ "maxLength": max_result_chars,
295
+ }
296
+ required.append("result")
297
+ elif status == "failed":
298
+ properties["error"] = {
299
+ "type": "object",
300
+ "required": ["code", "message"],
301
+ "properties": {
302
+ "code": {
303
+ "type": "string",
304
+ "minLength": 1,
305
+ "maxLength": max_error_code_chars,
306
+ },
307
+ "message": {
308
+ "type": "string",
309
+ "minLength": 1,
310
+ "maxLength": max_error_message_chars,
311
+ },
312
+ },
313
+ "additionalProperties": False,
314
+ }
315
+ required.append("error")
316
+ return {
317
+ "type": "object",
318
+ "required": required,
319
+ "properties": properties,
320
+ "additionalProperties": False,
321
+ }
322
+
323
+
324
+ def _contract_mapping(value: object, label: str) -> Mapping[str, object]:
325
+ if not isinstance(value, Mapping):
326
+ raise AgentContractError(f"{label} must be an object")
327
+ raw = cast(Mapping[object, object], value)
328
+ if any(not isinstance(key, str) for key in raw):
329
+ raise AgentContractError(f"{label} keys must be strings")
330
+ return cast(Mapping[str, object], raw)
331
+
332
+
333
+ def _validated_snapshot(value: object) -> AgentSnapshot:
334
+ if not isinstance(value, AgentSnapshot):
335
+ raise AgentContractError("snapshot must be an AgentSnapshot")
336
+ try:
337
+ return AgentSnapshot(
338
+ agent_id=value.agent_id,
339
+ description=value.description,
340
+ status=value.status,
341
+ background=value.background,
342
+ result=value.result,
343
+ error=value.error,
344
+ cancellation_requested=value.cancellation_requested,
345
+ )
346
+ except (TypeError, ValueError) as exc:
347
+ raise AgentContractError("snapshot fields are inconsistent") from exc
348
+
349
+
350
+ def _require_exact_keys(
351
+ value: Mapping[str, object],
352
+ label: str,
353
+ *,
354
+ required: frozenset[str],
355
+ allowed: frozenset[str],
356
+ ) -> None:
357
+ keys = frozenset(value)
358
+ missing = sorted(required - keys)
359
+ if missing:
360
+ raise AgentContractError(f"{label} is missing required fields: {', '.join(missing)}")
361
+ unknown = sorted(keys - allowed)
362
+ if unknown:
363
+ raise AgentContractError(f"{label} contains unknown fields: {', '.join(unknown)}")
364
+
365
+
366
+ def _contract_positive_int(value: object, label: str) -> int:
367
+ try:
368
+ return positive_int(value, label)
369
+ except ValueError as exc:
370
+ raise AgentContractError(str(exc)) from exc
371
+
372
+
373
+ def _contract_non_empty_string(value: object, label: str) -> str:
374
+ if not isinstance(value, str):
375
+ raise AgentContractError(f"{label} must be a string")
376
+ if not value:
377
+ raise AgentContractError(f"{label} must not be empty")
378
+ return value
379
+
380
+
381
+ def _bounded_non_empty_string(value: object, label: str, maximum: int) -> str:
382
+ text = _contract_non_empty_string(value, label)
383
+ return _bounded_string(text, label, maximum)
384
+
385
+
386
+ def _bounded_string(value: object, label: str, maximum: int) -> str:
387
+ if not isinstance(value, str):
388
+ raise AgentContractError(f"{label} must be a string")
389
+ if len(value) > maximum:
390
+ raise AgentContractError(f"{label} exceeds the configured character limit")
391
+ return value
392
+
393
+
394
+ __all__ = [
395
+ "DEFAULT_MAX_AGENT_ID_CHARS",
396
+ "DEFAULT_MAX_DESCRIPTION_CHARS",
397
+ "DEFAULT_MAX_ERROR_CODE_CHARS",
398
+ "DEFAULT_MAX_ERROR_MESSAGE_CHARS",
399
+ "DEFAULT_MAX_PROMPT_CHARS",
400
+ "DEFAULT_MAX_RESULT_CHARS",
401
+ "SCHEMA_VERSION",
402
+ "AgentContractError",
403
+ "agent_id_input_schema",
404
+ "agent_input_schema",
405
+ "build_wait_id",
406
+ "normalize_agent_id",
407
+ "normalize_agent_request",
408
+ "positive_int",
409
+ "snapshot_output_schema",
410
+ "snapshot_payload",
411
+ ]
@@ -0,0 +1,68 @@
1
+ """Host-owned execution port used by the Agent preset tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from jharness.kernel import RunContext
8
+ from jharness.tools.agent.models import AgentRequest, AgentSnapshot
9
+
10
+
11
+ @runtime_checkable
12
+ class AgentBackend(Protocol):
13
+ """Create, observe, wait for, and cancel Host-owned child Agents.
14
+
15
+ Implementations own authorization, idempotency, persistence, supervision, and
16
+ deriving a fresh Child Runtime from the trusted parent configuration. Methods must
17
+ be safe for concurrent calls from multiple immutable tool instances.
18
+ """
19
+
20
+ async def start_or_get(
21
+ self,
22
+ request: AgentRequest,
23
+ *,
24
+ parent: RunContext,
25
+ parent_tool_call_id: str,
26
+ ) -> AgentSnapshot:
27
+ """Idempotently create or return the Agent for one parent tool call.
28
+
29
+ For foreground requests, creation must also establish durable completion
30
+ delivery so a fast Child cannot race the parent's waiting checkpoint.
31
+ """
32
+
33
+ ...
34
+
35
+ async def get(
36
+ self,
37
+ agent_id: str,
38
+ *,
39
+ requester: RunContext,
40
+ ) -> AgentSnapshot:
41
+ """Return the current Agent snapshot without waiting."""
42
+
43
+ ...
44
+
45
+ async def wait_or_get(
46
+ self,
47
+ agent_id: str,
48
+ *,
49
+ requester: RunContext,
50
+ requester_tool_call_id: str,
51
+ ) -> AgentSnapshot:
52
+ """Atomically register a durable waiter or return a terminal snapshot."""
53
+
54
+ ...
55
+
56
+ async def cancel(
57
+ self,
58
+ agent_id: str,
59
+ *,
60
+ requester: RunContext,
61
+ requester_tool_call_id: str,
62
+ ) -> AgentSnapshot:
63
+ """Idempotently request cancellation and return the resulting snapshot."""
64
+
65
+ ...
66
+
67
+
68
+ __all__ = ["AgentBackend"]
@@ -0,0 +1,152 @@
1
+ """Immutable values shared by the Agent preset tools and their Host backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal, TypeAlias, cast
7
+
8
+ from jharness.kernel import ErrorInfo
9
+
10
+ AgentStatus: TypeAlias = Literal[
11
+ "queued",
12
+ "running",
13
+ "completed",
14
+ "failed",
15
+ "cancelled",
16
+ ]
17
+
18
+ _AGENT_STATUSES = frozenset[AgentStatus]({"queued", "running", "completed", "failed", "cancelled"})
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class AgentRequest:
23
+ """One validated request to create or recover a child Agent."""
24
+
25
+ description: str
26
+ prompt: str
27
+ background: bool = False
28
+
29
+ def __post_init__(self) -> None:
30
+ _non_empty_string(self.description, "description")
31
+ _non_empty_string(self.prompt, "prompt")
32
+ _boolean(self.background, "background")
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class AgentSnapshot:
37
+ """One immutable Host observation of an Agent's current state."""
38
+
39
+ agent_id: str
40
+ description: str
41
+ status: AgentStatus
42
+ background: bool
43
+ result: str | None = None
44
+ error: ErrorInfo | None = None
45
+ cancellation_requested: bool = False
46
+
47
+ def __post_init__(self) -> None:
48
+ _non_empty_string(self.agent_id, "agent_id")
49
+ _non_empty_string(self.description, "description")
50
+ status = _agent_status(self.status)
51
+ _boolean(self.background, "background")
52
+ result = cast(object, self.result)
53
+ if result is not None and not isinstance(result, str):
54
+ raise TypeError("result must be a string or None")
55
+ error = cast(object, self.error)
56
+ if error is not None and not isinstance(error, ErrorInfo):
57
+ raise TypeError("error must be an ErrorInfo or None")
58
+ cancellation_requested = _boolean(
59
+ self.cancellation_requested,
60
+ "cancellation_requested",
61
+ )
62
+ _validate_snapshot_state(status, result, error, cancellation_requested)
63
+
64
+
65
+ class AgentBackendError(Exception):
66
+ """A stable recoverable failure reported by the Host Agent backend."""
67
+
68
+ __slots__ = ("code", "message")
69
+
70
+ code: str
71
+ message: str
72
+
73
+ def __init__(self, code: str, message: str) -> None:
74
+ self.code = _non_empty_string(code, "code")
75
+ self.message = _non_empty_string(message, "message")
76
+ super().__init__(message)
77
+
78
+
79
+ def _agent_status(value: object) -> AgentStatus:
80
+ if not isinstance(value, str):
81
+ raise TypeError("status must be a string")
82
+ if value not in _AGENT_STATUSES:
83
+ raise ValueError(f"unsupported Agent status: {value}")
84
+ return value
85
+
86
+
87
+ def _validate_snapshot_state(
88
+ status: AgentStatus,
89
+ result: str | None,
90
+ error: ErrorInfo | None,
91
+ cancellation_requested: bool,
92
+ ) -> None:
93
+ if status == "completed":
94
+ _validate_completed(result, error, cancellation_requested)
95
+ return
96
+ if status == "failed":
97
+ _validate_failed(result, error, cancellation_requested)
98
+ return
99
+ if result is not None:
100
+ raise ValueError(f"{status} Agent snapshot cannot include result")
101
+ if error is not None:
102
+ raise ValueError(f"{status} Agent snapshot cannot include error")
103
+ if status == "cancelled" and not cancellation_requested:
104
+ raise ValueError("cancelled Agent snapshot requires cancellation_requested=true")
105
+
106
+
107
+ def _validate_completed(
108
+ result: str | None,
109
+ error: ErrorInfo | None,
110
+ cancellation_requested: bool,
111
+ ) -> None:
112
+ if result is None:
113
+ raise ValueError("completed Agent snapshot requires result")
114
+ if error is not None:
115
+ raise ValueError("completed Agent snapshot cannot include error")
116
+ if cancellation_requested:
117
+ raise ValueError("completed Agent snapshot cannot have cancellation_requested=true")
118
+
119
+
120
+ def _validate_failed(
121
+ result: str | None,
122
+ error: ErrorInfo | None,
123
+ cancellation_requested: bool,
124
+ ) -> None:
125
+ if error is None:
126
+ raise ValueError("failed Agent snapshot requires error")
127
+ if result is not None:
128
+ raise ValueError("failed Agent snapshot cannot include result")
129
+ if cancellation_requested:
130
+ raise ValueError("failed Agent snapshot cannot have cancellation_requested=true")
131
+
132
+
133
+ def _non_empty_string(value: object, label: str) -> str:
134
+ if not isinstance(value, str):
135
+ raise TypeError(f"{label} must be a string")
136
+ if not value:
137
+ raise ValueError(f"{label} must not be empty")
138
+ return value
139
+
140
+
141
+ def _boolean(value: object, label: str) -> bool:
142
+ if not isinstance(value, bool):
143
+ raise TypeError(f"{label} must be bool")
144
+ return value
145
+
146
+
147
+ __all__ = [
148
+ "AgentBackendError",
149
+ "AgentRequest",
150
+ "AgentSnapshot",
151
+ "AgentStatus",
152
+ ]