mistri 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -4
- data/README.md +111 -7
- data/lib/mistri/abort_signal.rb +10 -0
- data/lib/mistri/agent.rb +85 -90
- data/lib/mistri/child.rb +130 -0
- data/lib/mistri/compactor.rb +1 -1
- data/lib/mistri/console.rb +188 -0
- data/lib/mistri/dispatchers.rb +47 -0
- data/lib/mistri/event.rb +15 -5
- data/lib/mistri/locks/rails_cache.rb +48 -0
- data/lib/mistri/locks.rb +141 -0
- data/lib/mistri/mcp/client.rb +1 -1
- data/lib/mistri/mcp.rb +1 -1
- data/lib/mistri/providers/fake.rb +16 -2
- data/lib/mistri/result.rb +7 -2
- data/lib/mistri/retry_policy.rb +24 -2
- data/lib/mistri/session.rb +95 -5
- data/lib/mistri/skill.rb +1 -1
- data/lib/mistri/skills.rb +1 -1
- data/lib/mistri/spawner.rb +266 -0
- data/lib/mistri/stores/active_record.rb +21 -6
- data/lib/mistri/stores/jsonl.rb +3 -1
- data/lib/mistri/sub_agent.rb +191 -93
- data/lib/mistri/task_output.rb +40 -0
- data/lib/mistri/tool.rb +10 -1
- data/lib/mistri/tool_context.rb +3 -3
- data/lib/mistri/version.rb +1 -1
- data/lib/mistri.rb +11 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d46cab51a8a899ddd2e3cd0284a7e9eb4185d1362af158303adb97c2a404a657
|
|
4
|
+
data.tar.gz: 548bf1b8cdc9d22f304674eb5b876b624c16f6b3cc31839daba83909f78641a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69b2758b3b137714bb2bffc170ff6520f1d38851230ba19acabbb50042d28234ce934d74555fd328d12d1e8002afe6d96bab8dd21a06567411252e4aeabac973
|
|
7
|
+
data.tar.gz: 972354e7a95bebfa4f5918efa84f13cbc6e45e9e8dabdf49a60a4d210ecf7c11425866b2ad4d8b648916477568aa2ac69915c98d241311e3778c38667d570558
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,155 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.5.0] - 2026-07-08
|
|
7
|
+
|
|
8
|
+
- The children registry: Session#children lists every sub-agent a session
|
|
9
|
+
has spawned as a Mistri::Child, a window onto the child's own session
|
|
10
|
+
with name, status, report, transcript(tail:) with image bytes stripped,
|
|
11
|
+
and say(text) to steer it. All of it derives from the store, so it reads
|
|
12
|
+
the same from any process, while the child runs and forever after.
|
|
13
|
+
- Completion is a contract: every child ends by writing a terminal entry
|
|
14
|
+
(done with its report, stopped, or failed with the error), including
|
|
15
|
+
when the child's run raises, so a crashed child never reads as running.
|
|
16
|
+
|
|
17
|
+
- Spawn policy is an object: Mistri::Spawner carries the pool, types,
|
|
18
|
+
models, headcount, and dispatcher; SubAgent.spawner and SubAgent.pack
|
|
19
|
+
stay the front doors.
|
|
20
|
+
- Typed workers: the spawner takes types:, a host registry of Definitions
|
|
21
|
+
by name. A typed child takes its system prompt, tools, and model from
|
|
22
|
+
the definition; instructions appends; explicit tool and model args
|
|
23
|
+
override within the pool and allowlist. "general-purpose" stays the
|
|
24
|
+
built-in composable default. Types fail at construction, never
|
|
25
|
+
mid-spawn: a definition with unfilled placeholders or tools the pool
|
|
26
|
+
lacks is a boot-time ConfigurationError.
|
|
27
|
+
- max_children (default 4) caps live workers per session; a spawn past
|
|
28
|
+
the cap answers in band and freed slots reopen.
|
|
29
|
+
- SubAgent.pack returns the spawn tool plus the management console in one
|
|
30
|
+
call, the whole kit for a worker-running agent.
|
|
31
|
+
|
|
32
|
+
- Background mode: spawn_agent takes mode: "background" when the spawner
|
|
33
|
+
has a dispatcher, returning a truthful receipt immediately (what the
|
|
34
|
+
child's status says after dispatch, not what the mode promised) while
|
|
35
|
+
the parent keeps working; the report arrives on its own (above). The
|
|
36
|
+
dispatcher is a seam: Dispatchers::Inline (default degrade, synchronous
|
|
37
|
+
but honest) and Dispatchers::Thread ship in the gem, and a queue host
|
|
38
|
+
plugs one lambda whose job reconstructs tools from the serializable
|
|
39
|
+
spec and calls SubAgent.run_dispatched.
|
|
40
|
+
- Lifecycle is entries: subagent_dispatched and subagent_started join the
|
|
41
|
+
terminal, so status walks the store alone: queued, running, interrupted,
|
|
42
|
+
done, stopped, failed. A job that dies before starting reads :queued,
|
|
43
|
+
honestly.
|
|
44
|
+
- A background child runs on its own signal: the parent's turn is over, so
|
|
45
|
+
only stop_agent and the stop flag end it early. workspace: "parent"
|
|
46
|
+
requires inline mode, enforced in band.
|
|
47
|
+
|
|
48
|
+
- Report delivery: a background child's terminal outcome reports back to
|
|
49
|
+
its parent, exactly once. The report queues in the parent's inbox as a
|
|
50
|
+
typed subagent_report entry and folds at the next turn boundary the way
|
|
51
|
+
a steer does (the model sees `[Magpie finished] <report>`; failures
|
|
52
|
+
carry the error, stops say so), and a report landing as a run finishes
|
|
53
|
+
cleanly extends it one turn so the parent reacts. A :subagent_report
|
|
54
|
+
event (agent, session_id, status, content) closes the child's lane in
|
|
55
|
+
whatever UI watched the spawn. Session#pending_inbox is the
|
|
56
|
+
steers-and-reports view (hosts that wake idle sessions on steers should
|
|
57
|
+
watch it instead); Session#deliver_report is the idempotent primitive
|
|
58
|
+
underneath.
|
|
59
|
+
- Dispatched runs are fenced by the child's lease, taken before the
|
|
60
|
+
run-or-not decision: a queue that redelivers a live job leaves the
|
|
61
|
+
owner alone, a retry of a finished or cancelled child is a clean no-op,
|
|
62
|
+
and a retry of a child a crashed process left mid-run runs it again
|
|
63
|
+
(previously the guard refused it, wedging the child as interrupted
|
|
64
|
+
forever). Child#finished? and Child#error are new readers.
|
|
65
|
+
|
|
66
|
+
- The management console: Mistri::Console.tools returns list_agents,
|
|
67
|
+
read_agent (tail: to choose how much transcript, wait: to block for the
|
|
68
|
+
report with an in-band timeout), steer_agent, and stop_agent. Every tool
|
|
69
|
+
is a thin wrapper over Session#children and the Child facade, the same
|
|
70
|
+
functions a host UI calls, so agent and user control stay structurally
|
|
71
|
+
equal. Workers answer to name or session id uniformly in every tool;
|
|
72
|
+
duplicate names resolve to the latest spawn, ids stay unambiguous, and
|
|
73
|
+
every state answers honestly in band (already done, nothing to steer,
|
|
74
|
+
stopping needs a lock adapter).
|
|
75
|
+
|
|
76
|
+
- Stop one child, keep the run: every sub-agent now runs on its own signal
|
|
77
|
+
derived from the parent's (AbortSignal#derive), so the parent's abort
|
|
78
|
+
still cascades down while Child#stop ends a single worker and the parent
|
|
79
|
+
reasons on with "[the X sub-agent was stopped]". Cross-process stops ride
|
|
80
|
+
the lock adapter's flags; the lease thread turns them into the child's
|
|
81
|
+
cooperative abort within a tick.
|
|
82
|
+
- A run stopped during its tool phase now reports :aborted. The final
|
|
83
|
+
assistant message is clean in that case, so the message's stop reason
|
|
84
|
+
read :completed and a user-stopped run could claim success; the signal
|
|
85
|
+
is consulted alongside the message.
|
|
86
|
+
|
|
87
|
+
- The lock adapter: Mistri.locks takes an adapter for cross-process leases
|
|
88
|
+
and flags (Locks::Memory built in; Locks::RailsCache as an opt-in require,
|
|
89
|
+
the Stores::ActiveRecord pattern). Locks.hold keeps a lease alive on a
|
|
90
|
+
heartbeat from a background thread and releases it cleanly, join and all,
|
|
91
|
+
so a mid-renewal tick can never re-stamp a released lease.
|
|
92
|
+
- Children gain liveness: every child run holds a lease, and with an
|
|
93
|
+
adapter configured a child that died without writing its terminal entry
|
|
94
|
+
reads :interrupted instead of :running forever. Without an adapter
|
|
95
|
+
nothing changes.
|
|
96
|
+
|
|
97
|
+
- Session#transcript reads the whole conversation back from the store:
|
|
98
|
+
entries with image bytes stripped, and with include_children every
|
|
99
|
+
sub-agent's log spliced in after its link entry, tagged with an
|
|
100
|
+
"origin" key shaped exactly like the live stream's event origins
|
|
101
|
+
(nesting joined with ">"). A UI that rebuilds from the transcript shows
|
|
102
|
+
the lanes it showed live, running children's progress-so-far included;
|
|
103
|
+
hosts stop hand-walking link entries.
|
|
104
|
+
|
|
105
|
+
- Tool.define takes ends_turn: true for a tool that is the last word of
|
|
106
|
+
its turn: once it executes, the loop ends the run instead of prompting
|
|
107
|
+
the model again, so an ask_user tool hands the floor to a human
|
|
108
|
+
structurally instead of through prompt discipline. The whole batch it
|
|
109
|
+
arrived in still executes and is answered; a blocked or denied call
|
|
110
|
+
never executed, so the model keeps the floor; a parked approval outranks
|
|
111
|
+
it (the run suspends, and an approved ends_turn call ends the resumed
|
|
112
|
+
run). A pending steer stays queued for the next run. The Result says it
|
|
113
|
+
happened (Result#handed_off?), so hosts route on the handoff instead of
|
|
114
|
+
sniffing messages, and task mode returns the handoff as-is rather than
|
|
115
|
+
re-prompting for JSON while a human holds the floor.
|
|
116
|
+
|
|
117
|
+
- Store appends tolerate concurrent writers. Sessions have more than one
|
|
118
|
+
appender by design (the loop, a steer from a web process, a worker's
|
|
119
|
+
report from a job), so the ActiveRecord store's unique index is now
|
|
120
|
+
concurrency control rather than a tripwire: a writer that loses the
|
|
121
|
+
position race retries at the next slot, bounded, then raises loudly.
|
|
122
|
+
The JSONL store writes each line in a single call, so concurrent
|
|
123
|
+
appenders interleave whole lines, never fragments.
|
|
124
|
+
|
|
125
|
+
- The Fake provider streams tool-call arguments in chunks, and each
|
|
126
|
+
delta's partial carries the in-progress call with arguments parsed so
|
|
127
|
+
far, the same shape a real assembler builds. A consumer that renders
|
|
128
|
+
tool input as it arrives (a page preview, a code block) is now testable
|
|
129
|
+
headless.
|
|
130
|
+
- Each run of a named specialist can carry its own name: the delegate
|
|
131
|
+
tool takes an optional name argument, so two parallel researchers read
|
|
132
|
+
as Corgi and Beagle in lanes, lists, and links instead of "researcher"
|
|
133
|
+
twice. SubAgent.sanitize_label is the one shared sanitizer behind
|
|
134
|
+
specialist runs and spawner labels.
|
|
135
|
+
|
|
136
|
+
## [0.4.1] - 2026-07-06
|
|
137
|
+
|
|
138
|
+
- Terminal events are loop-owned: each attempt's :done or :error is held at
|
|
139
|
+
a gate and only the accepted attempt's terminal reaches the subscriber. A
|
|
140
|
+
transient failure that retries and recovers no longer shows the host an
|
|
141
|
+
error it then walks back, and repeated empty completions no longer emit a
|
|
142
|
+
:done per attempt.
|
|
143
|
+
- The :retry event carries structure: attempt, max_attempts, and delay ride
|
|
144
|
+
the event (and its wire form), so a sink can render "Retrying (2/3) in
|
|
145
|
+
1.3s" without parsing prose. The prose note stays as content. The event
|
|
146
|
+
no longer carries a stop reason, because a retry is not a stop.
|
|
147
|
+
- RetryPolicy owns attempt classification: error_for(message) returns the
|
|
148
|
+
provider's error or a synthesized EmptyCompletion for a blank answer.
|
|
149
|
+
RetryPolicy::EMPTY_COMPLETION replaces the agent-internal constant.
|
|
150
|
+
- The MCP client advertises protocol 2025-11-25, the current spec revision
|
|
151
|
+
it already negotiated.
|
|
152
|
+
- README: the :retry event and the terminal-events invariant are documented
|
|
153
|
+
in the reliability section; the MCP section documents eager listing,
|
|
154
|
+
refresh:, and the duplicate-name raise at Agent.new.
|
|
7
155
|
|
|
8
156
|
## [0.4.0] - 2026-07-05
|
|
9
157
|
|
|
@@ -124,7 +272,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
124
272
|
keep their old arity. Events gained an origin field.
|
|
125
273
|
|
|
126
274
|
- Task mode: Agent#task(input, schema:) runs an exchange that must end in
|
|
127
|
-
JSON matching the schema
|
|
275
|
+
JSON matching the schema: tools run as usual, providers constrain the
|
|
128
276
|
final answer natively where supported (Anthropic output_config, OpenAI
|
|
129
277
|
text.format strict, Gemini responseJsonSchema when no tools), and the
|
|
130
278
|
answer validates client-side everywhere. A violation goes back to the
|
|
@@ -144,8 +292,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
144
292
|
- Rails integration: `rails generate mistri:install YourModel` creates a
|
|
145
293
|
host-named entry model and its migration for Stores::ActiveRecord
|
|
146
294
|
(MEDIUMTEXT payload on MySQL-family adapters). Streaming sinks under
|
|
147
|
-
Mistri::Sinks
|
|
148
|
-
frames to any IO), and Coalesced (merges delta bursts to UI speed)
|
|
295
|
+
Mistri::Sinks: ActionCable (lazy server, injectable), SSE (outbound
|
|
296
|
+
frames to any IO), and Coalesced (merges delta bursts to UI speed), all
|
|
149
297
|
pure Ruby, usable as `agent.run(input, &sink)`. No Railtie: generators
|
|
150
298
|
auto-discover and everything else duck-types.
|
|
151
299
|
|
data/README.md
CHANGED
|
@@ -104,7 +104,7 @@ end
|
|
|
104
104
|
|
|
105
105
|
Handlers and hooks can take the run's context as a second argument, and
|
|
106
106
|
`context.app` carries whatever object you pass as `Mistri.agent(context:)`
|
|
107
|
-
|
|
107
|
+
(the acting user, a tenant, a request), so tools stay authorization-aware
|
|
108
108
|
without closure gymnastics:
|
|
109
109
|
|
|
110
110
|
```ruby
|
|
@@ -116,6 +116,21 @@ Mistri::Tool.define("book_hotel", "Books the chosen hotel.") do |args, context|
|
|
|
116
116
|
end
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
A tool can also be the last word of its turn. `ends_turn: true` makes the
|
|
120
|
+
loop end the run once the tool executes, instead of prompting the model
|
|
121
|
+
again: an `ask_user` tool hands the floor to a human structurally, no
|
|
122
|
+
"remember to stop after asking" prompt required. The result says it
|
|
123
|
+
happened (`result.handed_off?`), and the answer arrives as the next run's
|
|
124
|
+
input:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
ask_user = Mistri::Tool.define("ask_user", "Ask the human and wait.",
|
|
128
|
+
ends_turn: true,
|
|
129
|
+
schema: -> { string :question, "The question", required: true }) do |args|
|
|
130
|
+
"Question presented to the user."
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
119
134
|
## Human approval
|
|
120
135
|
|
|
121
136
|
Mark a tool `needs_approval: true` (or a predicate on its arguments) and the
|
|
@@ -152,6 +167,10 @@ finishes cleanly extends the run so it gets answered.
|
|
|
152
167
|
Mistri::Session.new(store:, id: session_id).steer("Make the headline blue instead.")
|
|
153
168
|
```
|
|
154
169
|
|
|
170
|
+
Background workers' reports arrive through the same inbox (see Sub-agents).
|
|
171
|
+
A host that wakes an idle session when a steer lands should watch
|
|
172
|
+
`session.pending_inbox`, which holds both, in arrival order.
|
|
173
|
+
|
|
155
174
|
## Sessions
|
|
156
175
|
|
|
157
176
|
A session is the durable record of a run: an append-only entry log over a
|
|
@@ -215,6 +234,10 @@ result = agent.task("Extract the pricing tiers from this page.", schema: schema)
|
|
|
215
234
|
result.output # => { "tiers" => [...] }, parsed and validated
|
|
216
235
|
```
|
|
217
236
|
|
|
237
|
+
A run that suspends for approval, or that an `ends_turn` tool ended,
|
|
238
|
+
returns as-is: validation applies to answers, not handoffs. Route on
|
|
239
|
+
`result.handed_off?` and ask again once the human's answer arrives.
|
|
240
|
+
|
|
218
241
|
## Skills
|
|
219
242
|
|
|
220
243
|
Expert playbooks with progressive disclosure: each skill costs one line in
|
|
@@ -248,7 +271,9 @@ agent = Mistri.agent(definition.model,
|
|
|
248
271
|
Delegate to a child agent with a clean context: exploration fills the
|
|
249
272
|
child's window, and only the final answer returns. Children run on their own
|
|
250
273
|
sessions in your store, linked in the parent transcript; their events stream
|
|
251
|
-
into the parent tagged with an `origin`.
|
|
274
|
+
into the parent tagged with an `origin`. Each run of a specialist can carry
|
|
275
|
+
its own name, so two parallel researchers read as Corgi and Beagle in your
|
|
276
|
+
UI instead of "researcher" twice.
|
|
252
277
|
|
|
253
278
|
```ruby
|
|
254
279
|
researcher = Mistri::SubAgent.new(
|
|
@@ -260,12 +285,65 @@ agent = Mistri.agent("claude-opus-4-8", tools: [researcher.tool])
|
|
|
260
285
|
```
|
|
261
286
|
|
|
262
287
|
Or hand the model an open spawn tool and let it compose its own workers:
|
|
263
|
-
a name
|
|
264
|
-
|
|
265
|
-
|
|
288
|
+
a name, instructions, a tool subset, and a host-allowlisted model per
|
|
289
|
+
child. Several spawns in one turn fan out in parallel. `pack` is the whole
|
|
290
|
+
kit: the spawn tool plus a management console (`list_agents`, `read_agent`,
|
|
291
|
+
`steer_agent`, `stop_agent`), with curated types and an optional
|
|
292
|
+
dispatcher:
|
|
266
293
|
|
|
267
294
|
```ruby
|
|
268
295
|
spawn = Mistri::SubAgent.spawner(provider: provider, tools: [fetch_page, search])
|
|
296
|
+
|
|
297
|
+
tools = Mistri::SubAgent.pack(
|
|
298
|
+
provider: provider, tools: [fetch_page, search],
|
|
299
|
+
types: { "researcher" => Mistri::Definition.load("agents/researcher.md") },
|
|
300
|
+
models: ["claude-haiku-4-5-20251001"],
|
|
301
|
+
dispatcher: Mistri::Dispatchers::Thread.new, # or one lambda onto your queue
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A typed worker takes its prompt, tools, and model from the host's
|
|
306
|
+
definition; `general-purpose` stays open for the model to compose.
|
|
307
|
+
`max_children` (default 4) caps live workers, and every policy violation
|
|
308
|
+
answers the model in band.
|
|
309
|
+
|
|
310
|
+
Everything cross-process (stopping a worker from another process, the
|
|
311
|
+
`:interrupted` liveness read, and the lease fence below) rides a lock
|
|
312
|
+
adapter; configure one at boot. Without it, workers still run, but stops
|
|
313
|
+
need the parent's own signal, a crashed child reads `:running`, and
|
|
314
|
+
dispatched retries are unfenced:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
Mistri.locks = Mistri::Locks::RailsCache.new # Locks::Memory for a single process
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
With a dispatcher, `spawn_agent` takes `mode: "background"`: the model gets
|
|
321
|
+
a truthful receipt at once and keeps working while the child runs. The
|
|
322
|
+
console manages the roster with the same functions a host UI calls, so
|
|
323
|
+
agent and user control stay structurally equal: either can read a worker's
|
|
324
|
+
transcript, steer it mid-run, or stop it, from any process. When a worker
|
|
325
|
+
finishes, its report delivers itself: a typed entry queues in the parent's
|
|
326
|
+
inbox and folds at the next turn boundary as `[Corgi finished] <report>`
|
|
327
|
+
(failures carry the error), while a `:subagent_report` event settles the
|
|
328
|
+
worker's lane in whatever UI watched the spawn. In a queue host, the job
|
|
329
|
+
rebuilds tools from the serializable spec and calls
|
|
330
|
+
`SubAgent.run_dispatched`, which fences on the child's lease: a redelivered
|
|
331
|
+
job leaves the running owner alone, a retry of a finished child is a no-op,
|
|
332
|
+
and a retry of a crashed one runs it again.
|
|
333
|
+
|
|
334
|
+
Everything about a child derives from the store and reads the same from
|
|
335
|
+
any process, while it runs and forever after:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
session.children # => [#<Mistri::Child Corgi running>, ...]
|
|
339
|
+
child.status # :queued, :running, :done, :stopped, :failed
|
|
340
|
+
child.report # the terminal entry's report, once finished
|
|
341
|
+
child.say("Check pricing too") # folds at the child's next turn boundary
|
|
342
|
+
child.stop # cooperative, cross-process, within a tick
|
|
343
|
+
|
|
344
|
+
session.transcript(include_children: true)
|
|
345
|
+
# the whole conversation, each child's log spliced at its link entry and
|
|
346
|
+
# origin-tagged like the live stream: a reloading UI rebuilds its lanes
|
|
269
347
|
```
|
|
270
348
|
|
|
271
349
|
## Editing documents
|
|
@@ -304,6 +382,12 @@ tools = Mistri::MCP.tools(client, prefix: "linear",
|
|
|
304
382
|
agent = Mistri.agent("claude-opus-4-8", tools: tools)
|
|
305
383
|
```
|
|
306
384
|
|
|
385
|
+
The bridge lists the server's tools once, at build time; `client.tools(refresh: true)`
|
|
386
|
+
re-lists when a host wants a changed toolset. `prefix:` namespaces local
|
|
387
|
+
names (`linear__create_issue`) because duplicate tool names raise at
|
|
388
|
+
`Agent.new`: collisions fail loud instead of one server's tool silently
|
|
389
|
+
shadowing another's.
|
|
390
|
+
|
|
307
391
|
Local stdio servers spawn as child processes, credentials in their
|
|
308
392
|
environment. That is also the whole "give the agent a browser" story:
|
|
309
393
|
|
|
@@ -369,6 +453,23 @@ budget = Mistri::Budget.new(turns: 20, cost_usd: 2.00)
|
|
|
369
453
|
policy = Mistri::RetryPolicy.new(attempts: 3)
|
|
370
454
|
```
|
|
371
455
|
|
|
456
|
+
Retries are invisible to the model but not to your UI: each backoff emits a
|
|
457
|
+
`:retry` event carrying `attempt`, `max_attempts`, and `delay`, so a sink can
|
|
458
|
+
show a live "reconnecting" state instead of a silent spinner. Terminal events
|
|
459
|
+
are loop-owned: `:done` and `:error` reach the subscriber only for the
|
|
460
|
+
accepted attempt, so a recovered retry never flashes an error it then walks
|
|
461
|
+
back.
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
agent.run("Plan the itinerary.") do |event|
|
|
465
|
+
case event.type
|
|
466
|
+
when :text_delta then stream(event.delta)
|
|
467
|
+
when :retry then banner("Retrying (#{event.attempt}/#{event.max_attempts}) in #{event.delay}s")
|
|
468
|
+
when :done, :error then clear_banner
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
```
|
|
472
|
+
|
|
372
473
|
## Images and provider options
|
|
373
474
|
|
|
374
475
|
```ruby
|
|
@@ -382,8 +483,11 @@ Mistri.agent("claude-opus-4-8", provider_options: { cache: false })
|
|
|
382
483
|
|
|
383
484
|
## Testing
|
|
384
485
|
|
|
385
|
-
`rake test` is hermetic and fast.
|
|
386
|
-
|
|
486
|
+
`rake test` is hermetic and fast. The Fake provider streams like the real
|
|
487
|
+
ones, tool-call arguments included: each delta's partial carries the
|
|
488
|
+
in-progress call with arguments parsed so far, so a UI that renders tool
|
|
489
|
+
input as it arrives tests headless. `rake integration` runs every feature
|
|
490
|
+
end to end against real provider APIs, once per model in the matrix: an
|
|
387
491
|
Anthropic, an OpenAI, and a Gemini model by default. Scenarios assert that
|
|
388
492
|
coined codenames (a ghost of a word like `Wraithowyn` exists in no training
|
|
389
493
|
data) flowed through tool results, summaries, and child agents: proof of
|
data/lib/mistri/abort_signal.rb
CHANGED
|
@@ -32,6 +32,16 @@ module Mistri
|
|
|
32
32
|
nil
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# A signal that trips when this one does, never the reverse: abort the
|
|
36
|
+
# derived signal alone and this one runs on. Returns [signal, handle];
|
|
37
|
+
# remove the handle when the derived work ends, so a finished child
|
|
38
|
+
# never leaks a callback into a later abort.
|
|
39
|
+
def derive
|
|
40
|
+
child = AbortSignal.new
|
|
41
|
+
handle = on_abort { child.abort!(reason) }
|
|
42
|
+
[child, handle]
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
# Register a callback for the moment of abort. Fires immediately when the
|
|
36
46
|
# signal is already tripped. Returns a handle for #remove_callback.
|
|
37
47
|
def on_abort(&callback)
|