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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +596 -3
- data/CONTRIBUTING.md +52 -0
- data/README.md +291 -306
- data/SECURITY.md +40 -0
- data/UPGRADING.md +640 -0
- data/assets/logo-animated.svg +30 -0
- data/assets/logo-dark.svg +14 -0
- data/assets/logo-light.svg +14 -0
- data/assets/logo.svg +14 -0
- data/assets/social-preview.png +0 -0
- data/docs/README.md +87 -0
- data/docs/context-and-workspaces.md +378 -0
- data/docs/mcp.md +366 -0
- data/docs/reliability.md +450 -0
- data/docs/sessions.md +295 -0
- data/docs/sub-agents.md +401 -0
- data/docs/tool-contracts.md +324 -0
- data/examples/approval.rb +36 -0
- data/examples/browser.rb +27 -0
- data/examples/page_editor.rb +31 -0
- data/examples/quickstart.rb +21 -0
- data/lib/generators/mistri/install/install_generator.rb +7 -3
- data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
- data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
- data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
- data/lib/mistri/abort_signal.rb +10 -0
- data/lib/mistri/agent.rb +635 -108
- data/lib/mistri/budget.rb +26 -1
- data/lib/mistri/child.rb +186 -0
- data/lib/mistri/compaction.rb +26 -10
- data/lib/mistri/compactor.rb +35 -12
- data/lib/mistri/console.rb +209 -0
- data/lib/mistri/content.rb +9 -3
- data/lib/mistri/dispatchers.rb +49 -0
- data/lib/mistri/errors.rb +83 -4
- data/lib/mistri/event.rb +30 -8
- data/lib/mistri/event_delivery.rb +60 -0
- data/lib/mistri/locks/rails_cache.rb +48 -0
- data/lib/mistri/locks.rb +141 -0
- data/lib/mistri/mcp/client.rb +74 -19
- data/lib/mistri/mcp/egress.rb +216 -0
- data/lib/mistri/mcp/oauth.rb +476 -127
- data/lib/mistri/mcp/wires.rb +115 -23
- data/lib/mistri/mcp.rb +43 -9
- data/lib/mistri/message.rb +21 -11
- data/lib/mistri/models.rb +160 -22
- data/lib/mistri/providers/anthropic/assembler.rb +282 -44
- data/lib/mistri/providers/anthropic/serializer.rb +14 -9
- data/lib/mistri/providers/anthropic.rb +29 -6
- data/lib/mistri/providers/fake.rb +36 -6
- data/lib/mistri/providers/gemini/assembler.rb +148 -21
- data/lib/mistri/providers/gemini/serializer.rb +78 -9
- data/lib/mistri/providers/gemini.rb +31 -5
- data/lib/mistri/providers/openai/assembler.rb +337 -60
- data/lib/mistri/providers/openai/serializer.rb +13 -12
- data/lib/mistri/providers/openai.rb +29 -5
- data/lib/mistri/providers/schema_capabilities.rb +214 -0
- data/lib/mistri/result.rb +8 -3
- data/lib/mistri/retry_policy.rb +2 -2
- data/lib/mistri/schema.rb +893 -75
- data/lib/mistri/session.rb +649 -47
- data/lib/mistri/sinks/coalesced.rb +17 -10
- data/lib/mistri/skill.rb +1 -1
- data/lib/mistri/skills.rb +1 -1
- data/lib/mistri/spawner.rb +316 -0
- data/lib/mistri/sse.rb +57 -14
- data/lib/mistri/stores/active_record.rb +22 -7
- data/lib/mistri/stores/jsonl.rb +3 -1
- data/lib/mistri/stores/memory.rb +21 -2
- data/lib/mistri/sub_agent/execution.rb +81 -0
- data/lib/mistri/sub_agent/runtime.rb +297 -0
- data/lib/mistri/sub_agent.rb +238 -103
- data/lib/mistri/task_output.rb +58 -0
- data/lib/mistri/tool.rb +102 -13
- data/lib/mistri/tool_arguments.rb +377 -0
- data/lib/mistri/tool_call.rb +43 -9
- data/lib/mistri/tool_context.rb +7 -5
- data/lib/mistri/tool_executor.rb +117 -26
- data/lib/mistri/tool_result.rb +15 -10
- data/lib/mistri/tools/edit_file.rb +62 -8
- data/lib/mistri/tools.rb +41 -4
- data/lib/mistri/transport.rb +149 -44
- data/lib/mistri/usage.rb +65 -13
- data/lib/mistri/version.rb +1 -1
- data/lib/mistri/workspace/active_record.rb +183 -3
- data/lib/mistri/workspace/directory.rb +28 -8
- data/lib/mistri/workspace/memory.rb +34 -9
- data/lib/mistri/workspace/single.rb +62 -5
- data/lib/mistri/workspace.rb +39 -0
- data/lib/mistri.rb +17 -1
- data/mistri.gemspec +34 -0
- metadata +38 -3
data/README.md
CHANGED
|
@@ -7,22 +7,36 @@
|
|
|
7
7
|
|
|
8
8
|
<p align="center"><strong>mistri</strong>, the agent harness for Ruby applications.</p>
|
|
9
9
|
|
|
10
|
-
<p align="center"><a href="https://mistri.sh">mistri.sh</a> · <a href="
|
|
10
|
+
<p align="center"><a href="https://mistri.sh">mistri.sh</a> · <a href="docs/README.md">documentation</a> · <a href="UPGRADING.md">upgrading</a></p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://rubygems.org/gems/mistri"><img alt="Gem Version" src="https://img.shields.io/gem/v/mistri"></a>
|
|
14
14
|
<a href="https://github.com/mcheemaa/mistri/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/mcheemaa/mistri/actions/workflows/ci.yml/badge.svg"></a>
|
|
15
15
|
<a href="https://codecov.io/gh/mcheemaa/mistri"><img alt="Coverage" src="https://img.shields.io/codecov/c/github/mcheemaa/mistri"></a>
|
|
16
16
|
<a href="mistri.gemspec"><img alt="Ruby >= 3.2" src="https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D"></a>
|
|
17
|
-
<a href="
|
|
17
|
+
<a href="mistri.gemspec"><img alt="Runtime dependencies: zero" src="https://img.shields.io/badge/runtime_deps-0-brightgreen"></a>
|
|
18
18
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
A mistri (Urdu: مستری) is the fixer: the skilled tradesperson who actually
|
|
22
|
-
gets it done.
|
|
23
|
-
model loop,
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
gets it done. Mistri is an application-embedded agent runtime for Ruby. It owns
|
|
23
|
+
the model loop, safe tool boundary, event stream, and append-only session
|
|
24
|
+
record. Your application owns authorization, policy, presentation, and the
|
|
25
|
+
semantics of its side effects.
|
|
26
|
+
|
|
27
|
+
Mistri works in plain Ruby applications, including Rails, Sinatra, Hanami, jobs,
|
|
28
|
+
and services. Rails integrations are optional. The gem declares zero runtime
|
|
29
|
+
dependencies.
|
|
30
|
+
|
|
31
|
+
## Start in sixty seconds
|
|
32
|
+
|
|
33
|
+
Add Mistri to your bundle:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "mistri"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then define a tool and run an agent:
|
|
26
40
|
|
|
27
41
|
```ruby
|
|
28
42
|
require "mistri"
|
|
@@ -31,146 +45,151 @@ weather = Mistri::Tool.define(
|
|
|
31
45
|
"get_weather", "Current weather for a city.",
|
|
32
46
|
schema: -> { string :city, "City name", required: true },
|
|
33
47
|
) do |args|
|
|
34
|
-
|
|
48
|
+
{ city: args.fetch("city"), forecast: "34 C and clear" }
|
|
35
49
|
end
|
|
36
50
|
|
|
37
|
-
agent = Mistri.agent("claude-opus-4-8", tools: [weather])
|
|
51
|
+
agent = Mistri.agent("claude-opus-4-8", tools: [weather])
|
|
38
52
|
|
|
39
|
-
agent.run("What should I wear in Lahore today?") do |event|
|
|
53
|
+
result = agent.run("What should I wear in Lahore today? One sentence.") do |event|
|
|
40
54
|
print event.delta if event.type == :text_delta
|
|
41
55
|
end
|
|
56
|
+
puts
|
|
42
57
|
```
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
web request; nothing sleeps waiting.
|
|
51
|
-
- **Three providers, frontier-deep.** Anthropic, OpenAI, and Gemini, each
|
|
52
|
-
streamed natively with thinking, prompt caching, parallel tool calls, and
|
|
53
|
-
constrained JSON output. One message model across all three.
|
|
54
|
-
- **Zero runtime dependencies.** Plain Ruby all the way down.
|
|
55
|
-
- **Verified against real APIs.** A live integration harness runs every
|
|
56
|
-
feature end to end on every provider (`rake integration`).
|
|
57
|
-
|
|
58
|
-
## Install
|
|
59
|
+
`Mistri.agent` infers the provider from the model ID and reads
|
|
60
|
+
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY`. Pass `api_key:`
|
|
61
|
+
explicitly when the host resolves credentials another way. A normally
|
|
62
|
+
completed invocation returns a `Mistri::Result` with status
|
|
63
|
+
predicates, final text, usage, and structured task output when requested.
|
|
64
|
+
Host-contract failures raise as documented in [Reliability](docs/reliability.md).
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
gem "mistri"
|
|
62
|
-
```
|
|
66
|
+
Runnable examples live in [`examples/`](examples).
|
|
63
67
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
agent = Mistri.agent("claude-opus-4-8")
|
|
68
|
-
result = agent.run("Name three Ruby web frameworks.")
|
|
69
|
-
puts result.text
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`Mistri.agent` infers the provider from the model id (`claude-*`, `gpt-*`,
|
|
73
|
-
`gemini-*`) and reads the matching key (`ANTHROPIC_API_KEY`,
|
|
74
|
-
`OPENAI_API_KEY`, `GEMINI_API_KEY`); pass `api_key:` to set it explicitly.
|
|
75
|
-
Every run returns a `Result`: `completed?`, `awaiting_approval?`,
|
|
76
|
-
`aborted?`, `errored?`, with `text` and (for tasks) `output`.
|
|
77
|
-
|
|
78
|
-
## Tools
|
|
79
|
-
|
|
80
|
-
A tool is a name, a description, an argument schema, and a block. The block
|
|
81
|
-
returns a String, a Hash (sent as JSON), or content such as an image. The
|
|
82
|
-
agent calls tools, feeds results back, and loops until the model answers;
|
|
83
|
-
independent calls in a turn run in parallel.
|
|
84
|
-
|
|
85
|
-
```ruby
|
|
86
|
-
weather = Mistri::Tool.define("get_weather", "Current weather for a city.", schema: lambda {
|
|
87
|
-
string :city, "City name", required: true
|
|
88
|
-
string :units, "Temperature units", enum: %w[celsius fahrenheit]
|
|
89
|
-
}) do |args|
|
|
90
|
-
Weather.for(args["city"], units: args["units"] || "celsius")
|
|
91
|
-
end
|
|
92
|
-
```
|
|
68
|
+
## Why Mistri
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
70
|
+
- **One session model.** The same append-only log runs over memory, JSONL,
|
|
71
|
+
Active Record, or a host store. With a persistent store, runs can suspend,
|
|
72
|
+
reload, resume, steer, and compact without inventing a second state model. See
|
|
73
|
+
[Sessions and control](docs/sessions.md).
|
|
74
|
+
- **Human approval does not hold a thread.** A gated tool parks the request and
|
|
75
|
+
returns. With a shared store, another process can record the decision later;
|
|
76
|
+
`resume` revalidates the exact call before execution.
|
|
77
|
+
- **Provider semantics survive the abstraction.** Anthropic, OpenAI, and
|
|
78
|
+
Gemini stream through direct implementations that retain each provider's
|
|
79
|
+
reasoning, tool-pairing, and replay requirements. Unknown model IDs still
|
|
80
|
+
pass through when their provider can be inferred.
|
|
81
|
+
- **Tool input crosses a real boundary.** Completed model arguments are owned,
|
|
82
|
+
bounded, validated, and deeply frozen before hooks, approval predicates, MCP,
|
|
83
|
+
or handlers see them. See [Tool contracts](docs/tool-contracts.md).
|
|
84
|
+
- **The runtime stays small.** The [gemspec](mistri.gemspec) declares no runtime
|
|
85
|
+
gem dependencies. Optional Rails adapters load only when required.
|
|
86
|
+
- **Real APIs are part of the test strategy.** The
|
|
87
|
+
[live integration suite](CONTRIBUTING.md#setup)
|
|
88
|
+
exercises critical control, persistence, delegation, MCP, and
|
|
89
|
+
structured-output paths across all three providers when their keys are
|
|
90
|
+
present. The default CI suite remains hermetic.
|
|
91
|
+
|
|
92
|
+
## Choose the right layer
|
|
93
|
+
|
|
94
|
+
These projects solve different problems:
|
|
95
|
+
|
|
96
|
+
| Need | Start with |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| A durable tool loop inside any Ruby application, with append-only sessions, approval, steering, compaction, linked workers, and provider-neutral MCP | **Mistri** |
|
|
99
|
+
| Broad model access plus media, embeddings, image generation, and Rails-backed chat | [RubyLLM](https://github.com/crmne/ruby_llm) |
|
|
100
|
+
| Agents expressed through Rails controllers, actions, callbacks, views, and Active Job | [Active Agent](https://github.com/activeagents/activeagent) |
|
|
101
|
+
|
|
102
|
+
Mistri is deliberately not a terminal UI, hosted agent service, broad media
|
|
103
|
+
client, policy engine, or exactly-once job system. The host application keeps
|
|
104
|
+
its users, authorization rules, queues, database, UI, idempotency keys, and
|
|
105
|
+
reconciliation policy. Mistri supplies the execution mechanism that connects
|
|
106
|
+
them.
|
|
107
|
+
|
|
108
|
+
## How the loop works
|
|
109
|
+
|
|
110
|
+
1. The input is appended to the session before the provider is called.
|
|
111
|
+
2. Provider events stream to the subscriber as the response arrives.
|
|
112
|
+
3. Completed tool calls cross Mistri's ownership and validation boundary.
|
|
113
|
+
4. Current host policy and approval rules run before a handler can start.
|
|
114
|
+
5. Tool results append to the same session, then the model continues until it
|
|
115
|
+
answers, hands off the turn, suspends for approval, aborts, or reaches a
|
|
116
|
+
budget.
|
|
117
|
+
|
|
118
|
+
Every completed message is appended to the chosen Session store before the next
|
|
119
|
+
model-visible turn. Opaque reasoning and pairing metadata replay only to the
|
|
120
|
+
provider that created them; the shared transcript stays provider-neutral.
|
|
121
|
+
|
|
122
|
+
## Tools and approval
|
|
123
|
+
|
|
124
|
+
A tool has a name, description, JSON Schema object, and handler. Independent
|
|
125
|
+
calls in one model turn execute concurrently up to `max_concurrency`.
|
|
97
126
|
|
|
98
127
|
```ruby
|
|
99
|
-
Mistri::Tool.define(
|
|
100
|
-
|
|
101
|
-
|
|
128
|
+
book_hotel = Mistri::Tool.define(
|
|
129
|
+
"book_hotel", "Books the selected hotel.",
|
|
130
|
+
schema: lambda {
|
|
131
|
+
string :hotel_id, "Hotel ID", required: true
|
|
132
|
+
number :total_usd, "Quoted total", required: true
|
|
133
|
+
},
|
|
134
|
+
needs_approval: ->(args) { args.fetch("total_usd") > 500 },
|
|
135
|
+
) do |args, context|
|
|
136
|
+
Bookings.create!(
|
|
137
|
+
hotel_id: args.fetch("hotel_id"),
|
|
138
|
+
traveler: context.app.fetch(:traveler),
|
|
139
|
+
)
|
|
102
140
|
end
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Handlers and hooks can take the run's context as a second argument, and
|
|
106
|
-
`context.app` carries whatever object you pass as `Mistri.agent(context:)`
|
|
107
|
-
— the acting user, a tenant, a request — so tools stay authorization-aware
|
|
108
|
-
without closure gymnastics:
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Bookings.create!(args, traveler: context.app[:traveler])
|
|
116
|
-
end
|
|
142
|
+
agent = Mistri.agent(
|
|
143
|
+
"claude-opus-4-8",
|
|
144
|
+
tools: [book_hotel],
|
|
145
|
+
context: { traveler: current_traveler },
|
|
146
|
+
)
|
|
117
147
|
```
|
|
118
148
|
|
|
119
|
-
|
|
149
|
+
Validation runs before the approval predicate, so policy never receives a
|
|
150
|
+
string where the schema promised a number. Invalid calls return a bounded,
|
|
151
|
+
model-readable error and valid siblings still run. Mistri never coerces model
|
|
152
|
+
input implicitly.
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
run suspends instead of executing it, instantly, with no thread waiting.
|
|
123
|
-
The decision is a one-line session write from any process, any time later;
|
|
124
|
-
`resume` settles it and carries on.
|
|
154
|
+
When a gated call arrives:
|
|
125
155
|
|
|
126
156
|
```ruby
|
|
127
|
-
book_hotel = Mistri::Tool.define("book_hotel", "Books the chosen hotel.",
|
|
128
|
-
needs_approval: ->(args) { args["total_usd"].to_i > 500 }) do |args|
|
|
129
|
-
Bookings.create!(args)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
157
|
result = agent.run("Book the corner suite for the Lisbon trip")
|
|
133
|
-
result.awaiting_approval?
|
|
134
|
-
|
|
135
|
-
# Days later, in a controller:
|
|
136
|
-
Mistri::Session.new(store:, id: session_id).approve(call_id) # or .deny(call_id, note: "...")
|
|
158
|
+
result.awaiting_approval? # => true; the handler did not run
|
|
137
159
|
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
call = result.pending.fetch(0)
|
|
161
|
+
agent.session.approve(call.id, note: "Approved by finance")
|
|
162
|
+
result = agent.resume
|
|
140
163
|
```
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
That is the smallest in-process flow. With a durable store, a controller,
|
|
166
|
+
route, mutation, job, or console can append the decision using only the Session
|
|
167
|
+
ID and call ID; a later process rebuilds current tools and policy before
|
|
168
|
+
resuming. See [Sessions and control](docs/sessions.md#human-approval).
|
|
144
169
|
|
|
145
|
-
|
|
170
|
+
Tool results can also carry host-only UI data or an explicit failure fact. See
|
|
171
|
+
[Tool contracts](docs/tool-contracts.md) for schemas, validators, normalizers,
|
|
172
|
+
`Mistri::ToolResult`, hooks, and `ends_turn`.
|
|
146
173
|
|
|
147
|
-
|
|
148
|
-
conversation at the next turn boundary; one that arrives as the model
|
|
149
|
-
finishes cleanly extends the run so it gets answered.
|
|
174
|
+
## Sessions and control
|
|
150
175
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Sessions
|
|
156
|
-
|
|
157
|
-
A session is the durable record of a run: an append-only entry log over a
|
|
158
|
-
pluggable store (memory, JSONL files, or your database).
|
|
176
|
+
The default memory store is useful for one process. JSONL persists the complete
|
|
177
|
+
append-only record on local disk without a database:
|
|
159
178
|
|
|
160
179
|
```ruby
|
|
161
|
-
store = Mistri::Stores::JSONL.new("tmp/sessions")
|
|
162
|
-
session = Mistri::Session.new(store:)
|
|
180
|
+
store = Mistri::Stores::JSONL.new("tmp/mistri-sessions")
|
|
181
|
+
session = Mistri::Session.new(store: store)
|
|
163
182
|
|
|
164
|
-
agent = Mistri.agent("claude-opus-4-8", session:)
|
|
183
|
+
agent = Mistri.agent("claude-opus-4-8", session: session)
|
|
165
184
|
agent.run("Start a haiku about the sea.")
|
|
166
185
|
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
# A later process can reload the same session by ID.
|
|
187
|
+
reloaded = Mistri::Session.new(store: store, id: session.id)
|
|
188
|
+
Mistri.agent("claude-opus-4-8", session: reloaded).run("Now finish it.")
|
|
170
189
|
```
|
|
171
190
|
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
For a database-backed store, implement `append(id, entry)` and `load(id)`, or
|
|
192
|
+
use the optional Active Record adapter:
|
|
174
193
|
|
|
175
194
|
```console
|
|
176
195
|
$ bin/rails generate mistri:install AgentEntry
|
|
@@ -178,255 +197,221 @@ $ bin/rails generate mistri:install AgentEntry
|
|
|
178
197
|
|
|
179
198
|
```ruby
|
|
180
199
|
require "mistri/stores/active_record"
|
|
200
|
+
|
|
181
201
|
store = Mistri::Stores::ActiveRecord.new(AgentEntry)
|
|
182
202
|
```
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Long sessions survive their context window: when the conversation grows into
|
|
187
|
-
the reserve headroom, the provider writes a visible structured summary and
|
|
188
|
-
replay continues from it. The full history stays in your store for
|
|
189
|
-
transcript views. On by default whenever the model's window is known;
|
|
190
|
-
`compaction: false` disables it.
|
|
204
|
+
Steering is another append, so a request handler or worker can redirect a live
|
|
205
|
+
exchange without sharing its Agent object:
|
|
191
206
|
|
|
192
207
|
```ruby
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
Mistri::Session.new(store: store, id: session_id).steer(
|
|
209
|
+
"Make the headline blue instead."
|
|
210
|
+
)
|
|
195
211
|
```
|
|
196
212
|
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
Only one Agent may actively call `run` or `resume` for a session at a time.
|
|
214
|
+
Approvals, steering, and worker reports may append concurrently. Read the
|
|
215
|
+
[reliability contract](docs/reliability.md) before connecting tools with
|
|
216
|
+
external side effects.
|
|
199
217
|
|
|
200
|
-
##
|
|
218
|
+
## Streaming into any Ruby application
|
|
201
219
|
|
|
202
|
-
|
|
203
|
-
constrain the final answer natively where they can, and the answer is
|
|
204
|
-
validated client-side everywhere. A violation goes back to the model once,
|
|
205
|
-
then raises. You get a guaranteed shape or a loud error, never silence.
|
|
220
|
+
Every run accepts a block. Handle events directly, or compose a sink:
|
|
206
221
|
|
|
207
222
|
```ruby
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
properties: { "tiers" => { type: "array", items: { type: "string" } } },
|
|
211
|
-
required: ["tiers"],
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
result = agent.task("Extract the pricing tiers from this page.", schema: schema)
|
|
215
|
-
result.output # => { "tiers" => [...] }, parsed and validated
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Skills
|
|
219
|
-
|
|
220
|
-
Expert playbooks with progressive disclosure: each skill costs one line in
|
|
221
|
-
the system prompt until the model decides it is relevant and pulls the full
|
|
222
|
-
body through an auto-provided `read_skill` tool.
|
|
223
|
-
|
|
224
|
-
```ruby
|
|
225
|
-
agent = Mistri.agent("claude-opus-4-8", skills: "app/skills") # or an array of Mistri::Skill
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
A skill is a `SKILL.md` (or flat `.md`) with `name:`/`description:`
|
|
229
|
-
frontmatter, or built from database rows with
|
|
230
|
-
`Mistri::Skill.new(name:, description:, body:)`.
|
|
223
|
+
sse = Mistri::Sinks::SSE.new(stream) # any IO-like object
|
|
224
|
+
sink = Mistri::Sinks::Coalesced.new(sse) # merge token bursts to UI speed
|
|
231
225
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
An agent as a markdown file: YAML frontmatter for config, the body as the
|
|
235
|
-
prompt, `{placeholders}` filled at build time (unfilled ones raise). Tool
|
|
236
|
-
names and any extra keys stay your vocabulary; the gem only reads the
|
|
237
|
-
file.
|
|
238
|
-
|
|
239
|
-
```ruby
|
|
240
|
-
definition = Mistri::Definition.load("app/agents/trip_planner.md")
|
|
241
|
-
agent = Mistri.agent(definition.model,
|
|
242
|
-
system: definition.render(first_name: traveler.first_name),
|
|
243
|
-
tools: registry.build(definition.tools, traveler))
|
|
226
|
+
agent.run(input, &sink)
|
|
244
227
|
```
|
|
245
228
|
|
|
246
|
-
|
|
229
|
+
`Mistri::Sinks::ActionCable` is available for Rails, but no Railtie is required.
|
|
230
|
+
The same event stream works in Sinatra, Rack, a WebSocket server, a background
|
|
231
|
+
job, or a test. Event types form an extensible union; consumers should handle
|
|
232
|
+
the types they use and ignore the rest.
|
|
247
233
|
|
|
248
|
-
|
|
249
|
-
child's window, and only the final answer returns. Children run on their own
|
|
250
|
-
sessions in your store, linked in the parent transcript; their events stream
|
|
251
|
-
into the parent tagged with an `origin`.
|
|
234
|
+
## Long conversations and structured tasks
|
|
252
235
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
system: "Research. Report findings only.", tools: [fetch_page],
|
|
258
|
-
)
|
|
259
|
-
agent = Mistri.agent("claude-opus-4-8", tools: [researcher.tool])
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Or hand the model an open spawn tool and let it compose its own workers:
|
|
263
|
-
a name for the event stream, instructions, a tool subset, and a
|
|
264
|
-
host-allowlisted model per child. Several spawns in one turn fan out in
|
|
265
|
-
parallel:
|
|
236
|
+
Compaction is on by default for catalogued models. When a session enters the
|
|
237
|
+
model's safe headroom, Mistri asks the provider for a visible structured
|
|
238
|
+
summary, appends it, and continues from the summary plus a recent tail. The
|
|
239
|
+
exact history remains in the store for transcripts.
|
|
266
240
|
|
|
267
241
|
```ruby
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
## Editing documents
|
|
242
|
+
agent.context_usage
|
|
243
|
+
# => { tokens: 141_000, window: 1_000_000, fraction: 0.141 }
|
|
272
244
|
|
|
273
|
-
|
|
274
|
-
`list_files`) work over a workspace: a directory, memory, ActiveRecord, or
|
|
275
|
-
a single value anywhere, like one database column holding a page:
|
|
276
|
-
|
|
277
|
-
```ruby
|
|
278
|
-
workspace = Mistri::Workspace::Single.new(
|
|
279
|
-
read: -> { page.html },
|
|
280
|
-
write: ->(html) { page.update!(html: html) },
|
|
281
|
-
path: "hero.html",
|
|
282
|
-
)
|
|
283
|
-
agent = Mistri.agent("claude-opus-4-8", tools: Mistri::Tools.files(workspace))
|
|
245
|
+
agent.compact # explicit checkpoint; returns nil when nothing should compact
|
|
284
246
|
```
|
|
285
247
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
## MCP
|
|
291
|
-
|
|
292
|
-
Bridge any Model Context Protocol server's tools into an agent. The client
|
|
293
|
-
speaks Streamable HTTP with zero new dependencies; auth is a token string
|
|
294
|
-
or a lambda that re-resolves once on 401, so refresh logic lives in one
|
|
295
|
-
place. Approval gates compose: a third-party write tool can require a
|
|
296
|
-
human.
|
|
248
|
+
Task mode requires a final JSON value matching a schema. Providers use native
|
|
249
|
+
constraints only when they can represent the contract; Mistri validates the
|
|
250
|
+
answer locally in every case.
|
|
297
251
|
|
|
298
252
|
```ruby
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
253
|
+
schema = {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
"tiers" => { type: "array", items: { type: "string" } },
|
|
257
|
+
},
|
|
258
|
+
required: ["tiers"],
|
|
259
|
+
}
|
|
303
260
|
|
|
304
|
-
|
|
261
|
+
result = agent.task("Extract the pricing tiers.", schema: schema)
|
|
262
|
+
result.output # parsed and validated
|
|
305
263
|
```
|
|
306
264
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`Agent.new`: collisions fail loud instead of one server's tool silently
|
|
311
|
-
shadowing another's.
|
|
312
|
-
|
|
313
|
-
Local stdio servers spawn as child processes, credentials in their
|
|
314
|
-
environment. That is also the whole "give the agent a browser" story:
|
|
315
|
-
|
|
316
|
-
```ruby
|
|
317
|
-
browser = Mistri::MCP::Client.new(
|
|
318
|
-
command: ["npx", "-y", "@playwright/mcp@latest", "--browser", "chrome", "--headless"],
|
|
319
|
-
)
|
|
320
|
-
agent = Mistri.agent("claude-opus-4-8",
|
|
321
|
-
tools: Mistri::MCP.tools(browser, allow: %w[browser_navigate browser_snapshot]))
|
|
322
|
-
```
|
|
265
|
+
Task validation applies when that invocation completes normally. An approval
|
|
266
|
+
suspension or `ends_turn` handoff returns with `output` nil; after settlement or
|
|
267
|
+
the human answer, call `task` again if structured output is still required.
|
|
323
268
|
|
|
324
|
-
|
|
325
|
-
|
|
269
|
+
See [Sessions and control](docs/sessions.md) for compaction boundaries,
|
|
270
|
+
cross-provider replay, stores, approvals, and transcripts.
|
|
326
271
|
|
|
327
|
-
|
|
328
|
-
$ bin/rails generate mistri:mcp McpConnection
|
|
329
|
-
```
|
|
272
|
+
## Sub-agents
|
|
330
273
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
controller, a GraphQL mutation, or a job. Registration happens as your
|
|
335
|
-
application: `client_name:` is yours to set.
|
|
274
|
+
Delegate work into a child session so exploration does not consume the parent's
|
|
275
|
+
context. Only the report returns to the parent, while the full child transcript
|
|
276
|
+
remains linked and inspectable.
|
|
336
277
|
|
|
337
278
|
```ruby
|
|
338
|
-
|
|
339
|
-
name: "
|
|
340
|
-
|
|
279
|
+
researcher = Mistri::SubAgent.new(
|
|
280
|
+
name: "researcher",
|
|
281
|
+
description: "Reads pages and answers factual questions.",
|
|
282
|
+
provider: Mistri.provider("claude-haiku-4-5"),
|
|
283
|
+
system: "Research the question. Return findings and sources.",
|
|
284
|
+
tools: [fetch_page],
|
|
341
285
|
)
|
|
342
|
-
# redirect the user to authorize_url; then, in the callback:
|
|
343
|
-
connection = McpConnection.complete(state: params[:state], code: params[:code])
|
|
344
286
|
|
|
345
|
-
agent = Mistri.agent("claude-opus-4-8", tools:
|
|
287
|
+
agent = Mistri.agent("claude-opus-4-8", tools: [researcher.tool])
|
|
346
288
|
```
|
|
347
289
|
|
|
348
|
-
|
|
290
|
+
Mistri also provides a host-bounded spawn tool, named worker types, background
|
|
291
|
+
dispatch with worker-side runtime reconstruction, exact capability grants,
|
|
292
|
+
reports, steering, stopping, and a management console. See
|
|
293
|
+
[Sub-agents](docs/sub-agents.md), including the runtime, concurrency, and lease
|
|
294
|
+
limits a production dispatcher must understand.
|
|
349
295
|
|
|
350
|
-
|
|
296
|
+
## Model Context Protocol
|
|
351
297
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
sink = Mistri::Sinks::Coalesced.new(cable) # merges token bursts to UI speed
|
|
355
|
-
|
|
356
|
-
agent.run(input, &sink)
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
`Mistri::Sinks::SSE.new(response.stream)` does the same for
|
|
360
|
-
`ActionController::Live`. There is no Railtie and nothing to configure;
|
|
361
|
-
the generator and stores duck-type into any app.
|
|
362
|
-
|
|
363
|
-
## Stopping, budgets, reliability
|
|
298
|
+
Bridge tools over Streamable HTTP or stdio, then apply the same local approval
|
|
299
|
+
and validation boundary:
|
|
364
300
|
|
|
365
301
|
```ruby
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
# Ceilings are opt-in and off by default.
|
|
371
|
-
budget = Mistri::Budget.new(turns: 20, cost_usd: 2.00)
|
|
372
|
-
|
|
373
|
-
# Transient failures (429, 5xx, timeouts) retry with backoff, invisibly to
|
|
374
|
-
# the model. On by default; retries: false disables.
|
|
375
|
-
policy = Mistri::RetryPolicy.new(attempts: 3)
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Retries are invisible to the model but not to your UI: each backoff emits a
|
|
379
|
-
`:retry` event carrying `attempt`, `max_attempts`, and `delay`, so a sink can
|
|
380
|
-
show a live "reconnecting" state instead of a silent spinner. Terminal events
|
|
381
|
-
are loop-owned: `:done` and `:error` reach the subscriber only for the
|
|
382
|
-
accepted attempt, so a recovered retry never flashes an error it then walks
|
|
383
|
-
back.
|
|
302
|
+
client = Mistri::MCP::Client.new(
|
|
303
|
+
url: "https://mcp.linear.app/mcp",
|
|
304
|
+
token: -> { connection.bearer_token },
|
|
305
|
+
)
|
|
384
306
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
when :done, :error then clear_banner
|
|
391
|
-
end
|
|
392
|
-
end
|
|
307
|
+
tools = Mistri::MCP.tools(
|
|
308
|
+
client,
|
|
309
|
+
prefix: "linear",
|
|
310
|
+
gates: { "create_issue" => true },
|
|
311
|
+
)
|
|
393
312
|
```
|
|
394
313
|
|
|
395
|
-
|
|
314
|
+
Remote URLs default to public HTTPS with validated, pinned DNS answers. Inbound
|
|
315
|
+
records are size-bounded. An unconfirmed `tools/call` is never replayed
|
|
316
|
+
automatically because the server may already have committed its side effect.
|
|
317
|
+
OAuth services are storage-agnostic; the Rails connection generator is one
|
|
318
|
+
optional host adapter.
|
|
319
|
+
|
|
320
|
+
Read [MCP](docs/mcp.md) for stdio, OAuth, private-network policy, schema
|
|
321
|
+
authority, response limits, and large-result resource links.
|
|
322
|
+
|
|
323
|
+
## Skills, definitions, and workspaces
|
|
324
|
+
|
|
325
|
+
- **Skills** expose one-line descriptions until the model requests the full
|
|
326
|
+
`SKILL.md`, keeping the system prompt small.
|
|
327
|
+
- **Definitions** load an agent's model, tool names, and prompt from Markdown
|
|
328
|
+
with YAML frontmatter and checked placeholders.
|
|
329
|
+
- **Workspaces** give the built-in read, write, edit, find, and list tools a
|
|
330
|
+
directory, memory value, Active Record table, or single host-owned value.
|
|
331
|
+
|
|
332
|
+
The edit engine refuses ambiguous matches and, when a workspace supports
|
|
333
|
+
conditional writes, rebases anchored edits without losing unrelated concurrent
|
|
334
|
+
changes. A Directory workspace rejects lexical escape and traversal through
|
|
335
|
+
existing symlinks in a stable, host-controlled tree; it is not an
|
|
336
|
+
operating-system sandbox. See
|
|
337
|
+
[Context and workspaces](docs/context-and-workspaces.md) and the runnable
|
|
338
|
+
[`page_editor.rb`](examples/page_editor.rb) example.
|
|
339
|
+
|
|
340
|
+
## Reliability contract
|
|
341
|
+
|
|
342
|
+
Mistri makes failures explicit, but it does not pretend distributed side
|
|
343
|
+
effects are exactly once:
|
|
344
|
+
|
|
345
|
+
- Provider requests retry transient failures with backoff. The MCP client never
|
|
346
|
+
replays an ambiguous tool call; the Agent bridge returns the uncertainty as
|
|
347
|
+
an error-marked tool result.
|
|
348
|
+
- A process can crash after a handler commits an external write but before the
|
|
349
|
+
result reaches the session. Tools with side effects must be idempotent or
|
|
350
|
+
reconcilable.
|
|
351
|
+
- Budgets are checked between model-visible turns, so they are soft ceilings.
|
|
352
|
+
- Provider and MCP records default to an 8 MiB per-record limit; streaming has
|
|
353
|
+
no total-size limit while each record remains safe.
|
|
354
|
+
- `Mistri::AbortSignal` is an in-process cooperative signal. Cross-process
|
|
355
|
+
worker control uses the configured lock adapter and is also cooperative.
|
|
356
|
+
|
|
357
|
+
The complete operational contract, event lifecycle, retry behavior, pricing
|
|
358
|
+
rules, and sink guidance live in [Reliability](docs/reliability.md).
|
|
359
|
+
|
|
360
|
+
## Documentation
|
|
361
|
+
|
|
362
|
+
All primary documentation is versioned with the code and renders directly on
|
|
363
|
+
GitHub:
|
|
364
|
+
|
|
365
|
+
| Guide | What it covers |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| [Documentation index](docs/README.md) | The complete task-oriented map |
|
|
368
|
+
| [Tool contracts](docs/tool-contracts.md) | Schemas, validation, approval, hooks, results, and handoff |
|
|
369
|
+
| [Sessions and control](docs/sessions.md) | Stores, replay, approvals, steering, compaction, and transcripts |
|
|
370
|
+
| [Context and workspaces](docs/context-and-workspaces.md) | Skills, definitions, context transforms, host-owned memory, and editable documents |
|
|
371
|
+
| [Sub-agents](docs/sub-agents.md) | Specialists, spawning, dispatch, locks, reports, and control |
|
|
372
|
+
| [MCP](docs/mcp.md) | HTTP and stdio, OAuth, egress policy, limits, and schema handling |
|
|
373
|
+
| [Reliability](docs/reliability.md) | Retries, events, budgets, stopping, concurrency, and side effects |
|
|
374
|
+
| [Upgrade guide](UPGRADING.md) | Required host changes between releases |
|
|
375
|
+
| [Changelog](CHANGELOG.md) | Complete release history and exact behavior changes |
|
|
376
|
+
|
|
377
|
+
## Testing and contributing
|
|
378
|
+
|
|
379
|
+
The default suite is hermetic and needs no API keys:
|
|
396
380
|
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
agent.run("What trend does this chart show?", images: [photo])
|
|
401
|
-
|
|
402
|
-
Mistri.agent("gpt-5.5", provider_options: { reasoning: { effort: "high" } })
|
|
403
|
-
Mistri.agent("claude-opus-4-8", provider_options: { cache: false })
|
|
381
|
+
```console
|
|
382
|
+
$ bundle exec rake test
|
|
383
|
+
$ bundle exec rubocop
|
|
404
384
|
```
|
|
405
385
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`rake test` is hermetic and fast. `rake integration` runs every feature end
|
|
409
|
-
to end against real provider APIs, once per model in the matrix: an
|
|
410
|
-
Anthropic, an OpenAI, and a Gemini model by default. Scenarios assert that
|
|
411
|
-
coined codenames (a ghost of a word like `Wraithowyn` exists in no training
|
|
412
|
-
data) flowed through tool results, summaries, and child agents: proof of
|
|
413
|
-
information flow, not model knowledge.
|
|
386
|
+
Live tests read provider keys from the gitignored
|
|
387
|
+
`.env.development.local` file:
|
|
414
388
|
|
|
415
389
|
```console
|
|
390
|
+
$ MISTRI_LIVE=1 bundle exec rake test
|
|
416
391
|
$ bundle exec rake integration
|
|
417
392
|
$ MISTRI_INTEGRATION_MODELS=claude-opus-4-8 bundle exec rake integration
|
|
418
393
|
```
|
|
419
394
|
|
|
420
|
-
|
|
395
|
+
The integration matrix exercises critical end-to-end scenarios against one
|
|
396
|
+
Anthropic, OpenAI, and Gemini model by default when the corresponding keys are
|
|
397
|
+
present. See
|
|
398
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for the project contract.
|
|
399
|
+
|
|
400
|
+
## Compatibility and support
|
|
401
|
+
|
|
402
|
+
Mistri supports Ruby 3.2 and newer. It is pre-1.0: minor releases may include
|
|
403
|
+
intentional contract changes with an explicit migration path. Read
|
|
404
|
+
[UPGRADING.md](UPGRADING.md) and [CHANGELOG.md](CHANGELOG.md) before upgrading.
|
|
421
405
|
|
|
422
|
-
|
|
423
|
-
|
|
406
|
+
Use [GitHub issues](https://github.com/mcheemaa/mistri/issues) for reproducible
|
|
407
|
+
bugs and focused feature proposals. Report vulnerabilities privately as
|
|
408
|
+
described in [SECURITY.md](SECURITY.md).
|
|
424
409
|
|
|
425
410
|
## Credits
|
|
426
411
|
|
|
427
412
|
Mistri's architecture is informed by [pi](https://github.com/badlogic/pi-mono)
|
|
428
|
-
by Mario Zechner. See NOTICE.
|
|
413
|
+
by Mario Zechner. See [NOTICE](NOTICE).
|
|
429
414
|
|
|
430
415
|
## License
|
|
431
416
|
|
|
432
|
-
MIT. See LICENSE.
|
|
417
|
+
MIT. See [LICENSE](LICENSE).
|