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,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mistri
4
+ module Providers
5
+ # Keeps provider grammar limits out of task semantics: an incompatible
6
+ # native schema is omitted while the prompt and local validator stay exact.
7
+ module SchemaCapabilities
8
+ COMMON_KEYWORDS = %w[
9
+ type properties required additionalProperties items enum description
10
+ ].freeze
11
+ PROFILES = {
12
+ openai: {
13
+ keywords: COMMON_KEYWORDS,
14
+ root_object: true,
15
+ boolean_schemas: false,
16
+ tuple_arrays: false,
17
+ all_properties_required: true,
18
+ max_depth: 10,
19
+ max_properties: 5000,
20
+ max_enum_values: 1000,
21
+ max_schema_string_chars: 120_000,
22
+ large_enum_threshold: 250,
23
+ max_large_enum_chars: 15_000
24
+ }.freeze,
25
+ anthropic: {
26
+ keywords: COMMON_KEYWORDS,
27
+ root_object: false,
28
+ boolean_schemas: false,
29
+ tuple_arrays: false,
30
+ all_properties_required: false,
31
+ max_optional_properties: 24
32
+ }.freeze,
33
+ gemini: {
34
+ keywords: (COMMON_KEYWORDS + %w[prefixItems format title]).freeze,
35
+ root_object: false,
36
+ boolean_schemas: true,
37
+ tuple_arrays: true,
38
+ all_properties_required: false,
39
+ formats: %w[date-time date time].freeze,
40
+ enum_types: %w[string number integer].freeze
41
+ }.freeze
42
+ }.freeze
43
+
44
+ module_function
45
+
46
+ def derive(schema, provider)
47
+ profile = PROFILES.fetch(provider)
48
+ state = { properties: 0, optional_properties: 0, enum_values: 0,
49
+ schema_string_chars: 0 }
50
+ return unless supported?(schema, profile, state, root: true, depth: 0)
51
+
52
+ schema
53
+ end
54
+
55
+ def supported?(schema, profile, state, depth:, root: false)
56
+ return profile[:boolean_schemas] if schema.equal?(true) || schema.equal?(false)
57
+ return false unless schema_shape_supported?(schema, profile)
58
+
59
+ type = schema["type"]
60
+ depth += 1 if %w[object array].include?(type)
61
+ return false unless node_supported?(schema, type, profile, state, root:, depth:)
62
+
63
+ child_schemas(schema).all? do |member|
64
+ supported?(member, profile, state, depth:)
65
+ end
66
+ end
67
+ private_class_method :supported?
68
+
69
+ def schema_shape_supported?(schema, profile)
70
+ schema.is_a?(Hash) && schema["type"].is_a?(String) &&
71
+ (schema.keys - profile[:keywords]).empty?
72
+ end
73
+ private_class_method :schema_shape_supported?
74
+
75
+ def node_supported?(schema, type, profile, state, root:, depth:)
76
+ within_limit?(depth, profile[:max_depth]) &&
77
+ root_supported?(type, profile, root) &&
78
+ object_supported?(schema, type, profile, state) &&
79
+ array_supported?(schema, type, profile) &&
80
+ format_supported?(schema, type, profile) &&
81
+ enum_supported?(schema, type, profile, state)
82
+ end
83
+ private_class_method :node_supported?
84
+
85
+ def root_supported?(type, profile, root)
86
+ !root || !profile[:root_object] || type == "object"
87
+ end
88
+ private_class_method :root_supported?
89
+
90
+ def object_supported?(schema, type, profile, state)
91
+ object_keywords = schema.key?("properties") || schema.key?("required") ||
92
+ schema.key?("additionalProperties")
93
+ return true unless type == "object" || object_keywords
94
+ return false unless valid_object_shape?(schema, type)
95
+
96
+ properties = schema["properties"]
97
+ required = schema["required"]
98
+ return false unless required_properties_declared?(required, properties)
99
+
100
+ required_index = required.to_h { |name| [name, true] }
101
+ record_object_limits(state, properties, required_index)
102
+ return false unless object_limits_supported?(state, profile)
103
+ return true unless profile[:all_properties_required]
104
+
105
+ required_index.length == properties.length &&
106
+ required_index.each_key.all? { |key| properties.key?(key) }
107
+ end
108
+ private_class_method :object_supported?
109
+
110
+ def required_properties_declared?(required, properties)
111
+ required.all? { |name| properties.key?(name) }
112
+ end
113
+ private_class_method :required_properties_declared?
114
+
115
+ def valid_object_shape?(schema, type)
116
+ type == "object" && schema["properties"].is_a?(Hash) &&
117
+ schema["required"].is_a?(Array) && schema["additionalProperties"] == false
118
+ end
119
+ private_class_method :valid_object_shape?
120
+
121
+ def record_object_limits(state, properties, required)
122
+ state[:properties] += properties.length
123
+ state[:optional_properties] += properties.keys.count { |name| !required.key?(name) }
124
+ state[:schema_string_chars] += properties.keys.sum(&:length)
125
+ end
126
+ private_class_method :record_object_limits
127
+
128
+ def object_limits_supported?(state, profile)
129
+ within_limit?(state[:properties], profile[:max_properties]) &&
130
+ within_limit?(state[:optional_properties], profile[:max_optional_properties]) &&
131
+ within_limit?(state[:schema_string_chars], profile[:max_schema_string_chars])
132
+ end
133
+ private_class_method :object_limits_supported?
134
+
135
+ def array_supported?(schema, type, profile)
136
+ array_keywords = schema.key?("items") || schema.key?("prefixItems")
137
+ return true unless type == "array" || array_keywords
138
+ return false unless type == "array"
139
+ return false if schema.key?("prefixItems") && !profile[:tuple_arrays]
140
+ return false unless schema.key?("items") || schema.key?("prefixItems")
141
+
142
+ true
143
+ end
144
+ private_class_method :array_supported?
145
+
146
+ def format_supported?(schema, type, profile)
147
+ return true unless schema.key?("format")
148
+
149
+ profile[:formats]&.include?(schema["format"]) && type == "string"
150
+ end
151
+ private_class_method :format_supported?
152
+
153
+ def enum_supported?(schema, type, profile, state)
154
+ values = schema["enum"]
155
+ return true unless values
156
+ return false unless values.is_a?(Array)
157
+ return false unless enum_domain_supported?(values, type, profile)
158
+
159
+ state[:enum_values] += values.length
160
+ strings = values.grep(String)
161
+ string_chars = strings.sum(&:length)
162
+ state[:schema_string_chars] += string_chars
163
+ return false unless within_limit?(state[:enum_values], profile[:max_enum_values])
164
+ return false unless within_limit?(state[:schema_string_chars],
165
+ profile[:max_schema_string_chars])
166
+
167
+ threshold = profile[:large_enum_threshold]
168
+ return true unless threshold && values.length > threshold && strings.length == values.length
169
+
170
+ string_chars <= profile[:max_large_enum_chars]
171
+ end
172
+ private_class_method :enum_supported?
173
+
174
+ def enum_domain_supported?(values, type, profile)
175
+ return true unless profile[:enum_types]
176
+ return false unless profile[:enum_types].include?(type)
177
+
178
+ case type
179
+ when "string" then values.all?(String)
180
+ when "number" then values.all? { |value| finite_number?(value) }
181
+ when "integer" then values.all? { |value| integer_number?(value) }
182
+ end
183
+ end
184
+ private_class_method :enum_domain_supported?
185
+
186
+ def finite_number?(value)
187
+ value.is_a?(Integer) || (value.is_a?(Float) && value.finite?)
188
+ end
189
+ private_class_method :finite_number?
190
+
191
+ def integer_number?(value)
192
+ value.is_a?(Integer) || (value.is_a?(Float) && value.finite? && value.to_i == value)
193
+ end
194
+ private_class_method :integer_number?
195
+
196
+ def within_limit?(value, limit)
197
+ !limit || value <= limit
198
+ end
199
+ private_class_method :within_limit?
200
+
201
+ def child_schemas(schema)
202
+ children = schema.fetch("properties", {}).values
203
+ items = schema["items"]
204
+ children << items if items.is_a?(Hash) || items.equal?(true) || items.equal?(false)
205
+ children.concat(schema.fetch("prefixItems", []))
206
+ additional = schema["additionalProperties"]
207
+ children << additional if additional.is_a?(Hash) || additional.equal?(true)
208
+ children
209
+ end
210
+ private_class_method :child_schemas
211
+ end
212
+ private_constant :SchemaCapabilities
213
+ end
214
+ end
data/lib/mistri/result.rb CHANGED
@@ -10,10 +10,14 @@ module Mistri
10
10
  # Reads delegate to the final message, so result.text works whether the run
