mirrors-sdk 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8031ec5dde9188cd42bfdad78edf5b7f94d09a7db3258f2b25c94e1acaa7ed49
4
- data.tar.gz: c138edebf9c993de0264c19110774d49e283c7a91237054e17f23bc33b222896
3
+ metadata.gz: 25ae33b5a913fbdf8f66e8f1673ece8e8c9a206716efabc5151272d7dca57e42
4
+ data.tar.gz: 71d38ed8938e392792f47c848af98820ac3b532f8d555d8aabb919b4f912310c
5
5
  SHA512:
6
- metadata.gz: 6c7b2b4237c20df46c699b4b9e6857b31da3d3a03982340719fef7c45ab3d4ef1a5c79342016b465a338c76543665d45d66a6c40611a478862da78dcc85b67ce
7
- data.tar.gz: 206264fe2b5ed439310c831743aff066007e31279743f063c866061ae8aacc55162ccc3e88295aa4bfe78f76995e86a4a9e321fd81e4286f850859021342523b
6
+ metadata.gz: c35f3460533c74ca03136cc903182d269c4db2a46b3c6f95bcc2fe059b7c4e619340f8305dbfe08b17ffed4af65feed6a2e96644f9ee9da89ab9b37639a3c085
7
+ data.tar.gz: 74b8a1d425009f49dfa5bc978d149ff9e5e4878ae2fb536771013f98428d0f9bc5951c0e1fb5173bddfa8865c3dd7bd6aeab618960741b2ad5698c3860bf69e2
data/README.md CHANGED
@@ -7,13 +7,13 @@ calls, tools, streaming, errors and cancellation.
7
7
  Requires Ruby 3.3 or later. Install a released version from RubyGems:
8
8
 
9
9
  ```sh
10
- gem install mirrors-sdk -v 0.1.0
10
+ gem install mirrors-sdk -v 0.2.0
11
11
  ```
12
12
 
13
13
  For an application using Bundler, add this to its Gemfile and run `bundle install`:
14
14
 
15
15
  ```ruby
16
- gem "mirrors-sdk", "~> 0.1.0", require: "mirrors_sdk"
16
+ gem "mirrors-sdk", "~> 0.2.0", require: "mirrors_sdk"
17
17
  ```
18
18
 
19
19
  To test an unreleased version, build and install from a reviewed checkout:
@@ -21,7 +21,7 @@ To test an unreleased version, build and install from a reviewed checkout:
21
21
  ```sh
22
22
  cd packages/ruby-sdk
23
23
  gem build mirrors-sdk.gemspec
24
- gem install --local mirrors-sdk-0.1.0.gem --no-document
24
+ gem install --local mirrors-sdk-0.2.0.gem --no-document
25
25
  ruby -rmirrors_sdk -e 'puts MirrorsSDK::VERSION'
26
26
  ```
27
27
 
@@ -36,6 +36,14 @@ is independent of the Python and TypeScript packages. The packaged
36
36
  [installation skill](skills/install-mirrors-ruby-sdk/SKILL.md) describes integration
37
37
  and verifies official publication before suggesting a registry install.
38
38
 
39
+ ### Upgrading from 0.1.0
40
+
41
+ Version 0.2.0 removes `agentContext.skills`. Remove that field before upgrading;
42
+ `agentContext` now requires exactly `{ "systemPrompt" => original_system_prompt }`.
43
+ Omit the object when the prompt is unknown. A snapshot containing `skills` is
44
+ rejected, including when a valid system prompt is present. Existing server readers
45
+ remain compatible with 0.1.0 clients.
46
+
39
47
  ## Integrate with the existing model loop
40
48
 
41
49
  Create one reusable client per agent configuration **after worker processes fork**.
@@ -122,26 +130,24 @@ milliseconds. Measure the whole user turn, including context waiting, models,
122
130
  tools, retries and streaming. Put `turnLatencyMs` on final capture only, excluding
123
131
  capture delivery; omit unknown duration.
124
132
 
125
- ## Optional original instructions
133
+ ## Optional original system prompt
126
134
 
127
- Attach application-supplied instructions once to the turn's trace:
135
+ Attach the application's original system prompt once to the turn's trace:
128
136
 
