jaato-sdk 0.16.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 (89) hide show
  1. jaato_sdk-0.16.0/PKG-INFO +783 -0
  2. jaato_sdk-0.16.0/PKG_README.md +758 -0
  3. jaato_sdk-0.16.0/README.md +597 -0
  4. jaato_sdk-0.16.0/jaato/__init__.py +148 -0
  5. jaato_sdk-0.16.0/jaato/_session.py +115 -0
  6. jaato_sdk-0.16.0/jaato_sdk/__init__.py +115 -0
  7. jaato_sdk-0.16.0/jaato_sdk/cascade_authoring.py +187 -0
  8. jaato_sdk-0.16.0/jaato_sdk/client/__init__.py +18 -0
  9. jaato_sdk-0.16.0/jaato_sdk/client/_event_stream.py +73 -0
  10. jaato_sdk-0.16.0/jaato_sdk/client/_handler_registry.py +176 -0
  11. jaato_sdk-0.16.0/jaato_sdk/client/_wake_client.py +103 -0
  12. jaato_sdk-0.16.0/jaato_sdk/client/config.py +415 -0
  13. jaato_sdk-0.16.0/jaato_sdk/client/convenience.py +689 -0
  14. jaato_sdk-0.16.0/jaato_sdk/client/errors.py +152 -0
  15. jaato_sdk-0.16.0/jaato_sdk/client/ipc.py +2930 -0
  16. jaato_sdk-0.16.0/jaato_sdk/client/recovery.py +1572 -0
  17. jaato_sdk-0.16.0/jaato_sdk/client/ws.py +382 -0
  18. jaato_sdk-0.16.0/jaato_sdk/completion_processors.py +271 -0
  19. jaato_sdk-0.16.0/jaato_sdk/conformance/__init__.py +53 -0
  20. jaato_sdk-0.16.0/jaato_sdk/conformance/conftest.py +118 -0
  21. jaato_sdk-0.16.0/jaato_sdk/conformance/daemon.py +294 -0
  22. jaato_sdk-0.16.0/jaato_sdk/conformance/test_invariants.py +399 -0
  23. jaato_sdk-0.16.0/jaato_sdk/constants.py +7 -0
  24. jaato_sdk-0.16.0/jaato_sdk/doctor.py +1032 -0
  25. jaato_sdk-0.16.0/jaato_sdk/event_bus.py +274 -0
  26. jaato_sdk-0.16.0/jaato_sdk/event_payloads.py +510 -0
  27. jaato_sdk-0.16.0/jaato_sdk/events.py +3171 -0
  28. jaato_sdk-0.16.0/jaato_sdk/helpers.py +167 -0
  29. jaato_sdk-0.16.0/jaato_sdk/media_identity.py +223 -0
  30. jaato_sdk-0.16.0/jaato_sdk/path_boundary.py +123 -0
  31. jaato_sdk-0.16.0/jaato_sdk/plugins/__init__.py +5 -0
  32. jaato_sdk-0.16.0/jaato_sdk/plugins/base.py +1169 -0
  33. jaato_sdk-0.16.0/jaato_sdk/plugins/model_provider/__init__.py +56 -0
  34. jaato_sdk-0.16.0/jaato_sdk/plugins/model_provider/types.py +1856 -0
  35. jaato_sdk-0.16.0/jaato_sdk/plugins/todo/__init__.py +37 -0
  36. jaato_sdk-0.16.0/jaato_sdk/plugins/todo/channels.py +257 -0
  37. jaato_sdk-0.16.0/jaato_sdk/plugins/todo/models.py +769 -0
  38. jaato_sdk-0.16.0/jaato_sdk/templates.py +95 -0
  39. jaato_sdk-0.16.0/jaato_sdk/tests/__init__.py +1 -0
  40. jaato_sdk-0.16.0/jaato_sdk/tests/test_cascade_authoring.py +98 -0
  41. jaato_sdk-0.16.0/jaato_sdk/tests/test_cascade_events.py +316 -0
  42. jaato_sdk-0.16.0/jaato_sdk/tests/test_classify_evaluation_kind.py +206 -0
  43. jaato_sdk-0.16.0/jaato_sdk/tests/test_client_resilience.py +74 -0
  44. jaato_sdk-0.16.0/jaato_sdk/tests/test_completion_processors.py +99 -0
  45. jaato_sdk-0.16.0/jaato_sdk/tests/test_convenience.py +615 -0
  46. jaato_sdk-0.16.0/jaato_sdk/tests/test_convenience_media_sink.py +117 -0
  47. jaato_sdk-0.16.0/jaato_sdk/tests/test_create_session_polymorphic.py +238 -0
  48. jaato_sdk-0.16.0/jaato_sdk/tests/test_create_session_says_why.py +246 -0
  49. jaato_sdk-0.16.0/jaato_sdk/tests/test_dispatch_precedes_the_typed_failure.py +175 -0
  50. jaato_sdk-0.16.0/jaato_sdk/tests/test_doctor.py +121 -0
  51. jaato_sdk-0.16.0/jaato_sdk/tests/test_doctor_driver_check.py +173 -0
  52. jaato_sdk-0.16.0/jaato_sdk/tests/test_doctor_secret_scrub.py +60 -0
  53. jaato_sdk-0.16.0/jaato_sdk/tests/test_doctor_session.py +78 -0
  54. jaato_sdk-0.16.0/jaato_sdk/tests/test_drain_task_lifecycle.py +270 -0
  55. jaato_sdk-0.16.0/jaato_sdk/tests/test_event_type_filter_names.py +128 -0
  56. jaato_sdk-0.16.0/jaato_sdk/tests/test_events_wire_format.py +231 -0
  57. jaato_sdk-0.16.0/jaato_sdk/tests/test_facade_sdk_only.py +113 -0
  58. jaato_sdk-0.16.0/jaato_sdk/tests/test_helpers.py +129 -0
  59. jaato_sdk-0.16.0/jaato_sdk/tests/test_one_pairing_rule.py +245 -0
  60. jaato_sdk-0.16.0/jaato_sdk/tests/test_open_event_stream.py +127 -0
  61. jaato_sdk-0.16.0/jaato_sdk/tests/test_profile_summary.py +141 -0
  62. jaato_sdk-0.16.0/jaato_sdk/tests/test_protocol_compat.py +110 -0
  63. jaato_sdk-0.16.0/jaato_sdk/tests/test_recovery_background_dispatch.py +89 -0
  64. jaato_sdk-0.16.0/jaato_sdk/tests/test_reliability_events.py +42 -0
  65. jaato_sdk-0.16.0/jaato_sdk/tests/test_sdk_parity_methods.py +457 -0
  66. jaato_sdk-0.16.0/jaato_sdk/tests/test_subscribe_api.py +284 -0
  67. jaato_sdk-0.16.0/jaato_sdk/tests/test_subscribe_load.py +162 -0
  68. jaato_sdk-0.16.0/jaato_sdk/tests/test_tool_result_is_error.py +82 -0
  69. jaato_sdk-0.16.0/jaato_sdk/tests/test_tool_result_status.py +93 -0
  70. jaato_sdk-0.16.0/jaato_sdk/tests/test_truncation_reason.py +155 -0
  71. jaato_sdk-0.16.0/jaato_sdk/tests/test_turn_timeout.py +116 -0
  72. jaato_sdk-0.16.0/jaato_sdk/tests/test_wake_client_methods.py +125 -0
  73. jaato_sdk-0.16.0/jaato_sdk/tests/test_ws_client.py +288 -0
  74. jaato_sdk-0.16.0/jaato_sdk/trace.py +226 -0
  75. jaato_sdk-0.16.0/jaato_sdk.egg-info/PKG-INFO +783 -0
  76. jaato_sdk-0.16.0/jaato_sdk.egg-info/SOURCES.txt +87 -0
  77. jaato_sdk-0.16.0/jaato_sdk.egg-info/dependency_links.txt +1 -0
  78. jaato_sdk-0.16.0/jaato_sdk.egg-info/entry_points.txt +2 -0
  79. jaato_sdk-0.16.0/jaato_sdk.egg-info/requires.txt +9 -0
  80. jaato_sdk-0.16.0/jaato_sdk.egg-info/top_level.txt +2 -0
  81. jaato_sdk-0.16.0/pyproject.toml +62 -0
  82. jaato_sdk-0.16.0/setup.cfg +4 -0
  83. jaato_sdk-0.16.0/tests/test_doctor_reuse_and_reactors.py +62 -0
  84. jaato_sdk-0.16.0/tests/test_presentation_override.py +42 -0
  85. jaato_sdk-0.16.0/tests/test_recovery_client_parity.py +212 -0
  86. jaato_sdk-0.16.0/tests/test_recovery_create_session.py +75 -0
  87. jaato_sdk-0.16.0/tests/test_session_new_correlation.py +138 -0
  88. jaato_sdk-0.16.0/tests/test_session_restored_deserialize.py +27 -0
  89. jaato_sdk-0.16.0/tests/test_sibling_name_reaches_the_wire.py +122 -0
