mistri 0.4.1 → 0.6.0

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +596 -3
  3. data/CONTRIBUTING.md +52 -0
  4. data/README.md +291 -306
  5. data/SECURITY.md +40 -0
  6. data/UPGRADING.md +640 -0
  7. data/assets/logo-animated.svg +30 -0
  8. data/assets/logo-dark.svg +14 -0
  9. data/assets/logo-light.svg +14 -0
  10. data/assets/logo.svg +14 -0
  11. data/assets/social-preview.png +0 -0
  12. data/docs/README.md +87 -0
  13. data/docs/context-and-workspaces.md +378 -0
  14. data/docs/mcp.md +366 -0
  15. data/docs/reliability.md +450 -0
  16. data/docs/sessions.md +295 -0
  17. data/docs/sub-agents.md +401 -0
  18. data/docs/tool-contracts.md +324 -0
  19. data/examples/approval.rb +36 -0
  20. data/examples/browser.rb +27 -0
  21. data/examples/page_editor.rb +31 -0
  22. data/examples/quickstart.rb +21 -0
  23. data/lib/generators/mistri/install/install_generator.rb +7 -3
  24. data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
  25. data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
  26. data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
  27. data/lib/mistri/abort_signal.rb +10 -0
  28. data/lib/mistri/agent.rb +635 -108
  29. data/lib/mistri/budget.rb +26 -1
  30. data/lib/mistri/child.rb +186 -0
  31. data/lib/mistri/compaction.rb +26 -10
  32. data/lib/mistri/compactor.rb +35 -12
  33. data/lib/mistri/console.rb +209 -0
  34. data/lib/mistri/content.rb +9 -3
  35. data/lib/mistri/dispatchers.rb +49 -0
  36. data/lib/mistri/errors.rb +83 -4
  37. data/lib/mistri/event.rb +30 -8
  38. data/lib/mistri/event_delivery.rb +60 -0
  39. data/lib/mistri/locks/rails_cache.rb +48 -0
  40. data/lib/mistri/locks.rb +141 -0
  41. data/lib/mistri/mcp/client.rb +74 -19
  42. data/lib/mistri/mcp/egress.rb +216 -0
  43. data/lib/mistri/mcp/oauth.rb +476 -127
  44. data/lib/mistri/mcp/wires.rb +115 -23
  45. data/lib/mistri/mcp.rb +43 -9
  46. data/lib/mistri/message.rb +21 -11
  47. data/lib/mistri/models.rb +160 -22
  48. data/lib/mistri/providers/anthropic/assembler.rb +282 -44
  49. data/lib/mistri/providers/anthropic/serializer.rb +14 -9
  50. data/lib/mistri/providers/anthropic.rb +29 -6
  51. data/lib/mistri/providers/fake.rb +36 -6
  52. data/lib/mistri/providers/gemini/assembler.rb +148 -21
  53. data/lib/mistri/providers/gemini/serializer.rb +78 -9
  54. data/lib/mistri/providers/gemini.rb +31 -5
  55. data/lib/mistri/providers/openai/assembler.rb +337 -60
  56. data/lib/mistri/providers/openai/serializer.rb +13 -12
  57. data/lib/mistri/providers/openai.rb +29 -5
  58. data/lib/mistri/providers/schema_capabilities.rb +214 -0
  59. data/lib/mistri/result.rb +8 -3
  60. data/lib/mistri/retry_policy.rb +2 -2
  61. data/lib/mistri/schema.rb +893 -75
  62. data/lib/mistri/session.rb +649 -47
  63. data/lib/mistri/sinks/coalesced.rb +17 -10
  64. data/lib/mistri/skill.rb +1 -1
  65. data/lib/mistri/skills.rb +1 -1
  66. data/lib/mistri/spawner.rb +316 -0
  67. data/lib/mistri/sse.rb +57 -14
  68. data/lib/mistri/stores/active_record.rb +22 -7
  69. data/lib/mistri/stores/jsonl.rb +3 -1
  70. data/lib/mistri/stores/memory.rb +21 -2
  71. data/lib/mistri/sub_agent/execution.rb +81 -0
  72. data/lib/mistri/sub_agent/runtime.rb +297 -0
  73. data/lib/mistri/sub_agent.rb +238 -103
  74. data/lib/mistri/task_output.rb +58 -0
  75. data/lib/mistri/tool.rb +102 -13
  76. data/lib/mistri/tool_arguments.rb +377 -0
  77. data/lib/mistri/tool_call.rb +43 -9
  78. data/lib/mistri/tool_context.rb +7 -5
  79. data/lib/mistri/tool_executor.rb +117 -26
  80. data/lib/mistri/tool_result.rb +15 -10
  81. data/lib/mistri/tools/edit_file.rb +62 -8
  82. data/lib/mistri/tools.rb +41 -4
  83. data/lib/mistri/transport.rb +149 -44
  84. data/lib/mistri/usage.rb +65 -13
  85. data/lib/mistri/version.rb +1 -1
  86. data/lib/mistri/workspace/active_record.rb +183 -3
  87. data/lib/mistri/workspace/directory.rb +28 -8
  88. data/lib/mistri/workspace/memory.rb +34 -9
  89. data/lib/mistri/workspace/single.rb +62 -5
  90. data/lib/mistri/workspace.rb +39 -0
  91. data/lib/mistri.rb +17 -1
  92. data/mistri.gemspec +34 -0
  93. metadata +38 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 820753fb8dd64f485da060568e6dbc8a7af3fa24d63a2cc7dc957b5dbfc9f1d6
4
- data.tar.gz: c3e7432802801d19979c3d2906f700fbe3170a1d4950d22f0da88e01b276b905
3
+ metadata.gz: facccd328e7961bebff3b247354d9a4f2c33ba71fdde43ccfc67802f519fe618
4
+ data.tar.gz: cbe2af2c3bff33d45591946cc314c69924ef5b64ceb9ddf74ad36bbd98a07085
5
5
  SHA512:
