jharness-tools 0.2.1__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 (30) hide show
  1. jharness_tools-0.2.1/.gitignore +36 -0
  2. jharness_tools-0.2.1/LICENSE +21 -0
  3. jharness_tools-0.2.1/PKG-INFO +61 -0
  4. jharness_tools-0.2.1/README.md +38 -0
  5. jharness_tools-0.2.1/pyproject.toml +38 -0
  6. jharness_tools-0.2.1/src/jharness/tools/__init__.py +20 -0
  7. jharness_tools-0.2.1/src/jharness/tools/agent/__init__.py +32 -0
  8. jharness_tools-0.2.1/src/jharness/tools/agent/_schema.py +411 -0
  9. jharness_tools-0.2.1/src/jharness/tools/agent/backend.py +68 -0
  10. jharness_tools-0.2.1/src/jharness/tools/agent/models.py +152 -0
  11. jharness_tools-0.2.1/src/jharness/tools/agent/response.py +355 -0
  12. jharness_tools-0.2.1/src/jharness/tools/agent/tools.py +611 -0
  13. jharness_tools-0.2.1/src/jharness/tools/filesystem/__init__.py +9 -0
  14. jharness_tools-0.2.1/src/jharness/tools/filesystem/_common.py +657 -0
  15. jharness_tools-0.2.1/src/jharness/tools/filesystem/_content.py +25 -0
  16. jharness_tools-0.2.1/src/jharness/tools/filesystem/_write_io.py +961 -0
  17. jharness_tools-0.2.1/src/jharness/tools/filesystem/edit.py +175 -0
  18. jharness_tools-0.2.1/src/jharness/tools/filesystem/glob.py +212 -0
  19. jharness_tools-0.2.1/src/jharness/tools/filesystem/grep.py +684 -0
  20. jharness_tools-0.2.1/src/jharness/tools/filesystem/read.py +205 -0
  21. jharness_tools-0.2.1/src/jharness/tools/filesystem/write.py +142 -0
  22. jharness_tools-0.2.1/src/jharness/tools/interaction/__init__.py +21 -0
  23. jharness_tools-0.2.1/src/jharness/tools/interaction/_schema.py +1044 -0
  24. jharness_tools-0.2.1/src/jharness/tools/interaction/ask_question.py +194 -0
  25. jharness_tools-0.2.1/src/jharness/tools/interaction/response.py +1032 -0
  26. jharness_tools-0.2.1/src/jharness/tools/py.typed +1 -0
  27. jharness_tools-0.2.1/src/jharness/tools/shell/__init__.py +5 -0
  28. jharness_tools-0.2.1/src/jharness/tools/shell/_output.py +66 -0
  29. jharness_tools-0.2.1/src/jharness/tools/shell/_runner.py +557 -0
  30. jharness_tools-0.2.1/src/jharness/tools/shell/bash.py +334 -0
