agenthooksprotocol 0.2.0__tar.gz → 0.4.0__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 (117) hide show
  1. {agenthooksprotocol-0.2.0/src/agenthooksprotocol.egg-info → agenthooksprotocol-0.4.0}/PKG-INFO +129 -83
  2. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/README.md +128 -82
  3. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/pyproject.toml +1 -1
  4. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/__init__.py +11 -1
  5. agenthooksprotocol-0.4.0/src/agenthooksprotocol/_boundaries.py +1358 -0
  6. agenthooksprotocol-0.4.0/src/agenthooksprotocol/_content.py +907 -0
  7. agenthooksprotocol-0.2.0/src/agenthooksprotocol/elicitation.py → agenthooksprotocol-0.4.0/src/agenthooksprotocol/_elicitation.py +57 -20
  8. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_grants.py +6 -21
  9. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_hooks.py +374 -31
  10. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/__init__.py +9720 -8518
  11. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/content.py +4 -28
  12. agenthooksprotocol-0.4.0/src/agenthooksprotocol/_models/effect.py +124 -0
  13. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/event.py +4 -4
  14. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/tool.py +2 -0
  15. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/ahp-codegen.lock.json +13 -13
  16. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/attachment.py +6 -1
  17. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/auth.py +8 -3
  18. agenthooksprotocol-0.4.0/src/agenthooksprotocol/codec.py +99 -0
  19. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/compaction.py +69 -56
  20. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/content.py +1 -0
  21. agenthooksprotocol-0.4.0/src/agenthooksprotocol/contract.py +56 -0
  22. agenthooksprotocol-0.4.0/src/agenthooksprotocol/effect.py +511 -0
  23. agenthooksprotocol-0.4.0/src/agenthooksprotocol/elicitation.py +8 -0
  24. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/event.py +105 -2
  25. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/generated.py +2022 -868
  26. agenthooksprotocol-0.4.0/src/agenthooksprotocol/integrations/pydantic.py +152 -0
  27. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/interop.py +89 -44
  28. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/lifecycle.py +30 -19
  29. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/lifecycle_client.py +5 -2
  30. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/lineage.py +17 -27
  31. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/registration.py +4 -1
  32. agenthooksprotocol-0.4.0/src/agenthooksprotocol/response.py +268 -0
  33. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/runtime.py +199 -73
  34. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/schemas.json +2806 -405
  35. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/asgi.py +2 -2
  36. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/attachments.py +6 -2
  37. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/hooks.py +13 -10
  38. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/stdio.py +6 -4
  39. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/state.py +3 -4
  40. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/tool.py +1 -0
  41. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/transports/http.py +10 -8
  42. agenthooksprotocol-0.4.0/src/agenthooksprotocol/wire.py +49 -0
  43. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0/src/agenthooksprotocol.egg-info}/PKG-INFO +129 -83
  44. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol.egg-info/SOURCES.txt +13 -1
  45. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_attachments.py +279 -276
  46. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_catalogue.py +27 -12
  47. agenthooksprotocol-0.4.0/tests/test_compaction.py +393 -0
  48. agenthooksprotocol-0.4.0/tests/test_compaction_wire.py +259 -0
  49. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_content.py +4 -1
  50. agenthooksprotocol-0.4.0/tests/test_content_lifetime.py +393 -0
  51. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_content_receipts.py +14 -6
  52. agenthooksprotocol-0.4.0/tests/test_correlated_core.py +293 -0
  53. agenthooksprotocol-0.4.0/tests/test_declared_contracts.py +506 -0
  54. agenthooksprotocol-0.4.0/tests/test_elicitation_adapter.py +83 -0
  55. agenthooksprotocol-0.4.0/tests/test_inline_messages.py +983 -0
  56. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_integration.py +68 -17
  57. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_integration_edges.py +44 -16
  58. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_interop.py +78 -22
  59. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_lifecycle.py +29 -4
  60. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_lifecycle_auth.py +3 -3
  61. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_observation.py +2 -2
  62. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_owned_content.py +76 -42
  63. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_owned_content_hooks.py +54 -51
  64. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_public_content.py +195 -204
  65. agenthooksprotocol-0.4.0/tests/test_public_contract_surface.py +86 -0
  66. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_public_hooks.py +59 -0
  67. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_registration_auth.py +20 -25
  68. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_safety.py +22 -8
  69. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_schema_corrections.py +41 -12
  70. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_sdk_unification.py +13 -8
  71. agenthooksprotocol-0.4.0/tests/test_shared_materialization.py +389 -0
  72. agenthooksprotocol-0.4.0/tests/test_typed_contract_regressions.py +419 -0
  73. agenthooksprotocol-0.4.0/tests/test_upload_planning.py +361 -0
  74. agenthooksprotocol-0.2.0/src/agenthooksprotocol/_boundaries.py +0 -498
  75. agenthooksprotocol-0.2.0/src/agenthooksprotocol/_content.py +0 -801
  76. agenthooksprotocol-0.2.0/src/agenthooksprotocol/_models/effect.py +0 -48
  77. agenthooksprotocol-0.2.0/src/agenthooksprotocol/codec.py +0 -32
  78. agenthooksprotocol-0.2.0/src/agenthooksprotocol/effect.py +0 -150
  79. agenthooksprotocol-0.2.0/src/agenthooksprotocol/integrations/pydantic.py +0 -28
  80. agenthooksprotocol-0.2.0/tests/test_compaction.py +0 -199
  81. agenthooksprotocol-0.2.0/tests/test_compaction_wire.py +0 -73
  82. agenthooksprotocol-0.2.0/tests/test_content_lifetime.py +0 -315
  83. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/LICENSE +0 -0
  84. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/setup.cfg +0 -0
  85. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_diagnostics.py +0 -0
  86. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/capability.py +0 -0
  87. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/registration.py +0 -0
  88. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/subscription.py +0 -0
  89. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/_models/transport.py +0 -0
  90. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/candidate.py +0 -0
  91. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/capability.py +0 -0
  92. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/catalogue.py +0 -0
  93. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/diagnostics.py +0 -0
  94. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/integrations/__init__.py +0 -0
  95. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/lifecycle_server.py +0 -0
  96. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/observation_chain.py +0 -0
  97. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/path.py +0 -0
  98. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/permission.py +0 -0
  99. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/py.typed +0 -0
  100. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/__init__.py +0 -0
  101. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/_framing.py +0 -0
  102. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/server/_json.py +0 -0
  103. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/subscription.py +0 -0
  104. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/transport.py +0 -0
  105. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/transports/__init__.py +0 -0
  106. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol/transports/stdio.py +0 -0
  107. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol.egg-info/dependency_links.txt +0 -0
  108. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol.egg-info/requires.txt +0 -0
  109. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/src/agenthooksprotocol.egg-info/top_level.txt +0 -0
  110. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_codec.py +0 -0
  111. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_fixture_public.py +0 -0
  112. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_observation_chain_transport.py +0 -0
  113. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_package.py +0 -0
  114. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_public_examples.py +0 -0
  115. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_public_server.py +0 -0
  116. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_session_manifest.py +0 -0
  117. {agenthooksprotocol-0.2.0 → agenthooksprotocol-0.4.0}/tests/test_structural_facade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenthooksprotocol
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Official non-normative Python SDK for the Agent Hooks Protocol
5
5
  Author: Agent Hooks Protocol maintainers