6
- metadata.gz: 78959db170082e7264a3657dec0ff1e21ebf496a2e2603fe98ca6a7b263f68f980456df3f2881d798f2a8e7a296902700b2dc4e155a9ef43f54a32df5ca75eb7
7
- data.tar.gz: 565be0a187d2651d92d43e59351090e0d6623dff13d70bbd1747e5c35f5167f162a4182c111e265734eadbc9aa465be003d5704da61495af281b321036eb0f26
6
+ metadata.gz: e3e47697cff3194fb8a5a9c2d4e8089fb006e4ed3aa863c85e2bf1d638c7d636c96b58c84117e2c26614f9475759e23839aef9f4ee796623e1aa7fab2bc6fccb
7
+ data.tar.gz: abfa098d94aa6306eed257ea8e7bc2c788cec5e22053880aa516c4aece167965079fc887bb3007b68fe9fdb6a5e638b2930ddb285cefc0a145c872619571c554
data/CHANGELOG.md CHANGED
@@ -5,6 +5,591 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.6.0] - 2026-07-12
9
+
10
+ - Synchronous event subscribers now propagate the exact exception object even
11
+ when its class also names an internal failure: provider `Mistri::Error`
12
+ values, transport I/O and timeout errors, SSE `JSON::ParserError`, hook
13
+ failures, and automatic-compaction `CompactionError` can no longer be
14
+ swallowed, retried, folded into an errored assistant turn, or persisted as a
15
+ policy or tool failure. An owner-scoped private marker preserves provenance
16
+ through nested inline agents and unwraps only at the boundary that first
17
+ observed the subscriber. A mid-stream subscriber failure closes the
18
+ interrupted provider connection before propagating. SSE parse tolerance now
19
+ covers `JSON.parse` only, never its callback. Genuine provider, hook, tool,
20
+ and compaction failures keep their existing contracts; configured tool
21
+ timeouts still include progress delivery. Asynchronous background delivery
22
+ remains unable to raise into a caller that has already returned. A callback
23
+ that aborts child execution records the original failure class in its failed
24
+ terminal and dispatcher diagnostic; a post-terminal report callback failure
25
+ leaves the already-durable terminal and parent inbox intact and appears in
26
+ the diagnostic. Active subscribers add one short-lived boundary per provider
27
+ turn, tool batch, hook phase, or automatic compaction and one method/rescue
28
+ frame per delivered event, with no parsing, buffering, I/O, or string work on
29
+ the successful path.
30
+ - Approval decisions now behave as a write-once register over the Session
31
+ store's durable order. The first valid decision is authoritative; repeating
32
+ that value is idempotent, while a conflicting caller receives
33
+ `Mistri::ConfigurationError`. Concurrent stale writers may both reach append,
34
+ but every later well-formed decision for that approval request is an inert
35
+ losing occurrence, including one that lands after the approved handler has
36
+ already returned. It cannot revoke the winner, alter settlement, or
37
+ permanently poison `open_approvals`, `resume`, replay, or compaction. The first
38
+ note remains authoritative.
39
+ Malformed decisions, decisions without a matching request, duplicate
40
+ requests, and ambiguous legacy reused-call approvals still fail closed or
41
+ interrupt conservatively. A newly appended decision adds one linear
42
+ control-state re-read to tell a racing loser that it lost; already-visible
43
+ idempotent retries do not append. No provider streaming path changes.
44
+ - Anchored `edit_file` changes now compose safely with concurrent writers when
45
+ their Workspace advertises atomic conditional writes. The new optional
46
+ contract is `atomic_writes?`, `snapshot(path)`, and
47
+ `compare_and_write(path, content, expected_revision:)`, with immutable
48
+ `Mistri::Workspace::Snapshot` values and a public
49
+ `Mistri::WorkspaceConflictError`. Memory implements the contract under its
50
+ mutex; the optional Active Record adapter advertises it only for PostgreSQL
51
+ or an InnoDB table on MySQL2 or Trilogy, using exact-byte SHA-256 revisions,
52
+ unique-index create-only CAS, and short transactions without requiring
53
+ `lock_version`. It rejects atomic operations inside a host transaction rather
54
+ than joining a stale or overlong boundary, validates model identity against
55
+ the database catalog, restricts atomic scope to owned scalar equality values,
56
+ and reads the committed row before returning its revision. Single opts in
57
+ through a host `synchronize:` callback that must enforce the same complete
58
+ boundary and outer-transaction rule. Directory, MyISAM, and existing custom
59
+ four-method workspaces retain their documented legacy behavior. `edit_file`
60
+ applies its existing exact/fuzzy anchor outside any lock and retries only a
61
+ lost conditional write, up to three total attempts. An unrelated concurrent
62
+ change is preserved, while a target that no longer matches remains an
63
+ actionable typed edit failure. Callback, validation, I/O, deadlock, and other
64
+ host errors are never retried. Tool names and schemas are unchanged. A
65
+ committed storage transformation produces an explicit
66
+ read-before-continuing failure instead of a false exact-replacement claim.
67
+ Atomic editing adds owned snapshot copies, three exact-byte linear hashes,
68
+ one exact committed-byte comparison, and one conditional commit to the
69
+ ordinary edit path; Memory also returns an owned read copy. Conflicts add at
70
+ most two complete reapplications. Active Record capability adds cached schema
71
+ inspection on the first capability check, outside the edit path. No
72
+ provider streaming path changes. Dedicated MySQL 8.4/InnoDB and PostgreSQL CI
73
+ jobs prove real locking, readback, and unique-create behavior while the gem
74
+ retains zero runtime dependencies.
75
+ - Built-in document reads, searches, and edits now return an explicitly failed
76
+ `ToolResult` when the document is missing or a requested replacement cannot
77
+ be applied. The same actionable text still reaches the model, but lifecycle
78
+ events, persisted tool messages, and provider-supported error signaling no
79
+ longer misclassify an expected document-tool failure as success. Tool names,
80
+ schemas, and successful results are unchanged.
81
+ - Background dispatch now crosses a versioned, deeply owned JSON capability
82
+ boundary instead of closing over the spawn-time provider, Tool objects, Agent
83
+ options, and workspace state. Every `dispatcher:` requires a host
84
+ `runtime_factory:` that constructs a `SubAgent::Runtime` inside the worker;
85
+ the runtime provider model and unique Tool names must match the durable grant
86
+ exactly before any provider call or tool side effect. Missing, additional,
87
+ renamed, duplicated, non-Tool, reconstructed statically approval-gated, and
88
+ synthetic Skill capabilities fail closed before execution. New immutable
89
+ specs carry `spec_version`, require a model identity, reject unknown fields,
90
+ and omit the model-controlled `workspace` field. The identical canonical
91
+ spec is stored in the child Session before dispatch; `run_dispatched` compares
92
+ the complete queue copy and uses the stored grant for execution and report
93
+ routing, so a changed capability, task, model, name, or parent ID leaves the
94
+ child untouched. Legacy unversioned specs containing `workspace` remain
95
+ executable. Finished redeliveries are rejected before factory construction;
96
+ a configured child lease also rejects concurrent live redeliveries. The
97
+ compatible direct form receives the same grant checks.
98
+ Runtime factory exceptions remain retryable for queues by default; built-in
99
+ in-process runners and an explicit `retry_factory_errors: false` persist a
100
+ terminal failure before re-raising. An optional Runtime `cleanup:` hook runs
101
+ exactly once without masking a primary error. Generic child Agent options
102
+ are allowlisted and travel as one isolated Hash, so they cannot replace the
103
+ durable Session, task, signal, event sink, or other lifecycle state; Runtime
104
+ cannot add Skill capabilities. The dispatched
105
+ lease now spans runtime construction, execution, cleanup, terminal
106
+ persistence, and parent report delivery; it suppresses ordinary stop and
107
+ redelivery races while the tokenless lease remains live. Queued and
108
+ interrupted cancellations now deliver their durable stopped report while holding that
109
+ lease even when the queue never starts or retries the job; repeating stop
110
+ reconciles a transient parent-report failure. A queue copy that does not
111
+ match the persisted grant raises the public, non-retryable
112
+ `Mistri::DispatchGrantError`, which is reserved for grant binding.
113
+ Model selection records only its identity at spawn time; its provider is
114
+ constructed in the worker. `tools: []` now grants no tools, while omission
115
+ keeps the general pool or typed defaults, so an empty typed Definition can no
116
+ longer inherit the entire pool. Duplicate pool, model, or model-selected tool
117
+ names fail before a child starts, and Spawner owns copies of its policy
118
+ arrays. Dispatch adds one bounded spec ownership/comparison and one linear
119
+ capability-name check per child, outside all provider streaming paths.
120
+ - Model-originated tool calls now cross a local execution boundary rather than
121
+ relying on provider guidance alone. Completed calls deeply own immutable JSON,
122
+ canonicalize symbol keys without coercing values, retain malformed encoded
123
+ input as a durable `arguments_error`, and stop at 8 MiB, 64 levels, 10,000
124
+ nodes, or a 64 KiB numeric token. The same numeric bound applies to encoded
125
+ JSON and programmatic custom-provider arguments. The Agent requires an object
126
+ argument envelope before `before_tool`, approval predicates, `ends_turn`,
127
+ handlers, or remote MCP calls. Invalid calls receive a bounded typed result in
128
+ band, emit no execution-start event, expose no received field values in core
129
+ errors, and do not prevent valid siblings from running. Results settled in
130
+ the same phase return in model-call order.
131
+ Completed tool-call IDs, names, signatures, and provider correlation IDs must
132
+ be non-empty UTF-8 strings. Live provider turns require IDs unique for the
133
+ session, and provider correlation IDs are unique within one assistant turn.
134
+ Persisted histories recognize only the `call_N` reuse without provider
135
+ correlation IDs that earlier releases synthesized for Gemini and the Fake
136
+ provider, after the prior occurrence was answered or crash-interrupted.
137
+ Replay, provider metadata, and compaction track each occurrence independently
138
+ while live IDs remain reserved for the full session. An unsettled reused-ID
139
+ approval that cannot distinguish its generation becomes an interrupted result
140
+ rather than executable stale authorization. Every other duplicate ID,
141
+ malformed call envelope, or non-assistant provider result rejects the whole
142
+ provider attempt before persistence, policy, or execution. The normal
143
+ provider retry contract retries unchanged history; exhaustion persists a
144
+ pairable error with no tool calls. An abort during normalization, validation,
145
+ policy, or approval evaluation interrupts every uncommitted call and parks no
146
+ approval. Custom
147
+ providers that emitted missing, blank, non-string, non-UTF-8, or duplicate
148
+ identifiers must correct their completed call contract. Each run or resume
149
+ validates every persisted message and audits call identity in one control-state
150
+ preflight before doing work, so malformed legacy history fails closed. The
151
+ preceding assistant entry remains the durable provider source; a normalized
152
+ approval stores the reviewed prepared call plus an explicit provenance
153
+ marker. Resume verifies pairing metadata and revalidates the exact approved
154
+ arguments without rerunning the normalizer. Requests must follow the exact
155
+ assistant call, and decisions must follow one request and carry an exact
156
+ boolean. Duplicate requests and malformed or mismatched controls fail closed.
157
+ The first valid durable decision wins; later well-formed decisions for that
158
+ approval request are inert. Legacy requests remain valid only when they
159
+ exactly mirror the source call.
160
+ Persisted tool results require one prior call with the exact name, settle in
161
+ assistant-call order within each direct or approval phase, and cannot cross
162
+ an unresolved approval; compaction may neither hide an open approval nor
163
+ split a completed call from its result.
164
+ `ToolCall#arguments_error?` is stable; its non-nil string remains an opaque
165
+ diagnostic rather than a public enum. Fake-provider default call IDs now use
166
+ a per-provider UUID namespace plus a monotonic sequence; explicitly scripted
167
+ `id:` values remain deterministic. New approval entries no longer duplicate
168
+ the source argument payload, and generated MySQL-family session tables use
169
+ LONGTEXT so a legal parallel tool turn is not constrained by MEDIUMTEXT's
170
+ 16 MiB ceiling. Existing MySQL-family stores must migrate `payload` to
171
+ `size: :long`; generators cannot alter an installed table. `Stores::Memory`
172
+ owns durable appends while returning fresh mutable snapshots, matching stores
173
+ that decode each read.
174
+ - Tool schemas compile once into the same deeply frozen canonical UTF-8 JSON
175
+ value used by provider serializers, so mutation and Ruby encoding cannot make
176
+ local validation disagree with the wire. Definition checks cover the complete
177
+ schema document. The zero-dependency runtime subset enforces JSON types and
178
+ unions, `enum`, `required`, `properties`, `prefixItems`, one-schema `items`,
179
+ boolean schemas, and boolean or schema-valued `additionalProperties`.
180
+ `argument_validator:` adds domain rules without weakening core;
181
+ `complete_argument_validator:` is separate explicit authority for every
182
+ argument-applicable non-empty `patternProperties` contract. Tool inputs use
183
+ JSON Schema 2020-12 and require an object root; legacy array-form `items` is
184
+ rejected in favor of `prefixItems`. A schema-less Tool is now a genuinely
185
+ closed no-argument contract; hosts that accepted model fields without
186
+ declaring them must add an explicit schema. The schema-less wire shape
187
+ changed with it, so provider prompt caches re-warm once per affected tool
188
+ set after upgrading. Supplied object schemas preserve
189
+ JSON Schema's default-open semantics unless a raw schema explicitly sets
190
+ `additionalProperties: false`; handlers should extract named fields instead
191
+ of mass-assigning model input. MCP bridging enforces directly reachable
192
+ portable constraints locally and keeps unsupported applicator subtrees and
193
+ other unimplemented standard assertions as guidance for the server, which the
194
+ MCP spec obligates to validate its own tool inputs. A complete validator takes
195
+ explicit whole-contract authority when local policy depends on those
196
+ assertions. External references and
197
+ explicit `inputSchema: null` are rejected in every mode, an omitted schema
198
+ still defaults to a no-argument object, and argument-applicable non-empty
199
+ `patternProperties` still requires complete authority. Common map schemas
200
+ work in core. Task mode rejects assertions local validation cannot guarantee
201
+ before making a provider request, then shares one deeply frozen strict schema
202
+ between its prompt and compiled local validator.
203
+ Providers derive a native constraint only for schema shapes their documented
204
+ and live-verified subsets accept; incompatible shapes fall back to the prompt
205
+ and local correction loop instead of failing the request. Native constraints
206
+ are limited to catalogued matching-provider models and documented provider
207
+ complexity ceilings; unknown models keep the local path. Raw task output is
208
+ byte-bounded, then lexically
209
+ width/depth-bounded before JSON parsing so wide hostile JSON is rejected
210
+ before parser allocation. A Tool-owned `argument_normalizer:` remains the
211
+ explicit compatibility seam and runs once before validation; approved calls
212
+ persist that canonical value and revalidate against the current schema on
213
+ resume without renormalizing or reevaluating approval. `edit_file` declares
214
+ its existing alias and string-boolean tolerance through that seam and rejects
215
+ alias collisions. Provider replay substitutes `{}` only as the wire placeholder
216
+ for an already rejected call, paired with its error result. Direct `Tool#call`
217
+ remains a trusted host path and skips Agent validation. Task output now uses
218
+ the same JSON resource limits. This is minor-release behavior for hosts whose
219
+ policy or handlers previously received malformed values, whose custom
220
+ validators relied on implicit full-schema authority, or whose code mutated or
221
+ indexed a Tool's raw schema with symbol keys. `Schema.strict` results are now
222
+ deeply frozen, and `Tool.define` raises when both `input_schema:` and `schema:`
223
+ are supplied instead of silently choosing one. Definition compilation is paid
224
+ once; a completed call adds one bounded ownership pass and one compiled schema
225
+ traversal, plus ownership only when a normalizer replaces the value. Streaming
226
+ tool snapshots deep-freeze each newly refreshed bounded preview once; deltas
227
+ that reuse the cached preview are O(1), and pairing strings are copied.
228
+ Completed call envelope verification is one linear pass over that turn's
229
+ calls. Run and resume add one linear control-state read of the session log to
230
+ seed O(1) identity checks. Encoded Anthropic/OpenAI arguments and task text add
231
+ one linear lexical scan before JSON parsing; it is off the token-delta path. A
232
+ normalized approval adds only a small provenance marker. Structured
233
+ output request building adds one bounded schema ownership pass; task plans
234
+ compile their local validator once, with no added work on token streaming.
235
+ Arguments are now deeply frozen before hooks and handlers; hosts that mutated
236
+ model-supplied hashes or arrays must copy them first.
237
+ - Provider replay now keeps opaque state with its provider of origin. Gemini
238
+ sends tool schemas through `parametersJsonSchema`, preserves each signed part
239
+ boundary, and returns Google's function-call ID exactly when one was supplied;
240
+ legacy calls without a wire ID get a collision-safe internal ID without
241
+ mutating the signed provider part. On pre-3 Gemini turns, an earlier gated
242
+ same-name/no-ID call is rejected in band if a later sibling would answer
243
+ first; Gemini 3 wire IDs retain out-of-order pairing. A MAX_TOKENS turn never
244
+ executes a returned call. OpenAI treats a missing completed `arguments` field
245
+ as malformed instead of executing `{}`. Anthropic and OpenAI no longer place
246
+ another provider's signatures on their wire. These replay checks add constant
247
+ metadata work per completed block. Terminal records now fence later known
248
+ content for all three providers. Anthropic enforces
249
+ sequential and open content-block indexes plus the delta kind expected by the
250
+ open block. OpenAI enforces one open output item, its kind, any present item or
251
+ output-index correlation, and terminal event/status agreement. Unknown future
252
+ event types remain ignorable. The checks add only constant primitive
253
+ comparisons per known delta, with no parsing, copying, or I/O.
254
+ Completed foreign tool exchanges project to neutral text when a session
255
+ moves to Gemini, preserving the result without manufacturing a Gemini
256
+ function call that lacks Google's thought signature.
257
+ Real providers now emit the same initial `:start` snapshot as the Fake, and
258
+ every opened text, thinking, or tool-call block emits its matching end before
259
+ a terminal failure. Interrupted tool calls remain explicitly incomplete and
260
+ are stripped before Agent persistence; interrupted Anthropic thinking never
261
+ replays a partial signature.
262
+ - Tool execution now emits `:tool_started` when each resolved call commits to
263
+ invocation and carries a structured failure fact end to end. A handler may
264
+ return `ToolResult(error: true)`; unknown tools, policy blocks,
265
+ handler exceptions, timeouts, pre-invocation interruptions, failed result
266
+ hooks, healed dangling calls, and MCP `isError` results set it automatically.
267
+ The flag is sticky through `after_tool` rewrites, so changing error text
268
+ cannot relabel a failed operation as successful. `Event#tool_error` is always
269
+ true or false on `:tool_result`; new tool messages persist the same value, and
270
+ legacy messages without it remain unknown and replay with the historical
271
+ unmarked provider shape. Human approval denial remains a normal control
272
+ outcome rather than an execution error. Anthropic receives `is_error: true`;
273
+ Gemini receives the documented `error` function-response key; OpenAI retains
274
+ textual output because its function-call output has no
275
+ execution-error member. Error text is not classified by prefix, and an error
276
+ bit never authorizes a mechanical replay because a side effect may already
277
+ have happened; the model may still choose a new call, so hosts retain
278
+ idempotency and reconciliation policy. `ends_turn` remains the host's stronger
279
+ floor-transfer policy even when a result is errored; `handed_off?` does not
280
+ claim confirmed tool success. The new lifecycle signal adds one synchronous
281
+ sink event per committed call. Partial streaming Event construction adds two
282
+ primitive checks and one immutable nil slot; partial Message construction
283
+ adds one nil check and one immutable nil slot, with no parsing, buffering,
284
+ I/O, or string work. Tool batches add one sentinel write at commitment and one
285
+ identity check per result so a hard worker exit distinguishes started calls
286
+ from untouched queue entries; same-batch ordering adds one short-lived
287
+ identity map and a linear post-join pass, plus a second identity map only when
288
+ `after_tool` is configured;
289
+ starts arrive on serialized worker-thread callbacks, while same-batch results
290
+ still emit in model-call order after the batch joins. `:tool_started` and
291
+ handler-progress subscriber exceptions propagate and never become tool
292
+ failures.
293
+ Unknown, blocked, denied, queued, and pre-invocation interrupted calls add no
294
+ start event.
295
+ - A blockless nested object in the tool-schema DSL now declares a freeform JSON
296
+ object instead of leaking `LocalJumpError` during application boot. A bare
297
+ top-level `Schema.build` raises `ConfigurationError` with a useful message.
298
+ Constrained-output preparation now refuses this freeform shape, and directly
299
+ encountered explicitly open objects, with a schema path instead of silently
300
+ closing them and changing their meaning. Existing block-built and raw tool
301
+ schemas keep their wire shape. Tool-schema work remains definition-time;
302
+ constrained-output request building adds one openness check to its existing
303
+ property/item traversal, with no streaming or token-path work.
304
+ - Provider and MCP JSON bodies, individual SSE lines, and stdio JSON records
305
+ now default to an 8 MiB ceiling, configurable with `max_record_bytes:`.
306
+ Successful bodies are counted incrementally, declared oversized bodies fail
307
+ before reading, streaming remains unlimited across individually safe records,
308
+ and error responses retain at most a 500-byte valid UTF-8 preview. Requests
309
+ use identity encoding so compressed expansion cannot bypass the boundary.
310
+ Overflow closes the connection or child process; an oversized MCP
311
+ `tools/call` response remains explicitly ambiguous and is never replayed.
312
+ Stdio timeouts now cover the complete record instead of only its first byte;
313
+ any in-flight stdio wire failure after `tools/call` is ambiguous, reaps the
314
+ child, and requires a clean handshake before the next operation. Explicitly
315
+ closing and reusing an MCP client also clears its session, negotiated
316
+ protocol version, and server information before the fresh handshake.
317
+ Each completed SSE record also receives a bounded 20,001-token lexical
318
+ preflight, derived from the 10,000-node canonical limit, plus depth and 64 KiB
319
+ numeric-token ceilings before JSON parsing. A distinct
320
+ `ResponseTooComplexError` preserves the exact boundary and bounds Gemini
321
+ argument allocation inside its enclosing record. Either size or complexity
322
+ overflow resets an MCP wire and its negotiated session. An unconfirmed
323
+ `tools/call` outcome becomes ambiguous and is never replayed; a matching result
324
+ received before an invalid trailing SSE record remains authoritative, while
325
+ the next operation still performs a fresh handshake. Fragmented Anthropic and
326
+ OpenAI tool arguments also carry an aggregate 8 MiB cap across SSE records.
327
+ Their live partial preview stops reparsing after a bounded 64 KiB schedule
328
+ while raw delta events continue unchanged; OpenAI's completed item remains
329
+ authoritative. Fragmented Anthropic thinking
330
+ signatures stop immediately at the same aggregate ceiling rather than
331
+ growing quadratically or replaying a truncated opaque value.
332
+ The hot path adds one byte-count addition and comparison per JSON socket or SSE
333
+ line fragment, then one bounded linear lexical pass over each completed SSE
334
+ record before the provider's existing JSON parse.
335
+ - Remote MCP and server-side OAuth requests now default to public HTTPS and
336
+ direct connections. Every DNS answer is checked against the IANA
337
+ special-purpose registries, including embedded NAT64 destinations; one unsafe
338
+ answer rejects the whole set. Approved addresses are pinned without changing
339
+ Host, SNI, or certificate identity, alternates are tried only before request
340
+ transmission, and every connection cycle resolves and validates a fresh DNS
341
+ set, including Net::HTTP's internal reconnect cycles. Redirects and ambient
342
+ proxies cannot bypass the boundary. `allow_non_public:` is a narrow host
343
+ callback for approved internal HTTPS and explicit loopback development.
344
+ Client construction remains DNS-free; a reused socket adds no request-path
345
+ work. A new connection cycle pays one validated DNS lookup, with approved
346
+ candidates tried only before request transmission.
347
+ - MCP custom headers are copied at client construction and transport-owned
348
+ headers are rejected; a token callable remains the dynamic authentication
349
+ path. Tool discovery rejects repeated cursors and defaults to at most 100
350
+ pages and 10,000 tools; hosts can adjust either ceiling.
351
+ - MCP OAuth discovery now follows the required protected-resource and
352
+ authorization-server candidate order, uses exact candidate-aware resource and
353
+ issuer binding, honors challenge scope, requires S256 PKCE, and never adds
354
+ `offline_access` as gem policy. OAuth response bodies are uncompressed and
355
+ bounded to 256 KiB; token error descriptions cannot reflect credentials into
356
+ exceptions. A pre-registered client now requires its exact trusted `issuer:`,
357
+ and completion and refresh rediscover the token endpoint from that persisted
358
+ issuer instead of trusting a stored endpoint. New confidential flows persist
359
+ an explicit authentication method; legacy rows with a secret and no method
360
+ retain their previous `client_secret_post` behavior.
361
+ - The MCP Rails generator now persists `issuer` instead of `token_endpoint` and
362
+ exposes `mcp_allow_non_public` as the host's shared egress-policy hook. Existing
363
+ generated models must add and pass an issuer, restart pending OAuth flows,
364
+ and thread their non-public policy through the client and all OAuth
365
+ operations. Existing connected rows must reconnect unless the application
366
+ independently recorded their exact historical issuer. An issuer must never
367
+ be inferred from a token endpoint.
368
+ - GPT-5.6 Sol, Terra, and Luna are catalogued with their 1.05M-token context,
369
+ 128K-token output ceiling, and standard paid pricing above and below the
370
+ 272K long-context boundary. The `gpt-5.6` alias resolves to Sol. OpenAI usage
371
+ now separates billable GPT-5.6 cache writes from uncached input, keeping
372
+ compaction, reported cost, and cost budgets accurate.
373
+ - Directory workspaces reject existing symlinks in model-controlled paths and
374
+ omit symlinked files from listings. In a stable, host-controlled tree, reads,
375
+ writes, and deletes no longer follow a link outside the configured root.
376
+ - Streamable HTTP no longer replays an MCP `tools/call` when its response cannot
377
+ be confirmed. The server may already have committed the tool's side effect,
378
+ so the call now raises `AmbiguousDeliveryError` with an explicit do-not-retry
379
+ warning instead of transparently executing twice.
380
+ - Refusals and content-filter stops surface honestly on every provider
381
+ instead of reading as clean stops or retryable truncations. Gemini's
382
+ verdict finish reasons (SAFETY, RECITATION, LANGUAGE, BLOCKLIST,
383
+ PROHIBITED_CONTENT, SPII, the image verdicts) and blocked prompts
384
+ (promptFeedback.blockReason, which previously read as a retryable
385
+ truncated stream) fail fast as Mistri::InvalidRequestError with the
386
+ wire word in the message. MISSING_THOUGHT_SIGNATURE is a deterministic input
387
+ failure; its model fumbles and incomplete sentinels
388
+ (MALFORMED_FUNCTION_CALL, UNEXPECTED_TOOL_CALL, TOO_MANY_TOOL_CALLS,
389
+ MALFORMED_RESPONSE, FINISH_REASON_UNSPECIFIED, NO_IMAGE, and the catch-all
390
+ OTHER pair, documented as unknown reasons rather than rulings) error retryably.
391
+ Anthropic's refusal
392
+ stop reason fails fast carrying stop_details' category and explanation
393
+ (the API's guidance is a different model, never a same-model retry),
394
+ and model_context_window_exceeded maps to :length per its guidance.
395
+ OpenAI's incomplete_details content_filter and structured-output refusal
396
+ parts or deltas fail fast instead of reading as a completed answer; unknown
397
+ incomplete reasons retry rather than becoming clean stops. Partial content
398
+ stays on the errored message for hosts to show. Genuinely undocumented future
399
+ Gemini stop reasons still tolerate as clean stops, unchanged. Anthropic and
400
+ OpenAI top-level stream errors now classify documented authentication,
401
+ request/policy, rate-limit, timeout, overload, and server failures by their
402
+ wire code rather than retrying deterministic 4xx errors.
403
+ - Active session execution is not yet leased. Store appends from approvals,
404
+ steers, and reports remain concurrency-safe, but hosts must allow only one
405
+ `run` or `resume` at a time for a session. Even with one runner, a crash,
406
+ result-store failure, or subscriber exception after invocation but before
407
+ result persistence can leave an approved call open for possible duplicate
408
+ re-execution. Simultaneous runners widen the same risk. Hosts must use
409
+ idempotency/reconciliation across that commit gap; durable atomic claiming is
410
+ the next synchronization feature, not simulated by a local mutex.
411
+ - Sinks::Coalesced is origin-aware and thread-safe: a background worker's
412
+ deltas no longer merge into the parent's (or another worker's) event at
413
+ the same content index, and concurrent emitters serialize on an
414
+ internal mutex instead of racing the buffer. One uncontended mutex
415
+ acquire per event, at coalesced rate; nothing changes on the token
416
+ path.
417
+ - OpenAI response.failed events classify by their error code instead of
418
+ all reading as a retryable truncated stream: rate limits and server
419
+ errors stay retryable, timeouts retry, and permanent rejections
420
+ (invalid_prompt, the image family) carry the new
421
+ Mistri::InvalidRequestError shape and fail fast, so the loop no longer
422
+ burns its retry budget on input the provider already ruled out. A
423
+ failed response without an error object now errors instead of reading
424
+ as a clean stop.
425
+ - The model catalog carries each model's published standard paid direct-API
426
+ prices. Assemblers price every turn's usage from them: message.usage.cost and
427
+ Result#usage now report list-price dollar estimates for catalogued models, and the
428
+ Budget cost_usd ceiling actually stops a run. It never could before:
429
+ nothing computed cost, so the comparison always saw zero. Pricing is
430
+ selected per request, including GPT-5.4/5.5 and Gemini Pro long-context
431
+ tiers and Sonnet 5's September 2026 rate change. Unpriced usage is marked
432
+ unknown instead of free. A cost ceiling rejects an unknown model or origin
433
+ at construction and fails closed when a request's pricing is unknown at
434
+ runtime. Catalog pricing requires an explicit `catalog_pricing:` opt-in on
435
+ custom origins and observes service-tier policy without changing it;
436
+ nonstandard or unreported tiers stay unknown. A deterministic standard tier
437
+ is required up front for cost-budgeted Anthropic and OpenAI agents; Gemini's
438
+ reported standard, Flex, or Priority tier is honored rather than assuming
439
+ standard. Cost ceilings remain soft and are checked between model-visible
440
+ turns. An unmetered attempt raises Mistri::BudgetError instead of retrying
441
+ under false certainty; the error and an unpriced_attempt session entry retain
442
+ its partial accounting. Run usage includes every retry and compaction attempt,
443
+ and retry session entries now carry their own usage. Truncated streams preserve
444
+ partial token counts but mark their dollar total unknown.
445
+ - Compaction now uses the published 1M context windows for catalogued Fable,
446
+ Opus, and Sonnet models and the 1.05M windows for GPT-5.4 and GPT-5.5.
447
+ Automatic headroom protects a full next output plus framing slack when the
448
+ provider shares input and output capacity; Gemini's separately published
449
+ input limit keeps input-only headroom. An explicit `reserve:` still wins as
450
+ host policy.
451
+ `Compaction#automatic_reserve?` lets hosts distinguish the default mode from
452
+ an explicit 16,384-token policy without changing `Compaction#reserve`.
453
+ Usage reported before the latest compaction no longer makes a fresh summary
454
+ appear full. A single run can compact repeatedly across tool turns while
455
+ keeping parallel calls paired with every result. Large tool results are
456
+ bounded only on the lossy summarizer wire, with their beginning and end
457
+ retained. That shortening never mutates the stored result or an ordinary
458
+ request that still replays it. Compacted replay intentionally remains
459
+ summary plus tail. The live
460
+ provider harness now proves two compactions, an exact tool-token handoff,
461
+ and final fact recall inside one run.
462
+
463
+ ## [0.5.0] - 2026-07-08
464
+
465
+ - The children registry: Session#children lists every sub-agent a session
466
+ has spawned as a Mistri::Child, a window onto the child's own session
467
+ with name, status, report, transcript(tail:) with image bytes stripped,
468
+ and say(text) to steer it. All of it derives from the store, so it reads
469
+ the same from any process, while the child runs and forever after.
470
+ - Completion is a contract: every child ends by writing a terminal entry
471
+ (done with its report, stopped, or failed with the error), including
472
+ when the child's run raises, so a crashed child never reads as running.
473
+
474
+ - Spawn policy is an object: Mistri::Spawner carries the pool, types,
475
+ models, headcount, and dispatcher; SubAgent.spawner and SubAgent.pack
476
+ stay the front doors.
477
+ - Typed workers: the spawner takes types:, a host registry of Definitions
478
+ by name. A typed child takes its system prompt, tools, and model from
479
+ the definition; instructions appends; explicit tool and model args
480
+ override within the pool and allowlist. "general-purpose" stays the
481
+ built-in composable default. Types fail at construction, never
482
+ mid-spawn: a definition with unfilled placeholders or tools the pool
483
+ lacks is a boot-time ConfigurationError.
484
+ - max_children (default 4) caps live workers per session; a spawn past
485
+ the cap answers in band and freed slots reopen.
486
+ - SubAgent.pack returns the spawn tool plus the management console in one
487
+ call, the whole kit for a worker-running agent.
488
+
489
+ - Background mode: spawn_agent takes mode: "background" when the spawner
490
+ has a dispatcher, returning a truthful receipt immediately (what the
491
+ child's status says after dispatch, not what the mode promised) while
492
+ the parent keeps working; the report arrives on its own (above). The
493
+ dispatcher is a seam: Dispatchers::Inline (default degrade, synchronous
494
+ but honest) and Dispatchers::Thread ship in the gem, and a queue host
495
+ plugs one lambda whose job reconstructs tools from the serializable
496
+ spec and calls SubAgent.run_dispatched.
497
+ - Lifecycle is entries: subagent_dispatched and subagent_started join the
498
+ terminal, so status walks the store alone: queued, running, interrupted,
499
+ done, stopped, failed. A job that dies before starting reads :queued,
500
+ honestly.
501
+ - A background child runs on its own signal: the parent's turn is over, so
502
+ only stop_agent and the stop flag end it early. workspace: "parent"
503
+ requires inline mode, enforced in band.
504
+
505
+ - Report delivery: a background child's terminal outcome reports back to
506
+ its parent. The report queues in the parent's inbox as a
507
+ typed subagent_report entry and folds at the next turn boundary the way
508
+ a steer does (the model sees `[Magpie finished] <report>`; failures
509
+ carry the error, stops say so), and a report landing as a run finishes
510
+ cleanly extends it one turn so the parent reacts. A :subagent_report
511
+ event (agent, session_id, status, content) closes the child's lane in
512
+ whatever UI watched the spawn. Session#pending_inbox is the
513
+ steers-and-reports view (hosts that wake idle sessions on steers should
514
+ watch it instead); Session#deliver_report drops sequential duplicate
515
+ delivery by child Session ID. Concurrent callers need serialization.
516
+ - With a lock adapter, dispatched runs use the child's lease before the
517
+ run-or-not decision: a queue that redelivers a live job leaves the
518
+ owner alone, a retry of a finished or cancelled child is a clean no-op,
519
+ and a retry of a child a crashed process left mid-run runs it again
520
+ (previously the guard refused it, wedging the child as interrupted
521
+ forever). Child#finished? and Child#error are new readers.
522
+
523
+ - The management console: Mistri::Console.tools returns list_agents,
524
+ read_agent (tail: to choose how much transcript, wait: to block for the
525
+ report with an in-band timeout), steer_agent, and stop_agent. Every tool
526
+ is a thin wrapper over Session#children and the Child facade, the same
527
+ functions a host UI calls, so agent and user control stay structurally
528
+ equal. Workers answer to name or session id uniformly in every tool;
529
+ duplicate names resolve to the latest spawn, ids stay unambiguous, and
530
+ every state answers honestly in band (already done, nothing to steer,
531
+ stopping needs a lock adapter).
532
+
533
+ - Stop one child, keep the run: every sub-agent now runs on its own signal
534
+ derived from the parent's (AbortSignal#derive), so the parent's abort
535
+ still cascades down while Child#stop ends a single worker and the parent
536
+ reasons on with "[the X sub-agent was stopped]". Cross-process stops ride
537
+ the lock adapter's flags; the lease thread turns them into the child's
538
+ cooperative abort within a tick.
539
+ - A run stopped during its tool phase now reports :aborted. The final
540
+ assistant message is clean in that case, so the message's stop reason
541
+ read :completed and a user-stopped run could claim success; the signal
542
+ is consulted alongside the message.
543
+
544
+ - The lock adapter: Mistri.locks takes an adapter for cross-process leases
545
+ and flags (Locks::Memory built in; Locks::RailsCache as an opt-in require,
546
+ the Stores::ActiveRecord pattern). Locks.hold keeps a lease alive on a
547
+ heartbeat from a background thread and releases it cleanly, join and all,
548
+ so a mid-renewal tick can never re-stamp a released lease.
549
+ - Children gain liveness: every child run holds a lease, and with an
550
+ adapter configured a child that died without writing its terminal entry
551
+ reads :interrupted instead of :running forever. Without an adapter
552
+ nothing changes.
553
+
554
+ - Session#transcript reads the whole conversation back from the store:
555
+ entries with image bytes stripped, and with include_children every
556
+ sub-agent's log spliced in after its link entry, tagged with an
557
+ "origin" key shaped exactly like the live stream's event origins
558
+ (nesting joined with ">"). A UI that rebuilds from the transcript shows
559
+ the lanes it showed live, running children's progress-so-far included;
560
+ hosts stop hand-walking link entries.
561
+
562
+ - Tool.define takes ends_turn: true for a tool that is the last word of
563
+ its turn: once it executes, the loop ends the run instead of prompting
564
+ the model again, so an ask_user tool hands the floor to a human
565
+ structurally instead of through prompt discipline. The whole batch it
566
+ arrived in still executes and is answered; a blocked or denied call
567
+ never executed, so the model keeps the floor; a parked approval outranks
568
+ it (the run suspends, and an approved ends_turn call ends the resumed
569
+ run). A pending steer stays queued for the next run. The Result says it
570
+ happened (Result#handed_off?), so hosts route on the handoff instead of
571
+ sniffing messages, and task mode returns the handoff as-is rather than
572
+ re-prompting for JSON while a human holds the floor.
573
+
574
+ - Store appends tolerate concurrent writers. Sessions have more than one
575
+ appender by design (the loop, a steer from a web process, a worker's
576
+ report from a job), so the ActiveRecord store's unique index is now
577
+ concurrency control rather than a tripwire: a writer that loses the
578
+ position race retries at the next slot, bounded, then raises loudly.
579
+ The JSONL store writes each line in a single call, so concurrent
580
+ appenders interleave whole lines, never fragments.
581
+
582
+ - The Fake provider streams tool-call arguments in chunks, and each
583
+ delta's partial carries the in-progress call with arguments parsed so
584
+ far, the same shape a real assembler builds. A consumer that renders
585
+ tool input as it arrives (a page preview, a code block) is now testable
586
+ headless.
587
+ - Each run of a named specialist can carry its own name: the delegate
588
+ tool takes an optional name argument, so two parallel researchers read
589
+ as Corgi and Beagle in lanes, lists, and links instead of "researcher"
590
+ twice. SubAgent.sanitize_label is the one shared sanitizer behind
591
+ specialist runs and spawner labels.
592
+
8
593
  ## [0.4.1] - 2026-07-06
9
594
 
10
595
  - Terminal events are loop-owned: each attempt's :done or :error is held at
@@ -144,7 +729,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
144
729
  keep their old arity. Events gained an origin field.
145
730
 
146
731
  - Task mode: Agent#task(input, schema:) runs an exchange that must end in
147
- JSON matching the schema tools run as usual, providers constrain the
732
+ JSON matching the schema: tools run as usual, providers constrain the
148
733
  final answer natively where supported (Anthropic output_config, OpenAI
149
734
  text.format strict, Gemini responseJsonSchema when no tools), and the
150
735
  answer validates client-side everywhere. A violation goes back to the
@@ -164,8 +749,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
164
749
  - Rails integration: `rails generate mistri:install YourModel` creates a
165
750
  host-named entry model and its migration for Stores::ActiveRecord
166
751
  (MEDIUMTEXT payload on MySQL-family adapters). Streaming sinks under
167
- Mistri::Sinks ActionCable (lazy server, injectable), SSE (outbound
168
- frames to any IO), and Coalesced (merges delta bursts to UI speed) all
752
+ Mistri::Sinks: ActionCable (lazy server, injectable), SSE (outbound
753
+ frames to any IO), and Coalesced (merges delta bursts to UI speed), all
169
754
  pure Ruby, usable as `agent.run(input, &sink)`. No Railtie: generators
170
755
  auto-discover and everything else duck-types.
171
756
 
@@ -287,4 +872,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
287
872
 
288
873
  - Reserved the gem name.
289
874
 
875
+ [Unreleased]: https://github.com/mcheemaa/mistri/compare/v0.6.0...HEAD
876
+ [0.6.0]: https://github.com/mcheemaa/mistri/compare/v0.5.0...v0.6.0
877
+ [0.5.0]: https://github.com/mcheemaa/mistri/compare/v0.4.1...v0.5.0
878
+ [0.4.1]: https://github.com/mcheemaa/mistri/compare/v0.4.0...v0.4.1
879
+ [0.4.0]: https://github.com/mcheemaa/mistri/compare/v0.3.0...v0.4.0
880
+ [0.3.0]: https://github.com/mcheemaa/mistri/compare/v0.2.1...v0.3.0
881
+ [0.2.1]: https://github.com/mcheemaa/mistri/compare/v0.2.0...v0.2.1
882
+ [0.2.0]: https://github.com/mcheemaa/mistri/compare/v0.1.0...v0.2.0
290
883
  [0.1.0]: https://github.com/mcheemaa/mistri/releases/tag/v0.1.0