@@ -0,0 +1,36 @@
1
+ # OS/editor noise
2
+ .DS_Store
3
+ Thumbs.db
4
+ *.swp
5
+ *.swo
6
+
7
+ # Local assistant state
8
+ .codex/
9
+ .claude/
10
+ .gemini/
11
+ .aider*
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.py[cod]
16
+ .python-version
17
+ .venv/
18
+ venv/
19
+ ENV/
20
+
21
+ # Python tooling caches and reports
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .pyright/
25
+ .mypy_cache/
26
+ .coverage
27
+ coverage/
28
+ htmlcov/
29
+
30
+ # Packaging/build artifacts
31
+ dist/
32
+ build/
33
+ *.egg-info/
34
+
35
+ # Logs
36
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JHarness contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: jharness-tools
3
+ Version: 0.2.1
4
+ Summary: Ready-to-use filesystem, shell, interaction, and agent tools for JHarness
5
+ Project-URL: Documentation, https://github.com/Ezio2000/jharness/tree/main/docs
6
+ Project-URL: Repository, https://github.com/Ezio2000/jharness.git
7
+ Author: JHarness contributors
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: jharness-kernel==0.2.1
21
+ Requires-Dist: regex>=2024.11.6
22
+ Description-Content-Type: text/markdown
23
+
24
+ # jharness-tools
25
+
26
+ Ready-to-use filesystem, shell, interaction, and child-agent tools implementing the
27
+ public JHarness kernel tool contracts.
28
+
29
+ ```bash
30
+ pip install jharness-tools
31
+ ```
32
+
33
+ ```python
34
+ from jharness.tools import GlobTool, GrepTool, ReadTool
35
+ ```
36
+
37
+ Installing this distribution installs the matching `jharness-kernel` version.
38
+
39
+ ## Security and Lifecycle Defaults
40
+
41
+ `BashTool` starts Bash with `--noprofile --norc`, a bounded command, bounded output,
42
+ and a minimal environment. The default environment copies only available platform
43
+ keys needed for executable lookup, locale, home, and temporary directories. The
44
+ explicit `environment` mapping overlays those values. Set `inherit_environment=True`
45
+ only when exposing all host variables—including credentials and shell startup
46
+ controls—is intended. Bash commands remain capable of every filesystem and network
47
+ operation allowed by the operating system; the workspace working directory is not a
48
+ sandbox.
49
+
50
+ Process cleanup and workspace containment use the strongest supported platform
51
+ primitives but do not create an operating-system sandbox. Deployment limitations for
52
+ process trees, hard links, containers, and mutually untrusted writers are documented in
53
+ the project [security policy](https://github.com/Ezio2000/jharness/blob/main/SECURITY.md).
54
+
55
+ Filesystem tools reject path escapes and hide their private atomic-write temporary
56
+ names. Search tools skip those names.
57
+
58
+ `GrepTool` bounds files, bytes read, matches, per-match text, and total serialized
59
+ output. `Agent` requires approval because it delegates a child run with host-selected
60
+ capabilities. The host-owned `AgentBackend` owns authorization, idempotency, depth,
61
+ supervision, and telemetry.
@@ -0,0 +1,38 @@
1
+ # jharness-tools
2
+
3
+ Ready-to-use filesystem, shell, interaction, and child-agent tools implementing the
4
+ public JHarness kernel tool contracts.
5
+
6
+ ```bash
7
+ pip install jharness-tools
8
+ ```
9
+
10
+ ```python
11
+ from jharness.tools import GlobTool, GrepTool, ReadTool
12
+ ```
13
+
14
+ Installing this distribution installs the matching `jharness-kernel` version.
15
+
16
+ ## Security and Lifecycle Defaults
17
+
18
+ `BashTool` starts Bash with `--noprofile --norc`, a bounded command, bounded output,
19
+ and a minimal environment. The default environment copies only available platform
20
+ keys needed for executable lookup, locale, home, and temporary directories. The
21
+ explicit `environment` mapping overlays those values. Set `inherit_environment=True`
22
+ only when exposing all host variables—including credentials and shell startup
23
+ controls—is intended. Bash commands remain capable of every filesystem and network
24
+ operation allowed by the operating system; the workspace working directory is not a
25
+ sandbox.
26
+
27
+ Process cleanup and workspace containment use the strongest supported platform
28
+ primitives but do not create an operating-system sandbox. Deployment limitations for
29
+ process trees, hard links, containers, and mutually untrusted writers are documented in
30
+ the project [security policy](https://github.com/Ezio2000/jharness/blob/main/SECURITY.md).
31
+
32
+ Filesystem tools reject path escapes and hide their private atomic-write temporary
33
+ names. Search tools skip those names.
34
+
35
+ `GrepTool` bounds files, bytes read, matches, per-match text, and total serialized
36
+ output. `Agent` requires approval because it delegates a child run with host-selected
37
+ capabilities. The host-owned `AgentBackend` owns authorization, idempotency, depth,
38
+ supervision, and telemetry.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "jharness-tools"
3
+ version = "0.2.1"
4
+ description = "Ready-to-use filesystem, shell, interaction, and agent tools for JHarness"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "JHarness contributors" }]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "jharness-kernel==0.2.1",
23
+ "regex>=2024.11.6",
24
+ ]
25
+
26
+ [project.urls]
27
+ Documentation = "https://github.com/Ezio2000/jharness/tree/main/docs"
28
+ Repository = "https://github.com/Ezio2000/jharness.git"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/jharness"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src/jharness/tools", "LICENSE", "README.md", "pyproject.toml"]
@@ -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
+ ]