6
6
  License:
@@ -266,6 +266,7 @@ Capability builders are immutable: reusing a base declaration does not widen it.
266
266
 
267
267
  Runnable, statically checked consumer programs live in [`examples/`](examples):
268
268
 
269
+ - `value_codecs.py`: independent argument/result/provenance codecs, admission-time invariants, and a form schema derived from its answer type.
269
270
  - `typed_tool.py`: JSON registration, generated tool input, typed grants, initial state, real in-process ASGI handler, borrowed HTTP client, and explicit application decoding.
270
271
  - `common_cases.py`: rewrite/allow, deny, invalid application shape, host policy, occurrence narrowing, native initial state, and fail-open/fail-closed reports. The host operation is an in-memory execution recorder, not a shell.
271
272
  - `stdio_tool.py` / `stdio_backend.py`: a real owned backend process using the same public Handler, denial, and child cleanup.
@@ -282,9 +283,15 @@ Generated constructors build JSON-shaped protocol values. Generated `TypedDict`
282
283
 
283
284
  Capabilities explicitly grant effects and target operations. Supply event capabilities at harness construction; a per-occurrence capability override can narrow them, never widen them. `initial_state` carries the host's native prior decision for one occurrence. A hook-supplied result is a candidate, not an assertion that an operation executed. The host owns its ordinary input validation and actual execution.
284
285
 
286
+ Supply `input_codec=`, `result_codec=`, and optional `provenance_codec=` to a named hook method or `dispatch` to declare caller types. Complete staged values are decoded and validated before publication. Object merge is shallow: omitted fields remain unchanged, literal null is retained, and nested values replace whole values. An invalid response publishes none of its effects; earlier accepted responses remain visible to later interceptors. A codec must preserve the wire data it receives, including opaque extra fields. Lossy serialization rejects admission rather than dropping fields.
287
+
288
+ `result.decoded_input` exposes the declared argument value. `result.decoded_candidate` is an application `Candidate[Result, Provenance]`, or `None` when there is no candidate. Its value can itself be `None`; optional provenance is omitted from `candidate.to_dict()` when absent. Raw `input` and `candidate` views remain detached snapshots. For explicitly selected opaque host slots, use `event_codecs={("params",): codec}` with event-relative paths. Unspecified native, provider, task, permission, and discovery values are not interpreted.
289
+
290
+ For forms, construct `PydanticFormContract(TypeAdapter(Answer))` from one answer declaration. `request(message)` derives the restricted MCP schema; `result_codec()` decodes the complete MCP result and its accepted answer while preserving opaque result metadata. Unsupported shapes and schema constraints are rejected during construction. Decline, cancel, and URL results have no form content. See [`examples/value_codecs.py`](examples/value_codecs.py).
291
+
285
292
  After settlement, explicitly decode effective input with `result.decode_input(codec)`. A codec implements `encode(value)` and `decode(json_value)`; `IdentityCodec` copies JSON. The optional `integrations.pydantic.PydanticCodec` accepts a Pydantic `TypeAdapter`. A decoding error does not retroactively reject protocol effects: retain the settled result, raw effective input, accepted responses, and diagnostics, and report host-level rejection without executing.
286
293
 
287
- `result.permission` is the generated `Permission` enum (`NONE`, `ALLOW`, `ASK`, `DENY`) read from canonical settlement, not an independent decision engine. `NONE` is not approval; `ASK` needs host approval; interruption never authorizes execution. `result.input` is detached accepted JSON, not a claim that backend modifications satisfy an application's original type. `decode_input(codec)` remains an explicit, post-settlement host check. Candidate, canonical event, accepted responses, and diagnostics remain accessible.
294
+ `result.permission` is the generated `Permission` enum (`NONE`, `ALLOW`, `ASK`, `DENY`) read from canonical settlement, not an independent decision engine. `NONE` is not approval; `ASK` needs host approval; interruption never authorizes execution. `result.input` is detached accepted JSON. Declared codecs validate accepted modifications; without a declared codec, the host supplies application validation. `decode_input(codec)` remains an explicit, post-settlement host check. Candidate, canonical event, accepted responses, and diagnostics remain accessible.
288
295
 
289
296
  `state.initial(Permission.ALLOW)` represents a native decision **already made for this exact occurrence**, not an authorization shortcut. Its default candidate is absent (`null`); `state.Candidate(value=None)` instead means a supplied JSON-null result. Optional typed provenance records facts, not authenticated identity or execution proof. Canonical modifications still invalidate candidates/approval as specified.
290
297
 
@@ -327,36 +334,69 @@ Observation is one-way. It does not acquire effect authority and never waits for
327
334
 
328
335
  The attachment receiver verifies byte length and digest through EOF before committing caller-provided immutable storage. Receiver-allocated references are published only after authorization and successful storage commit. Applications choose storage durability and retention.
329
336
 
330
- ## Body-bound boundaries
331
-
332
- Use `ContentContext` for MCP elicitation and compaction bodies. Its asynchronous `resolve(reference)` and `upload(bytes)` functions are trusted storage/transport adapters, not application-schema callbacks. `upload` must return a verified `ContentUploadReceipt` (`ref`, `size`, `sha256`) after storage commit. The SDK checks receipt length and digest against the actual uploaded bytes. `resolve` receives only `{ "ref": ... }` and must resolve immutable bytes within its authorized storage scope; event metadata is not an integrity authority. The SDK detects changes to previously resolved bytes.
333
-
334
- Bindings are explicit paths within the canonical event:
335
-
336
- | Boundary | Binding |
337
- | --- | --- |
338
- | `user_elicitation_request` | `{"request": ("elicitation", "request")}` |
339
- | `user_elicitation_result` | `{"content": ("elicitation", "result")}` plus `original_request` |
340
- | `context_compact_before` | `{"instructions": ("instructions",)}` |
341
- | `context_compact_after` | `{"summary": ("summary",)}` |
342
-
343
- Pass `content=context` to the named boundary. An elicitation result needs the original canonical request snapshot; the SDK does not fabricate correlation, form/URL support, or MCP defaults. Metadata and omitted selections do not read body bytes. Changed bodies receive new immutable references, and a changed compaction input invalidates an earlier supplied summary.
344
-
345
- ### Lazy owned sources
346
-
347
- `OwnedContentSource(stream, max_bytes=..., timeout=...)` wraps an async native `read(size)`/`receive(size)` stream with `aclose()`. Construction does not read. Size/hash expectations are optional and verified, not trusted. Generated inputs expose named `bind_<slot>_source(source)` methods; repeated content slots additionally require `index=`. For example, `input.bind_items_source(source, index=0)` binds an existing metadata item without putting the stream into wire JSON.
337
+ ## Inline messages and specialized boundaries
348
338
 
