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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +469 -4
  3. data/CONTRIBUTING.md +52 -0
  4. data/README.md +289 -385
  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/agent.rb +575 -55
  28. data/lib/mistri/budget.rb +26 -1
  29. data/lib/mistri/child.rb +72 -16
  30. data/lib/mistri/compaction.rb +26 -10
  31. data/lib/mistri/compactor.rb +34 -11
  32. data/lib/mistri/console.rb +28 -7
  33. data/lib/mistri/content.rb +9 -3
  34. data/lib/mistri/dispatchers.rb +14 -12
  35. data/lib/mistri/errors.rb +83 -4
  36. data/lib/mistri/event.rb +24 -8
  37. data/lib/mistri/event_delivery.rb +60 -0
  38. data/lib/mistri/locks.rb +3 -3
  39. data/lib/mistri/mcp/client.rb +74 -19
  40. data/lib/mistri/mcp/egress.rb +216 -0
  41. data/lib/mistri/mcp/oauth.rb +476 -127
  42. data/lib/mistri/mcp/wires.rb +115 -23
  43. data/lib/mistri/mcp.rb +42 -8
  44. data/lib/mistri/message.rb +21 -11
  45. data/lib/mistri/models.rb +160 -22
  46. data/lib/mistri/providers/anthropic/assembler.rb +282 -44
  47. data/lib/mistri/providers/anthropic/serializer.rb +14 -9
  48. data/lib/mistri/providers/anthropic.rb +29 -6
  49. data/lib/mistri/providers/fake.rb +26 -10
  50. data/lib/mistri/providers/gemini/assembler.rb +148 -21
  51. data/lib/mistri/providers/gemini/serializer.rb +78 -9
  52. data/lib/mistri/providers/gemini.rb +31 -5
  53. data/lib/mistri/providers/openai/assembler.rb +337 -60
  54. data/lib/mistri/providers/openai/serializer.rb +13 -12
  55. data/lib/mistri/providers/openai.rb +29 -5
  56. data/lib/mistri/providers/schema_capabilities.rb +214 -0
  57. data/lib/mistri/result.rb +1 -1
  58. data/lib/mistri/schema.rb +893 -75
  59. data/lib/mistri/session.rb +560 -48
  60. data/lib/mistri/sinks/coalesced.rb +17 -10
  61. data/lib/mistri/spawner.rb +111 -61
  62. data/lib/mistri/sse.rb +57 -14
  63. data/lib/mistri/stores/active_record.rb +1 -1
  64. data/lib/mistri/stores/memory.rb +21 -2
  65. data/lib/mistri/sub_agent/execution.rb +81 -0
  66. data/lib/mistri/sub_agent/runtime.rb +297 -0
  67. data/lib/mistri/sub_agent.rb +124 -87
  68. data/lib/mistri/task_output.rb +24 -6
  69. data/lib/mistri/tool.rb +93 -13
  70. data/lib/mistri/tool_arguments.rb +377 -0
  71. data/lib/mistri/tool_call.rb +43 -9
  72. data/lib/mistri/tool_context.rb +4 -2
  73. data/lib/mistri/tool_executor.rb +117 -26
  74. data/lib/mistri/tool_result.rb +15 -10
  75. data/lib/mistri/tools/edit_file.rb +62 -8
  76. data/lib/mistri/tools.rb +41 -4
  77. data/lib/mistri/transport.rb +149 -44
  78. data/lib/mistri/usage.rb +65 -13
  79. data/lib/mistri/version.rb +1 -1
  80. data/lib/mistri/workspace/active_record.rb +183 -3
  81. data/lib/mistri/workspace/directory.rb +28 -8
  82. data/lib/mistri/workspace/memory.rb +34 -9
  83. data/lib/mistri/workspace/single.rb +62 -5
  84. data/lib/mistri/workspace.rb +39 -0
  85. data/lib/mistri.rb +6 -1
  86. data/mistri.gemspec +34 -0
  87. metadata +31 -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,7 +10,7 @@ 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
15
  # handed_off marks a run that ended because an ends_turn tool executed:
16
16
  # complete, but the final word was the tool's, and whatever comes next (a