11
11
  # completed or suspended.
12
12
  # output is a task's validated value, nil on plain runs. usage is the
13
- # run's own accounting: every persisted turn plus compaction calls, summed
13
+ # run's own accounting: every provider attempt plus compaction calls, summed
14
14
  # (a resumed run counts from the resume; task sums across its fix passes).
15
- Result = Data.define(:message, :status, :pending, :output, :usage) do
16
- def initialize(message:, status:, pending: [], output: nil, usage: Usage.zero)
15
+ # handed_off marks a run that ended because an ends_turn tool executed:
16
+ # complete, but the final word was the tool's, and whatever comes next (a
17
+ # human's answer) arrives as the next run's input.
18
+ Result = Data.define(:message, :status, :pending, :output, :usage, :handed_off) do
19
+ def initialize(message:, status:, pending: [], output: nil, usage: Usage.zero,
20
+ handed_off: false)
17
21
  super
18
22
  end
19
23
 
@@ -22,6 +26,7 @@ module Mistri
22
26
  def aborted? = status == :aborted
23
27
  def stopped_by_budget? = status == :budget
24
28
  def errored? = status == :error
29
+ def handed_off? = handed_off
25
30
 
26
31
  def text = message&.text
27
32
  def stop_reason = message&.stop_reason
@@ -4,8 +4,8 @@ module Mistri
4
4
  # When a failed turn is worth retrying, and how long to wait. Transient
5
5
  # failures (rate limits, overload, server errors, timeouts, dropped or
6
6
  # truncated streams) retry with jittered exponential backoff, honoring the
7
- # provider's retry-after when it sent one. Everything else auth, invalid
8
- # requests, our own bugs fails fast.
7
+ # provider's retry-after when it sent one. Everything else (auth, invalid
8
+ # requests, our own bugs) fails fast.
9
9
  #
10
10
  # attempts counts retries, not calls: attempts 3 means up to four requests.
11
11
  class RetryPolicy