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.
- jharness_tools-0.2.1/.gitignore +36 -0
- jharness_tools-0.2.1/LICENSE +21 -0
- jharness_tools-0.2.1/PKG-INFO +61 -0
- jharness_tools-0.2.1/README.md +38 -0
- jharness_tools-0.2.1/pyproject.toml +38 -0
- jharness_tools-0.2.1/src/jharness/tools/__init__.py +20 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/__init__.py +32 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/_schema.py +411 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/backend.py +68 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/models.py +152 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/response.py +355 -0
- jharness_tools-0.2.1/src/jharness/tools/agent/tools.py +611 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/__init__.py +9 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/_common.py +657 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/_content.py +25 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/_write_io.py +961 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/edit.py +175 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/glob.py +212 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/grep.py +684 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/read.py +205 -0
- jharness_tools-0.2.1/src/jharness/tools/filesystem/write.py +142 -0
- jharness_tools-0.2.1/src/jharness/tools/interaction/__init__.py +21 -0
- jharness_tools-0.2.1/src/jharness/tools/interaction/_schema.py +1044 -0
- jharness_tools-0.2.1/src/jharness/tools/interaction/ask_question.py +194 -0
- jharness_tools-0.2.1/src/jharness/tools/interaction/response.py +1032 -0
- jharness_tools-0.2.1/src/jharness/tools/py.typed +1 -0
- jharness_tools-0.2.1/src/jharness/tools/shell/__init__.py +5 -0
- jharness_tools-0.2.1/src/jharness/tools/shell/_output.py +66 -0
- jharness_tools-0.2.1/src/jharness/tools/shell/_runner.py +557 -0
- 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
|
+
]
|