349
- Pass the bound input to a named hook method with `uploads={backend_id: authorized_upload_callback}`. Each callback receives immutable bytes and returns a receiver-allocated canonical upload receipt; it can use `auth.AuthenticatedHTTPTransport.upload(..., authentication=upload_binding)`. Metadata, omit, and unmatched receivers consume no bytes and require no uploader. Selected body delivery snapshots once within limits, hashes actual raw bytes, uploads independently for each authorized destination, and verifies receipts before event delivery. Missing receiver authorization fails before reading. Use `ContentSources` to supply explicit source bindings, or `ContentContext` to resolve canonical references.
339
+ Model-visible inputs contain canonical messages with a `role` and ordered
340
+ `parts`. Supported roles are `system`, `developer`, `user`, `assistant`, and
341
+ `tool`. A text part has `kind="text"`, `mediaType="text/plain"`, and inline
342
+ `text`. Serialized JSON is ordinary text. An attachment part has
343
+ `kind="attachment"`, a non-text/non-JSON media type, and an immutable binary
344
+ body. Construction supplies missing message/part IDs and marks them
345
+ `synthesized=True`; explicit IDs remain stable.
350
346
 
351
- Ownership transfers to the source wrapper, then to the hook operation when passed. Streams close on success, failure, cancellation, and unused selection; snapshots release when the call finishes. Closed wrappers also release their reader, so retaining a source or result does not retain the reader’s input buffer. Sources are single-operation values, not concurrently reusable. Call `aclose()` or use an async context manager for a source that never reaches a hook call. Cleanup is shielded and bounded separately from the operation budget.
347
+ ```python
348
+ from agenthooksprotocol import Attachment
349
+
350
+ input = {
351
+ "trigger": "manual",
352
+ "items": [{"role": "user", "parts": [
353
+ {"kind": "text", "text": "Review this report."},
354
+ {"kind": "attachment", "mediaType": "application/pdf",
355
+ "body": Attachment.from_bytes(pdf_bytes)},
356
+ ]}],
357
+ }
358
+ result = await hooks.context_compact_before(input, uploads=authorized_uploads)
359
+ async with result:
360
+ data = await result.attachments.read("context.compact.before.items_parts[0][1]")
361
+ ```
352
362
 
353
- For custom transport integrations, `begin(canonical_request)` returns a `PendingInvocation` with separate acquisition, cancellation, and acceptance. `exchange(canonical_request)` uses the same settlement engine. Content-aware pending invocations use `await accept_content()`; this keeps upload completion and cancellation inside the publication boundary. The ordinary named-method path does this automatically. Pending invocations retain selected bytes only until acceptance, cancellation, acquisition/finalization failure, or harness close. Failed finalization may be retried and resolves fresh bytes. Harness tracking is weak: dropping a pending invocation does not keep its prepared payload alive. Terminal handles also detach their resolver/uploader context, so retained handles do not retain the caller’s backing store. Returned references continue to resolve in caller-owned storage; cleanup never clears that persistent store.
363
+ Named hook methods also accept canonical dictionaries with the same direct
364
+ attachment bodies. The runtime collects sources and creates receiver-specific
365
+ references internally. Construction and metadata/omit projection do not read
366
+ or upload attachments. Only schema-owned slots are projected; native payloads,
367
+ extensions, and application arguments are opaque. Selection is a ceiling for
368
+ each receiver's view: metadata/omit views contain no inline text or attachment
369
+ references. Independent receivers apply their own content selections.
370
+
371
+ Compaction `instructions` and `summary` are canonical text-part lists.
372
+ Replacement substitutes the list; merge appends in order. Model-visible message
373
+ list targets (`prompt`, `request`, `response`, and tool `output`) have the same
374
+ list operations. Tool output edits operate on `tool.after.items`; application
375
+ structures are serialized as text parts. Standalone `content` edits require the
376
+ boundary context: `user.message.outbound` uses a message list, and accepted form
377
+ answers at `user.elicitation.result` use an MCP answer object. Object targets such as tool input and workspace changes use shallow object merge. A response settles
378
+ atomically, and each serial receiver sees the committed state. Changed
379
+ compaction instructions invalidate an earlier supplied summary.
380
+
381
+ MCP elicitation `request` and `result` contain selected text parts whose `text`
382
+ is the serialized complete MCP JSON payload. The SDK validates parsed payloads,
383
+ mode grants, form answers, and correlation without inserting defaults. Result
384
+ edits require `original_request=` with the original canonical intercept request
385
+ snapshot; answer-object merge is shallow. Elicitation request bodies are immutable
386
+ to modification effects. No text/JSON upload callback is needed. Metadata and omit selections
387
+ cannot authorize body-dependent effects.
388
+
389
+ For custom transports, `begin(canonical_request, value_codecs=ValueCodecs(...))` separates acquisition,
390
+ cancellation, and acceptance; `exchange(canonical_request)` uses the same
391
+ settlement engine. Elicitation correlation can be supplied through
392
+ `ContentContext(original_request=..., bindings={"content": ("elicitation", "result")},
393
+ principal=authenticated_identity)` and `await pending.accept_content()`.
354
394
 
355
395
  ## Generated models and provenance
356
396
 
357
397
  Semantic namespaces (`event`, `tool`, `effect`, `capability`, and registration models) and all named event boundaries come from schema metadata, not a selected handwritten helper list. Constructors may supply fixed protocol literals; parsers never repair missing input. Open fields are retained for forward-compatible JSON round trips.
358
398
 
359
- The low-level `generated` module exports the full schema's typed models, `parse_<root>` / `encode_<root>` codecs, `PROTOCOL_VERSION`, `SCHEMA_REVISION`, and JSON types. `ahp-codegen.lock.json` records immutable generator provenance. Change canonical schemas and generator source in the protocol repository rather than editing generated files.
399
+ The `wire` namespace exposes canonical JSON-shaped request, notification, effect, and response types with structural codecs. Server callbacks use these raw wire contracts. The low-level `generated` module exports the full schema's typed models, `parse_<root>` / `encode_<root>` codecs, `PROTOCOL_VERSION`, `SCHEMA_REVISION`, and JSON types. `ahp-codegen.lock.json` records immutable generator provenance. Change canonical schemas and generator source in the protocol repository rather than editing generated files.
360
400
 
361
401
  ## Development
362
402
 
@@ -374,12 +414,12 @@ Integration tests also use a matching sibling `../agent-hooks-protocol` checkout
374
414
  Generated facade constructors retain nested model types through schema