@@ -0,0 +1,783 @@
1
+ Metadata-Version: 2.4
2
+ Name: jaato-sdk
3
+ Version: 0.16.0
4
+ Summary: Jaato SDK - Protocol and client library for jaato server
5
+ Author: apanoia
6
+ License-Expression: BUSL-1.1
7
+ Project-URL: Repository, https://github.com/Jaato-framework-and-examples/jaato
8
+ Project-URL: Issues, https://github.com/Jaato-framework-and-examples/jaato/issues
9
+ Keywords: jaato,sdk,ipc,client
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: python-dotenv
19
+ Requires-Dist: pydantic<3,>=2.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: pytest-asyncio; extra == "dev"
23
+ Provides-Extra: ws
24
+ Requires-Dist: websockets>=12.0; extra == "ws"
25
+
26
+ # Changelog
27
+
28
+ ## 0.16.0 (2026-09-09)
29
+
30
+ - ci: publish to PyPI, and add jaato-eval to the release set (#923)
31
+ - fix(enrichment): every dict tool result reaches the enrichment chain (#922) (#924)
32
+ - fix(session): record signal_completion's tool result so a completed session stays revivable (#913) (#915)
33
+ - feat(resume): both resume verbs carry an attachment (#845) (#914)
34
+ - feat(web): replace the web-client PoC with jaato-web, a browser client on @jaato/sdk
35
+ - fix(runner): carry slot-scoped plugin instances across cascade sessions (#890) (#891)
36
+ - fix(service_connector): mark call_service results untrusted (#857) (#878)
37
+ - feat(permission): the resolved event, the ledger and the session record name who decided (#859) (#876)
38
+ - feat(security): scrub secret env vars from model-driven subprocesses by default (#863) (#872)
39
+ - fix(media): the spoken transcript rides the final media chunk to the client (#869) (#871)
40
+ - fix(gc): evict consumed inbound media, and let GC see media at all (#850) (#853)
41
+ - feat(scaffold): the sweep gate as one set, and make its ceiling actually work (#768 #769 #770 #772) (#849)
42
+ - fix(scaffold): the templates take their turn from the SDK facade (#820 #821 #825 #826 #827 #822)
43
+ - feat(media): deliver binary media chunks, and know when a spoken turn ended (#824)
44
+ - chore: normalise line endings to LF, once, in one commit (#794) (#807)
45
+ - feat(events): carry the billed prompt/output split on the wire (#802) (#803)
46
+ - fix(providers): a stream that dies mid-response is not a finished turn (#687) (#786)
47
+ - fix(tui,clarification): answer a batched clarification instead of hanging (#704) (#783)
48
+ - fix(sdk,session): a turn boundary is not the session's terminus (#767) (#774)
49
+ - fix(providers): one prompt-token convention, converted at the seam (#758) (#763)
50
+ - fix(session): a turn cut off at the output cap must be continued, not lost (#749) (#759)
51
+ - fix(session): a call the severed turn never ran must still be answered (#751) (#757)
52
+ - fix(providers): unreadable tool-call arguments must not become a call (#750) (#753)
53
+ - fix(providers): a truncated turn must not report as a tool-use turn (#745) (#747)
54
+ - fix(daemon): refuse a relative path at the process boundary (#742) (#744)
55
+ - fix(cache): model tiers × prompt caching — the knobs never arrived, and the cost was invisible (#737)
56
+ - feat(plugins): declare the session-persistence contract, and use it for permissions (#708)
57
+ - fix(memory): list_memory_tags reported an empty store while holding a raw queue (#664)
58
+ - ci: run the SDK's package-internal tests, and refreeze the 102 baselines nobody was running (#661)
59
+ - ci: cyclomatic-complexity ratchet, fixed so it cannot silently skip (#656)
60
+ - feat(events): a stage that was asked twice to signal completion, and never did, says so (#654)
61
+ - feat(doctor): point at the generator, and spot a driver that has gone stale (#651)
62
+ - feat(scaffold): a `sweep` archetype for N independent arms (#649)
63
+ - feat(sdk): the completeness rule ships as a function, not as advice (#648)
64
+ - feat(sdk): a live conformance suite, run against a real daemon in CI (#646)
65
+ - feat(sdk): one pairing rule, in the layer consumers can import (#640)
66
+ - fix(sdk): the breaking change needs a version that says so (#636)
67
+ - feat(sdk)!: create_session says which failure happened (#635)
68
+ - fix(delivery): unreachable was five outcomes wearing one word (#634)
69
+ - fix(inject): an inject into an idle session was a black hole, and said "ok" (#619)
70
+ - fix(executor): two 2-tuple conventions shared one representation (#608)
71
+ - feat(protocol): every event says which session it is about (#603)
72
+ - feat(protocol): correlate session.new with the event that answers it (#598)
73
+ - fix(sdk): sibling_name reaches the facade — and peer→sibling for the coordination surface (#593)
74
+ - feat(queue): SourceType.PEER, idle-only, and tier membership declared once (#590)
75
+ - feat(gc): typed GC lifecycle events on the bus (#587)
76
+ - fix(sdk): IPCRecoveryClient accepts config_root/apparmor; parity test covers ctor args (#585)
77
+ - fix(budget): surface a ceiling refusal as a terminal, typed event (#584)
78
+ - IPCRecoveryClient: catch up to IPCClient, and a test so it cannot drift again
79
+ - Cascade refusal must reach the client, not just the daemon log
80
+ - IPC verbs for cascade budgets: set / get / clear
81
+ - Thread spend_total_tokens to the client (UsageBreakdown)
82
+ - Fix: envelope producer never populated budget_control (feature was dead)
83
+ - feat(session): surface abnormal finish reasons to clients
84
+ - HistoryEvent: accept the runner's rich turn_accounting (relax Dict[str,int]→Any)
85
+ - refactor(facade): ship the `jaato` convenience facade in jaato-sdk (sdk-only client) (#527)
86
+ - test(events): add missing wire-format baselines for session.restored/woken/wake_bind_result (#528)
87
+ - feat(sdk): typed client wake methods — bind_wake / unbind_wake / cascade_register (#525)
88
+ - feat(sdk): public open_event_stream() — synchronous-subscribe event iterator (#524)
89
+ - fix(wake): headless client-tool dispatch — whitelist sync + drive-after-wiring (#521)
90
+ - feat(server): durable deferred-turn wake (Option 2 — cold-revive-and-act) (#520)
91
+ - feat(server): surface daemon wake endpoint on bind_wake (production routing) (#519)
92
+ - feat(server): wake binding registry + bind_wake/unbind_wake (PR 2a, mode-B foundation) (#517)
93
+ - security: untrusted-content trust boundary for web_fetch/web_search/MCP (#495)
94
+ - Security hardening + per-session egress confinement (proxy + cgroup-nft) (#492)
95
+ - fix(tool-results): keep result STRUCTURED; move model-facing steering to ToolResult.model_suffix (#490)
96
+ - fix(sdk): register session.restored in the deserializer dispatch table (#467)
97
+ - docs: resync README + web docs to WS-recovery / TLS / scaffold / presentation surface (#466)
98
+ - feat(sdk): presentation= override + recovery host-tool/batch proxies (Telegram WS migration) (#465)
99
+ - fix(sdk): IPCRecoveryClient.create_session accepts timeout — drop-in parity with IPCClient (#397)
100
+ - feat(sdk): ssl=/ca= on WSClient + WSRecoveryClient for wss:// (self-signed / dev CAs) (#462)
101
+ - feat(sdk): WSRecoveryClient — auto-reconnect for a remote WS daemon (#459)
102
+ - docs+scaffold: multi-transport (in-process / IPC / WebSocket) (#445)
103
+ - feat(sdk): WSClient — WebSocket transport for the facade (remote daemon) (#444)
104
+ - fix(sdk): facade never deadlocks on a raising on_permission callback (#434)
105
+ - docs(sdk): complete the convenience-facade docstring — client-agnostic (#432)
106
+ - fix(sdk): IPCRecoveryClient background event pump (facade-over-recovery hang) + workspace_path str coerce
107
+ - feat(sdk): Session.client — public accessor for mixing facade + low-level
108
+ - feat(sdk): facade gaps — config_root/apparmor, per-turn parallel_tools/attachments
109
+ - feat(sdk): client_tools= on IPCClient.session — host tools via the facade
110
+ - feat(sdk): convenience facade Phase 2 — Session.stream() + IPCRecoveryClient.session()
111
+ - test(sdk): refresh stale gate.released wire baseline (session_id field)
112
+ - feat(sdk): high-level convenience facade (IPCClient.session / ask / complete)
113
+ - feat(events): bridge gate.released onto the reactor event bus
114
+ - feat(doctor): stale premium-reactor detection + reuse-vs-fresh advisory
115
+ - feat(multimodal): ferry user-message attachments to the runner-tier model
116
+ - feat(client-tools): IPC transport support + SDK register_client_tools + host-tools scaffold
117
+ - feat(tooling): runtime-entity debugging — explain runtime + doctor --session
118
+ - feat(tooling): WebSocket awareness in doctor, scaffold, and the client skill
119
+ - feat(cli): jaato-doctor console script + document both dev-tool shortcuts
120
+ - test(sdk): regenerate event wire-format baselines after additive field changes
121
+ - fix(sdk): client connection resilience — env_file=None, cold-start timeout, stale-pidfile
122
+ - feat(sdk): doctor daemon-env report drives off the introspected read-set
123
+ - feat(sdk): doctor reports the JAATO_* env vars the daemon was fired with
124
+ - feat(sdk): client doctor + jaato-scaffold tool + provider knob/quirk contract
125
+ - feat(reliability): compute is_error_result on tool.call_completed
126
+ - feat(sdk): reliability reactor event types (emit substrate)
127
+ - fix(sdk): add session_id to AgentCreatedPayload to match the server event
128
+ - feat(events): SlotSettledEvent.terminal_reason — fixes slot.settled-vs-recovery double-advance
129
+ - feat(events): AgentErrorEvent + on_agent_error recovery contract
130
+ - feat(result_grep): model-directed tool-result grep filtering + greppable_content trait
131
+ - feat(events): SlotSettledEvent — universal per-cascade-stage handoff signal (replaces SlotReusableEvent)
132
+ - feat(events): add agent_id (stage name) to SlotReusableEvent payload
133
+ - feat(events): SlotReusableEvent — emit when a pool slot is reusable (cascade warm-reuse)
134
+ - feat(lifecycle): processor-gated is_complete + server-side auto-finalize
135
+ - fix(sdk): create_session surfaces all ErrorEvents, not only non-recoverable
136
+ - feat(sdk+core): expose ToolResult.enrichment_metadata on the tool-call ledger
137
+ - feat(core): cascade.cancel(cid) IPC verb + reactor-suppression predicate
138
+ - feat(sdk): add session_id field to AgentCreatedEvent (#205)
139
+ - fix(sdk): cascade_events filters by event_type client-side (#191)
140
+ - fix(bus): bridge SessionTerminatedEvent to EventBus (Bug C) (#189)
141
+ - feat(processor): ProcessorResult TypedDict + jaato_sdk.cascade_authoring umbrella (#187)
142
+ - feat(events): carry error context onto SessionTerminatedEvent (Q2) (#186)
143
+ - feat(sdk): expose ToolCallEntry TypedDict for completion processors (#184)
144
+ - feat(cascade-as-client): Phase 2 — SDK IPC RPC verbs + disconnect cleanup (#180)
145
+ - fix(sdk): plumb cascade_driver_id kwarg through IPCClient.create_session (#170)
146
+ - feat(cascade-sharing): Phase 1a — reset_for_next_session protocol + 8-plugin audit (#160)
147
+ - feat(apparmor): plugin-contribution hook (Phase 0, template v20)
148
+ - session_manager: defer-and-flush foundation for disk-restored sessions (Phase 3 §3.12 + peer-review M5/N1)
149
+ - sdk 0.13.0: replace single-reader gate with drain-task + subscriber queues
150
+ - sdk 0.12.0: client_type is mandatory on IPCClient and IPCRecoveryClient
151
+ - sdk 0.11.0: relocate classify_template_evaluation_kind to SDK boundary
152
+ - server 0.6.27 + sdk 0.10.0: SessionTerminatedEvent + cancellation-aware end_session
153
+ - sdk 0.9.0: minor bump for AppArmor dispatch + bridged-payload alignment
154
+ - server 0.6.1: AppArmor dispatch + subagent profile resolution + completion-nudge guard (#49)
155
+ - sdks: align bridged payloads + tests with gap 1-5 wire shapes
156
+ - release: coordinated bump for gap 1-5 SDK release
157
+ - sdks: protocol-version compat (gap 5)
158
+ - sdks: subscribe API, profile picker, inline session spec, cost/usage refactor
159
+ - SDK + TUI parity: end_session / delete_session typed methods
160
+ - SDK parity: complete TS surface + tool-execute-result on both sides
161
+ - SDK helpers: compute_cache_hit_percent + TUI adoption
162
+ - Phase 1: SDK feature parity — typed WS verbs + JaatoClient methods
163
+ - Phase 0: Migrate jaato-sdk events.py from @dataclass to pydantic
164
+ - Document WebSocket transport in jaato-sdk README
165
+ - Expand jaato-sdk README with protocol and client reference
166
+ - SDK file staging: binary-framed StageFilesRequest for WS clients
167
+ - signal_completion: typed payloads from profile-declared schema
168
+ - Permission timeout configurable per-client via ClientConfigRequest
169
+ - Bundle tool_id_mappings in SessionInfoEvent for guaranteed delivery
170
+ - ToolIdRegistryEvent: server pushes ID→name mapping to clients
171
+ - WS: read category descriptions from tools.register_client message
172
+ - Fork-replay primitives: 5 general-purpose capabilities for session manipulation
173
+ - Confine tool execution by default; opt out via TRAIT_FRAMEWORK_LEVEL
174
+ - Fix: resolve relative trace paths against JAATO_WORKSPACE_ROOT
175
+ - Include comment in PermissionResolvedEvent for WS clients
176
+ - Migrate plugin config schemas from PluginSetting to JSON Schema
177
+ - Add introspectable plugin settings via PluginSetting and get_config_schema()
178
+ - Complete agent/profile split: SDK, TUI, and deprecation warning
179
+ - Remove icon and icon_name from profiles
180
+ - Emit events.subscribed notification for external event subscriptions
181
+ - Add event.external WS handler for client-to-agent event injection
182
+ - Add error field to AgentCompletedPayload
183
+ - Include cancellation reason in [Generation cancelled] messages
184
+
185
+ ---
186
+
187
+ # jaato-sdk
188
+
189
+ Python client SDK for connecting to a [jaato](https://github.com/Jaato-framework-and-examples/jaato) server. Provides the wire protocol, async IPC client, and an auto-reconnecting recovery client.
190
+
191
+ ## Installation
192
+
193
+ ```bash
194
+ pip install jaato-sdk
195
+ ```
196
+
197
+ ## Quick Start
198
+
199
+ The simplest path is the **convenience facade** — `jaato.session(mode=...)` + `Session.ask` / `.complete` / `.stream`. The same code runs the agent embedded in your process, against a local daemon, or against a remote one — flip `mode` (see [Transports](#transports--three-ways-to-run-the-same-agent)):
200
+
201
+ ```python
202
+ import asyncio
203
+ import jaato
204
+
205
+ async def main():
206
+ async with jaato.session(mode="in_process",
207
+ profile={"model": "...", "provider": "..."}) as s:
208
+ print(await s.ask("Hello!"))
209
+ async for chunk in s.stream("Tell me a story."):
210
+ print(chunk, end="", flush=True)
211
+
212
+ asyncio.run(main())
213
+ ```
214
+
215
+ For full control over the event stream, permissions, and the connection lifecycle, use a client directly:
216
+
217
+ ```python
218
+ import asyncio
219
+ from jaato_sdk import IPCRecoveryClient, EventType
220
+
221
+ async def main():
222
+ client = IPCRecoveryClient() # default: /tmp/jaato.sock (Windows: \\.\pipe\jaato)
223
+
224
+ # Typed event handlers — register before connect() to capture
225
+ # the inaugural ConnectedEvent.
226
+ client.subscribe(EventType.AGENT_OUTPUT, lambda e: print(e.text, end=""))
227
+ client.subscribe(EventType.TOOL_CALL_START, lambda e: print(f"\n[tool: {e.tool_name}]"))
228
+
229
+ await client.connect()
230
+ await client.create_session()
231
+ await client.send_message("Hello!")
232
+
233
+ # Drive the event loop so the dispatcher fires. Either iterate
234
+ # client.events() (legacy style) or await client.drain_events()
235
+ # to let your subscribers do the work.
236
+ await client.drain_events()
237
+
238
+ asyncio.run(main())
239
+ ```
240
+
241
+ ## Core Concepts
242
+
243
+ ### Server-first architecture
244
+
245
+ In the daemon transports (`ipc` / `ws`), the agent runs in a separate **jaato server** process; the SDK is a transport layer that ships JSON-encoded events to the server and yields them back to your code as Python dataclasses. The `in_process` transport is the embedded alternative — the agent runs *in your process* with no daemon — and exposes the **same** facade, so you can develop embedded and deploy behind a daemon (or the reverse) without changing your agent code.
246
+
247
+ ```
248
+ your code ──► IPCRecoveryClient ──► /tmp/jaato.sock ──► jaato server (agent loop)
249
+
250
+ your code ◄── IPCRecoveryClient ◄── /tmp/jaato.sock ◄──────────┘
251
+ (events)
252
+ ```
253
+
254
+ If no server is running and `auto_start=True` (the default), the client launches `python -m server --daemon` for you.
255
+
256
+ ### Transports — three ways to run the same agent
257
+
258
+ The convenience facade (`Session.ask` / `.complete` / `.stream`) runs over **three transports**. Pick one with `jaato.session(mode=...)`: the session spec and the facade are identical — `mode` is the only thing that changes.
259
+
260
+ | Mode | Transport | Client | Use when |
261
+ |---|---|---|---|
262
+ | `in_process` | none — embedded in your process | `InProcessClient` | No daemon, no socket. The agent runs *in your Python process* — lowest latency, simplest deploy. |
263
+ | `ipc` | Unix domain socket / Windows named pipe | `IPCClient` | A daemon on the **same machine** (TUI, scripts, local tooling). Auto-start, framing, multi-session. |
264
+ | `ws` | WebSocket (`ws://` / `wss://`) | `WSClient` | A **remote** daemon (and the protocol any browser / JavaScript client speaks). Bearer-authenticated. |
265
+
266
+ ```python
267
+ import jaato
268
+
269
+ # Embedded — no daemon, the agent runs in your process:
270
+ async with jaato.session(mode="in_process",
271
+ profile={"model": "...", "provider": "..."}) as s:
272
+ print(await s.ask("Hi"))
273
+
274
+ # Local daemon over a Unix socket:
275
+ async with jaato.session(mode="ipc", profile="researcher") as s:
276
+ print(await s.ask("Hi"))
277
+
278
+ # Remote daemon over WebSocket:
279
+ async with jaato.session(mode="ws", url="wss://host:8080", token="...",
280
+ profile="researcher") as s:
281
+ print(await s.ask("Hi"))
282
+ ```
283
+
284
+ The wire protocol *above* the transport is identical — the same `Event` JSON frames — so `WSClient` is `IPCClient` with only the transport swapped (WS frames self-delimit; no length prefix), and `InProcessClient` is the embedded analog that replicates in-process what the daemon does for a connected session. One example runs every way by flipping `mode`.
285
+
286
+ To start the server with WebSocket enabled:
287
+
288
+ ```bash
289
+ python -m server --ipc-socket /tmp/jaato.sock --web-socket :8080 --daemon
290
+ ```
291
+
292
+ WS clients authenticate with a bearer token (auto-generated to `~/.jaato/ws.token` on first start) sent either as `Authorization: Bearer <token>` on the upgrade request or as `?token=<token>` for browsers that can't set headers. The server stores only the SHA-256 digest and rejects bad tokens with WS close code 1008 before any session work happens. The Python `WSClient` ships in this SDK — install the optional `websockets` dependency with `pip install 'jaato-sdk[ws]'`.
293
+
294
+ ### Clients
295
+
296
+ | Client | Transport | Use when |
297
+ |---|---|---|
298
+ | `InProcessClient` | embedded (no daemon) | Run the agent in your own process — `jaato.session(mode="in_process")`. |
299
+ | `IPCClient` | Unix socket | A thin, transparent connection to a **local** daemon. No retries — if the server goes away, your iterator ends. |
300
+ | `WSClient` | WebSocket | A **remote** daemon over `ws://` / `wss://`. `IPCClient` with the transport swapped; needs the `jaato-sdk[ws]` extra. |
301
+ | `IPCRecoveryClient` | Unix socket | Automatic reconnection with exponential backoff + session reattachment. Recommended for long-running IPC apps. |
302
+ | `WSRecoveryClient` | WebSocket | Automatic reconnection + session reattachment over WebSocket — a `WSClient` subclass with the same recovery machinery (and `on_status_change`) as `IPCRecoveryClient`. Recommended for long-running remote apps; needs the `jaato-sdk[ws]` extra. |
303
+
304
+ All five expose the **same facade-client contract**, so the convenience `Session` (`ask` / `complete` / `stream`) and the transport-agnostic `jaato.session(mode=...)` entry ride on any of them. The recovery clients wrap their base transport with a state machine and a configurable retry policy; they expose the same request methods plus connection-lifecycle hooks. With the facade, pass `recovery=True` on a daemon transport to get the recovery client:
305
+
306
+ ```python
307
+ import jaato
308
+
309
+ # IPCRecoveryClient — auto-reconnect over the local socket:
310
+ async with jaato.session(mode="ipc", recovery=True, profile="researcher",
311
+ on_status_change=lambda st: print(st.state)) as s:
312
+ print(await s.ask("Long task..."))
313
+
314
+ # WSRecoveryClient — auto-reconnect over WebSocket, trusting a self-signed
315
+ # wss:// cert via a per-connection CA bundle:
316
+ async with jaato.session(mode="ws", url="wss://host:8080", token="...",
317
+ recovery=True, ca="/etc/jaato/dev-ca.pem",
318
+ on_status_change=lambda st: print(st.state)) as s:
319
+ print(await s.ask("Long task..."))
320
+ ```
321
+
322
+ `recovery=True` works on the two daemon transports (`ipc` / `ws`); `mode="in_process", recovery=True` raises `ValueError` (no daemon to reconnect to). `IPCRecoveryClient.create_session(timeout=...)` mirrors `IPCClient.create_session` for drop-in parity.
323
+
324
+ ### WS TLS (`ssl=` / `ca=`)
325
+
326
+ For a self-signed or internal `wss://` endpoint, `WSClient` / `WSRecoveryClient` (and `jaato.session(mode="ws", ...)`) accept `ssl=` (an `ssl.SSLContext`, or `True`/`False`) and `ca=` (a CA-bundle path). A `ca` path is loaded into a default verifying context; `ssl` wins if both are set. They are scoped **per connection** — loaded into the connection's `SSLContext`, never `os.environ` — so, unlike an `SSL_CERT_FILE` env hack, they cannot leak into a subprocess-restarted daemon's outbound HTTPS (the Python analog of Node's `NODE_EXTRA_CA_CERTS`).
327
+
328
+ ### Events vs requests
329
+
330
+ Everything on the wire is an `Event` dataclass.
331
+
332
+ - **Server → Client events** describe what the agent is doing: `AgentOutputEvent`, `ToolCallStartEvent`, `PermissionRequestedEvent`, `PlanUpdatedEvent`, `TurnCompletedEvent`, `ErrorEvent`, …
333
+ - **Client → Server requests** are the same `Event` shape but flow the other way: `SendMessageRequest`, `PermissionResponseRequest`, `StopRequest`, `CommandRequest`, …
334
+
335
+ You never construct request events directly in normal usage — the client provides typed methods like `send_message()`, `respond_to_permission()`, `stop()`. Construct the request dataclasses only when you need to send something the convenience methods don't cover (use `client.execute_command()` for that).
336
+
337
+ ## Event Flow
338
+
339
+ ### connect() sequence
340
+
341
+ ```
342
+ client.connect()
343
+ ├─ open socket / pipe
344
+ ├─ wait for ConnectedEvent # carries client_id + server_version
345
+ ├─ send CommandRequest(set_workspace) # client cwd
346
+ └─ send ClientConfigRequest # env file + PresentationContext
347
+ ```
348
+
349
+ After `connect()` returns `True`, the server has accepted the connection but no session is attached yet. Either call `create_session()` to spawn a new one or `attach_session(id)` to resume an existing one.
350
+
351
+ ### send_message() sequence
352
+
353
+ ```
354
+ client.send_message("Read config.json")
355
+ ├─ SendMessageRequest # → server
356
+
357
+ ├─ AgentOutputEvent {source: "model", text: "I'll read..."}
358
+ ├─ AgentOutputEvent {source: "model", text: " the file."}
359
+ ├─ ToolCallStartEvent {tool_name: "read", tool_args: {...}, call_id: "..."}
360
+ ├─ ToolOutputEvent {chunk: "..."} # if the tool streams
361
+ ├─ ToolCallEndEvent {call_id: "...", success: true}
362
+ ├─ AgentOutputEvent {source: "model", text: "The file..."}
363
+ └─ TurnCompletedEvent {usage: UsageBreakdown(...), duration_seconds: 1.5}
364
+ ```
365
+
366
+ `AgentOutputEvent.mode` is `"write"` for a new block of output and `"append"` for streaming continuation chunks. `source` is one of `"model"`, `"tool"`, `"system"`, or a plugin name.
367
+
368
+ The three usage-bearing events (`TurnCompletedEvent`, `TurnProgressEvent`, `ContextUpdatedEvent`) all carry the same `UsageBreakdown` shape — token counts, cache hits, reasoning/thinking tokens, and `cost_usd` populated when the daemon can derive it. Cost resolution: provider-reported (e.g. claude_cli) wins over pricing-table computed from `.jaato/pricing.json`; otherwise `None` (never silently zero). See [docs/sdk-pricing.md](../docs/sdk-pricing.md) for the full pricing contract.
369
+
370
+ GC configuration is its own event (`GCConfigEvent`) since v1.0 — subscribe to that for status-bar GC display rather than reading from `ContextUpdatedEvent`.
371
+
372
+ ### Permission flow
373
+
374
+ When a tool needs approval, the server pauses and emits a permission request. The client responds with one of the offered keys.
375
+
376
+ ```
377
+ ToolCallStartEvent
378
+ PermissionRequestedEvent { request_id, tool_name, tool_args, response_options, prompt_lines }
379
+ PermissionInputModeEvent { request_id } # signal: take input now
380
+
381
+ │ client.respond_to_permission(request_id, "y") # → server
382
+
383
+ PermissionResolvedEvent { request_id, response, granted }
384
+ ToolCallEndEvent { ... } # tool runs
385
+ ```
386
+
387
+ Permission response keys (returned in `response_options`):
388
+
389
+ | Key | Meaning |
390
+ |-----|---------|
391
+ | `y` | allow this tool execution |
392
+ | `n` | deny this tool execution |
393
+ | `a` / `always` | allow and whitelist the tool for this session |
394
+ | `t` / `turn` | allow remaining tool calls this turn |
395
+ | `i` / `idle` | allow until the session goes idle |
396
+ | `once` | allow once without remembering |
397
+ | `all` | allow all future requests in this session |
398
+ | `never` | deny and blacklist the tool for this session |
399
+ | `c:<text>` | deny **with feedback** the model sees as the tool result |
400
+ | `yc:<text>` | allow **with feedback** the model sees alongside the tool result |
401
+ | `e` | edit the arguments and re-prompt (pass `edited_arguments=...`); only offered when the request has editable content |
402
+
403
+ The two comment variants let you steer the model without simply rejecting the call. Pass them to `respond_to_permission` as a single string with the prefix and the text:
404
+
405
+ ```python
406
+ await client.respond_to_permission(request_id, "c:please check the file size first")
407
+ await client.respond_to_permission(request_id, "yc:ok but write the result to /tmp/audit.log")
408
+ ```
409
+
410
+ The server strips the `c:` / `yc:` prefix and forwards the comment to the model alongside the deny/allow decision. Empty text after the prefix falls back to plain `n` / `y`.
411
+
412
+ ### Cancellation
413
+
414
+ `await client.stop()` sends a `StopRequest`. The server cancels in-flight tool calls and the streaming model call; expect to see an `AgentStatusChangedEvent(status="error")` or a `TurnCompletedEvent` with cancellation metadata, then the iterator continues normally.
415
+
416
+ ## Client Options
417
+
418
+ ```python
419
+ client = IPCRecoveryClient(
420
+ socket_path="/tmp/jaato.sock", # Unix socket or Windows pipe name
421
+ config=RecoveryConfig(...), # see "Auto-reconnection" below
422
+ auto_start=True, # spawn server daemon if not running
423
+ env_file=".env", # client env forwarded to server (relative to workspace)
424
+ workspace_path=Path.cwd(), # what the server sees as the working directory
425
+ config_root=None, # optional: <path>/.jaato override for read-only config
426
+ apparmor=False, # optional: opt into per-session AppArmor confinement
427
+ on_status_change=lambda s: ..., # ConnectionStatus callback
428
+ )
429
+ ```
430
+
431
+ `IPCClient` takes the same parameters minus `config` and `on_status_change`.
432
+
433
+ ### `config_root`
434
+
435
+ When set, decouples *where the agent runs* (`workspace_path`) from *where the daemon reads its read-only framework config* — profiles, agent .md files, prompts, references, completion_schemas, instructions, scripts, services. The daemon scans `<config_root>` instead of `<workspace_path>/.jaato/`. The user-tier `~/.jaato/` is always honored.
436
+
437
+ Pair with a `workspace_path` that does **not** contain a `.jaato/` symlink to give the agent's filesystem tools no visibility into the framework config.
438
+
439
+ ### `apparmor`
440
+
441
+ Default `False`. Set to `True` to ask the daemon to confine each session created on this connection with a per-session AppArmor profile. Useful for orchestrator-driven harnesses where the LLM-driven tool plugins (`cli`, `file_edit`, `interactive_shell`) are the threat surface and a hallucinated path should be blocked at the kernel level rather than only inside the sandbox dir.
442
+
443
+ The profile grants:
444
+ - `workspace_path` — read/write
445
+ - `config_root` — read-only (when set)
446
+ - `~/.jaato/{agents,profiles,prompts,...}` — read-only
447
+ - `~/.jaato/memories` — read/write
448
+ - venv + jaato source tree — read-only
449
+
450
+ When AppArmor is unavailable on the host (non-Linux, kernel module not loaded, `apparmor_parser` missing) the session falls back to running unconfined — but **does not** fail silently. The daemon always emits a `SystemMessageEvent` to the client describing the outcome: style `"info"` with prefix `[apparmor] confinement applied (...)` when enforcement is in effect, or style `"warning"` with prefix `[apparmor] requested but ...` when it isn't (and why). Print these in your event-handling loop so the user can see at a glance whether kernel confinement is really active for the run, instead of having to tail `/tmp/jaato.log`. See [`docs/apparmor-setup.md`](../docs/apparmor-setup.md) for prerequisites.
451
+
452
+ The default remains `False` so today's TUI / IPC behavior is unchanged: a local user already has full filesystem access and confining their sessions adds friction without adding security.
453
+
454
+ ## Client State
455
+
456
+ ```python
457
+ client.is_connected # bool
458
+ client.is_reconnecting # bool (recovery client only)
459
+ client.is_closed # bool (recovery client only)
460
+ client.state # ConnectionState enum (recovery client only)
461
+ client.session_id # currently attached session, or None
462
+ client.client_id # assigned by the server on connect
463
+ client.server_version # server package version, or None on pre-0.2.28 servers
464
+ client.get_status() # → ConnectionStatus dataclass (recovery client only)
465
+ ```
466
+
467
+ `ConnectionState` values: `DISCONNECTED`, `CONNECTING`, `CONNECTED`, `RECONNECTING`, `DISCONNECTING`, `CLOSED`.
468
+
469
+ The recovery client refuses to send while reconnecting and raises `ReconnectingError`. Once `state == CLOSED` (max attempts exceeded, or `close()` called), it raises `ConnectionClosedError` and cannot be revived — construct a new instance.
470
+
471
+ ## Methods
472
+
473
+ ### Lifecycle
474
+
475
+ ```python
476
+ await client.connect(timeout=5.0)
477
+ await client.disconnect() # graceful, can be reconnected
478
+ await client.close() # permanent (recovery client only)
479
+ ```
480
+
481
+ ### Sessions
482
+
483
+ ```python
484
+ # By profile name — references .jaato/profiles/<name>.json on the server
485
+ await client.create_session(
486
+ name="my-session",
487
+ profile="researcher",
488
+ agent="reviewer",
489
+ agent_params={"focus": "security"},
490
+ )
491
+
492
+ # By inline spec — same shape as a profile JSON, no disk file needed
493
+ await client.create_session(
494
+ name="ops-task",
495
+ profile={
496
+ "model": "claude-sonnet-4-5",
497
+ "provider": "anthropic",
498
+ "plugins": ["cli", "web_search"],
499
+ "system_instructions": "You are an operations engineer.",
500
+ # Any other field a profile JSON accepts: plugin_configs, gc,
501
+ # env, max_turns, runtime_limits, model_tiers, ...
502
+ },
503
+ )
504
+
505
+ await client.attach_session(session_id)
506
+ await client.get_default_session()
507
+ await client.list_sessions() # response arrives as SessionListEvent
508
+ await client.list_profiles() # response arrives as SessionProfilesEvent
509
+ ```
510
+
511
+ #### Profile picker — SessionProfilesEvent shape
512
+
513
+ `list_profiles()` triggers a `SessionProfilesEvent` with a stable, versioned shape — pin against `schema_version` if you build a profile-picker UI:
514
+
515
+ ```python
516
+ event.schema_version # "1.0" — bumped only on breaking shape changes
517
+ event.profiles # List[ProfileSummary]
518
+ event.parse_errors # List[ProfileParseError] — broken files surface here, not in `profiles`
519
+ ```
520
+
521
+ `ProfileSummary` exposes the safe-to-display subset of a profile (full field list in `jaato_sdk/events.py`):
522
+
523
+ | Field | Purpose |
524
+ |---|---|
525
+ | `name`, `description` | identity |
526
+ | `plugins`, `preloaded_plugins`, `plugin_configs` | capabilities |
527
+ | `model`, `provider`, `max_turns`, `model_tiers` | runtime |
528
+ | `gc`, `runtime_limits`, `completion_payload_schema` | structural config (dicts, expose as-is) |
529
+ | `env_var_names` | **names only** — env values never leave the daemon |
530
+
531
+ Deliberately **not** exposed: `system_instructions` (deprecated, now lives in agents), `icon_name` (deprecated), `inherits` (resolved during discovery), env values (sensitive). Profile-author secrets should always go through `${VAR}` indirection in `env`.
532
+
533
+ #### `profile` parameter polymorphism
534
+
535
+ The `profile` parameter is polymorphic:
536
+
537
+ - **`str`** → references a profile JSON on the server's disk under `.jaato/profiles/`. Use this when an operator has curated profiles for human users.
538
+ - **`dict`** → inline spec with the same shape. Use this when you're an orchestrator with your own governance layer and don't want to depend on disk files.
539
+
540
+ The two forms are mutually exclusive — pass one or the other. The server validates inline specs and rejects them with an `ErrorEvent` if `model` is missing (no silent default fallback). `agent` and `agent_params` are independent of `profile` and compose with either form: profile decides *capabilities* (model, plugins, GC), agent decides *persona* (system instructions / personality).
541
+
542
+ `create_session()` returns the new session id when no event iterator is active; otherwise it is fire-and-forget and the id arrives via the event stream as a `SessionInfoEvent`.
543
+
544
+ ### Messages and replies
545
+
546
+ ```python
547
+ await client.send_message("Build the README", attachments=[...])
548
+ await client.respond_to_permission(request_id, "y")
549
+ await client.respond_to_permission(request_id, "e", edited_arguments={"path": "..."})
550
+ await client.respond_to_clarification(request_id, "use json")
551
+ await client.respond_to_clarification_batch(request_id, answers) # one frame for a multi-question batch
552
+ await client.respond_to_reference_selection(request_id, "1,3,4")
553
+ await client.stop()
554
+ ```
555
+
556
+ `respond_to_clarification_batch(request_id, answers)` emits a `ClarificationBatchResponseEvent` — the blessed batch form for WS / chat clients answering a whole question set at once, versus the per-question `respond_to_clarification`. `IPCRecoveryClient` also proxies `register_client_tools` (it remembers the registered tool set and re-registers on reconnect, so host tools survive a daemon restart) and `list_sessions`.
557
+
558
+ ### Commands and metadata
559
+
560
+ ```python
561
+ await client.execute_command("model", ["claude-sonnet-4-5"])
562
+ await client.request_command_list() # response: CommandListEvent
563
+ await client.request_history() # response: HistoryEvent
564
+ await client.disable_tool("bash")
565
+ ```
566
+
567
+ ### Event stream
568
+
569
+ There are two ways to consume events: typed subscriptions (recommended) or the raw async iterator. They cooperate — subscribers always fire, and the iterator yields the same events.
570
+
571
+ #### Typed subscriptions
572
+
573
+ ```python
574
+ from jaato_sdk import EventType
575
+
576
+ # One handler per type — only fires for that event type.
577
+ unsub = client.subscribe(EventType.PERMISSION_REQUESTED, on_perm)
578
+
579
+ # Fire once, then auto-unsubscribe.
580
+ unsub = client.subscribe_once(EventType.AGENT_COMPLETED, on_done)
581
+
582
+ # Catchall (every event regardless of type).
583
+ unsub = client.subscribe_all(lambda e: log(e))
584
+
585
+ # Register many in one call; unsub_all() removes them atomically.
586
+ unsub_all = client.subscribe_many({
587
+ EventType.PERMISSION_REQUESTED: on_perm,
588
+ EventType.TOOL_CALL_START: on_tool_start,
589
+ EventType.AGENT_COMPLETED: on_done,
590
+ })
591
+ ```
592
+
593
+ Handlers may be sync (`def`) or async (`async def`). Async handlers are scheduled fire-and-forget on the current event loop — order of *delivery* is FIFO, but order of *completion* is not guaranteed. Exceptions and rejections are logged and swallowed; one bad handler never breaks the stream or affects others. Subscribing during dispatch only takes effect for the next event (the handler list is snapshotted before iterating).
594
+
595
+ For the dispatcher to actually fire handlers, your code must drive the loop:
596
+
597
+ ```python
598
+ # Option A — let subscribers do all the work
599
+ await client.drain_events()
600
+
601
+ # Option B — iterate and react to specific events directly
602
+ async for event in client.events():
603
+ ...
604
+ ```
605
+
606
+ The async iterator exits cleanly on disconnect. With `IPCRecoveryClient`, it survives reconnects: events from the new connection are yielded transparently after the gap, and subscribed handlers continue firing without re-registration.
607
+
608
+ #### Migration from `set_event_callback`
609
+
610
+ The old single-callback API was removed in jaato-sdk 0.4.0 — replace it with `subscribe_all`:
611
+
612
+ ```python
613
+ # before
614
+ client.set_event_callback(handle)
615
+ await client.receive_events()
616
+
617
+ # after
618
+ client.subscribe_all(handle)
619
+ await client.drain_events()
620
+ ```
621
+
622
+ ## Auto-reconnection
623
+
624
+ `IPCRecoveryClient` retries with exponential backoff plus jitter and reattaches to the previous session on success.
625
+
626
+ ```python
627
+ from jaato_sdk import RecoveryConfig
628
+
629
+ config = RecoveryConfig(
630
+ enabled=True,
631
+ max_attempts=10,
632
+ base_delay=1.0, # seconds
633
+ max_delay=60.0,
634
+ jitter_factor=0.3, # ±30% random jitter
635
+ connection_timeout=5.0,
636
+ reattach_session=True, # call attach_session() with the previous id after reconnect
637
+ )
638
+ ```
639
+
640
+ Status callback:
641
+
642
+ ```python
643
+ from jaato_sdk import ConnectionState
644
+
645
+ def on_status(status):
646
+ if status.state == ConnectionState.RECONNECTING:
647
+ print(f"reconnecting {status.attempt}/{status.max_attempts} "
648
+ f"in {status.next_retry_in:.1f}s ({status.last_error})")
649
+
650
+ client = IPCRecoveryClient(on_status_change=on_status)
651
+ ```
652
+
653
+ The recovery loop classifies errors as **transient** (retried — `ConnectionRefusedError`, `ConnectionResetError`, timeouts) or **permanent** (not retried — `IncompatibleServerError`, `FileNotFoundError`, permission/auth failures). Permanent errors transition straight to `CLOSED`.
654
+
655
+ ### Protocol version mismatch
656
+
657
+ Each client pins a minimum **wire-protocol** version (`MIN_PROTOCOL_VERSION = "1.0"` on `IPCClient`, overridable per-instance via the `min_protocol_version=` constructor arg). On `connect()` the SDK reads `ConnectedEvent.protocol_version` from the daemon and runs a semver-flavoured compat check:
658
+
659
+ - Server major must equal client major (otherwise wire shapes are incompatible)
660
+ - Server minor must be ≥ client's required minor (otherwise daemon is missing fields the client expects)
661
+ - Server with newer minor is fine — additive optional fields the client will ignore
662
+
663
+ Mismatch raises `IncompatibleServerError` carrying both `server_protocol` and `min_protocol`, with a hint in the message about *why* (major mismatch vs missing minor). The recovery client classifies it as permanent — no retries. The daemon's package version (`server_version`) is reported for diagnostics but **not** used for the compat check; pin against `protocol_version` so a daemon bug-fix release doesn't require every client to re-pin.
664
+
665
+ See [docs/sdk-protocol-versioning.md](../docs/sdk-protocol-versioning.md) for the bump policy and the CHANGELOG of past wire versions.
666
+
667
+ ## Configuration
668
+
669
+ The recovery client picks up settings from these places, highest precedence first:
670
+
671
+ 1. Environment variables
672
+ 2. `<workspace>/.jaato/client.json`
673
+ 3. `~/.jaato/client.json`
674
+ 4. Built-in defaults
675
+
676
+ ```python
677
+ from jaato_sdk.client import load_client_config, get_recovery_config
678
+
679
+ config = load_client_config(workspace_path=Path.cwd())
680
+ recovery = get_recovery_config(workspace_path=Path.cwd())
681
+ ```
682
+
683
+ | Environment variable | RecoveryConfig field |
684
+ |---|---|
685
+ | `JAATO_IPC_AUTO_RECONNECT` | `enabled` |
686
+ | `JAATO_IPC_RETRY_MAX_ATTEMPTS` | `max_attempts` |
687
+ | `JAATO_IPC_RETRY_BASE_DELAY` | `base_delay` |
688
+ | `JAATO_IPC_RETRY_MAX_DELAY` | `max_delay` |
689
+ | `JAATO_IPC_RETRY_JITTER` | `jitter_factor` |
690
+ | `JAATO_IPC_CONNECTION_TIMEOUT` | `connection_timeout` |
691
+ | `JAATO_IPC_REATTACH_SESSION` | `reattach_session` |
692
+
693
+ ## Presentation Context
694
+
695
+ Every client tells the server what its display surface looks like so the agent can adapt its output (avoid wide tables on a phone, skip mermaid on a TUI, etc.). The default is a generic terminal — override it before `connect()` if you're building a different kind of client.
696
+
697
+ ```python
698
+ from jaato_sdk import PresentationContext, ClientType, CommunicationStyle
699
+
700
+ presentation = PresentationContext(
701
+ content_width=72,
702
+ client_type=ClientType.CHAT, # TERMINAL | WEB | CHAT | API
703
+ supports_tables=False,
704
+ supports_expandable_content=True, # client wraps overflow itself
705
+ communication_style=CommunicationStyle.CONVERSATIONAL,
706
+ )
707
+ ```
708
+
709
+ `ClientType` describes the kind of surface, not a specific app. Telegram, Slack and WhatsApp bots are all `CHAT`. `CommunicationStyle.CONVERSATIONAL` tells the model to send short, frequent updates; `NARRATIVE` tells it to deliver one well-structured response at the end. When `communication_style` is left `None`, `CHAT` defaults to conversational and everything else to narrative.
710
+
711
+ Pass a `PresentationContext` (or a plain `dict`) as `presentation=` to the client constructor — `IPCClient` / `WSClient` / `IPCRecoveryClient` / `WSRecoveryClient`, their `.session(...)`, or `jaato.session(mode="ipc"|"ws", presentation=...)`. It **replaces** the auto-derived terminal context at connect-time config-send, so a chat / web client whose capabilities differ from a TUI's (e.g. narrow `content_width`, `supports_tables=False`, `supports_images=True`, `supports_expandable_content=True`, `client_type=CHAT`) declares them once. A recovery client threads `presentation=` through every inner client it rebuilds, so it survives reconnection. (`presentation=` is not yet wired for `mode="in_process"`.)
712
+
713
+ ```python
714
+ async with jaato.session(mode="ws", url="wss://host:8080", token="...",
715
+ presentation={"client_type": "chat",
716
+ "content_width": 72,
717
+ "supports_tables": False}) as s:
718
+ print(await s.ask("Summarize the incident."))
719
+ ```
720
+
721
+ ## Building a Custom Client
722
+
723
+ Everything you need to drive the server yourself:
724
+
725
+ ```python
726
+ from jaato_sdk import IPCClient
727
+ from jaato_sdk.events import (
728
+ # Server → Client
729
+ ConnectedEvent,
730
+ AgentOutputEvent,
731
+ ToolCallStartEvent, ToolCallEndEvent, ToolOutputEvent,
732
+ PermissionRequestedEvent, PermissionInputModeEvent, PermissionResolvedEvent,
733
+ ClarificationRequestedEvent, ReferenceSelectionRequestedEvent,
734
+ PlanUpdatedEvent, PlanStepUpdatedEvent, PlanClearedEvent,
735
+ ContextUpdatedEvent, TurnCompletedEvent, TurnProgressEvent,
736
+ UsageBreakdown, GCConfigEvent,
737
+ SystemMessageEvent, ErrorEvent, RetryEvent, InitProgressEvent,
738
+ SessionInfoEvent, SessionListEvent, SessionProfilesEvent,
739
+
740
+ # Client → Server
741
+ SendMessageRequest, PermissionResponseRequest, ClarificationResponseRequest,
742
+ ReferenceSelectionResponseRequest, StopRequest, CommandRequest,
743
+ HistoryRequest, ClientConfigRequest, ToolDisableRequest,
744
+ )
745
+ ```
746
+
747
+ A reference TUI implementation lives at [`jaato-tui`](../jaato-tui/) in the same repo.
748
+
749
+ ## Low-Level API
750
+
751
+ Every event is a serializable dataclass. If you need to bypass the client (for example, embedding the protocol in a different transport), you can drive serialization directly:
752
+
753
+ ```python
754
+ from jaato_sdk.events import serialize_event, deserialize_event, SendMessageRequest
755
+
756
+ wire = serialize_event(SendMessageRequest(text="hi")) # → JSON string
757
+ event = deserialize_event(wire) # → typed dataclass
758
+ ```
759
+
760
+ Framing differs by transport:
761
+
762
+ - **IPC** — each frame is a 4-byte big-endian length prefix followed by the JSON payload. Max message size is 10 MiB. The Unix-socket variant uses `asyncio.open_unix_connection`; on Windows the SDK uses `loop.create_pipe_connection` against `\\.\pipe\<name>`.
763
+ - **WebSocket** — one event per WS text frame, no length prefix (WS frames itself). Authenticate with a bearer token on the upgrade request or via `?token=...`.
764
+
765
+ ## Tracing
766
+
767
+ The SDK ships a small tracing helper that the server picks up via `JAATO_TRACE_LOG` and `PROVIDER_TRACE_LOG`:
768
+
769
+ ```python
770
+ from jaato_sdk import trace, provider_trace, trace_write, resolve_trace_path
771
+ ```
772
+
773
+ These write JSONL records to per-agent files under the configured trace directory. Useful for offline replay and debugging — leave them off in production unless you need them.
774
+
775
+ ## Requirements
776
+
777
+ - Python 3.10+
778
+ - A reachable jaato server (auto-started by default)
779
+ - `python-dotenv` (the only runtime dependency)
780
+
781
+ ## License
782
+
783
+ BUSL-1.1