129
137
  ```ruby
130
138
  trace["agentContext"] = {
131
- "systemPrompt" => original_system_prompt,
132
- "skills" => [{ "name" => "account-policy", "content" => application_skill_text }]
139
+ "systemPrompt" => original_system_prompt
133
140
  }
134
141
  ```
135
142
 
136
- Either field may be omitted; the object must contain at least one. Capture the
137
- original prompt before appending Mirrors context. Include `agentContext` explicitly
143
+ The object requires exactly one `systemPrompt` string. Capture the original prompt
144
+ before appending Mirrors context. Include `agentContext` explicitly
138
145
  on the final capture to retain it: values are never inherited from an earlier
139
- snapshot or another turn. Empty prompt or skills values mean explicitly empty;
140
- omit unknown values. The SDK does not read files, discover skills or assert that
141
- supplied skills ran. Preserve existing system messages without synthesizing copies.
146
+ snapshot or another turn. An empty string means explicitly no prompt; omit
147
+ `agentContext` when the prompt is unknown. Preserve existing system messages
148
+ without synthesizing copies.
142
149
  Compatible API and worker readers are required; installing a gem does not deploy
143
- the service. Ruby supports both prompt and skill fields from the TypeScript
144
- contract; the current Python SDK accepts only the prompt field.
150
+ the service. This prompt-only object matches the TypeScript and Python SDKs.
145
151
 
146
152
  ## Receipts, cancellation and shutdown
147
153
 
@@ -178,8 +184,7 @@ inherited across a fork stays inert; create a new client in the child process.
178
184
  | --- | --- |
179
185
  | Encoded snapshot | 1,000,000 bytes; 10,000 messages; 50,000 JSON nodes; depth 32 |
180
186
  | Agent context | 262,144 UTF-8 JSON bytes within the snapshot budget |
181
- | Prompt / each skill content | 100,000 UTF-16 code units |
182
- | Skills | 64 unique names; names at most 128 UTF-16 code units |
187
+ | Original system prompt | 100,000 UTF-16 code units |
183
188
  | Pending captures | 16 prefixes and 16 finals, independently |
184
189
  | Active executions / recent final receipts | 64 / 128 |
185
190
  | Pending retrievals | 16, independent of capture |
@@ -219,7 +224,7 @@ hosted Mirrors service.
219
224
  ## Publishing through GitHub Actions
220
225
 
221
226
  The `Ruby SDK` workflow publishes tags named `ruby-sdk-v<VERSION>`, such as
222
- `ruby-sdk-v0.1.0`. All three Ruby versions must pass verification before the
227
+ `ruby-sdk-v0.2.0`. All three Ruby versions must pass verification before the
223
228
  `publish` job can upload the exact gem built and tested by the Ruby 3.4 job.
224
229
  Pull requests and manual workflow runs only verify; they never publish.
225
230
 
@@ -258,8 +263,8 @@ TypeScript packages. Merge the reviewed SDK and workflow, then tag the intended
258
263
  merged commit after its checks pass:
259
264
 
260
265
  ```sh
261
- git tag ruby-sdk-v0.1.0 <verified-merged-commit>
262
- git push origin refs/tags/ruby-sdk-v0.1.0
266
+ git tag ruby-sdk-v0.2.0 <verified-merged-commit>
267
+ git push origin refs/tags/ruby-sdk-v0.2.0
263
268
  ```
264
269
 
265
270
  The tag must exactly match `MirrorsSDK::VERSION`; the publish job also checks the
@@ -3,49 +3,24 @@
3
3
  require "json"
4
4
 
5
5
  module MirrorsSDK
6
- # Original instructions are optional evidence, never modifications to model input.
6
+ # The original system prompt is optional evidence, never a modification to model input.
7
7
  class AgentContextWire
