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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mistri
|
|
4
|
+
# Agent options a child may inherit without replacing lifecycle-owned
|
|
5
|
+
# provider, session, prompt, tools, task, signal, or event state.
|
|
6
|
+
module ChildAgentOptions
|
|
7
|
+
ALLOWED = %i[
|
|
8
|
+
budget max_concurrency transform_context compaction retries skills
|
|
9
|
+
before_tool after_tool context
|
|
10
|
+
].freeze
|
|
11
|
+
private_constant :ALLOWED
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def validate!(options)
|
|
16
|
+
unsupported = options.keys - ALLOWED
|
|
17
|
+
return if unsupported.empty?
|
|
18
|
+
|
|
19
|
+
raise ConfigurationError,
|
|
20
|
+
"unsupported sub-agent options: #{unsupported.sort.join(", ")}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
private_constant :ChildAgentOptions
|
|
24
|
+
|
|
25
|
+
class SubAgent
|
|
26
|
+
DISPATCH_SPEC_VERSION = 1
|
|
27
|
+
DISPATCH_SPEC_KEYS = %w[
|
|
28
|
+
spec_version name session_id parent_session_id type instructions task tool_names model
|
|
29
|
+
].freeze
|
|
30
|
+
UNSET_RUNTIME_FIELD = Object.new.freeze
|
|
31
|
+
private_constant :DISPATCH_SPEC_KEYS, :UNSET_RUNTIME_FIELD
|
|
32
|
+
|
|
33
|
+
# The live dependencies a host constructs for one dispatched child.
|
|
34
|
+
# Mistri verifies the provider and tools against the durable spec; the
|
|
35
|
+
# host owns tenant scope, backend isolation, and the freshness of every
|
|
36
|
+
# object placed here.
|
|
37
|
+
class Runtime
|
|
38
|
+
attr_reader :provider, :system, :tools, :schema, :agent_options
|
|
39
|
+
|
|
40
|
+
def initialize(provider:, system: nil, tools: [], schema: nil, cleanup: nil,
|
|
41
|
+
**agent_options)
|
|
42
|
+
raise ArgumentError, "runtime tools must be an Array" unless tools.is_a?(Array)
|
|
43
|
+
unless cleanup.nil? || cleanup.respond_to?(:call)
|
|
44
|
+
raise ArgumentError, "runtime cleanup must be callable"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@provider = provider
|
|
48
|
+
@system = system
|
|
49
|
+
@tools = Array.new(tools).freeze
|
|
50
|
+
@schema = schema
|
|
51
|
+
@cleanup = cleanup
|
|
52
|
+
@agent_options = agent_options.dup.freeze
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close = @cleanup&.call
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The fail-closed boundary between a durable dispatch spec and live Ruby
|
|
60
|
+
# dependencies. Kept separate from SubAgent's execution lifecycle so each
|
|
61
|
+
# concern stays auditable.
|
|
62
|
+
class RuntimeContract
|
|
63
|
+
Resolved = Data.define(:provider, :system, :tools, :schema, :agent_options)
|
|
64
|
+
private_constant :Resolved
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
def own_spec(spec)
|
|
68
|
+
owned, error = ToolArguments.canonicalize(spec)
|
|
69
|
+
unless error.nil? && owned.is_a?(Hash)
|
|
70
|
+
raise ConfigurationError, "dispatched child spec must be a bounded JSON object"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
owned
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_identity!(spec)
|
|
77
|
+
%w[name session_id].each do |field|
|
|
78
|
+
value = spec[field]
|
|
79
|
+
unless value.is_a?(String) && !value.empty?
|
|
80
|
+
raise ConfigurationError, "dispatched child spec needs a non-empty #{field}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bind_spec(entries, supplied)
|
|
86
|
+
dispatched = entries.find { |entry| entry["type"] == Child::DISPATCHED }
|
|
87
|
+
raise DispatchGrantError, "child session has no durable dispatch grant" unless dispatched
|
|
88
|
+
|
|
89
|
+
stored = dispatched["spec"]
|
|
90
|
+
return supplied if stored.nil? && supplied["spec_version"].nil?
|
|
91
|
+
unless stored
|
|
92
|
+
raise DispatchGrantError, "versioned child is missing its durable dispatch grant"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
authoritative = own_spec(stored)
|
|
96
|
+
unless supplied == authoritative
|
|
97
|
+
raise DispatchGrantError, "queue payload does not match the durable dispatch grant"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
authoritative
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_resolution!(factory:, direct:)
|
|
104
|
+
supplied = direct.values.any? { |value| !value.equal?(UNSET_RUNTIME_FIELD) }
|
|
105
|
+
if factory
|
|
106
|
+
raise ArgumentError, "choose runtime_factory or direct runtime fields" if supplied
|
|
107
|
+
unless factory.respond_to?(:call)
|
|
108
|
+
raise ConfigurationError, "runtime_factory must be callable"
|
|
109
|
+
end
|
|
110
|
+
elsif direct.fetch(:provider).equal?(UNSET_RUNTIME_FIELD)
|
|
111
|
+
raise ConfigurationError, "run_dispatched needs runtime_factory or provider"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_spec!(spec)
|
|
116
|
+
version = spec["spec_version"]
|
|
117
|
+
validate_version!(version)
|
|
118
|
+
validate_v1_shape!(spec) if version == DISPATCH_SPEC_VERSION
|
|
119
|
+
validate_task!(spec["task"])
|
|
120
|
+
validate_model!(spec["model"], version: version)
|
|
121
|
+
validate_tool_names!(spec["tool_names"])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve(spec, factory:, direct:, &factory_failed)
|
|
125
|
+
return resolve_factory(spec, factory, &factory_failed) if factory
|
|
126
|
+
|
|
127
|
+
resolve_direct(direct)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_runtime!(runtime, spec)
|
|
131
|
+
provider = runtime.provider
|
|
132
|
+
system = runtime.system
|
|
133
|
+
tools = runtime.tools
|
|
134
|
+
schema = runtime.schema
|
|
135
|
+
options = runtime.agent_options
|
|
136
|
+
validate_provider!(provider, spec["model"])
|
|
137
|
+
validate_agent_options!(options)
|
|
138
|
+
ordered = validate_tools!(tools, spec.fetch("tool_names"))
|
|
139
|
+
Resolved.new(provider: provider, system: system, tools: ordered,
|
|
140
|
+
schema: schema, agent_options: options)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cleanup(runtime)
|
|
144
|
+
runtime&.close
|
|
145
|
+
nil
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
e
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def validate_version!(version)
|
|
153
|
+
return if version.nil? || version == DISPATCH_SPEC_VERSION
|
|
154
|
+
|
|
155
|
+
raise ConfigurationError,
|
|
156
|
+
"unsupported dispatched child spec version #{version.inspect}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def validate_task!(task)
|
|
160
|
+
return if task.is_a?(String) && !task.empty?
|
|
161
|
+
|
|
162
|
+
raise ConfigurationError, "dispatched child spec needs a non-empty task"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def validate_v1_shape!(spec)
|
|
166
|
+
extra = spec.keys - DISPATCH_SPEC_KEYS
|
|
167
|
+
unless extra.empty?
|
|
168
|
+
raise ConfigurationError,
|
|
169
|
+
"dispatched child spec has unknown fields: #{extra.join(", ")}"
|
|
170
|
+
end
|
|
171
|
+
validate_optional_string!(spec["parent_session_id"], "parent_session_id")
|
|
172
|
+
validate_required_string!(spec["type"], "type")
|
|
173
|
+
validate_optional_string!(spec["instructions"], "instructions")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_model!(model, version:)
|
|
177
|
+
return if version.nil? && model.nil?
|
|
178
|
+
return if model.is_a?(String) && !model.empty?
|
|
179
|
+
|
|
180
|
+
raise ConfigurationError, "dispatched child spec model must be a non-empty string"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def validate_required_string!(value, field)
|
|
184
|
+
return if value.is_a?(String) && !value.empty?
|
|
185
|
+
|
|
186
|
+
raise ConfigurationError,
|
|
187
|
+
"dispatched child spec #{field} must be a non-empty string"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def validate_optional_string!(value, field)
|
|
191
|
+
return if value.nil? || value.is_a?(String)
|
|
192
|
+
|
|
193
|
+
raise ConfigurationError,
|
|
194
|
+
"dispatched child spec #{field} must be a string or null"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_tool_names!(names)
|
|
198
|
+
valid = names.is_a?(Array) &&
|
|
199
|
+
names.all? { |name| name.is_a?(String) && !name.empty? }
|
|
200
|
+
unless valid
|
|
201
|
+
raise ConfigurationError,
|
|
202
|
+
"dispatched child spec tool_names must be non-empty strings"
|
|
203
|
+
end
|
|
204
|
+
return if names.uniq.length == names.length
|
|
205
|
+
|
|
206
|
+
raise ConfigurationError, "dispatched child spec has duplicate tool names"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def resolve_factory(spec, factory)
|
|
210
|
+
runtime = begin
|
|
211
|
+
factory.call(spec)
|
|
212
|
+
rescue DispatchGrantError
|
|
213
|
+
yield if block_given?
|
|
214
|
+
raise ConfigurationError,
|
|
215
|
+
"runtime_factory must not raise Mistri::DispatchGrantError; " \
|
|
216
|
+
"that class is reserved for queue grant verification"
|
|
217
|
+
rescue StandardError
|
|
218
|
+
yield if block_given?
|
|
219
|
+
raise
|
|
220
|
+
end
|
|
221
|
+
return runtime if runtime.instance_of?(Runtime)
|
|
222
|
+
|
|
223
|
+
raise ConfigurationError,
|
|
224
|
+
"runtime_factory must return Mistri::SubAgent::Runtime"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def resolve_direct(direct)
|
|
228
|
+
provider = direct.fetch(:provider)
|
|
229
|
+
Runtime.new(provider: provider,
|
|
230
|
+
system: value_or_nil(direct.fetch(:system)),
|
|
231
|
+
tools: value_or(direct.fetch(:tools), []),
|
|
232
|
+
schema: value_or_nil(direct.fetch(:schema)),
|
|
233
|
+
**value_or(direct.fetch(:agent_options), {}))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def value_or(value, fallback)
|
|
237
|
+
value.equal?(UNSET_RUNTIME_FIELD) ? fallback : value
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def value_or_nil(value) = value_or(value, nil)
|
|
241
|
+
|
|
242
|
+
def validate_provider!(provider, expected_model)
|
|
243
|
+
unless provider.respond_to?(:stream)
|
|
244
|
+
raise ConfigurationError, "background runtime provider must respond to stream"
|
|
245
|
+
end
|
|
246
|
+
return unless expected_model
|
|
247
|
+
|
|
248
|
+
actual = provider.model.to_s if provider.respond_to?(:model)
|
|
249
|
+
return if actual == expected_model
|
|
250
|
+
|
|
251
|
+
raise ConfigurationError,
|
|
252
|
+
"background runtime model #{actual.inspect} does not match " \
|
|
253
|
+
"the granted model #{expected_model.inspect}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def validate_agent_options!(options)
|
|
257
|
+
ChildAgentOptions.validate!(options)
|
|
258
|
+
return unless options.key?(:skills)
|
|
259
|
+
|
|
260
|
+
raise ConfigurationError,
|
|
261
|
+
"background runtime skills would add tools outside the durable grant"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def validate_tools!(tools, expected)
|
|
265
|
+
unless tools.all?(Tool)
|
|
266
|
+
raise ConfigurationError, "background runtime tools must be Mistri::Tool instances"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
names = tools.map(&:name)
|
|
270
|
+
if names.uniq.length != names.length
|
|
271
|
+
raise ConfigurationError, "background runtime has duplicate tool names"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
check_exact_grant!(expected, names)
|
|
275
|
+
by_name = tools.to_h { |tool| [tool.name, tool] }
|
|
276
|
+
ordered = expected.map { |name| by_name.fetch(name) }
|
|
277
|
+
SubAgent.forbid_gated!(ordered)
|
|
278
|
+
ordered
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def check_exact_grant!(expected, names)
|
|
282
|
+
missing = expected - names
|
|
283
|
+
extra = names - expected
|
|
284
|
+
return if missing.empty? && extra.empty?
|
|
285
|
+
|
|
286
|
+
details = []
|
|
287
|
+
details << "missing: #{missing.join(", ")}" if missing.any?
|
|
288
|
+
details << "extra: #{extra.join(", ")}" if extra.any?
|
|
289
|
+
raise ConfigurationError,
|
|
290
|
+
"background runtime tools do not match the durable grant " \
|
|
291
|
+
"(#{details.join("; ")})"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
private_constant :RuntimeContract
|
|
296
|
+
end
|
|
297
|
+
end
|