375
415
  compositions. MCP connection `gaps` parameters accept lists of typed gap models,
376
416
  and HTTP, SSE, stdio, and custom connection objects expose typed location and gap
377
- attributes. `ModelVisibleItem` has composed content constructors with typed roles.
417
+ attributes. `ModelVisibleItem` is a canonical message with a typed role and ordered parts.
378
418
 
379
419
  Facade objects are mappings with read-only attributes.
380
420
  Optional attributes return `None` when absent; mapping membership still records
381
- presence. Attributes conflicting with mapping methods use a trailing underscore
382
- (such as `items_`), so `dict.items()` keeps working. Wire parsers return lossless mappings, not hydrated facade instances.
421
+ presence. Omitted and explicit-empty response effects are both neutral; decoded effects provide an empty list without modifying the source wire object. Attributes conflicting with mapping methods use a trailing underscore
422
+ (such as `items_`), so `dict.items()` keeps working. Raw wire parsers return lossless mappings. `response.response_for_request(event_type, raw)` returns the generated contextual response selected by the originating event type, not by payload shape.
383
423
 
384
424
  Use generated nested models for typed constructor arguments. Wire constructors, dictionary decoding, and runtime parsing
385
425
  check location/evidence alternatives and other structural schema constraints. Application
@@ -413,13 +453,7 @@ the same structural descriptor engine as generated `parse_*` functions. The
413
453
  original composition is retained, including required members, explicit nulls,
414
454
  literals, closed enums, forbidden combinations, and union ambiguity. This is the
415
455
  SDK's structural contract, not complete contextual protocol validation.
416
- `from_dict` does not supply omitted wire literals or defaults. It validates once
417
- and privately hydrates nested typed models without recursively calling public
418
- constructors. For example, a decoded HTTP connection supports
419
- `connection.gaps[0].reason`, just like a constructed connection. Union selection
420
- reuses memoized descriptor checks from that decode; subtrees are not repeatedly
421
- validated during hydration. Extension/application values remain ordinary JSON
422
- values. Descriptors and hydration metadata are cached.
456
+ `from_dict` checks complete wire values and preserves optional-field presence. Decoded known nested values expose the same attributes as constructed values, such as `connection.gaps[0].reason`. Extension and application bags remain intact and do not acquire effect or attachment authority.
423
457
 
424
458
  Invalid wire models raise `ValueError`; validation failures carry `result`,
425
459
  `diagnostics`, and `raw` attributes. Use `parse_*` when you need the full result on
@@ -437,12 +471,7 @@ objects are validated at the dispatch/parse boundary. Direct dictionary
437
471
  mutation, standard-library `json.loads`, TypedDict annotations, and type assertions
438
472
  are not SDK validation entrypoints. Re-parse after manually mutating a mapping.
439
473
 
440
- Wire constructors also privately hydrate nested mappings: for example,
441
- `capability.Capabilities(effects=["modify"], modify={}).modify.input` returns
442
- `None`, not an attribute error. Already typed child models retain their identity;
443
- plain nested mappings become typed models after the constructor's single cached
444
- validation pass. Family queries include all generated per-occurrence capability
445
- models, such as `capability.ToolBeforeCapabilities`.
474
+ Nested model attributes expose declared protocol fields. For example, `capability.Capabilities(effects=["modify"], modify={}).modify.input` is `None` when that grant is absent. Family queries are available on generic and per-occurrence capability models.
446
475
 
447
476
  Constructor aliases cannot silently overwrite wire keys. Supplying both
448
477
  `tool_name=` and `toolName=` raises `TypeError`, even if the values agree. For
@@ -454,48 +483,65 @@ keys such as `toolName` and `tool_name` remain distinct wire/extension keys.
454
483
 
455
484
  ### Owned attachments
456
485
 
457
- The owned attachment APIs in this section are not included in the published 0.1.1 package.
458
-
459
486
  Use `Attachment.from_bytes(data)` for immutable Python `bytes`, or
