mistri 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +596 -3
  3. data/CONTRIBUTING.md +52 -0
  4. data/README.md +291 -306
  5. data/SECURITY.md +40 -0
  6. data/UPGRADING.md +640 -0
  7. data/assets/logo-animated.svg +30 -0
  8. data/assets/logo-dark.svg +14 -0
  9. data/assets/logo-light.svg +14 -0
  10. data/assets/logo.svg +14 -0
  11. data/assets/social-preview.png +0 -0
  12. data/docs/README.md +87 -0
  13. data/docs/context-and-workspaces.md +378 -0
  14. data/docs/mcp.md +366 -0
  15. data/docs/reliability.md +450 -0
  16. data/docs/sessions.md +295 -0
  17. data/docs/sub-agents.md +401 -0
  18. data/docs/tool-contracts.md +324 -0
  19. data/examples/approval.rb +36 -0
  20. data/examples/browser.rb +27 -0
  21. data/examples/page_editor.rb +31 -0
  22. data/examples/quickstart.rb +21 -0
  23. data/lib/generators/mistri/install/install_generator.rb +7 -3
  24. data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
  25. data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
  26. data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
  27. data/lib/mistri/abort_signal.rb +10 -0
  28. data/lib/mistri/agent.rb +635 -108
  29. data/lib/mistri/budget.rb +26 -1
  30. data/lib/mistri/child.rb +186 -0
  31. data/lib/mistri/compaction.rb +26 -10
  32. data/lib/mistri/compactor.rb +35 -12
  33. data/lib/mistri/console.rb +209 -0
  34. data/lib/mistri/content.rb +9 -3
  35. data/lib/mistri/dispatchers.rb +49 -0
  36. data/lib/mistri/errors.rb +83 -4
  37. data/lib/mistri/event.rb +30 -8
  38. data/lib/mistri/event_delivery.rb +60 -0
  39. data/lib/mistri/locks/rails_cache.rb +48 -0
  40. data/lib/mistri/locks.rb +141 -0
  41. data/lib/mistri/mcp/client.rb +74 -19
  42. data/lib/mistri/mcp/egress.rb +216 -0
  43. data/lib/mistri/mcp/oauth.rb +476 -127
  44. data/lib/mistri/mcp/wires.rb +115 -23
  45. data/lib/mistri/mcp.rb +43 -9
  46. data/lib/mistri/message.rb +21 -11
  47. data/lib/mistri/models.rb +160 -22
  48. data/lib/mistri/providers/anthropic/assembler.rb +282 -44
  49. data/lib/mistri/providers/anthropic/serializer.rb +14 -9
  50. data/lib/mistri/providers/anthropic.rb +29 -6
  51. data/lib/mistri/providers/fake.rb +36 -6
  52. data/lib/mistri/providers/gemini/assembler.rb +148 -21
  53. data/lib/mistri/providers/gemini/serializer.rb +78 -9
  54. data/lib/mistri/providers/gemini.rb +31 -5
  55. data/lib/mistri/providers/openai/assembler.rb +337 -60
  56. data/lib/mistri/providers/openai/serializer.rb +13 -12
  57. data/lib/mistri/providers/openai.rb +29 -5
  58. data/lib/mistri/providers/schema_capabilities.rb +214 -0
  59. data/lib/mistri/result.rb +8 -3
  60. data/lib/mistri/retry_policy.rb +2 -2
  61. data/lib/mistri/schema.rb +893 -75
  62. data/lib/mistri/session.rb +649 -47
  63. data/lib/mistri/sinks/coalesced.rb +17 -10
  64. data/lib/mistri/skill.rb +1 -1
  65. data/lib/mistri/skills.rb +1 -1
  66. data/lib/mistri/spawner.rb +316 -0
  67. data/lib/mistri/sse.rb +57 -14
  68. data/lib/mistri/stores/active_record.rb +22 -7
  69. data/lib/mistri/stores/jsonl.rb +3 -1
  70. data/lib/mistri/stores/memory.rb +21 -2
  71. data/lib/mistri/sub_agent/execution.rb +81 -0
  72. data/lib/mistri/sub_agent/runtime.rb +297 -0
  73. data/lib/mistri/sub_agent.rb +238 -103
  74. data/lib/mistri/task_output.rb +58 -0
  75. data/lib/mistri/tool.rb +102 -13
  76. data/lib/mistri/tool_arguments.rb +377 -0
  77. data/lib/mistri/tool_call.rb +43 -9
  78. data/lib/mistri/tool_context.rb +7 -5
  79. data/lib/mistri/tool_executor.rb +117 -26
  80. data/lib/mistri/tool_result.rb +15 -10
  81. data/lib/mistri/tools/edit_file.rb +62 -8
  82. data/lib/mistri/tools.rb +41 -4
  83. data/lib/mistri/transport.rb +149 -44
  84. data/lib/mistri/usage.rb +65 -13
  85. data/lib/mistri/version.rb +1 -1
  86. data/lib/mistri/workspace/active_record.rb +183 -3
  87. data/lib/mistri/workspace/directory.rb +28 -8
  88. data/lib/mistri/workspace/memory.rb +34 -9
  89. data/lib/mistri/workspace/single.rb +62 -5
  90. data/lib/mistri/workspace.rb +39 -0
  91. data/lib/mistri.rb +17 -1
  92. data/mistri.gemspec +34 -0
  93. metadata +38 -3
@@ -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