foam-otel 1.2.0 → 1.2.1
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/GOTCHAS.md +16 -0
- data/README.md +16 -1
- data/RESEARCH.md +44 -2
- data/lib/foam/otel/init.rb +18 -1
- data/lib/foam/otel/llm/gemini_shim.rb +13 -4
- data/lib/foam/otel/llm/ruby_llm_shim.rb +42 -7
- data/lib/foam/otel/version.rb +9 -1
- 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: 4e10efb9aaacdda1498a19efaa4af8338afbb98d89880cc6f662aa99205aab59
|
|
4
|
+
data.tar.gz: 436060cbcfc7ed33c171ab230a60d4f8b62a882ce46be22bcd81e9c497edbc84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 528f57356698e39e691b08bcc0b6f00bc409ad133afca0ac0bdeeb2d0caffbc052a7a19d7fe63b0285230eb16cd26117ede4bc0824c7c0f31377a4a983ad7458
|
|
7
|
+
data.tar.gz: 9e445e3ce3924ff924afd66e98c35b58fe536ae2d546a0f151d3d870f9359974b3bbfd781871fd39fef67d3d509573cab7c2ce573531067eedf3623943967f32
|
data/GOTCHAS.md
CHANGED
|
@@ -681,6 +681,22 @@ anthropic 1.59.0, gemini-ai 4.3.0, ruby_llm 1.16.0).
|
|
|
681
681
|
beside it (`lib/foam/otel/runtime_metrics.rb` `guarded_callback`).
|
|
682
682
|
*Test*: `spec/runtime_metrics_spec.rb` ("a poisoned reading is
|
|
683
683
|
fail-to-dark", the foreign-slot and idempotence cases).
|
|
684
|
+
- **G16 — Sidekiq's queue-hop trace shape is `propagation_style: :link`**:
|
|
685
|
+
the bundled contrib instrumentation's default (installed source:
|
|
686
|
+
opentelemetry-instrumentation-sidekiq-0.29.0 `instrumentation.rb:104`)
|
|
687
|
+
makes a performed job a SEPARATE trace whose first (consumer) span carries
|
|
688
|
+
a span LINK back to the enqueuing producer span — NOT a child inside the
|
|
689
|
+
web request's trace. Foam ships that upstream default untouched (rule 26:
|
|
690
|
+
the contrib pin sets the semconv; `FLOOR_INSTRUMENTATION_CONFIG` overrides
|
|
691
|
+
only the datastore raw-capture posture), so dashboards that follow ONLY
|
|
692
|
+
parent/child edges see the web trace end at the enqueue — follow the link,
|
|
693
|
+
or clamp the posture per deployment with the standard contrib lever
|
|
694
|
+
`OTEL_RUBY_INSTRUMENTATION_SIDEKIQ_CONFIG_OPTS='propagation_style=child'`
|
|
695
|
+
for one continuous web→job trace. Context is propagated across the hop
|
|
696
|
+
either way — a hop that DROPS it is a correctness bug, never accepted.
|
|
697
|
+
*Test*: `spec/floor_sidekiq_wire_spec.rb` (real Redis enqueue→perform:
|
|
698
|
+
producer span joins the request trace; consumer span LINKS the producer
|
|
699
|
+
span on the wire).
|
|
684
700
|
|
|
685
701
|
---
|
|
686
702
|
|
data/README.md
CHANGED
|
@@ -80,6 +80,14 @@ attributes, URLs/query strings, DB statement text, log bodies, LLM content —
|
|
|
80
80
|
is captured RAW.** Pass `redact_keys`/`redact_pii_keys` to mask or erase
|
|
81
81
|
specific fields (see below).
|
|
82
82
|
|
|
83
|
+
> **Sidekiq trace shape** (GOTCHAS G16): the bundled instrumentation keeps
|
|
84
|
+
> its upstream default `propagation_style: :link` — a performed job runs as
|
|
85
|
+
> its OWN trace whose first span LINKS back to the enqueuing span (context is
|
|
86
|
+
> always propagated across the hop; proven against a real Redis in
|
|
87
|
+
> `spec/floor_sidekiq_wire_spec.rb`). Prefer one continuous web→job trace?
|
|
88
|
+
> Set the standard contrib lever:
|
|
89
|
+
> `OTEL_RUBY_INSTRUMENTATION_SIDEKIQ_CONFIG_OPTS='propagation_style=child'`.
|
|
90
|
+
|
|
83
91
|
### The LLM surface — OpenAI, Anthropic, Gemini (and ruby_llm)
|
|
84
92
|
|
|
85
93
|
The Ruby contrib registry ships no LLM instrumentation, so foam does: thin,
|
|
@@ -87,7 +95,14 @@ presence-checked shims over the SDKs' public call sites, emitting the standard
|
|
|
87
95
|
`gen_ai.*` semantic conventions — activity (model, response id, finish
|
|
88
96
|
reasons, token usage, latency) AND prompt/response **content, RAW**
|
|
89
97
|
(`gen_ai.input.messages` / `gen_ai.output.messages` /
|
|
90
|
-
`gen_ai.system_instructions`).
|
|
98
|
+
`gen_ai.system_instructions`). Agent tool loops ride in full: tool-call
|
|
99
|
+
turns keep their tool names/arguments and tool-result turns keep their
|
|
100
|
+
`tool_call_id` linkage inside the message content. `gen_ai.operation.name`
|
|
101
|
+
(and the `{operation} {model}` span name) follows the semconv operation for
|
|
102
|
+
each API shape — `chat` for the OpenAI/Anthropic/ruby_llm chat seams,
|
|
103
|
+
`generate_content` for Gemini's generateContent, fleet-identical with the
|
|
104
|
+
js/python Gemini paths (**wire change in 1.2.1**: Gemini spans previously
|
|
105
|
+
said `chat`). Covered: the official `openai` SDK
|
|
91
106
|
(chat.completions.create + responses.create), the official `anthropic` SDK
|
|
92
107
|
(messages.create), Gemini via the `gemini-ai` gem (Google ships no official
|
|
93
108
|
Ruby SDK), and `ruby_llm` (one seam covering all its providers). Absent SDK =
|
data/RESEARCH.md
CHANGED
|
@@ -151,6 +151,20 @@ the customer lists in `redact_keys`/`redact_pii_keys`. An FDE can still clamp
|
|
|
151
151
|
context calls for it — per-customer tightening (rule 18 / Area 4), not the
|
|
152
152
|
default.
|
|
153
153
|
|
|
154
|
+
**Sidekiq queue-hop posture (documented, GOTCHAS G16):** `-sidekiq 0.29.0`
|
|
155
|
+
defaults `propagation_style: :link` (installed source `instrumentation.rb:104`)
|
|
156
|
+
— a performed job is a SEPARATE trace whose consumer span LINKS the enqueuing
|
|
157
|
+
producer span. Foam ships the upstream default untouched
|
|
158
|
+
(`FLOOR_INSTRUMENTATION_CONFIG` overrides only the datastore raw-capture
|
|
159
|
+
posture); the continuous web→job shape is one env lever away
|
|
160
|
+
(`OTEL_RUBY_INSTRUMENTATION_SIDEKIQ_CONFIG_OPTS='propagation_style=child'`).
|
|
161
|
+
Context always survives the hop. Wire-proven per rule 8b against a REAL
|
|
162
|
+
redis-server with the REAL sidekiq gem (enqueue → Redis → embedded-server
|
|
163
|
+
perform; producer/consumer spans + the link asserted from the exporter):
|
|
164
|
+
`spec/floor_sidekiq_wire_spec.rb`. The Faraday floor entry carries the same
|
|
165
|
+
rule-8b emission proof against a local listener:
|
|
166
|
+
`spec/floor_wire_spec.rb`.
|
|
167
|
+
|
|
154
168
|
**Runtime + GC metrics (hand-written — landed,
|
|
155
169
|
`lib/foam/otel/runtime_metrics.rb`).** Ruby has no drop-in upstream metrics
|
|
156
170
|
instrumentation gem (unlike Python's `-system-metrics` or the Node
|
|
@@ -165,8 +179,19 @@ construction (rule 26). Tests: `spec/runtime_metrics_spec.rb`.
|
|
|
165
179
|
|
|
166
180
|
**Excluded (with reason):** `-factory_bot`, `-rspec` (test-only, no production
|
|
167
181
|
value); `-net_ldap` (niche, PII/credential-adjacent — explicit FDE opt-in);
|
|
182
|
+
`-logger 0.4.0` (SUPERSEDED by foam's own stdlib Logger bridge, §4: both
|
|
183
|
+
patch the SAME `Logger#add` seam, and foam's bridge keeps the lazy-block
|
|
184
|
+
contract, both loop guards, the logs-slot gating, and the opt-in redaction
|
|
185
|
+
path the contrib bridge lacks. Two bridges on one seam would export every
|
|
186
|
+
host log line TWICE — rule 12 never-double-attach — so init's bundle-driven
|
|
187
|
+
sweep pins the contrib gem disabled (`SUPERSEDED_INSTRUMENTATION_CONFIG`,
|
|
188
|
+
`lib/foam/otel/init.rb`; proven by `spec/floor_spec.rb`); an FDE who
|
|
189
|
+
deliberately wants the contrib bridge instead passes its instance via
|
|
190
|
+
`additional_instrumentations`, which bypasses the sweep map);
|
|
168
191
|
`all`/`base` (not instrumentations — foam deliberately does NOT depend on the
|
|
169
|
-
`all` meta-gem, which would drag test gems).
|
|
192
|
+
`all` meta-gem, which would drag test gems). With the 17-gem floor, the 27
|
|
193
|
+
when-present gems, and these 4 exclusions, all 48 registry gems have a
|
|
194
|
+
written disposition (rule 6: no silent omissions).
|
|
170
195
|
|
|
171
196
|
**Known-bad / crash history (rule 6 / R3):** action_pack overrides rack config
|
|
172
197
|
(contrib #88); active_record 0.2.2 failed on Rails 7 (#38); `-que` NoMethodError
|
|
@@ -197,6 +222,14 @@ one prepend covers most framework logging in practice;
|
|
|
197
222
|
https://github.com/open-telemetry/opentelemetry-ruby/discussions/1789 is
|
|
198
223
|
open/unresolved) — a documented gap, narrowed to the non-Logger-backed exotics.
|
|
199
224
|
|
|
225
|
+
The official contrib `opentelemetry-instrumentation-logger 0.4.0` bridges the
|
|
226
|
+
same `Logger#add` seam and is deliberately NOT bundled — census disposition
|
|
227
|
+
EXCLUDED/superseded (§3): foam's bridge additionally holds the lazy-block
|
|
228
|
+
contract, the OTel-internal-logger identity guard, the re-entrancy flag, the
|
|
229
|
+
logs-slot gate, and the opt-in redaction path, and shipping/sweeping both
|
|
230
|
+
would double-export every host log line (rule 12). init's sweep pins the
|
|
231
|
+
contrib gem disabled even when an FDE bundles it (`spec/floor_spec.rb`).
|
|
232
|
+
|
|
200
233
|
---
|
|
201
234
|
|
|
202
235
|
## 5. LLM ecosystem (rule 31)
|
|
@@ -210,7 +243,16 @@ shims patch the SDKs' public call sites — official `openai`
|
|
|
210
243
|
(messages.create), `gemini-ai` (generate_content / stream_generate_content;
|
|
211
244
|
Google ships no official Ruby SDK), and `ruby_llm` (Provider#complete, one
|
|
212
245
|
seam covering all its providers) — emitting the same GenAI semconv attribute
|
|
213
|
-
set the fleet's js/python gap-fillers emit.
|
|
246
|
+
set the fleet's js/python gap-fillers emit. `gen_ai.operation.name` matches
|
|
247
|
+
the fleet per API shape: `chat` on the chat seams, `generate_content` on the
|
|
248
|
+
Gemini shim (aligned in 1.2.1 — Ruby previously emitted `chat` for Gemini, a
|
|
249
|
+
cross-language vocabulary split against the js shim and python's official
|
|
250
|
+
google-genai instrumentor). Agent-loop tool activity is captured on the
|
|
251
|
+
ruby_llm seam too: assistant tool-call turns serialize their `tool_calls`
|
|
252
|
+
(id/name/arguments) and tool-result turns their `tool_call_id` into
|
|
253
|
+
`gen_ai.input.messages`/`gen_ai.output.messages`
|
|
254
|
+
(`spec/llm_ruby_llm_spec.rb` drives a real `chat.with_tool` loop on the
|
|
255
|
+
wire); the Anthropic shim carries tool_use blocks verbatim inside content. **LLM traces are captured in
|
|
214
256
|
full — activity (model, response id, finish reasons, tokens, latency) AND
|
|
215
257
|
prompt/completion content, raw by default** (2026-07-26 ruling: LLM content
|
|
216
258
|
is never gated or dropped; the FDE reactive clamp is the only per-customer
|
data/lib/foam/otel/init.rb
CHANGED
|
@@ -470,6 +470,21 @@ module Foam
|
|
|
470
470
|
"OpenTelemetry::Instrumentation::Mongo" => { db_statement: :include }.freeze,
|
|
471
471
|
}.freeze
|
|
472
472
|
|
|
473
|
+
# Contrib gems foam's OWN floor pieces supersede (rule-6 census:
|
|
474
|
+
# EXCLUDED with reason, RESEARCH.md §3): the contrib
|
|
475
|
+
# opentelemetry-instrumentation-logger bridges the SAME `Logger#add`
|
|
476
|
+
# seam foam's LoggerBridge owns (lazy-block contract, both loop
|
|
477
|
+
# guards, logs-slot gating — logger_bridge.rb). If an FDE bundles it,
|
|
478
|
+
# the bundle-driven sweep below would install it BESIDE foam's prepend
|
|
479
|
+
# and every host log line would export twice (rule 12: never
|
|
480
|
+
# double-attach) — so the sweep pins it disabled. An FDE who
|
|
481
|
+
# deliberately wants the contrib bridge instead passes its instance
|
|
482
|
+
# via `additional_instrumentations` (install_additional installs
|
|
483
|
+
# directly and never reads this map — the explicit choice wins).
|
|
484
|
+
SUPERSEDED_INSTRUMENTATION_CONFIG = {
|
|
485
|
+
"OpenTelemetry::Instrumentation::Logger" => { enabled: false }.freeze,
|
|
486
|
+
}.freeze
|
|
487
|
+
|
|
473
488
|
# Tier-1/2 activation (rules 5-7): install every bundled official
|
|
474
489
|
# opentelemetry-instrumentation-* gem, presence-checked by the gem
|
|
475
490
|
# itself. The outbound loop guard (rule 24) is wired by pre-installing
|
|
@@ -505,7 +520,9 @@ module Foam
|
|
|
505
520
|
|
|
506
521
|
configure_loop_guard(config)
|
|
507
522
|
install_additional(additional)
|
|
508
|
-
OpenTelemetry::Instrumentation.registry.install_all(
|
|
523
|
+
OpenTelemetry::Instrumentation.registry.install_all(
|
|
524
|
+
FLOOR_INSTRUMENTATION_CONFIG.merge(SUPERSEDED_INSTRUMENTATION_CONFIG)
|
|
525
|
+
)
|
|
509
526
|
Diagnostics.info("instrumentations installed: #{gems.length} bundled gem(s)")
|
|
510
527
|
rescue StandardError => e
|
|
511
528
|
Diagnostics.warn("instrumentation activation failed: #{e.class}: #{e.message}")
|
|
@@ -10,9 +10,14 @@ module Foam
|
|
|
10
10
|
# stream_generate_content (the gem consumes the SSE stream INSIDE the
|
|
11
11
|
# call and returns the collected events, so both seams yield the full
|
|
12
12
|
# response synchronously). gen_ai.provider.name uses the semconv
|
|
13
|
-
# well-known value for the Gemini API: "gcp.gemini"
|
|
13
|
+
# well-known value for the Gemini API: "gcp.gemini", and
|
|
14
|
+
# gen_ai.operation.name / the span name use "generate_content" — the
|
|
15
|
+
# semconv well-known operation for this API shape, exactly what the
|
|
16
|
+
# fleet's js shim and python's official google-genai instrumentor emit
|
|
17
|
+
# (GenAI-uniformity ruling 2026-07-26; aligned from "chat" in 1.2.1).
|
|
14
18
|
module GeminiShim
|
|
15
19
|
PROVIDER = "gcp.gemini"
|
|
20
|
+
OPERATION = "generate_content"
|
|
16
21
|
SUPPORTED = Gem::Requirement.new(">= 4.0", "< 5")
|
|
17
22
|
|
|
18
23
|
class << self
|
|
@@ -58,7 +63,7 @@ module Foam
|
|
|
58
63
|
nil
|
|
59
64
|
end
|
|
60
65
|
LLM.request_attributes(
|
|
61
|
-
provider: PROVIDER, operation:
|
|
66
|
+
provider: PROVIDER, operation: OPERATION,
|
|
62
67
|
model: model,
|
|
63
68
|
input_messages: payload[:contents] || payload["contents"],
|
|
64
69
|
system_instructions: payload[:system_instruction] || payload["system_instruction"]
|
|
@@ -90,7 +95,9 @@ module Foam
|
|
|
90
95
|
def generate_content(payload, server_sent_events: nil, &callback)
|
|
91
96
|
Foam::Otel::LLM.observe(
|
|
92
97
|
provider: GeminiShim::PROVIDER,
|
|
93
|
-
|
|
98
|
+
operation: GeminiShim::OPERATION,
|
|
99
|
+
request: Foam::Otel::LLM.safe_request(provider: GeminiShim::PROVIDER,
|
|
100
|
+
operation: GeminiShim::OPERATION) do
|
|
94
101
|
GeminiShim.request(self, payload)
|
|
95
102
|
end,
|
|
96
103
|
response_extractor: GeminiShim::RESPONSE
|
|
@@ -100,7 +107,9 @@ module Foam
|
|
|
100
107
|
def stream_generate_content(payload, server_sent_events: nil, &callback)
|
|
101
108
|
Foam::Otel::LLM.observe(
|
|
102
109
|
provider: GeminiShim::PROVIDER,
|
|
103
|
-
|
|
110
|
+
operation: GeminiShim::OPERATION,
|
|
111
|
+
request: Foam::Otel::LLM.safe_request(provider: GeminiShim::PROVIDER,
|
|
112
|
+
operation: GeminiShim::OPERATION) do
|
|
104
113
|
GeminiShim.request(self, payload)
|
|
105
114
|
end,
|
|
106
115
|
response_extractor: GeminiShim::RESPONSE
|
|
@@ -73,15 +73,44 @@ module Foam
|
|
|
73
73
|
)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# RubyLLM::Message list → plain
|
|
76
|
+
# RubyLLM::Message list → plain message hashes for the RAW
|
|
77
77
|
# input.messages JSON (Content objects degrade to_s, never dropped).
|
|
78
|
+
# Agent-loop turns keep their tool activity (rule 8a/8c floor —
|
|
79
|
+
# tool names/arguments are activity signal): an assistant tool-call
|
|
80
|
+
# turn carries "tool_calls" (id/name/arguments verbatim) and a
|
|
81
|
+
# tool-result turn carries its "tool_call_id" linkage.
|
|
78
82
|
def serialize_messages(messages)
|
|
79
83
|
Array(messages).map do |message|
|
|
80
|
-
{
|
|
84
|
+
entry = {
|
|
81
85
|
"role" => (message.respond_to?(:role) ? message.role.to_s : "user"),
|
|
82
86
|
"content" => content_of(message),
|
|
83
87
|
}
|
|
88
|
+
tool_calls = serialize_tool_calls(message)
|
|
89
|
+
entry["tool_calls"] = tool_calls if tool_calls
|
|
90
|
+
tool_call_id = message.respond_to?(:tool_call_id) ? message.tool_call_id : nil
|
|
91
|
+
entry["tool_call_id"] = tool_call_id.to_s if tool_call_id && !tool_call_id.to_s.empty?
|
|
92
|
+
entry
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError, SystemStackError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# RubyLLM::Message#tool_calls is a Hash{id => ToolCall} — each call
|
|
99
|
+
# rides as {id:, name:, arguments:} verbatim. nil when absent/empty
|
|
100
|
+
# (the key is omitted, matching the non-agentic message shape).
|
|
101
|
+
def serialize_tool_calls(message)
|
|
102
|
+
return nil unless message.respond_to?(:tool_calls)
|
|
103
|
+
|
|
104
|
+
calls = message.tool_calls
|
|
105
|
+
calls = calls.values if calls.is_a?(Hash)
|
|
106
|
+
serialized = Array(calls).map do |call|
|
|
107
|
+
{
|
|
108
|
+
"id" => (call.respond_to?(:id) ? call.id : nil),
|
|
109
|
+
"name" => (call.respond_to?(:name) ? call.name : nil),
|
|
110
|
+
"arguments" => (call.respond_to?(:arguments) ? call.arguments : nil),
|
|
111
|
+
}
|
|
84
112
|
end
|
|
113
|
+
serialized.empty? ? nil : serialized
|
|
85
114
|
rescue StandardError, SystemStackError
|
|
86
115
|
nil
|
|
87
116
|
end
|
|
@@ -96,20 +125,26 @@ module Foam
|
|
|
96
125
|
end
|
|
97
126
|
end
|
|
98
127
|
|
|
99
|
-
# The returned RubyLLM::Message → response attributes.
|
|
128
|
+
# The returned RubyLLM::Message → response attributes. A tool-call
|
|
129
|
+
# turn (the assistant asking for a tool run) keeps its tool names +
|
|
130
|
+
# arguments on the output side too — without them the agent loop's
|
|
131
|
+
# model turns are invisible activity.
|
|
100
132
|
RESPONSE = lambda do |message|
|
|
101
133
|
next nil unless message.respond_to?(:content)
|
|
102
134
|
|
|
103
135
|
input_tokens = message.respond_to?(:input_tokens) ? message.input_tokens : nil
|
|
104
136
|
output_tokens = message.respond_to?(:output_tokens) ? message.output_tokens : nil
|
|
137
|
+
output_message = {
|
|
138
|
+
"role" => (message.respond_to?(:role) ? message.role.to_s : "assistant"),
|
|
139
|
+
"content" => RubyLLMShim.content_of(message),
|
|
140
|
+
}
|
|
141
|
+
tool_calls = RubyLLMShim.serialize_tool_calls(message)
|
|
142
|
+
output_message["tool_calls"] = tool_calls if tool_calls
|
|
105
143
|
LLM.response_attributes(
|
|
106
144
|
model: message.respond_to?(:model_id) ? message.model_id : nil,
|
|
107
145
|
input_tokens: input_tokens.is_a?(Numeric) ? input_tokens.to_i : nil,
|
|
108
146
|
output_tokens: output_tokens.is_a?(Numeric) ? output_tokens.to_i : nil,
|
|
109
|
-
output_messages: [
|
|
110
|
-
"role" => (message.respond_to?(:role) ? message.role.to_s : "assistant"),
|
|
111
|
-
"content" => RubyLLMShim.content_of(message),
|
|
112
|
-
}]
|
|
147
|
+
output_messages: [output_message]
|
|
113
148
|
)
|
|
114
149
|
end
|
|
115
150
|
|
data/lib/foam/otel/version.rb
CHANGED
|
@@ -9,6 +9,14 @@ module Foam
|
|
|
9
9
|
# request-context architecture is gone; the init surface changed.
|
|
10
10
|
# 1.0.1 (#80): HTTP-client instrumentation class resolved for the loop guard.
|
|
11
11
|
# 1.0.2: FOAM_OTEL_TOKEN doc convention (token comment source change); drop-in.
|
|
12
|
-
|
|
12
|
+
# 1.2.1: review-fleet fixes. DELIBERATE WIRE CHANGE (GenAI-uniformity
|
|
13
|
+
# ruling): Gemini shim gen_ai.operation.name + span name "chat" →
|
|
14
|
+
# "generate_content", matching the js/python Gemini emissions. ruby_llm
|
|
15
|
+
# shim now captures agent-loop tool activity (tool_calls id/name/
|
|
16
|
+
# arguments + tool_call_id) in gen_ai input/output messages. Contrib
|
|
17
|
+
# -logger pinned disabled in the sweep (foam's LoggerBridge supersedes
|
|
18
|
+
# it — rule 12 double-bridge guard). Sidekiq + Faraday floor entries
|
|
19
|
+
# wire-proven (rule 8b); Sidekiq :link posture documented (GOTCHAS G16).
|
|
20
|
+
VERSION = "1.2.1"
|
|
13
21
|
end
|
|
14
22
|
end
|