460
- `Attachment.lazy(async_loader, aclose=async_cleanup)` to defer reading. Bind it
461
- with the typed input's `bind_*_source` method on an item without a body reference.
462
- Content IDs, media types, and other metadata belong on the content item. See the
463
- runnable [file attachment example](examples/file_attachment.py).
464
-
465
- Dispatch transfers ownership once. Metadata-only and unmatched deliveries do not
466
- invoke the loader. Selected body deliveries share one immutable snapshot, with
467
- receiver-specific references. Successful results own the bytes **or still-unread
468
- loader**, independent of `Hooks` shutdown. Use `async with result:` or
469
- `await result.aclose()` and read with
470
- `await result.attachments.read("context.compact.before.items[0]")`. Cleanup runs
471
- for unopened sources too. Do not reuse an attachment across invocations; create a
472
- new attachment (which can share the same immutable `bytes`) instead.
473
-
474
- The default accepted size is 4 MiB and lazy loading timeout is 30 seconds;
475
- constructors accept `max_bytes` and lazy construction accepts `timeout`. Loaders
476
- must bound their own allocations. `Attachment(async_stream, ...)` also supports
477
- the bounded `read(size)`/`receive(max_bytes)` source contract. Stream cleanup
478
- is shielded and bounded to one second. Cancellation and failure never retry a
479
- loader. Python mutable buffers are rejected, and reads
480
- return immutable bytes.
481
-
482
- Remote body delivery requires an authorized receiver uploader through
483
- `uploads={backend_id: async_upload}`. The uploader supplies a reference that the
484
- receiver can read.
485
- Metadata-only delivery and result reads require no uploader.
486
-
487
- Attachments own their immutable bytes directly. A lazy loader's `bytes` object
488
- becomes the attachment snapshot; upload and result reads borrow that same object.
489
- Receiver applications manage upload storage.
490
-
491
- Compaction text and elicitation JSON effects read selected attachment
492
- owners. An accepted replacement creates a new effective attachment; subsequent
493
- deliveries and the result use that owner. Original attachment size bounds also
494
- apply to replacements. Generated candidates are available through
495
- `await result.attachments.read("candidate")`. Binary/file editing is not supported.
496
- Elicitation results require their original request correlation context;
497
- `ContentContext.attachments` can provide the original request owner without a
498
- resolver. A resolver is an optional
499
- adapter for externally supplied references.
500
-
487
+ `Attachment.lazy(async_loader, aclose=async_cleanup)` to defer reading. Put
488
+ `attachment` directly in an attachment part's `body`. Named `Hooks` methods
489
+ accept both dictionary inputs and typed event inputs with the same owner.
490
+ For a custom `OwnedContentSource`, use the host-only `content.OwnedAttachment(source)` wrapper in an attachment part's body. This wrapper does not read the source and is not a wire value. The SDK transfers and closes its source under the same invocation ownership rules.
491
+
492
+ Metadata belongs on the part. See the runnable
493
+ [file attachment example](examples/file_attachment.py).
494
+
495
+ Dispatch transfers ownership once. Successful results retain the exact owner,
496
+ including an unread loader, independently of `Hooks` shutdown. Callers close
497
+ returned content owners with `async with result:` or `await result.aclose()`.
498
+ Cleanup runs for unopened sources too. Create a new attachment for each
499
+ invocation; eager attachments can share the same immutable `bytes` object.
500
+
501
+ The default accepted size is 4 MiB and lazy loading timeout is 30 seconds.
502
+ Constructors accept `max_bytes`; lazy construction accepts `timeout`. Loaders
503
+ must bound their own allocations. `Attachment(async_stream, ...)` supports
504
+ bounded `read(size)`/`receive(max_bytes)` streams. Stream cleanup is shielded and
505
+ bounded to one second. Cancellation and failure never retry a loader. Mutable
506
+ buffers are rejected, and reads return immutable bytes.
507
+
508
+ Selected remote binary delivery requires an authorized uploader through
509
+ `uploads={backend_id: async_upload}`. Each uploader receives the exact owner's
510
+ immutable bytes and returns a committed receipt with `ref`, `size`, and `sha256`.
511
+ The SDK validates the receipt against the actual bytes before sending the event.
512
+ Upload callbacks remain caller-authorized and scoped to their backend; event
513
+ credentials do not authorize uploads to another destination.
514
+
515
+ `Hooks(..., max_concurrent_uploads=8)` bounds concurrent binary uploads.
516
+ The value must be a positive integer. Before serial interception
517
+ begins, the SDK prepares uploads for body-selected attachment slots in all
518
+ matched subscriptions, including observers. One attachment owner is read once
519
+ for multiple subscribers. Metadata, omit, and unmatched selections do not read
520
+ or upload its bytes. Result reads and metadata-only delivery require no uploader.
521
+
522
+ Only confirmed receipts are reused, for the exact attachment owner and backend
523
+ ID/subscription index. An uncalled interceptor receives a settled observation
524
+ with its confirmed reference after denial or stop. Upload failures are handled by
525
+ the affected subscription's failure policy when its delivery is reached; they
526
+ do not prevent independent destinations from preparing their uploads. An upload
527
+ can commit even if an earlier interceptor later removes the attachment or halts
528
+ the chain. Receiver applications manage separate storage durability and retention.
529
+ Cancellation cancels and joins pending upload work and closes invocation-owned
530
+ sources. Overlapping reads join one materialization and receive the same immutable
531
+ bytes or terminal error. Cancelling one reader leaves materialization active for
532
+ other readers; the last cancelled reader or owner closure cancels and joins it.
533
+ The attachment owns its single pending materialization independently of readers.
534
+ A cancelled reader returns and releases its upload permit while another reader
535
+ continues. The last reader or owner closure joins cleanup; completed workers are
536
+ not retained.
537
+
538
+ Attachments are the sole byte owners. Binary/file editing is not supported.
501
539
  Results are invocation-local, not session archives.
540
+
541
+ ### Advanced validation and dispatch
542
+
543
+ `runtime.Validator` validates bundled canonical schemas and structural codecs. It does not establish request correlation, effect grants, host approval, or permission to execute. Use `Hooks` for complete interception admission.
544
+
545
+ `server.hooks.Engine` is the framework-neutral backend dispatcher. Supply an explicit validator or static harness manifest when needed. Engine callbacks return protocol effects; the receiving harness validates authority and settles them atomically.
546
+
547
+ Host session counters (`turns`, `modelRequests`, `toolCalls`, `inputTokens`, `outputTokens`, and extra counter names) are exact nonnegative integers. The SDK preserves supplied values and does not infer counters from events.
@@ -42,6 +42,7 @@ Capability builders are immutable: reusing a base declaration does not widen it.
42
42
 
43
43
  Runnable, statically checked consumer programs live in [`examples/`](examples):
44
44
 
45
+ - `value_codecs.py`: independent argument/result/provenance codecs, admission-time invariants, and a form schema derived from its answer type.
45
46
  - `typed_tool.py`: JSON registration, generated tool input, typed grants, initial state, real in-process ASGI handler, borrowed HTTP client, and explicit application decoding.
46
47
  - `common_cases.py`: rewrite/allow, deny, invalid application shape, host policy, occurrence narrowing, native initial state, and fail-open/fail-closed reports. The host operation is an in-memory execution recorder, not a shell.
47
48
  - `stdio_tool.py` / `stdio_backend.py`: a real owned backend process using the same public Handler, denial, and child cleanup.
@@ -58,9 +59,15 @@ Generated constructors build JSON-shaped protocol values. Generated `TypedDict`
58
59
 
59
60
  Capabilities explicitly grant effects and target operations. Supply event capabilities at harness construction; a per-occurrence capability override can narrow them, never widen them. `initial_state` carries the host's native prior decision for one occurrence. A hook-supplied result is a candidate, not an assertion that an operation executed. The host owns its ordinary input validation and actual execution.
60
61
 
62
+ Supply `input_codec=`, `result_codec=`, and optional `provenance_codec=` to a named hook method or `dispatch` to declare caller types. Complete staged values are decoded and validated before publication. Object merge is shallow: omitted fields remain unchanged, literal null is retained, and nested values replace whole values. An invalid response publishes none of its effects; earlier accepted responses remain visible to later interceptors. A codec must preserve the wire data it receives, including opaque extra fields. Lossy serialization rejects admission rather than dropping fields.
63
+
64
+ `result.decoded_input` exposes the declared argument value. `result.decoded_candidate` is an application `Candidate[Result, Provenance]`, or `None` when there is no candidate. Its value can itself be `None`; optional provenance is omitted from `candidate.to_dict()` when absent. Raw `input` and `candidate` views remain detached snapshots. For explicitly selected opaque host slots, use `event_codecs={("params",): codec}` with event-relative paths. Unspecified native, provider, task, permission, and discovery values are not interpreted.
65
+
66
+ For forms, construct `PydanticFormContract(TypeAdapter(Answer))` from one answer declaration. `request(message)` derives the restricted MCP schema; `result_codec()` decodes the complete MCP result and its accepted answer while preserving opaque result metadata. Unsupported shapes and schema constraints are rejected during construction. Decline, cancel, and URL results have no form content. See [`examples/value_codecs.py`](examples/value_codecs.py).
67
+
61
68
  After settlement, explicitly decode effective input with `result.decode_input(codec)`. A codec implements `encode(value)` and `decode(json_value)`; `IdentityCodec` copies JSON. The optional `integrations.pydantic.PydanticCodec` accepts a Pydantic `TypeAdapter`. A decoding error does not retroactively reject protocol effects: retain the settled result, raw effective input, accepted responses, and diagnostics, and report host-level rejection without executing.
62
69
 
