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 +4 -4
- data/README.md +24 -19
- data/lib/mirrors_sdk/agent_context.rb +6 -31
- data/lib/mirrors_sdk/version.rb +1 -1
- data/lib/mirrors_sdk/wire.rb +2 -1
- data/skills/install-mirrors-ruby-sdk/SKILL.md +8 -7
- data/test/integration_test.rb +26 -6
- data/test/wire_test.rb +12 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25ae33b5a913fbdf8f66e8f1673ece8e8c9a206716efabc5151272d7dca57e42
|
|
4
|
+
data.tar.gz: 71d38ed8938e392792f47c848af98820ac3b532f8d555d8aabb919b4f912310c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
133
|
+
## Optional original system prompt
|
|
126
134
|
|
|
127
|
-
Attach application
|
|
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
|
-
|
|
137
|
-
|
|
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.
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
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
|
-
|
|
|
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.
|
|
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.
|
|
262
|
-
git push origin refs/tags/ruby-sdk-v0.
|
|
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
|
-
#
|
|
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) &&
|
|
16
|
-
raise ArgumentError, "
|
|
13
|
+
unless value.instance_of?(Hash) && value.keys == ["systemPrompt"]
|
|
14
|
+
raise ArgumentError, "Agent context requires a systemPrompt field"
|
|
17
15
|
end
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
data/lib/mirrors_sdk/version.rb
CHANGED
data/lib/mirrors_sdk/wire.rb
CHANGED
|
@@ -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(
|
|
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
|
|
127
|
+
## Optional original system prompt
|
|
128
128
|
|
|
129
129
|
When the installed SDK and deployed API/worker readers support it, supply original
|
|
130
|
-
instructions
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
the final snapshot: previous values are never inherited. Omit
|
|
134
|
-
prompt
|
|
135
|
-
|
|
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
|
|
data/test/integration_test.rb
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
117
|
+
evidence = trace.merge("agentContext" => { "systemPrompt" => "Original prompt" })
|
|
118
|
+
prefix = @client.capture_trace(evidence)
|
|
115
119
|
assert_equal "unconfirmed", prefix.wait(1).status
|
|
116
|
-
|
|
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(
|
|
124
|
-
assert_equal "terminal_conflict", @client.capture_trace(
|
|
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", "../../
|
|
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
|
|
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
|
|
65
|
-
|
|
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" => [
|
|
70
|
-
{ "
|
|
71
|
-
{ "
|
|
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
|