mistri 0.5.0 → 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 +469 -4
- data/CONTRIBUTING.md +52 -0
- data/README.md +289 -385
- 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/agent.rb +575 -55
- data/lib/mistri/budget.rb +26 -1
- data/lib/mistri/child.rb +72 -16
- data/lib/mistri/compaction.rb +26 -10
- data/lib/mistri/compactor.rb +34 -11
- data/lib/mistri/console.rb +28 -7
- data/lib/mistri/content.rb +9 -3
- data/lib/mistri/dispatchers.rb +14 -12
- data/lib/mistri/errors.rb +83 -4
- data/lib/mistri/event.rb +24 -8
- data/lib/mistri/event_delivery.rb +60 -0
- data/lib/mistri/locks.rb +3 -3
- 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 +42 -8
- 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 +26 -10
- 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 +1 -1
- data/lib/mistri/schema.rb +893 -75
- data/lib/mistri/session.rb +560 -48
- data/lib/mistri/sinks/coalesced.rb +17 -10
- data/lib/mistri/spawner.rb +111 -61
- data/lib/mistri/sse.rb +57 -14
- data/lib/mistri/stores/active_record.rb +1 -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 +124 -87
- data/lib/mistri/task_output.rb +24 -6
- data/lib/mistri/tool.rb +93 -13
- data/lib/mistri/tool_arguments.rb +377 -0
- data/lib/mistri/tool_call.rb +43 -9
- data/lib/mistri/tool_context.rb +4 -2
- 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 +6 -1
- data/mistri.gemspec +34 -0
- metadata +31 -3
data/README.md
CHANGED
|
@@ -7,189 +7,189 @@
|
|
|
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
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
weather = Mistri::Tool.define(
|
|
31
|
-
"get_weather", "Current weather for a city.",
|
|
32
|
-
schema: -> { string :city, "City name", required: true },
|
|
33
|
-
) do |args|
|
|
34
|
-
Weather.for(args["city"])
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
agent = Mistri.agent("claude-opus-4-8", tools: [weather]) # reads ANTHROPIC_API_KEY
|
|
38
|
-
|
|
39
|
-
agent.run("What should I wear in Lahore today?") do |event|
|
|
40
|
-
print event.delta if event.type == :text_delta
|
|
41
|
-
end
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Why Mistri
|
|
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.
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
your own store. Runs stop, resume, steer, and compact from any process.
|
|
48
|
-
- **Fire-and-forget human approval.** A gated tool suspends the run and
|
|
49
|
-
returns immediately. The approval can arrive two days later from a bare
|
|
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`).
|
|
31
|
+
## Start in sixty seconds
|
|
57
32
|
|
|
58
|
-
|
|
33
|
+
Add Mistri to your bundle:
|
|
59
34
|
|
|
60
35
|
```ruby
|
|
61
36
|
gem "mistri"
|
|
62
37
|
```
|
|
63
38
|
|
|
64
|
-
|
|
39
|
+
Then define a tool and run an agent:
|
|
65
40
|
|
|
66
41
|
```ruby
|
|
67
|
-
|
|
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.
|
|
42
|
+
require "mistri"
|
|
84
43
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
string :city, "City name", required: true
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Weather.for(args["city"], units: args["units"] || "celsius")
|
|
44
|
+
weather = Mistri::Tool.define(
|
|
45
|
+
"get_weather", "Current weather for a city.",
|
|
46
|
+
schema: -> { string :city, "City name", required: true },
|
|
47
|
+
) do |args|
|
|
48
|
+
{ city: args.fetch("city"), forecast: "34 C and clear" }
|
|
91
49
|
end
|
|
92
|
-
```
|
|
93
50
|
|
|
94
|
-
|
|
95
|
-
interface. The `ui` payload rides the `:tool_result` event and persists with
|
|
96
|
-
the session, but never reaches a provider:
|
|
51
|
+
agent = Mistri.agent("claude-opus-4-8", tools: [weather])
|
|
97
52
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
page = apply(args)
|
|
101
|
-
Mistri::ToolResult.new(content: "Saved.", ui: { "html" => page })
|
|
53
|
+
result = agent.run("What should I wear in Lahore today? One sentence.") do |event|
|
|
54
|
+
print event.delta if event.type == :text_delta
|
|
102
55
|
end
|
|
56
|
+
puts
|
|
103
57
|
```
|
|
104
58
|
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
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).
|
|
109
65
|
|
|
110
|
-
|
|
111
|
-
agent = Mistri.agent("claude-opus-4-8", tools: tools,
|
|
112
|
-
context: { traveler: current_traveler })
|
|
66
|
+
Runnable examples live in [`examples/`](examples).
|
|
113
67
|
|
|
114
|
-
|
|
115
|
-
Bookings.create!(args, traveler: context.app[:traveler])
|
|
116
|
-
end
|
|
117
|
-
```
|
|
68
|
+
## Why Mistri
|
|
118
69
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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`.
|
|
125
126
|
|
|
126
127
|
```ruby
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
)
|
|
131
140
|
end
|
|
141
|
+
|
|
142
|
+
agent = Mistri.agent(
|
|
143
|
+
"claude-opus-4-8",
|
|
144
|
+
tools: [book_hotel],
|
|
145
|
+
context: { traveler: current_traveler },
|
|
146
|
+
)
|
|
132
147
|
```
|
|
133
148
|
|
|
134
|
-
|
|
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.
|
|
135
153
|
|
|
136
|
-
|
|
137
|
-
run suspends instead of executing it, instantly, with no thread waiting.
|
|
138
|
-
The decision is a one-line session write from any process, any time later;
|
|
139
|
-
`resume` settles it and carries on.
|
|
154
|
+
When a gated call arrives:
|
|
140
155
|
|
|
141
156
|
```ruby
|
|
142
|
-
book_hotel = Mistri::Tool.define("book_hotel", "Books the chosen hotel.",
|
|
143
|
-
needs_approval: ->(args) { args["total_usd"].to_i > 500 }) do |args|
|
|
144
|
-
Bookings.create!(args)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
157
|
result = agent.run("Book the corner suite for the Lisbon trip")
|
|
148
|
-
result.awaiting_approval?
|
|
158
|
+
result.awaiting_approval? # => true; the handler did not run
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# Then, in a worker:
|
|
154
|
-
Mistri.agent("claude-opus-4-8", tools: tools, session: reloaded).resume
|
|
160
|
+
call = result.pending.fetch(0)
|
|
161
|
+
agent.session.approve(call.id, note: "Approved by finance")
|
|
162
|
+
result = agent.resume
|
|
155
163
|
```
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
|
|
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).
|
|
159
169
|
|
|
160
|
-
|
|
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`.
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
conversation at the next turn boundary; one that arrives as the model
|
|
164
|
-
finishes cleanly extends the run so it gets answered.
|
|
174
|
+
## Sessions and control
|
|
165
175
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
```
|
|
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
|
-
|
|
174
|
-
## Sessions
|
|
175
|
-
|
|
176
|
-
A session is the durable record of a run: an append-only entry log over a
|
|
177
|
-
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:
|
|
178
178
|
|
|
179
179
|
```ruby
|
|
180
|
-
store = Mistri::Stores::JSONL.new("tmp/sessions")
|
|
181
|
-
session = Mistri::Session.new(store:)
|
|
180
|
+
store = Mistri::Stores::JSONL.new("tmp/mistri-sessions")
|
|
181
|
+
session = Mistri::Session.new(store: store)
|
|
182
182
|
|
|
183
|
-
agent = Mistri.agent("claude-opus-4-8", session:)
|
|
183
|
+
agent = Mistri.agent("claude-opus-4-8", session: session)
|
|
184
184
|
agent.run("Start a haiku about the sea.")
|
|
185
185
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
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.")
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
For a database-backed store, implement `append(id, entry)` and `load(id)`, or
|
|
192
|
+
use the optional Active Record adapter:
|
|
193
193
|
|
|
194
194
|
```console
|
|
195
195
|
$ bin/rails generate mistri:install AgentEntry
|
|
@@ -197,317 +197,221 @@ $ bin/rails generate mistri:install AgentEntry
|
|
|
197
197
|
|
|
198
198
|
```ruby
|
|
199
199
|
require "mistri/stores/active_record"
|
|
200
|
+
|
|
200
201
|
store = Mistri::Stores::ActiveRecord.new(AgentEntry)
|
|
201
202
|
```
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
Long sessions survive their context window: when the conversation grows into
|
|
206
|
-
the reserve headroom, the provider writes a visible structured summary and
|
|
207
|
-
replay continues from it. The full history stays in your store for
|
|
208
|
-
transcript views. On by default whenever the model's window is known;
|
|
209
|
-
`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:
|
|
210
206
|
|
|
211
207
|
```ruby
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
Mistri::Session.new(store: store, id: session_id).steer(
|
|
209
|
+
"Make the headline blue instead."
|
|
210
|
+
)
|
|
214
211
|
```
|
|
215
212
|
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
218
217
|
|
|
219
|
-
##
|
|
218
|
+
## Streaming into any Ruby application
|
|
220
219
|
|
|
221
|
-
|
|
222
|
-
constrain the final answer natively where they can, and the answer is
|
|
223
|
-
validated client-side everywhere. A violation goes back to the model once,
|
|
224
|
-
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:
|
|
225
221
|
|
|
226
222
|
```ruby
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
properties: { "tiers" => { type: "array", items: { type: "string" } } },
|
|
230
|
-
required: ["tiers"],
|
|
231
|
-
}
|
|
223
|
+
sse = Mistri::Sinks::SSE.new(stream) # any IO-like object
|
|
224
|
+
sink = Mistri::Sinks::Coalesced.new(sse) # merge token bursts to UI speed
|
|
232
225
|
|
|
233
|
-
|
|
234
|
-
result.output # => { "tiers" => [...] }, parsed and validated
|
|
226
|
+
agent.run(input, &sink)
|
|
235
227
|
```
|
|
236
228
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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.
|
|
240
233
|
|
|
241
|
-
##
|
|
234
|
+
## Long conversations and structured tasks
|
|
242
235
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
246
240
|
|
|
247
241
|
```ruby
|
|
248
|
-
agent
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
A skill is a `SKILL.md` (or flat `.md`) with `name:`/`description:`
|
|
252
|
-
frontmatter, or built from database rows with
|
|
253
|
-
`Mistri::Skill.new(name:, description:, body:)`.
|
|
254
|
-
|
|
255
|
-
## Definitions
|
|
242
|
+
agent.context_usage
|
|
243
|
+
# => { tokens: 141_000, window: 1_000_000, fraction: 0.141 }
|
|
256
244
|
|
|
257
|
-
|
|
258
|
-
prompt, `{placeholders}` filled at build time (unfilled ones raise). Tool
|
|
259
|
-
names and any extra keys stay your vocabulary; the gem only reads the
|
|
260
|
-
file.
|
|
261
|
-
|
|
262
|
-
```ruby
|
|
263
|
-
definition = Mistri::Definition.load("app/agents/trip_planner.md")
|
|
264
|
-
agent = Mistri.agent(definition.model,
|
|
265
|
-
system: definition.render(first_name: traveler.first_name),
|
|
266
|
-
tools: registry.build(definition.tools, traveler))
|
|
245
|
+
agent.compact # explicit checkpoint; returns nil when nothing should compact
|
|
267
246
|
```
|
|
268
247
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
child's window, and only the final answer returns. Children run on their own
|
|
273
|
-
sessions in your store, linked in the parent transcript; their events stream
|
|
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.
|
|
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.
|
|
277
251
|
|
|
278
252
|
```ruby
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Or hand the model an open spawn tool and let it compose its own workers:
|
|
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:
|
|
293
|
-
|
|
294
|
-
```ruby
|
|
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:
|
|
253
|
+
schema = {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
"tiers" => { type: "array", items: { type: "string" } },
|
|
257
|
+
},
|
|
258
|
+
required: ["tiers"],
|
|
259
|
+
}
|
|
315
260
|
|
|
316
|
-
|
|
317
|
-
|
|
261
|
+
result = agent.task("Extract the pricing tiers.", schema: schema)
|
|
262
|
+
result.output # parsed and validated
|
|
318
263
|
```
|
|
319
264
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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:
|
|
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.
|
|
336
268
|
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
347
|
-
```
|
|
269
|
+
See [Sessions and control](docs/sessions.md) for compaction boundaries,
|
|
270
|
+
cross-provider replay, stores, approvals, and transcripts.
|
|
348
271
|
|
|
349
|
-
##
|
|
272
|
+
## Sub-agents
|
|
350
273
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
354
277
|
|
|
355
278
|
```ruby
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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],
|
|
360
285
|
)
|
|
361
|
-
agent = Mistri.agent("claude-opus-4-8", tools: Mistri::Tools.files(workspace))
|
|
362
|
-
```
|
|
363
286
|
|
|
364
|
-
|
|
365
|
-
match refuses (never silently edits the wrong place), and a near-miss error
|
|
366
|
-
names the closest region so the model's retry is one-shot.
|
|
367
|
-
|
|
368
|
-
## MCP
|
|
369
|
-
|
|
370
|
-
Bridge any Model Context Protocol server's tools into an agent. The client
|
|
371
|
-
speaks Streamable HTTP with zero new dependencies; auth is a token string
|
|
372
|
-
or a lambda that re-resolves once on 401, so refresh logic lives in one
|
|
373
|
-
place. Approval gates compose: a third-party write tool can require a
|
|
374
|
-
human.
|
|
375
|
-
|
|
376
|
-
```ruby
|
|
377
|
-
client = Mistri::MCP::Client.new(url: "https://mcp.linear.app/mcp",
|
|
378
|
-
token: -> { connection.bearer_token })
|
|
379
|
-
tools = Mistri::MCP.tools(client, prefix: "linear",
|
|
380
|
-
gates: { "create_issue" => true })
|
|
381
|
-
|
|
382
|
-
agent = Mistri.agent("claude-opus-4-8", tools: tools)
|
|
383
|
-
```
|
|
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
|
-
|
|
391
|
-
Local stdio servers spawn as child processes, credentials in their
|
|
392
|
-
environment. That is also the whole "give the agent a browser" story:
|
|
393
|
-
|
|
394
|
-
```ruby
|
|
395
|
-
browser = Mistri::MCP::Client.new(
|
|
396
|
-
command: ["npx", "-y", "@playwright/mcp@latest", "--browser", "chrome", "--headless"],
|
|
397
|
-
)
|
|
398
|
-
agent = Mistri.agent("claude-opus-4-8",
|
|
399
|
-
tools: Mistri::MCP.tools(browser, allow: %w[browser_navigate browser_snapshot]))
|
|
287
|
+
agent = Mistri.agent("claude-opus-4-8", tools: [researcher.tool])
|
|
400
288
|
```
|
|
401
289
|
|
|
402
|
-
|
|
403
|
-
|
|
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.
|
|
404
295
|
|
|
405
|
-
|
|
406
|
-
$ bin/rails generate mistri:mcp McpConnection
|
|
407
|
-
```
|
|
296
|
+
## Model Context Protocol
|
|
408
297
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`.complete`, `.refresh`) are storage-agnostic, so the same flow works from a
|
|
412
|
-
controller, a GraphQL mutation, or a job. Registration happens as your
|
|
413
|
-
application: `client_name:` is yours to set.
|
|
298
|
+
Bridge tools over Streamable HTTP or stdio, then apply the same local approval
|
|
299
|
+
and validation boundary:
|
|
414
300
|
|
|
415
301
|
```ruby
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
302
|
+
client = Mistri::MCP::Client.new(
|
|
303
|
+
url: "https://mcp.linear.app/mcp",
|
|
304
|
+
token: -> { connection.bearer_token },
|
|
419
305
|
)
|
|
420
|
-
# redirect the user to authorize_url; then, in the callback:
|
|
421
|
-
connection = McpConnection.complete(state: params[:state], code: params[:code])
|
|
422
|
-
|
|
423
|
-
agent = Mistri.agent("claude-opus-4-8", tools: connection.tools(prefix: "linear"))
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## Streaming into Rails
|
|
427
306
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
agent.run(input, &sink)
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
`Mistri::Sinks::SSE.new(response.stream)` does the same for
|
|
438
|
-
`ActionController::Live`. There is no Railtie and nothing to configure;
|
|
439
|
-
the generator and stores duck-type into any app.
|
|
440
|
-
|
|
441
|
-
## Stopping, budgets, reliability
|
|
442
|
-
|
|
443
|
-
```ruby
|
|
444
|
-
# Trip the signal from anywhere; the partial turn persists, resume is clean.
|
|
445
|
-
signal = Mistri::AbortSignal.new
|
|
446
|
-
agent.run("Draft a long essay.", signal: signal)
|
|
447
|
-
|
|
448
|
-
# Ceilings are opt-in and off by default.
|
|
449
|
-
budget = Mistri::Budget.new(turns: 20, cost_usd: 2.00)
|
|
450
|
-
|
|
451
|
-
# Transient failures (429, 5xx, timeouts) retry with backoff, invisibly to
|
|
452
|
-
# the model. On by default; retries: false disables.
|
|
453
|
-
policy = Mistri::RetryPolicy.new(attempts: 3)
|
|
454
|
-
```
|
|
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
|
|
307
|
+
tools = Mistri::MCP.tools(
|
|
308
|
+
client,
|
|
309
|
+
prefix: "linear",
|
|
310
|
+
gates: { "create_issue" => true },
|
|
311
|
+
)
|
|
471
312
|
```
|
|
472
313
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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:
|
|
479
380
|
|
|
480
|
-
|
|
481
|
-
|
|
381
|
+
```console
|
|
382
|
+
$ bundle exec rake test
|
|
383
|
+
$ bundle exec rubocop
|
|
482
384
|
```
|
|
483
385
|
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
491
|
-
Anthropic, an OpenAI, and a Gemini model by default. Scenarios assert that
|
|
492
|
-
coined codenames (a ghost of a word like `Wraithowyn` exists in no training
|
|
493
|
-
data) flowed through tool results, summaries, and child agents: proof of
|
|
494
|
-
information flow, not model knowledge.
|
|
386
|
+
Live tests read provider keys from the gitignored
|
|
387
|
+
`.env.development.local` file:
|
|
495
388
|
|
|
496
389
|
```console
|
|
390
|
+
$ MISTRI_LIVE=1 bundle exec rake test
|
|
497
391
|
$ bundle exec rake integration
|
|
498
392
|
$ MISTRI_INTEGRATION_MODELS=claude-opus-4-8 bundle exec rake integration
|
|
499
393
|
```
|
|
500
394
|
|
|
501
|
-
|
|
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.
|
|
502
405
|
|
|
503
|
-
|
|
504
|
-
|
|
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).
|
|
505
409
|
|
|
506
410
|
## Credits
|
|
507
411
|
|
|
508
412
|
Mistri's architecture is informed by [pi](https://github.com/badlogic/pi-mono)
|
|
509
|
-
by Mario Zechner. See NOTICE.
|
|
413
|
+
by Mario Zechner. See [NOTICE](NOTICE).
|
|
510
414
|
|
|
511
415
|
## License
|
|
512
416
|
|
|
513
|
-
MIT. See LICENSE.
|
|
417
|
+
MIT. See [LICENSE](LICENSE).
|