63
- `result.permission` is the generated `Permission` enum (`NONE`, `ALLOW`, `ASK`, `DENY`) read from canonical settlement, not an independent decision engine. `NONE` is not approval; `ASK` needs host approval; interruption never authorizes execution. `result.input` is detached accepted JSON, not a claim that backend modifications satisfy an application's original type. `decode_input(codec)` remains an explicit, post-settlement host check. Candidate, canonical event, accepted responses, and diagnostics remain accessible.
70
+ `result.permission` is the generated `Permission` enum (`NONE`, `ALLOW`, `ASK`, `DENY`) read from canonical settlement, not an independent decision engine. `NONE` is not approval; `ASK` needs host approval; interruption never authorizes execution. `result.input` is detached accepted JSON. Declared codecs validate accepted modifications; without a declared codec, the host supplies application validation. `decode_input(codec)` remains an explicit, post-settlement host check. Candidate, canonical event, accepted responses, and diagnostics remain accessible.
64
71
 
65
72
  `state.initial(Permission.ALLOW)` represents a native decision **already made for this exact occurrence**, not an authorization shortcut. Its default candidate is absent (`null`); `state.Candidate(value=None)` instead means a supplied JSON-null result. Optional typed provenance records facts, not authenticated identity or execution proof. Canonical modifications still invalidate candidates/approval as specified.
66
73
 
@@ -103,36 +110,69 @@ Observation is one-way. It does not acquire effect authority and never waits for
103
110
 
104
111
  The attachment receiver verifies byte length and digest through EOF before committing caller-provided immutable storage. Receiver-allocated references are published only after authorization and successful storage commit. Applications choose storage durability and retention.
105
112
 
106
- ## Body-bound boundaries
107
-
108
- Use `ContentContext` for MCP elicitation and compaction bodies. Its asynchronous `resolve(reference)` and `upload(bytes)` functions are trusted storage/transport adapters, not application-schema callbacks. `upload` must return a verified `ContentUploadReceipt` (`ref`, `size`, `sha256`) after storage commit. The SDK checks receipt length and digest against the actual uploaded bytes. `resolve` receives only `{ "ref": ... }` and must resolve immutable bytes within its authorized storage scope; event metadata is not an integrity authority. The SDK detects changes to previously resolved bytes.
109
-
110
- Bindings are explicit paths within the canonical event:
111
-
112
- | Boundary | Binding |
113
- | --- | --- |
114
- | `user_elicitation_request` | `{"request": ("elicitation", "request")}` |
115
- | `user_elicitation_result` | `{"content": ("elicitation", "result")}` plus `original_request` |
116
- | `context_compact_before` | `{"instructions": ("instructions",)}` |
117
- | `context_compact_after` | `{"summary": ("summary",)}` |
118
-
119
- Pass `content=context` to the named boundary. An elicitation result needs the original canonical request snapshot; the SDK does not fabricate correlation, form/URL support, or MCP defaults. Metadata and omitted selections do not read body bytes. Changed bodies receive new immutable references, and a changed compaction input invalidates an earlier supplied summary.
120
-
121
- ### Lazy owned sources
122
-
123
- `OwnedContentSource(stream, max_bytes=..., timeout=...)` wraps an async native `read(size)`/`receive(size)` stream with `aclose()`. Construction does not read. Size/hash expectations are optional and verified, not trusted. Generated inputs expose named `bind_<slot>_source(source)` methods; repeated content slots additionally require `index=`. For example, `input.bind_items_source(source, index=0)` binds an existing metadata item without putting the stream into wire JSON.
113
+ ## Inline messages and specialized boundaries
124
114
 
125
- Pass the bound input to a named hook method with `uploads={backend_id: authorized_upload_callback}`. Each callback receives immutable bytes and returns a receiver-allocated canonical upload receipt; it can use `auth.AuthenticatedHTTPTransport.upload(..., authentication=upload_binding)`. Metadata, omit, and unmatched receivers consume no bytes and require no uploader. Selected body delivery snapshots once within limits, hashes actual raw bytes, uploads independently for each authorized destination, and verifies receipts before event delivery. Missing receiver authorization fails before reading. Use `ContentSources` to supply explicit source bindings, or `ContentContext` to resolve canonical references.
115
+ Model-visible inputs contain canonical messages with a `role` and ordered
116
+ `parts`. Supported roles are `system`, `developer`, `user`, `assistant`, and
117
+ `tool`. A text part has `kind="text"`, `mediaType="text/plain"`, and inline
118
+ `text`. Serialized JSON is ordinary text. An attachment part has
119
+ `kind="attachment"`, a non-text/non-JSON media type, and an immutable binary
120
+ body. Construction supplies missing message/part IDs and marks them
121
+ `synthesized=True`; explicit IDs remain stable.
126
122
 
127
- Ownership transfers to the source wrapper, then to the hook operation when passed. Streams close on success, failure, cancellation, and unused selection; snapshots release when the call finishes. Closed wrappers also release their reader, so retaining a source or result does not retain the reader’s input buffer. Sources are single-operation values, not concurrently reusable. Call `aclose()` or use an async context manager for a source that never reaches a hook call. Cleanup is shielded and bounded separately from the operation budget.
123
+ ```python
124
+ from agenthooksprotocol import Attachment
125
+
126
+ input = {
127
+ "trigger": "manual",
128
+ "items": [{"role": "user", "parts": [
129
+ {"kind": "text", "text": "Review this report."},
130
+ {"kind": "attachment", "mediaType": "application/pdf",
131
+ "body": Attachment.from_bytes(pdf_bytes)},
132
+ ]}],
133
+ }
134
+ result = await hooks.context_compact_before(input, uploads=authorized_uploads)
135
+ async with result:
136
+ data = await result.attachments.read("context.compact.before.items_parts[0][1]")
137
+ ```
128
138
 
