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.
Files changed (58) hide show
  1. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/.gitignore +3 -12
  2. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/LICENSE +1 -1
  3. jharness_kernel-0.2.2/PKG-INFO +36 -0
  4. jharness_kernel-0.2.2/README.md +15 -0
  5. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/pyproject.toml +10 -11
  6. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/__init__.py +4 -0
  7. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/deadline.py +19 -6
  8. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/tooling.py +6 -0
  9. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/commands.py +9 -2
  10. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/errors.py +16 -0
  11. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/json_values.py +5 -0
  12. jharness_kernel-0.2.2/src/jharness/kernel/py.typed +1 -0
  13. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/_helpers.py +12 -3
  14. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/messages.py +15 -4
  15. jharness_kernel-0.1.2/PKG-INFO +0 -339
  16. jharness_kernel-0.1.2/README.md +0 -314
  17. jharness_kernel-0.1.2/src/jharness/kernel/py.typed +0 -0
  18. jharness_kernel-0.1.2/tests/test_diagnostics.py +0 -750
  19. jharness_kernel-0.1.2/tests/test_runtime.py +0 -473
  20. jharness_kernel-0.1.2/tests/test_runtime_concurrency_edges.py +0 -308
  21. jharness_kernel-0.1.2/tests/test_values.py +0 -535
  22. jharness_kernel-0.1.2/tests/test_wire.py +0 -1135
  23. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_digest.py +0 -0
  24. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/__init__.py +0 -0
  25. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/change.py +0 -0
  26. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/commit.py +0 -0
  27. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/engine.py +0 -0
  28. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/planning.py +0 -0
  29. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_engine/verification.py +0 -0
  30. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_history.py +0 -0
  31. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/_validation.py +0 -0
  32. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/approval.py +0 -0
  33. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/checkpoint.py +0 -0
  34. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/context.py +0 -0
  35. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/control.py +0 -0
  36. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/__init__.py +0 -0
  37. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/trace.py +0 -0
  38. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/diagnostics/verification.py +0 -0
  39. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/events.py +0 -0
  40. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/history.py +0 -0
  41. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/invocation.py +0 -0
  42. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/limits.py +0 -0
  43. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/messages.py +0 -0
  44. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/models.py +0 -0
  45. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/repository.py +0 -0
  46. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/runtime.py +0 -0
  47. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/snapshot.py +0 -0
  48. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/state.py +0 -0
  49. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/tools.py +0 -0
  50. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/__init__.py +0 -0
  51. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/checkpoint.py +0 -0
  52. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/events.py +0 -0
  53. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/models.py +0 -0
  54. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/requests.py +0 -0
  55. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/snapshot.py +0 -0
  56. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/state.py +0 -0
  57. {jharness_kernel-0.1.2 → jharness_kernel-0.2.2}/src/jharness/kernel/wire/tools.py +0 -0
  58. {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 AI assistant/workspace state
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
@@ -1,4 +1,4 @@
1
- +MIT License
1
+ MIT License
2
2
 
3
3
  Copyright (c) 2026 JHarness contributors
4
4
 
@@ -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.1.2"
4
- description = "JHarness model-neutral immutable runtime kernel"
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-python/tree/main/docs"
27
- Homepage = "https://github.com/Ezio2000/jharness-python"
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, cast
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
- try:
105
- async with asyncio.timeout(0.1):
106
- await cast(Awaitable[object], task)
107
- except (asyncio.CancelledError, Exception):
108
- pass
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 ValueError("suspension_mismatch: selector does not match checkpoint")
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 ValueError("resume messages require a planning continuation")
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")
@@ -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
- result = float(value)
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=tuple(
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=({} if "data" not in fields else json_object(fields["data"], "opaque part data")),
195
+ data=opaque_data,
185
196
  metadata=json_object(fields["metadata"], "content metadata"),
186
197
  )
187
198