8
- TRIM = /\A[\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+|[\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+\z/.freeze
9
-
10
8
  def self.length(value)
11
9
  value.encode(Encoding::UTF_16LE).bytesize / 2
12
10
  end
13
11
 
14
12
  def self.validate(value)
15
- unless value.instance_of?(Hash) && !value.empty? && (value.keys - %w[systemPrompt skills]).empty?
16
- raise ArgumentError, "Invalid agent context"
13
+ unless value.instance_of?(Hash) && value.keys == ["systemPrompt"]
14
+ raise ArgumentError, "Agent context requires a systemPrompt field"
17
15
  end
18
- if value.key?("systemPrompt")
19
- prompt = value["systemPrompt"]
20
- unless prompt.instance_of?(String) && length(prompt) <= 100_000
21
- raise ArgumentError, "Invalid system prompt"
22
- end
16
+ prompt = value["systemPrompt"]
17
+ unless prompt.instance_of?(String) && length(prompt) <= 100_000
18
+ raise ArgumentError, "Invalid system prompt"
23
19
  end
24
- validate_skills(value["skills"]) if value.key?("skills")
25
20
  if JSON.generate(value).bytesize > 262_144
26
21
  raise ArgumentError, "Agent context exceeds the encoded size limit"
27
22
  end
28
23
  value
29
24
  end
30
-
31
- def self.validate_skills(skills)
32
- unless skills.instance_of?(Array) && skills.length <= 64
33
- raise ArgumentError, "Invalid agent skills"
34
- end
35
- names = {}
36
- skills.each do |skill|
37
- unless skill.instance_of?(Hash) && skill.keys.sort == %w[content name]
38
- raise ArgumentError, "Invalid agent skill"
39
- end
40
- name, content = skill.values_at("name", "content")
41
- unless name.instance_of?(String) && !name.gsub(TRIM, "").empty? &&
42
- length(name) <= 128 && !name.match?(/\p{Cc}/) && !names[name] &&
43
- content.instance_of?(String) && !content.empty? && length(content) <= 100_000
44
- raise ArgumentError, "Invalid agent skill"
45
- end
46
- names[name] = true
47
- end
48
- end
49
- private_class_method :validate_skills
50
25
  end
51
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MirrorsSDK
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -10,6 +10,7 @@ module MirrorsSDK
10
10
  class Wire
11
11
  MODES = %w[active shadow off].freeze
12
12
  BASE64 = /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/.freeze
13
+ TRIM = /\A[\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+|[\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+\z/.freeze
13
14
 
14
15
  def self.encode(value, limit: 1_000_000, deadline: nil)
15
16
  Snapshot.encode(value, limit: limit, deadline: deadline)
@@ -207,7 +208,7 @@ module MirrorsSDK
207
208
  { "id" => 256, "segment" => 64 }.each do |field, limit|
208
209
  next unless value.key?(field)
209
210
 
210
- trimmed = text(value[field]).gsub(AgentContextWire::TRIM, "")
211
+ trimmed = text(value[field]).gsub(TRIM, "")
211
212
  unless AgentContextWire.length(trimmed).between?(1, limit) && !trimmed.match?(/\p{Cc}/)
212
213
  raise ArgumentError, "Invalid end user"
213
214
  end
@@ -124,15 +124,16 @@ execution, persist the pair and keep it fixed across snapshots/retries. Timestam
124
124
  message counts and process counters are invalid. Branches need distinct chat IDs.
125
125
  Optional `user` contains an opaque internal `id` and optional cohort `segment`.
126
126
 
127
- ## Optional original instructions
127
+ ## Optional original system prompt
128
128
 
129
129
  When the installed SDK and deployed API/worker readers support it, supply original
130
- instructions on `trace["agentContext"]`: `systemPrompt` and/or `skills`, where each
131
- skill has `name` and `content`. Read the application's prompt before adding Mirrors
132
- context; do not load arbitrary files or skills automatically. Include the object on
133
- the final snapshot: previous values are never inherited. Omit unknown fields; empty
134
- prompt/skills explicitly mean empty. Respect the installed README's character,
135
- count and byte limits. Preserve existing system messages without synthesizing copies.
130
+ instructions as `trace["agentContext"] = { "systemPrompt" => original_system_prompt }`.
131
+ Read the application's prompt before adding Mirrors context. The object accepts
132
+ only the `systemPrompt` string; do not load arbitrary files automatically. Include
133
+ it on the final snapshot: previous values are never inherited. Omit `agentContext`
134
+ when the prompt is unknown; an empty string explicitly means no prompt. Respect
135
+ the installed README's character and byte limits. Preserve existing system messages
136
+ without synthesizing copies.
136
137
 
137
138
  ## Verify and shut down
138
139
 
@@ -17,7 +17,7 @@ class IntegrationTest < Minitest::Test
17
17
  def test_capture_and_context_match_existing_protocols
18
18
  wait_for_active
19
19
  evidence = trace.merge("conversationId" => "chat", "conversationRevision" => 3,
20
- "agentContext" => { "systemPrompt" => "Original prompt", "skills" => [{ "name" => "search", "content" => "Search first" }] })
20
+ "agentContext" => { "systemPrompt" => "Original prompt" })
21
21
  prefix = @client.capture_trace(evidence)
22
22
  result = @client.get_context(evidence)
23
23
  assert_equal @server.context_text, result.context
@@ -33,6 +33,7 @@ class IntegrationTest < Minitest::Test
33
33
  assert_equal 12.5, captures.last[:body]["trace"]["turnLatencyMs"]
34
34
  request = @server.requests("retrieve-context").last
35
35
  assert_equal 1, request[:body]["protocol"]
36
+ assert_equal({ "systemPrompt" => "Original prompt" }, request[:body]["trace"]["agentContext"])
36
37
  assert_equal Digest::SHA256.hexdigest("retrieve-context\n#{request[:raw]}"), request[:headers]["idempotency-key"]
37
38
  assert_equal "execution_closed", @client.get_context(evidence).reason
38
39
  end
@@ -40,13 +41,15 @@ class IntegrationTest < Minitest::Test
40
41
  def test_capture_does_not_wait_for_http_or_hold_context
41
42
  wait_for_active
42
43
  @server.capture_delay = 0.2
43
- evidence = trace
44
+ evidence = trace.merge("agentContext" => { "systemPrompt" => "Original prompt" })
44
45
  started = monotonic
45
46
  ticket = @client.capture_trace(evidence)
46
47
  assert_operator monotonic - started, :<, 0.1
47
48
  evidence["messages"][0]["content"] = "Mutated after capture"
49
+ evidence["agentContext"]["systemPrompt"] = "Mutated after capture"
48
50
  eventually { @server.requests("trace").any? }
49
51
  assert_equal "Hello", @server.requests("trace").first[:body]["trace"]["messages"][0]["content"]
52
+ assert_equal "Original prompt", @server.requests("trace").first[:body]["trace"]["agentContext"]["systemPrompt"]
50
53
  assert_equal @server.context_text, @client.get_context(trace).context
51
54
  assert_equal "pending", ticket.receipt.status
52
55
  assert_equal "accepted", ticket.wait(1).status
@@ -111,17 +114,34 @@ class IntegrationTest < Minitest::Test
111
114
 
112
115
  def test_uncertain_prefix_replays_identical_bytes_before_final
113
116
  @server.capture_failures = 1
114
- prefix = @client.capture_trace(trace)
117
+ evidence = trace.merge("agentContext" => { "systemPrompt" => "Original prompt" })
118
+ prefix = @client.capture_trace(evidence)
115
119
  assert_equal "unconfirmed", prefix.wait(1).status
116
- final = @client.capture_trace(trace, final: true, terminal: "completed")
120
+ evidence["agentContext"]["systemPrompt"] = "Final prompt"
121
+ final = @client.capture_trace(evidence, final: true, terminal: "completed")
117
122
  assert_equal "accepted", final.wait(1).status
118
123
  records = @server.requests("trace")
119
124
  assert_equal 3, records.length
120
125
  assert_equal records[0][:raw], records[1][:raw]
121
126
  assert_equal records[0][:headers]["idempotency-key"], records[1][:headers]["idempotency-key"]
127
+ assert_equal "Original prompt", records[1][:body]["trace"]["agentContext"]["systemPrompt"]
128
+ assert_equal "Final prompt", records[2][:body]["trace"]["agentContext"]["systemPrompt"]
122
129
  assert_equal 2, records[2][:body]["ordinal"]
123
- assert_same final, @client.capture_trace(trace, final: true, terminal: "completed")
124
- assert_equal "terminal_conflict", @client.capture_trace(trace, final: true, terminal: "failed").receipt.reason
130
+ assert_same final, @client.capture_trace(evidence, final: true, terminal: "completed")
131
+ assert_equal "terminal_conflict", @client.capture_trace(evidence, final: true, terminal: "failed").receipt.reason
132
+ end
133
+
134
+ def test_skills_reject_capture_and_context_without_http
135
+ wait_for_active
136
+ [{ "skills" => [] }, { "systemPrompt" => "Original prompt", "skills" => [] }].each_with_index do |context, index|
137
+ evidence = trace("invalid-context-#{index}").merge("agentContext" => context)
138
+ result = @client.get_context(evidence)
139
+ assert_empty result.context
140
+ assert_equal "invalid_evidence", result.reason
141
+ assert_equal "invalid_evidence", @client.capture_trace(evidence).receipt.reason
142
+ end
143
+ assert_empty @server.requests("trace")
144
+ assert_empty @server.requests("retrieve-context")
125
145
  end
126
146
 
127
147
  def test_flush_snapshots_pending_and_close_disposes
data/test/wire_test.rb CHANGED
@@ -32,7 +32,7 @@ class WireTest < Minitest::Test
32
32
 
33
33
  def test_shared_canonical_fixture_is_preserved
34
34
  fixture = JSON.parse(File.read(File.join(__dir__, "fixtures/canonical-events.json")))
35
- ["../../python-sdk/tests/fixtures/canonical-events.json", "../../remote-sdk/test/fixtures/canonical-events.json"].each do |path|
35
+ ["../../python-sdk/tests/fixtures/canonical-events.json", "../../typescript-sdk/test/fixtures/canonical-events.json"].each do |path|
36
36
  source = File.expand_path(path, __dir__)
37
37
  assert_equal JSON.parse(File.read(source)), fixture if File.exist?(source)
38
38
  end
@@ -59,17 +59,20 @@ class WireTest < Minitest::Test
59
59
  end
60
60
  end
61
61
 
62
- def test_agent_context_matches_prompt_and_skill_limits
62
+ def test_agent_context_copies_original_prompt_and_enforces_limits
63
+ refute MirrorsSDK::Wire.snapshot(trace).key?("agentContext")
63
64
  ["", " Original prompt\n", "💡" * 50_000].each do |prompt|
64
- source = trace(agentContext: { "systemPrompt" => prompt, "skills" => [{ "name" => "search", "content" => "Find evidence" }] })
65
- assert_equal source, MirrorsSDK::Wire.snapshot(source)
65
+ source = trace(agentContext: { "systemPrompt" => prompt })
66
+ copied = MirrorsSDK::Wire.snapshot(source)
67
+ assert_equal source, copied
68
+ refute_same source["agentContext"], copied["agentContext"]
69
+ source["agentContext"]["systemPrompt"] = "Changed after capture"
70
+ assert_equal prompt, copied["agentContext"]["systemPrompt"]
66
71
  end
67
- assert_equal({ "skills" => [] }, MirrorsSDK::Wire.snapshot(trace(agentContext: { "skills" => [] }))["agentContext"])
68
72
  [{}, nil, { "systemPrompt" => nil }, { "systemPrompt" => "💡" * 50_001 },
69
- { "systemPrompt" => "界" * 87_376 }, { "skills" => [{ "name" => " ", "content" => "x" }] },
70
- { "skills" => [{ "name" => "a", "content" => "" }] },
71
- { "skills" => Array.new(2) { { "name" => "same", "content" => "x" } } },
72
- { "skills" => [{ "name" => "a\n", "content" => "x" }] },
73
+ { "systemPrompt" => "界" * 87_376 }, { "skills" => [] },
74
+ { "systemPrompt" => "x", "skills" => [] },
75
+ { "systemPrompt" => "x", "skills" => [{ "name" => "search", "content" => "Find evidence" }] },
73
76
  { "systemPrompt" => "x", "unknown" => true }].each do |agent_context|
74
77
  assert_raises(ArgumentError) { MirrorsSDK::Wire.snapshot(trace(agentContext: agent_context)) }
75
78
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mirrors-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mirrors