129
- For custom transport integrations, `begin(canonical_request)` returns a `PendingInvocation` with separate acquisition, cancellation, and acceptance. `exchange(canonical_request)` uses the same settlement engine. Content-aware pending invocations use `await accept_content()`; this keeps upload completion and cancellation inside the publication boundary. The ordinary named-method path does this automatically. Pending invocations retain selected bytes only until acceptance, cancellation, acquisition/finalization failure, or harness close. Failed finalization may be retried and resolves fresh bytes. Harness tracking is weak: dropping a pending invocation does not keep its prepared payload alive. Terminal handles also detach their resolver/uploader context, so retained handles do not retain the caller’s backing store. Returned references continue to resolve in caller-owned storage; cleanup never clears that persistent store.
139
+ Named hook methods also accept canonical dictionaries with the same direct
140
+ attachment bodies. The runtime collects sources and creates receiver-specific
141
+ references internally. Construction and metadata/omit projection do not read
142
+ or upload attachments. Only schema-owned slots are projected; native payloads,
143
+ extensions, and application arguments are opaque. Selection is a ceiling for
144
+ each receiver's view: metadata/omit views contain no inline text or attachment
145
+ references. Independent receivers apply their own content selections.
146
+
147
+ Compaction `instructions` and `summary` are canonical text-part lists.
148
+ Replacement substitutes the list; merge appends in order. Model-visible message
149
+ list targets (`prompt`, `request`, `response`, and tool `output`) have the same
150
+ list operations. Tool output edits operate on `tool.after.items`; application
151
+ structures are serialized as text parts. Standalone `content` edits require the
152
+ boundary context: `user.message.outbound` uses a message list, and accepted form
153
+ answers at `user.elicitation.result` use an MCP answer object. Object targets such as tool input and workspace changes use shallow object merge. A response settles
154
+ atomically, and each serial receiver sees the committed state. Changed
155
+ compaction instructions invalidate an earlier supplied summary.
156
+
157
+ MCP elicitation `request` and `result` contain selected text parts whose `text`
158
+ is the serialized complete MCP JSON payload. The SDK validates parsed payloads,
159
+ mode grants, form answers, and correlation without inserting defaults. Result
160
+ edits require `original_request=` with the original canonical intercept request
161
+ snapshot; answer-object merge is shallow. Elicitation request bodies are immutable
162
+ to modification effects. No text/JSON upload callback is needed. Metadata and omit selections
163
+ cannot authorize body-dependent effects.
164
+
165
+ For custom transports, `begin(canonical_request, value_codecs=ValueCodecs(...))` separates acquisition,
166
+ cancellation, and acceptance; `exchange(canonical_request)` uses the same
167
+ settlement engine. Elicitation correlation can be supplied through
168
+ `ContentContext(original_request=..., bindings={"content": ("elicitation", "result")},
169
+ principal=authenticated_identity)` and `await pending.accept_content()`.
130
170
 
131
171
  ## Generated models and provenance
132
172
 
133
173
  Semantic namespaces (`event`, `tool`, `effect`, `capability`, and registration models) and all named event boundaries come from schema metadata, not a selected handwritten helper list. Constructors may supply fixed protocol literals; parsers never repair missing input. Open fields are retained for forward-compatible JSON round trips.
134
174
 
135
- The low-level `generated` module exports the full schema's typed models, `parse_<root>` / `encode_<root>` codecs, `PROTOCOL_VERSION`, `SCHEMA_REVISION`, and JSON types. `ahp-codegen.lock.json` records immutable generator provenance. Change canonical schemas and generator source in the protocol repository rather than editing generated files.
175
+ The `wire` namespace exposes canonical JSON-shaped request, notification, effect, and response types with structural codecs. Server callbacks use these raw wire contracts. The low-level `generated` module exports the full schema's typed models, `parse_<root>` / `encode_<root>` codecs, `PROTOCOL_VERSION`, `SCHEMA_REVISION`, and JSON types. `ahp-codegen.lock.json` records immutable generator provenance. Change canonical schemas and generator source in the protocol repository rather than editing generated files.
136
176
 
137
177
  ## Development
138
178
 
@@ -150,12 +190,12 @@ Integration tests also use a matching sibling `../agent-hooks-protocol` checkout
150
190
  Generated facade constructors retain nested model types through schema
151
191
  compositions. MCP connection `gaps` parameters accept lists of typed gap models,
152
192
  and HTTP, SSE, stdio, and custom connection objects expose typed location and gap
153
- attributes. `ModelVisibleItem` has composed content constructors with typed roles.
193
+ attributes. `ModelVisibleItem` is a canonical message with a typed role and ordered parts.
154
194
 
155
195
  Facade objects are mappings with read-only attributes.
156
196
  Optional attributes return `None` when absent; mapping membership still records
157
- presence. Attributes conflicting with mapping methods use a trailing underscore
158
- (such as `items_`), so `dict.items()` keeps working. Wire parsers return lossless mappings, not hydrated facade instances.
197
+ presence. Omitted and explicit-empty response effects are both neutral; decoded effects provide an empty list without modifying the source wire object. Attributes conflicting with mapping methods use a trailing underscore
198
+ (such as `items_`), so `dict.items()` keeps working. Raw wire parsers return lossless mappings. `response.response_for_request(event_type, raw)` returns the generated contextual response selected by the originating event type, not by payload shape.
159
199
 
160
200
  Use generated nested models for typed constructor arguments. Wire constructors, dictionary decoding, and runtime parsing
161
201
  check location/evidence alternatives and other structural schema constraints. Application
@@ -189,13 +229,7 @@ the same structural descriptor engine as generated `parse_*` functions. The
189
229
  original composition is retained, including required members, explicit nulls,
190
230
  literals, closed enums, forbidden combinations, and union ambiguity. This is the
191
231
  SDK's structural contract, not complete contextual protocol validation.
192
- `from_dict` does not supply omitted wire literals or defaults. It validates once
193
- and privately hydrates nested typed models without recursively calling public
194
- constructors. For example, a decoded HTTP connection supports
195
- `connection.gaps[0].reason`, just like a constructed connection. Union selection
196
- reuses memoized descriptor checks from that decode; subtrees are not repeatedly
197
- validated during hydration. Extension/application values remain ordinary JSON
198
- values. Descriptors and hydration metadata are cached.
232
+ `from_dict` checks complete wire values and preserves optional-field presence. Decoded known nested values expose the same attributes as constructed values, such as `connection.gaps[0].reason`. Extension and application bags remain intact and do not acquire effect or attachment authority.
199
233
 
200
234
  Invalid wire models raise `ValueError`; validation failures carry `result`,
201
235
  `diagnostics`, and `raw` attributes. Use `parse_*` when you need the full result on
@@ -213,12 +247,7 @@ objects are validated at the dispatch/parse boundary. Direct dictionary
213
247
  mutation, standard-library `json.loads`, TypedDict annotations, and type assertions
214
248
  are not SDK validation entrypoints. Re-parse after manually mutating a mapping.
215
249
 
216
- Wire constructors also privately hydrate nested mappings: for example,
217
- `capability.Capabilities(effects=["modify"], modify={}).modify.input` returns
218
- `None`, not an attribute error. Already typed child models retain their identity;
219
- plain nested mappings become typed models after the constructor's single cached
220
- validation pass. Family queries include all generated per-occurrence capability
221
- models, such as `capability.ToolBeforeCapabilities`.
250
+ Nested model attributes expose declared protocol fields. For example, `capability.Capabilities(effects=["modify"], modify={}).modify.input` is `None` when that grant is absent. Family queries are available on generic and per-occurrence capability models.
222
251
 
223
252
  Constructor aliases cannot silently overwrite wire keys. Supplying both
224
253
  `tool_name=` and `toolName=` raises `TypeError`, even if the values agree. For
@@ -230,48 +259,65 @@ keys such as `toolName` and `tool_name` remain distinct wire/extension keys.
230
259
 
231
260
  ### Owned attachments
232
261
 
