graphmind 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +39 -0
- data/LICENSE +21 -0
- data/README.md +464 -0
- data/lib/graphmind/api.rb +267 -0
- data/lib/graphmind/clock.rb +49 -0
- data/lib/graphmind/edit_guard.rb +87 -0
- data/lib/graphmind/env.rb +83 -0
- data/lib/graphmind/errors.rb +40 -0
- data/lib/graphmind/gate_engine.rb +282 -0
- data/lib/graphmind/held_ledger.rb +209 -0
- data/lib/graphmind/ids.rb +52 -0
- data/lib/graphmind/integrations/ruby_llm.rb +449 -0
- data/lib/graphmind/integrations/ruby_openai.rb +388 -0
- data/lib/graphmind/integrations/support.rb +459 -0
- data/lib/graphmind/jack.rb +201 -0
- data/lib/graphmind/loop_guard.rb +516 -0
- data/lib/graphmind/protocol.rb +123 -0
- data/lib/graphmind/railtie.rb +66 -0
- data/lib/graphmind/redaction.rb +781 -0
- data/lib/graphmind/ring_buffer.rb +75 -0
- data/lib/graphmind/runtime.rb +50 -0
- data/lib/graphmind/safe.rb +48 -0
- data/lib/graphmind/session.rb +1180 -0
- data/lib/graphmind/shrink.rb +787 -0
- data/lib/graphmind/token_batcher.rb +84 -0
- data/lib/graphmind/transport.rb +331 -0
- data/lib/graphmind/version.rb +5 -0
- data/lib/graphmind/websocket.rb +448 -0
- data/lib/graphmind/wrap.rb +295 -0
- data/lib/graphmind.rb +54 -0
- metadata +83 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "clock"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "gate_engine"
|
|
6
|
+
require_relative "ids"
|
|
7
|
+
|
|
8
|
+
module Graphmind
|
|
9
|
+
# Gating a plain callable — where `inject` and `retry` actually work.
|
|
10
|
+
#
|
|
11
|
+
# A callback or a patched provider client can *observe* and *hold*, but only
|
|
12
|
+
# a wrapper that owns the call site can substitute a result. So this is the
|
|
13
|
+
# sharp end of the debugger:
|
|
14
|
+
#
|
|
15
|
+
# * `before` gate before the body runs — `inject` returns the debugger's
|
|
16
|
+
# value instead of calling it at all;
|
|
17
|
+
# * `error` gate when the body raises, BEFORE the caller sees the error —
|
|
18
|
+
# `inject` swallows the error and returns a value, `retry` re-runs the
|
|
19
|
+
# body, `continue` re-raises the original, `abort` aborts the run;
|
|
20
|
+
# * `after` gate post-body, pre-return (fires only in step mode or with an
|
|
21
|
+
# explicit `after` breakpoint — decisions.md #2).
|
|
22
|
+
#
|
|
23
|
+
# Gates hold the CALLING thread, so a wrapped tool called from a Puma worker
|
|
24
|
+
# holds exactly that request and nothing else.
|
|
25
|
+
module Wrap
|
|
26
|
+
MAX_ARGS_PREVIEW = 32
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# The core gated invocation. `body` is the real work.
|
|
31
|
+
#
|
|
32
|
+
# `output_for` maps the body's result to what the viewer should show (an
|
|
33
|
+
# LLM response object is not something you want rendered raw), and
|
|
34
|
+
# `finish_extra` contributes extra node.finished fields (token usage).
|
|
35
|
+
# Both are optional and never allowed to break the call.
|
|
36
|
+
# `inject_as` adapts a value the debugger injected — which arrived as
|
|
37
|
+
# plain JSON over the socket — into whatever the call site expects
|
|
38
|
+
# (a provider response object, say). Without it, `inject` is only usable
|
|
39
|
+
# where the host already speaks Hash/String.
|
|
40
|
+
# `full_input: true` records `input` as given — it must already be
|
|
41
|
+
# JSON-safe (Integrations::Support.record) — instead of through the
|
|
42
|
+
# session's bounded preview: an LLM prompt is recorded in full (contract
|
|
43
|
+
# C1; the 512 KB per-event shrink is the only bound).
|
|
44
|
+
def invoke(session, node_id:, kind:, name:, parent_id: nil, input: nil, output_for: nil,
|
|
45
|
+
finish_extra: nil, inject_as: nil, full_input: false, &body)
|
|
46
|
+
return body.call if session.nil? || !session.enabled? || session.disposed?
|
|
47
|
+
|
|
48
|
+
instance_id = Ids.next_id("call")
|
|
49
|
+
# exec.paused names this execution (parallel calls of one tool stay apart).
|
|
50
|
+
node = GateNode.new(node_id, kind, name, instance_id)
|
|
51
|
+
ctx = session.current_run
|
|
52
|
+
started = Clock.now_ms
|
|
53
|
+
if full_input && !input.nil?
|
|
54
|
+
session.start_node(node_id: node_id, kind: kind, name: name, instance_id: instance_id,
|
|
55
|
+
parent_id: parent_id, extra: { "input" => input })
|
|
56
|
+
else
|
|
57
|
+
session.start_node(node_id: node_id, kind: kind, name: name, instance_id: instance_id,
|
|
58
|
+
parent_id: parent_id, input: input)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
finish = lambda do |output, status, extra = nil|
|
|
62
|
+
session.finish_node(
|
|
63
|
+
node_id: node_id,
|
|
64
|
+
instance_id: instance_id,
|
|
65
|
+
duration_ms: Clock.elapsed_ms(started),
|
|
66
|
+
status: status,
|
|
67
|
+
output: output,
|
|
68
|
+
extra: extra
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
loop do
|
|
73
|
+
pre = session.gate("before", node)
|
|
74
|
+
if pre.abort?
|
|
75
|
+
finish.call(nil, "aborted")
|
|
76
|
+
raise session.abort_error(ctx)
|
|
77
|
+
end
|
|
78
|
+
if pre.inject?
|
|
79
|
+
finish.call(pre.output, "ok", { "injected" => true })
|
|
80
|
+
return coerce_injected(inject_as, pre.output)
|
|
81
|
+
end
|
|
82
|
+
# `retry` before execution is equivalent to continue.
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
result = body.call
|
|
86
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
87
|
+
unless gateable_error?(e)
|
|
88
|
+
finish.call(nil, Errors.abort_error?(e) ? "aborted" : "error")
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
91
|
+
session.error_node(node_id, instance_id, e)
|
|
92
|
+
decision = session.gate("error", node)
|
|
93
|
+
if decision.inject?
|
|
94
|
+
finish.call(decision.output, "ok",
|
|
95
|
+
{ "injected" => true, "recoveredFromError" => true })
|
|
96
|
+
return coerce_injected(inject_as, decision.output)
|
|
97
|
+
end
|
|
98
|
+
next if decision.retry?
|
|
99
|
+
|
|
100
|
+
if decision.abort?
|
|
101
|
+
finish.call(nil, "aborted")
|
|
102
|
+
raise session.abort_error(ctx)
|
|
103
|
+
end
|
|
104
|
+
finish.call(nil, "error")
|
|
105
|
+
raise
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
post = session.gate("after", node)
|
|
109
|
+
if post.inject?
|
|
110
|
+
finish.call(post.output, "ok", { "injected" => true })
|
|
111
|
+
return coerce_injected(inject_as, post.output)
|
|
112
|
+
end
|
|
113
|
+
next if post.retry?
|
|
114
|
+
|
|
115
|
+
output = output_for ? project(output_for, result) : result
|
|
116
|
+
if post.abort?
|
|
117
|
+
finish.call(output, "aborted")
|
|
118
|
+
raise session.abort_error(ctx)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
finish.call(output, "ok", project(finish_extra, result))
|
|
122
|
+
return result
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Adapt an injected JSON value to the host's expected type. A coercion
|
|
127
|
+
# that fails falls back to the raw value rather than breaking the call.
|
|
128
|
+
def coerce_injected(coercer, value)
|
|
129
|
+
return value if coercer.nil?
|
|
130
|
+
|
|
131
|
+
coercer.call(value)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A viewer-facing projection must never break the host's call.
|
|
137
|
+
def project(mapper, result)
|
|
138
|
+
return nil if mapper.nil?
|
|
139
|
+
|
|
140
|
+
mapper.call(result)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Only ordinary errors are gated. A debugger-driven abort surfacing from
|
|
146
|
+
# the body is terminal, and Interrupt / SystemExit / SignalException must
|
|
147
|
+
# never be held.
|
|
148
|
+
def gateable_error?(error)
|
|
149
|
+
return false unless error.is_a?(StandardError)
|
|
150
|
+
|
|
151
|
+
!Errors.abort_error?(error)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A readable `input` payload for the viewer, keyed by parameter name when
|
|
155
|
+
# the callable exposes one.
|
|
156
|
+
def describe(parameters, args, kwargs)
|
|
157
|
+
out = {}
|
|
158
|
+
index = 0
|
|
159
|
+
Array(parameters).each do |kind, pname|
|
|
160
|
+
case kind
|
|
161
|
+
when :req, :opt
|
|
162
|
+
break if index >= args.length
|
|
163
|
+
|
|
164
|
+
out[pname.to_s] = args[index]
|
|
165
|
+
index += 1
|
|
166
|
+
when :rest
|
|
167
|
+
rest = args[index..] || []
|
|
168
|
+
out[(pname || :args).to_s] = rest unless rest.empty?
|
|
169
|
+
index = args.length
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
leftover = args[index..] || []
|
|
173
|
+
out["args"] = leftover.first(MAX_ARGS_PREVIEW) unless leftover.empty?
|
|
174
|
+
kwargs.each { |k, v| out[k.to_s] = v } if kwargs.is_a?(Hash)
|
|
175
|
+
out
|
|
176
|
+
rescue StandardError
|
|
177
|
+
{ "args" => Array(args).first(MAX_ARGS_PREVIEW) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def callable_name(callable)
|
|
181
|
+
return callable.name.to_s if callable.respond_to?(:name) && callable.name
|
|
182
|
+
|
|
183
|
+
"callable"
|
|
184
|
+
rescue StandardError
|
|
185
|
+
"callable"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parameters_of(callable)
|
|
189
|
+
callable.respond_to?(:parameters) ? callable.parameters : []
|
|
190
|
+
rescue StandardError
|
|
191
|
+
[]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A callable wrapped with GraphMind gates. Quacks like the original: #call,
|
|
196
|
+
# #to_proc, #arity, and `.()`.
|
|
197
|
+
class GatedCallable
|
|
198
|
+
attr_reader :name, :node_id, :kind, :original
|
|
199
|
+
|
|
200
|
+
def initialize(callable, session_of, name: nil, kind: "tool", node_id: nil, parent_id: nil)
|
|
201
|
+
@original = callable
|
|
202
|
+
@session_of = session_of
|
|
203
|
+
@name = (name || Wrap.callable_name(callable)).to_s
|
|
204
|
+
@kind = kind
|
|
205
|
+
@node_id = node_id || Ids.tool_node_id(@name)
|
|
206
|
+
@parent_id = parent_id
|
|
207
|
+
@parameters = Wrap.parameters_of(callable)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def call(*args, **kwargs, &block)
|
|
211
|
+
session = @session_of.call
|
|
212
|
+
body = -> { kwargs.empty? ? @original.call(*args, &block) : @original.call(*args, **kwargs, &block) }
|
|
213
|
+
return body.call if session.nil? || !session.enabled? || session.disposed?
|
|
214
|
+
|
|
215
|
+
Wrap.invoke(
|
|
216
|
+
session,
|
|
217
|
+
node_id: @node_id,
|
|
218
|
+
kind: @kind,
|
|
219
|
+
name: @name,
|
|
220
|
+
parent_id: @parent_id,
|
|
221
|
+
input: Wrap.describe(@parameters, args, kwargs),
|
|
222
|
+
&body
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
alias [] call
|
|
227
|
+
alias === call
|
|
228
|
+
|
|
229
|
+
def to_proc
|
|
230
|
+
method(:call).to_proc
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def arity
|
|
234
|
+
@original.respond_to?(:arity) ? @original.arity : -1
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def parameters = @parameters
|
|
238
|
+
def graphmind_wrapped? = true
|
|
239
|
+
|
|
240
|
+
def respond_to_missing?(symbol, include_private = false)
|
|
241
|
+
@original.respond_to?(symbol, include_private) || super
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def method_missing(symbol, ...)
|
|
245
|
+
return @original.public_send(symbol, ...) if @original.respond_to?(symbol)
|
|
246
|
+
|
|
247
|
+
super
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
module Wrap
|
|
252
|
+
module_function
|
|
253
|
+
|
|
254
|
+
def gate_callable(callable, session_of, name: nil, kind: "tool", node_id: nil, parent_id: nil)
|
|
255
|
+
return callable if callable.is_a?(GatedCallable)
|
|
256
|
+
|
|
257
|
+
GatedCallable.new(callable, session_of, name: name, kind: kind, node_id: node_id,
|
|
258
|
+
parent_id: parent_id)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def wrapped?(callable)
|
|
262
|
+
callable.is_a?(GatedCallable) ||
|
|
263
|
+
(callable.respond_to?(:graphmind_wrapped?) && callable.graphmind_wrapped?)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Wrap a Hash of {name => callable}, an Array of callables, or a single
|
|
267
|
+
# callable. Shape in, shape out.
|
|
268
|
+
def wrap_tools(tools, session_of)
|
|
269
|
+
case tools
|
|
270
|
+
when Hash
|
|
271
|
+
tools.each_with_object({}) do |(key, fn), out|
|
|
272
|
+
out[key] = wrapped?(fn) ? fn : gate_callable(fn, session_of, name: key.to_s)
|
|
273
|
+
end
|
|
274
|
+
when Array
|
|
275
|
+
tools.map { |fn| wrapped?(fn) ? fn : gate_callable(fn, session_of) }
|
|
276
|
+
else
|
|
277
|
+
raise ArgumentError, "wrap_tools expects a callable, an Array, or a {name => callable} Hash" unless tools.respond_to?(:call)
|
|
278
|
+
|
|
279
|
+
gate_callable(tools, session_of)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Gate one method on one object, in place. The object keeps its API; the
|
|
284
|
+
# method now pauses.
|
|
285
|
+
def wrap_method(target, method_name, session_of, name: nil, kind: "tool", parent_id: nil)
|
|
286
|
+
original = target.method(method_name)
|
|
287
|
+
gated = gate_callable(original, session_of, name: name || method_name.to_s, kind: kind,
|
|
288
|
+
parent_id: parent_id)
|
|
289
|
+
target.define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
290
|
+
gated.call(*args, **kwargs, &block)
|
|
291
|
+
end
|
|
292
|
+
target
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
data/lib/graphmind.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GraphMind — a live debugger for AI agents.
|
|
4
|
+
#
|
|
5
|
+
# Phoenix and Langfuse show you what your agent *did*. GraphMind attaches while
|
|
6
|
+
# it is happening: an instrumented app streams execution events to a local
|
|
7
|
+
# viewer, which renders the run as a live graph and can HOLD execution —
|
|
8
|
+
# before an LLM step, before/after a tool call, or on error — then resume with
|
|
9
|
+
# continue / retry / inject / abort.
|
|
10
|
+
#
|
|
11
|
+
# Quickstart:
|
|
12
|
+
#
|
|
13
|
+
# require "graphmind"
|
|
14
|
+
#
|
|
15
|
+
# gm = Graphmind.configure(app: "support-agent")
|
|
16
|
+
# client = gm.instrument_openai(OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"]))
|
|
17
|
+
#
|
|
18
|
+
# search = gm.tool("search_flights") { |origin:, destination:| Flights.search(origin, destination) }
|
|
19
|
+
#
|
|
20
|
+
# gm.run("handle-ticket") do
|
|
21
|
+
# client.chat(parameters: { model: "gpt-4o-mini", messages: [...] })
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Then run `npx graphmind-ai` and open http://127.0.0.1:4747. With nothing
|
|
25
|
+
# attached, all of the above is a no-op.
|
|
26
|
+
#
|
|
27
|
+
# Everything fails open: no debugger means near-zero overhead, and a debugger
|
|
28
|
+
# that disconnects mid-hold auto-continues every held gate.
|
|
29
|
+
module Graphmind
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
require_relative "graphmind/version"
|
|
33
|
+
require_relative "graphmind/errors"
|
|
34
|
+
require_relative "graphmind/env"
|
|
35
|
+
require_relative "graphmind/ids"
|
|
36
|
+
require_relative "graphmind/protocol"
|
|
37
|
+
require_relative "graphmind/safe"
|
|
38
|
+
require_relative "graphmind/ring_buffer"
|
|
39
|
+
require_relative "graphmind/gate_engine"
|
|
40
|
+
require_relative "graphmind/clock"
|
|
41
|
+
require_relative "graphmind/held_ledger"
|
|
42
|
+
require_relative "graphmind/redaction"
|
|
43
|
+
require_relative "graphmind/loop_guard"
|
|
44
|
+
require_relative "graphmind/shrink"
|
|
45
|
+
require_relative "graphmind/edit_guard"
|
|
46
|
+
require_relative "graphmind/websocket"
|
|
47
|
+
require_relative "graphmind/transport"
|
|
48
|
+
require_relative "graphmind/token_batcher"
|
|
49
|
+
require_relative "graphmind/runtime"
|
|
50
|
+
require_relative "graphmind/session"
|
|
51
|
+
require_relative "graphmind/wrap"
|
|
52
|
+
require_relative "graphmind/api"
|
|
53
|
+
|
|
54
|
+
require_relative "graphmind/railtie" if defined?(::Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: graphmind
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.6.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- GraphMind
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
GraphMind attaches to an agent while it is running. Your instrumented app
|
|
14
|
+
streams execution events to a local viewer, which renders the run as a live
|
|
15
|
+
graph and can hold execution — before an LLM step, before/after a tool call,
|
|
16
|
+
or on error — then resume with continue / retry / inject / abort.
|
|
17
|
+
|
|
18
|
+
Local-first (127.0.0.1), zero runtime dependencies, fails open: with no
|
|
19
|
+
debugger attached everything is a no-op, and a debugger that disconnects
|
|
20
|
+
mid-hold releases every gate.
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE
|
|
27
|
+
- README.md
|
|
28
|
+
- lib/graphmind.rb
|
|
29
|
+
- lib/graphmind/api.rb
|
|
30
|
+
- lib/graphmind/clock.rb
|
|
31
|
+
- lib/graphmind/edit_guard.rb
|
|
32
|
+
- lib/graphmind/env.rb
|
|
33
|
+
- lib/graphmind/errors.rb
|
|
34
|
+
- lib/graphmind/gate_engine.rb
|
|
35
|
+
- lib/graphmind/held_ledger.rb
|
|
36
|
+
- lib/graphmind/ids.rb
|
|
37
|
+
- lib/graphmind/integrations/ruby_llm.rb
|
|
38
|
+
- lib/graphmind/integrations/ruby_openai.rb
|
|
39
|
+
- lib/graphmind/integrations/support.rb
|
|
40
|
+
- lib/graphmind/jack.rb
|
|
41
|
+
- lib/graphmind/loop_guard.rb
|
|
42
|
+
- lib/graphmind/protocol.rb
|
|
43
|
+
- lib/graphmind/railtie.rb
|
|
44
|
+
- lib/graphmind/redaction.rb
|
|
45
|
+
- lib/graphmind/ring_buffer.rb
|
|
46
|
+
- lib/graphmind/runtime.rb
|
|
47
|
+
- lib/graphmind/safe.rb
|
|
48
|
+
- lib/graphmind/session.rb
|
|
49
|
+
- lib/graphmind/shrink.rb
|
|
50
|
+
- lib/graphmind/token_batcher.rb
|
|
51
|
+
- lib/graphmind/transport.rb
|
|
52
|
+
- lib/graphmind/version.rb
|
|
53
|
+
- lib/graphmind/websocket.rb
|
|
54
|
+
- lib/graphmind/wrap.rb
|
|
55
|
+
homepage: https://graphmind.ai
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://graphmind.ai
|
|
60
|
+
source_code_uri: https://github.com/Hegazy360/GraphMind
|
|
61
|
+
bug_tracker_uri: https://github.com/Hegazy360/GraphMind/issues
|
|
62
|
+
changelog_uri: https://github.com/Hegazy360/GraphMind/blob/master/CHANGELOG.md
|
|
63
|
+
documentation_uri: https://github.com/Hegazy360/GraphMind/blob/master/ruby/README.md
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.1.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.20
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Live debugger for AI agents — attach to a running Ruby agent, watch it as
|
|
82
|
+
a graph, pause and inject at LLM and tool boundaries.
|
|
83
|
+
test_files: []
|