jharness-kernel 0.1.2__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/.gitignore +3 -12
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/LICENSE +1 -1
- jharness_kernel-0.2.2/PKG-INFO +36 -0
- jharness_kernel-0.2.2/README.md +15 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/pyproject.toml +10 -11
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/__init__.py +4 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/deadline.py +19 -6
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/tooling.py +6 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/commands.py +9 -2
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/errors.py +16 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/json_values.py +5 -0
- jharness_kernel-0.2.2/src/jharness/kernel/py.typed +1 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/_helpers.py +12 -3
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/messages.py +15 -4
- jharness_kernel-0.1.2/PKG-INFO +0 -339
- jharness_kernel-0.1.2/README.md +0 -314
- jharness_kernel-0.1.2/src/jharness/kernel/py.typed +0 -0
- jharness_kernel-0.1.2/tests/test_diagnostics.py +0 -750
- jharness_kernel-0.1.2/tests/test_runtime.py +0 -473
- jharness_kernel-0.1.2/tests/test_runtime_concurrency_edges.py +0 -308
- jharness_kernel-0.1.2/tests/test_values.py +0 -535
- jharness_kernel-0.1.2/tests/test_wire.py +0 -1135
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_digest.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/__init__.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/change.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/commit.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/engine.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/planning.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/verification.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_history.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_validation.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/approval.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/checkpoint.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/context.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/control.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/__init__.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/trace.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/verification.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/events.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/history.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/invocation.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/limits.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/messages.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/models.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/repository.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/runtime.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/snapshot.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/state.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/tools.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/__init__.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/checkpoint.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/events.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/models.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/requests.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/snapshot.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/state.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/tools.py +0 -0
- {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/trace.py +0 -0
|
@@ -4,15 +4,12 @@ Thumbs.db
|
|
|
4
4
|
*.swp
|
|
5
5
|
*.swo
|
|
6
6
|
|
|
7
|
-
# Local
|
|
7
|
+
# Local assistant state
|
|
8
8
|
.codex/
|
|
9
9
|
.claude/
|
|
10
10
|
.gemini/
|
|
11
11
|
.aider*
|
|
12
12
|
|
|
13
|
-
# Pinned specification snapshot generated by scripts/sync_spec.py
|
|
14
|
-
.jharness-spec/
|
|
15
|
-
|
|
16
13
|
# Python
|
|
17
14
|
__pycache__/
|
|
18
15
|
*.py[cod]
|
|
@@ -21,12 +18,13 @@ __pycache__/
|
|
|
21
18
|
venv/
|
|
22
19
|
ENV/
|
|
23
20
|
|
|
24
|
-
# Python tooling caches
|
|
21
|
+
# Python tooling caches and reports
|
|
25
22
|
.pytest_cache/
|
|
26
23
|
.ruff_cache/
|
|
27
24
|
.pyright/
|
|
28
25
|
.mypy_cache/
|
|
29
26
|
.coverage
|
|
27
|
+
coverage/
|
|
30
28
|
htmlcov/
|
|
31
29
|
|
|
32
30
|
# Packaging/build artifacts
|
|
@@ -34,12 +32,5 @@ dist/
|
|
|
34
32
|
build/
|
|
35
33
|
*.egg-info/
|
|
36
34
|
|
|
37
|
-
# Node/TypeScript SDK artifacts
|
|
38
|
-
node_modules/
|
|
39
|
-
coverage/
|
|
40
|
-
.turbo/
|
|
41
|
-
.next/
|
|
42
|
-
*.tsbuildinfo
|
|
43
|
-
|
|
44
35
|
# Logs
|
|
45
36
|
*.log
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jharness-kernel
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Durable model and tool execution kernel 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
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# jharness-kernel
|
|
23
|
+
|
|
24
|
+
The dependency-free JHarness runtime kernel, including lifecycle values, execution,
|
|
25
|
+
checkpoints, diagnostics, extension ports, and portable wire codecs.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install jharness-kernel
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from jharness.kernel import Message, Runtime
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The source, contracts, and release process are maintained in the
|
|
36
|
+
[JHarness repository](https://github.com/Ezio2000/jharness).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# jharness-kernel
|
|
2
|
+
|
|
3
|
+
The dependency-free JHarness runtime kernel, including lifecycle values, execution,
|
|
4
|
+
checkpoints, diagnostics, extension ports, and portable wire codecs.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install jharness-kernel
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from jharness.kernel import Message, Runtime
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The source, contracts, and release process are maintained in the
|
|
15
|
+
[JHarness repository](https://github.com/Ezio2000/jharness).
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "jharness-kernel"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Durable model and tool execution kernel for JHarness"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
authors = [{ name = "JHarness contributors" }]
|
|
10
|
-
keywords = ["ai", "agent", "model", "runtime", "tools"]
|
|
11
10
|
classifiers = [
|
|
12
11
|
"Development Status :: 3 - Alpha",
|
|
13
12
|
"License :: OSI Approved :: MIT License",
|
|
14
13
|
"Operating System :: OS Independent",
|
|
15
|
-
"Programming Language :: Python :: 3",
|
|
16
14
|
"Programming Language :: Python :: 3 :: Only",
|
|
17
15
|
"Programming Language :: Python :: 3.11",
|
|
18
16
|
"Programming Language :: Python :: 3.12",
|
|
@@ -23,14 +21,15 @@ classifiers = [
|
|
|
23
21
|
dependencies = []
|
|
24
22
|
|
|
25
23
|
[project.urls]
|
|
26
|
-
Documentation = "https://github.com/Ezio2000/jharness
|
|
27
|
-
|
|
28
|
-
Issues = "https://github.com/Ezio2000/jharness-python/issues"
|
|
29
|
-
Repository = "https://github.com/Ezio2000/jharness-python.git"
|
|
30
|
-
|
|
31
|
-
[tool.hatch.build.targets.wheel]
|
|
32
|
-
packages = ["src/jharness"]
|
|
24
|
+
Documentation = "https://github.com/Ezio2000/jharness/tree/main/docs"
|
|
25
|
+
Repository = "https://github.com/Ezio2000/jharness.git"
|
|
33
26
|
|
|
34
27
|
[build-system]
|
|
35
28
|
requires = ["hatchling"]
|
|
36
29
|
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/jharness"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = ["src/jharness/kernel", "LICENSE", "README.md", "pyproject.toml"]
|
|
@@ -34,6 +34,7 @@ from jharness.kernel.errors import (
|
|
|
34
34
|
ModelErrorInfo,
|
|
35
35
|
ProtocolError,
|
|
36
36
|
RepositoryError,
|
|
37
|
+
RequestError,
|
|
37
38
|
RevisionConflict,
|
|
38
39
|
ToolError,
|
|
39
40
|
)
|
|
@@ -41,6 +42,7 @@ from jharness.kernel.events import Event, EventKind
|
|
|
41
42
|
from jharness.kernel.history import HistoryReducer, HistoryRewrite
|
|
42
43
|
from jharness.kernel.invocation import Invocation
|
|
43
44
|
from jharness.kernel.json_values import (
|
|
45
|
+
MAX_JSON_NESTING_DEPTH,
|
|
44
46
|
JsonScalar,
|
|
45
47
|
JsonValue,
|
|
46
48
|
freeze_json_value,
|
|
@@ -102,6 +104,7 @@ from jharness.kernel.tools import (
|
|
|
102
104
|
)
|
|
103
105
|
|
|
104
106
|
__all__ = [
|
|
107
|
+
"MAX_JSON_NESTING_DEPTH",
|
|
105
108
|
"ActiveState",
|
|
106
109
|
"ApprovalAllow",
|
|
107
110
|
"ApprovalDecision",
|
|
@@ -154,6 +157,7 @@ __all__ = [
|
|
|
154
157
|
"Planning",
|
|
155
158
|
"ProtocolError",
|
|
156
159
|
"RepositoryError",
|
|
160
|
+
"RequestError",
|
|
157
161
|
"ResponseFormat",
|
|
158
162
|
"ResumedFact",
|
|
159
163
|
"RevisionConflict",
|
|
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
|
7
|
+
from contextlib import suppress
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from time import time
|
|
9
|
-
from typing import Any, TypeVar
|
|
10
|
+
from typing import Any, TypeVar
|
|
10
11
|
|
|
11
12
|
from jharness.kernel.control import CancelTool, Control, ControlInbox, Insert
|
|
12
13
|
|
|
@@ -101,8 +102,20 @@ def _consume_non_interrupting(
|
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
async def _settle(task: asyncio.Future[Any]) -> None:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
done, _ = await asyncio.wait({task}, timeout=0.1)
|
|
106
|
+
if task in done:
|
|
107
|
+
_consume(task)
|
|
108
|
+
return
|
|
109
|
+
task.cancel()
|
|
110
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
111
|
+
{
|
|
112
|
+
"message": "JHarness abandoned a port task that ignored cancellation",
|
|
113
|
+
"future": task,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
task.add_done_callback(_consume)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _consume(task: asyncio.Future[Any]) -> None:
|
|
120
|
+
with suppress(asyncio.CancelledError, Exception):
|
|
121
|
+
task.exception()
|
|
@@ -460,6 +460,12 @@ async def _cancel_and_settle(tasks: set[asyncio.Task[Any]]) -> None:
|
|
|
460
460
|
_, pending = await asyncio.wait(tasks, timeout=0.1)
|
|
461
461
|
for task in pending:
|
|
462
462
|
task.cancel()
|
|
463
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
464
|
+
{
|
|
465
|
+
"message": "JHarness abandoned a tool task that ignored cancellation",
|
|
466
|
+
"task": task,
|
|
467
|
+
}
|
|
468
|
+
)
|
|
463
469
|
task.add_done_callback(_consume)
|
|
464
470
|
|
|
465
471
|
|
|
@@ -15,6 +15,7 @@ from jharness.kernel._validation import (
|
|
|
15
15
|
)
|
|
16
16
|
from jharness.kernel.checkpoint import Checkpoint
|
|
17
17
|
from jharness.kernel.context import RunContext
|
|
18
|
+
from jharness.kernel.errors import RequestError
|
|
18
19
|
from jharness.kernel.messages import Message
|
|
19
20
|
from jharness.kernel.state import Planning, Suspended, ToolsPending
|
|
20
21
|
|
|
@@ -102,10 +103,16 @@ class ResumeRequest:
|
|
|
102
103
|
if self.selector is not None:
|
|
103
104
|
expect_instance(self.selector, SuspensionSelector, "resume selector")
|
|
104
105
|
if not self.selector.matches(state):
|
|
105
|
-
raise
|
|
106
|
+
raise RequestError(
|
|
107
|
+
"suspension_mismatch",
|
|
108
|
+
"resume selector does not match the suspended checkpoint",
|
|
109
|
+
)
|
|
106
110
|
messages = expect_instance_tuple(self.append_messages, Message, "resume append_messages")
|
|
107
111
|
if messages and isinstance(state.resume_to, ToolsPending):
|
|
108
|
-
raise
|
|
112
|
+
raise RequestError(
|
|
113
|
+
"messages_require_planning",
|
|
114
|
+
"resume messages require a planning continuation",
|
|
115
|
+
)
|
|
109
116
|
if any(message.role not in {"system", "user", "external"} for message in messages):
|
|
110
117
|
raise ValueError("resume messages must use a regular role")
|
|
111
118
|
object.__setattr__(self, "append_messages", messages)
|
|
@@ -27,6 +27,22 @@ class KernelError(Exception):
|
|
|
27
27
|
class ProtocolError(KernelError):
|
|
28
28
|
"""A value or extension violated a kernel protocol invariant."""
|
|
29
29
|
|
|
30
|
+
__slots__ = ("code",)
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, *, code: str = "protocol_error") -> None:
|
|
33
|
+
super().__init__(expect_non_empty_str(message, "protocol error message"))
|
|
34
|
+
self.code = expect_non_empty_str(code, "protocol error code")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RequestError(KernelError):
|
|
38
|
+
"""A runtime request violated a stable request-level invariant."""
|
|
39
|
+
|
|
40
|
+
__slots__ = ("code",)
|
|
41
|
+
|
|
42
|
+
def __init__(self, code: str, message: str) -> None:
|
|
43
|
+
super().__init__(expect_non_empty_str(message, "request error message"))
|
|
44
|
+
self.code = expect_non_empty_str(code, "request error code")
|
|
45
|
+
|
|
30
46
|
|
|
31
47
|
class ToolError(KernelError):
|
|
32
48
|
"""A tool catalog, binding, or progress boundary failed."""
|
|
@@ -10,6 +10,7 @@ from typing import Any, NoReturn, SupportsIndex, TypeAlias, cast
|
|
|
10
10
|
|
|
11
11
|
JsonScalar: TypeAlias = None | bool | int | float | str
|
|
12
12
|
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
|
13
|
+
MAX_JSON_NESTING_DEPTH = 128
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class FrozenJsonList(Sequence[Any]):
|
|
@@ -205,6 +206,10 @@ class _JsonCopier:
|
|
|
205
206
|
self.active_containers.remove(identity)
|
|
206
207
|
|
|
207
208
|
def _enter_container(self, value: object, path: str) -> int:
|
|
209
|
+
if len(self.active_containers) >= MAX_JSON_NESTING_DEPTH:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"{path} exceeds the maximum JSON nesting depth of {MAX_JSON_NESTING_DEPTH}"
|
|
212
|
+
)
|
|
208
213
|
identity = id(value)
|
|
209
214
|
if identity in self.active_containers:
|
|
210
215
|
raise ValueError(f"{path} must not contain a reference cycle")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -7,7 +7,7 @@ from math import isfinite
|
|
|
7
7
|
from typing import Any, TypeVar, cast
|
|
8
8
|
|
|
9
9
|
from jharness.kernel.errors import ProtocolError
|
|
10
|
-
from jharness.kernel.json_values import thaw_json_value
|
|
10
|
+
from jharness.kernel.json_values import MAX_JSON_NESTING_DEPTH, thaw_json_value
|
|
11
11
|
|
|
12
12
|
T = TypeVar("T")
|
|
13
13
|
|
|
@@ -20,7 +20,7 @@ def decode_document(value: object, label: str, decoder: Callable[[object], T]) -
|
|
|
20
20
|
return decoder(value)
|
|
21
21
|
except ProtocolError:
|
|
22
22
|
raise
|
|
23
|
-
except (KeyError, TypeError, ValueError) as exc:
|
|
23
|
+
except (KeyError, OverflowError, RecursionError, TypeError, ValueError) as exc:
|
|
24
24
|
raise ProtocolError(f"invalid {label}: {exc}") from exc
|
|
25
25
|
|
|
26
26
|
|
|
@@ -80,7 +80,12 @@ def optional_integer(value: object, label: str, *, minimum: int | None = None) -
|
|
|
80
80
|
def number(value: object, label: str, *, minimum: float | None = None) -> float:
|
|
81
81
|
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
82
82
|
raise ProtocolError(f"{label} must be a number")
|
|
83
|
-
|
|
83
|
+
if isinstance(value, int) and abs(value) > (2**53 - 1):
|
|
84
|
+
raise ProtocolError(f"{label} integer must be within the safe JSON number range")
|
|
85
|
+
try:
|
|
86
|
+
result = float(value)
|
|
87
|
+
except OverflowError as exc:
|
|
88
|
+
raise ProtocolError(f"{label} must be representable as a finite number") from exc
|
|
84
89
|
if not isfinite(result):
|
|
85
90
|
raise ProtocolError(f"{label} must be finite")
|
|
86
91
|
if minimum is not None and result < minimum:
|
|
@@ -169,6 +174,10 @@ def _validate_json(value: object, label: str, active: set[int]) -> None:
|
|
|
169
174
|
|
|
170
175
|
|
|
171
176
|
def _validate_container(value: object, label: str, active: set[int]) -> None:
|
|
177
|
+
if len(active) >= MAX_JSON_NESTING_DEPTH:
|
|
178
|
+
raise ProtocolError(
|
|
179
|
+
f"{label} exceeds the maximum JSON nesting depth of {MAX_JSON_NESTING_DEPTH}"
|
|
180
|
+
)
|
|
172
181
|
identity = id(value)
|
|
173
182
|
if identity in active:
|
|
174
183
|
raise ProtocolError(f"{label} contains a cycle")
|
|
@@ -83,11 +83,17 @@ def decode_message_value(value: object) -> Message:
|
|
|
83
83
|
"assistant message",
|
|
84
84
|
{"role", "parts", "tool_calls", "metadata"},
|
|
85
85
|
)
|
|
86
|
+
calls = tuple(
|
|
87
|
+
decode_tool_call_value(item) for item in array(fields["tool_calls"], "tool calls")
|
|
88
|
+
)
|
|
89
|
+
if len({call.id for call in calls}) != len(calls):
|
|
90
|
+
raise ProtocolError(
|
|
91
|
+
"assistant tool call ids must be unique",
|
|
92
|
+
code="duplicate_tool_call_id",
|
|
93
|
+
)
|
|
86
94
|
return Message.assistant(
|
|
87
95
|
_decode_parts(fields["parts"], "assistant parts"),
|
|
88
|
-
tool_calls=
|
|
89
|
-
decode_tool_call_value(item) for item in array(fields["tool_calls"], "tool calls")
|
|
90
|
-
),
|
|
96
|
+
tool_calls=calls,
|
|
91
97
|
metadata=json_object(fields["metadata"], "message metadata"),
|
|
92
98
|
)
|
|
93
99
|
if role == "tool":
|
|
@@ -170,6 +176,11 @@ def decode_content_part_value(value: object) -> ContentPart:
|
|
|
170
176
|
{"type", "metadata"},
|
|
171
177
|
{"text", "uri", "media_type", "name", "data"},
|
|
172
178
|
)
|
|
179
|
+
opaque_data: Mapping[str, Any] = (
|
|
180
|
+
{} if "data" not in fields else json_object(fields["data"], "opaque part data")
|
|
181
|
+
)
|
|
182
|
+
if "data" in fields and not opaque_data:
|
|
183
|
+
raise ProtocolError("opaque part data must not be empty when present")
|
|
173
184
|
return ContentPart(
|
|
174
185
|
type=part_type,
|
|
175
186
|
text=_present_string(fields, "text", "opaque part text"),
|
|
@@ -181,7 +192,7 @@ def decode_content_part_value(value: object) -> ContentPart:
|
|
|
181
192
|
non_empty=True,
|
|
182
193
|
),
|
|
183
194
|
name=_present_string(fields, "name", "opaque part name", non_empty=True),
|
|
184
|
-
data=
|
|
195
|
+
data=opaque_data,
|
|
185
196
|
metadata=json_object(fields["metadata"], "content metadata"),
|
|
186
197
|
)
|
|
187
198
|
|