233
- The owned attachment APIs in this section are not included in the published 0.1.1 package.
234
-
235
262
  Use `Attachment.from_bytes(data)` for immutable Python `bytes`, or
236
- `Attachment.lazy(async_loader, aclose=async_cleanup)` to defer reading. Bind it
237
- with the typed input's `bind_*_source` method on an item without a body reference.
238
- Content IDs, media types, and other metadata belong on the content item. See the
239
- runnable [file attachment example](examples/file_attachment.py).
240
-
241
- Dispatch transfers ownership once. Metadata-only and unmatched deliveries do not
242
- invoke the loader. Selected body deliveries share one immutable snapshot, with
243
- receiver-specific references. Successful results own the bytes **or still-unread
244
- loader**, independent of `Hooks` shutdown. Use `async with result:` or
245
- `await result.aclose()` and read with
246
- `await result.attachments.read("context.compact.before.items[0]")`. Cleanup runs
247
- for unopened sources too. Do not reuse an attachment across invocations; create a
248
- new attachment (which can share the same immutable `bytes`) instead.
249
-
250
- The default accepted size is 4 MiB and lazy loading timeout is 30 seconds;
251
- constructors accept `max_bytes` and lazy construction accepts `timeout`. Loaders
252
- must bound their own allocations. `Attachment(async_stream, ...)` also supports
253
- the bounded `read(size)`/`receive(max_bytes)` source contract. Stream cleanup
254
- is shielded and bounded to one second. Cancellation and failure never retry a
255
- loader. Python mutable buffers are rejected, and reads
256
- return immutable bytes.
257
-
258
- Remote body delivery requires an authorized receiver uploader through
259
- `uploads={backend_id: async_upload}`. The uploader supplies a reference that the
260
- receiver can read.
261
- Metadata-only delivery and result reads require no uploader.
262
-
263
- Attachments own their immutable bytes directly. A lazy loader's `bytes` object
264
- becomes the attachment snapshot; upload and result reads borrow that same object.
265
- Receiver applications manage upload storage.
266
-
267
- Compaction text and elicitation JSON effects read selected attachment
268
- owners. An accepted replacement creates a new effective attachment; subsequent
269
- deliveries and the result use that owner. Original attachment size bounds also
270
- apply to replacements. Generated candidates are available through
271
- `await result.attachments.read("candidate")`. Binary/file editing is not supported.
272
- Elicitation results require their original request correlation context;
273
- `ContentContext.attachments` can provide the original request owner without a
274
- resolver. A resolver is an optional
275
- adapter for externally supplied references.
276
-
263
+ `Attachment.lazy(async_loader, aclose=async_cleanup)` to defer reading. Put
264
+ `attachment` directly in an attachment part's `body`. Named `Hooks` methods
265
+ accept both dictionary inputs and typed event inputs with the same owner.
266
+ For a custom `OwnedContentSource`, use the host-only `content.OwnedAttachment(source)` wrapper in an attachment part's body. This wrapper does not read the source and is not a wire value. The SDK transfers and closes its source under the same invocation ownership rules.
267
+
268
+ Metadata belongs on the part. See the runnable
269
+ [file attachment example](examples/file_attachment.py).
270
+
271
+ Dispatch transfers ownership once. Successful results retain the exact owner,
272
+ including an unread loader, independently of `Hooks` shutdown. Callers close
273
+ returned content owners with `async with result:` or `await result.aclose()`.
274
+ Cleanup runs for unopened sources too. Create a new attachment for each
275
+ invocation; eager attachments can share the same immutable `bytes` object.
276
+
277
+ The default accepted size is 4 MiB and lazy loading timeout is 30 seconds.
278
+ Constructors accept `max_bytes`; lazy construction accepts `timeout`. Loaders
279
+ must bound their own allocations. `Attachment(async_stream, ...)` supports
280
+ bounded `read(size)`/`receive(max_bytes)` streams. Stream cleanup is shielded and
281
+ bounded to one second. Cancellation and failure never retry a loader. Mutable
282
+ buffers are rejected, and reads return immutable bytes.
283
+
284
+ Selected remote binary delivery requires an authorized uploader through
285
+ `uploads={backend_id: async_upload}`. Each uploader receives the exact owner's
286
+ immutable bytes and returns a committed receipt with `ref`, `size`, and `sha256`.
287
+ The SDK validates the receipt against the actual bytes before sending the event.
288
+ Upload callbacks remain caller-authorized and scoped to their backend; event
289
+ credentials do not authorize uploads to another destination.
290
+
291
+ `Hooks(..., max_concurrent_uploads=8)` bounds concurrent binary uploads.
292
+ The value must be a positive integer. Before serial interception
293
+ begins, the SDK prepares uploads for body-selected attachment slots in all
294
+ matched subscriptions, including observers. One attachment owner is read once
295
+ for multiple subscribers. Metadata, omit, and unmatched selections do not read
296
+ or upload its bytes. Result reads and metadata-only delivery require no uploader.
297
+
298
+ Only confirmed receipts are reused, for the exact attachment owner and backend
299
+ ID/subscription index. An uncalled interceptor receives a settled observation
300
+ with its confirmed reference after denial or stop. Upload failures are handled by
301
+ the affected subscription's failure policy when its delivery is reached; they
302
+ do not prevent independent destinations from preparing their uploads. An upload
303
+ can commit even if an earlier interceptor later removes the attachment or halts
304
+ the chain. Receiver applications manage separate storage durability and retention.
305
+ Cancellation cancels and joins pending upload work and closes invocation-owned
306
+ sources. Overlapping reads join one materialization and receive the same immutable
307
+ bytes or terminal error. Cancelling one reader leaves materialization active for
308
+ other readers; the last cancelled reader or owner closure cancels and joins it.
309
+ The attachment owns its single pending materialization independently of readers.
310
+ A cancelled reader returns and releases its upload permit while another reader
311
+ continues. The last reader or owner closure joins cleanup; completed workers are
312
+ not retained.
313
+
314
+ Attachments are the sole byte owners. Binary/file editing is not supported.
277
315
  Results are invocation-local, not session archives.
316
+
317
+ ### Advanced validation and dispatch
318
+
319
+ `runtime.Validator` validates bundled canonical schemas and structural codecs. It does not establish request correlation, effect grants, host approval, or permission to execute. Use `Hooks` for complete interception admission.
320
+
321
+ `server.hooks.Engine` is the framework-neutral backend dispatcher. Supply an explicit validator or static harness manifest when needed. Engine callbacks return protocol effects; the receiving harness validates authority and settles them atomically.
322
+
323
+ Host session counters (`turns`, `modelRequests`, `toolCalls`, `inputTokens`, `outputTokens`, and extra counter names) are exact nonnegative integers. The SDK preserves supplied values and does not infer counters from events.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agenthooksprotocol"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Official non-normative Python SDK for the Agent Hooks Protocol"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"