llm.rb 15.0.3 → 15.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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +433 -3
  3. data/README.md +188 -71
  4. data/bin/llm.rb +50 -7
  5. data/data/alibaba.json +912 -823
  6. data/data/anthropic.json +234 -187
  7. data/data/bedrock.json +3702 -2058
  8. data/data/deepinfra.json +1288 -951
  9. data/data/deepseek.json +87 -53
  10. data/data/google.json +670 -670
  11. data/data/mistral.json +501 -460
  12. data/data/moonshot.json +43 -248
  13. data/data/openai.json +1008 -914
  14. data/data/openrouter.json +14417 -0
  15. data/data/xai.json +213 -201
  16. data/data/zai.json +242 -149
  17. data/docs/deepdive/advanced/compaction.md +5 -5
  18. data/docs/deepdive/advanced/context.md +8 -6
  19. data/docs/deepdive/advanced/guard.md +2 -2
  20. data/docs/deepdive/features/builtin_tools.md +93 -22
  21. data/docs/deepdive/features/{repl.md → console.md} +28 -28
  22. data/docs/deepdive/features/database.md +3 -3
  23. data/docs/deepdive/fundamentals/agents.md +13 -12
  24. data/docs/deepdive/fundamentals/providers.md +91 -6
  25. data/docs/deepdive/fundamentals/skills.md +14 -6
  26. data/docs/deepdive/fundamentals/stream.md +4 -4
  27. data/docs/deepdive/fundamentals/tools.md +63 -31
  28. data/docs/deepdive/reference/cost.md +2 -2
  29. data/docs/deepdive/reference/model_registry.md +2 -2
  30. data/docs/deepdive/reference/tracer.md +15 -13
  31. data/docs/deepdive.md +2 -2
  32. data/lib/llm/active_record/acts_as_agent.rb +9 -5
  33. data/lib/llm/agent.rb +40 -15
  34. data/lib/llm/{repl → console}/bar.rb +3 -3
  35. data/lib/llm/{repl → console}/buffer.rb +24 -9
  36. data/lib/llm/{repl → console}/color.rb +2 -2
  37. data/lib/llm/{repl → console}/command.rb +12 -12
  38. data/lib/llm/{repl → console}/commands/exit.rb +4 -4
  39. data/lib/llm/{repl → console}/commands/help.rb +1 -1
  40. data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
  41. data/lib/llm/{repl → console}/commands/model.rb +2 -2
  42. data/lib/llm/{repl → console}/input/cache.rb +2 -2
  43. data/lib/llm/{repl → console}/input/char.rb +2 -2
  44. data/lib/llm/{repl → console}/input/row.rb +1 -1
  45. data/lib/llm/{repl → console}/input.rb +18 -10
  46. data/lib/llm/console/markdown/parser.rb +78 -0
  47. data/lib/llm/{repl → console}/markdown/table.rb +8 -5
  48. data/lib/llm/{repl → console}/markdown.rb +13 -30
  49. data/lib/llm/console/node.rb +69 -0
  50. data/lib/llm/{repl → console}/status.rb +11 -11
  51. data/lib/llm/{repl → console}/stream.rb +36 -9
  52. data/lib/llm/{repl → console}/walker.rb +1 -1
  53. data/lib/llm/{repl → console}/window.rb +17 -17
  54. data/lib/llm/{repl.rb → console.rb} +39 -19
  55. data/lib/llm/context/deserializer.rb +2 -1
  56. data/lib/llm/context.rb +29 -13
  57. data/lib/llm/cost.rb +13 -0
  58. data/lib/llm/function/async/reactor.rb +20 -1
  59. data/lib/llm/function/fork/task.rb +14 -10
  60. data/lib/llm/function.rb +1 -1
  61. data/lib/llm/json_adapter.rb +40 -28
  62. data/lib/llm/message.rb +7 -0
  63. data/lib/llm/provider.rb +31 -10
  64. data/lib/llm/providers/alibaba.rb +1 -1
  65. data/lib/llm/providers/anthropic.rb +1 -1
  66. data/lib/llm/providers/bedrock/models.rb +2 -2
  67. data/lib/llm/providers/bedrock.rb +1 -1
  68. data/lib/llm/providers/deepseek.rb +1 -1
  69. data/lib/llm/providers/google.rb +1 -1
  70. data/lib/llm/providers/ollama.rb +1 -1
  71. data/lib/llm/providers/openai/responses.rb +2 -1
  72. data/lib/llm/providers/openai.rb +2 -1
  73. data/lib/llm/providers/openrouter.rb +87 -0
  74. data/lib/llm/schema/leaf.rb +34 -2
  75. data/lib/llm/schema.rb +4 -2
  76. data/lib/llm/sequel/agent.rb +9 -5
  77. data/lib/llm/skill.rb +7 -1
  78. data/lib/llm/stream.rb +8 -3
  79. data/lib/llm/tool/param.rb +5 -1
  80. data/lib/llm/tool.rb +5 -0
  81. data/lib/llm/tools/bundle.rb +53 -0
  82. data/lib/llm/tools/edit-file.rb +7 -2
  83. data/lib/llm/tools/exec.rb +78 -0
  84. data/lib/llm/tools/git.rb +27 -26
  85. data/lib/llm/tools/mkdir.rb +12 -19
  86. data/lib/llm/tools/read_file.rb +69 -9
  87. data/lib/llm/tools/rg.rb +20 -24
  88. data/lib/llm/tools/ruby.rb +17 -25
  89. data/lib/llm/tools/utils.rb +75 -2
  90. data/lib/llm/tools/write_file.rb +4 -1
  91. data/lib/llm/tracer/logger.rb +2 -2
  92. data/lib/llm/tracer/pretty_logger.rb +4 -4
  93. data/lib/llm/tracer/telemetry.rb +2 -2
  94. data/lib/llm/tracer.rb +33 -0
  95. data/lib/llm/transport/curb.rb +5 -3
  96. data/lib/llm/transport/http.rb +5 -2
  97. data/lib/llm/transport/persistent_http.rb +6 -4
  98. data/lib/llm/transport/utils.rb +8 -6
  99. data/lib/llm/version.rb +1 -1
  100. data/lib/llm.rb +18 -12
  101. data/llm.gemspec +8 -8
  102. metadata +80 -37
  103. data/lib/llm/repl/node.rb +0 -44
  104. data/lib/llm/tools/shell.rb +0 -55
@@ -32,8 +32,8 @@ Tools that spawn subprocesses can include
32
32
  [`LLM::Tool::Utils`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html)
33
33
  to get shared
34
34
  [`wait(command:, timeout:)`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html#wait-instance_method)
35
- and `now` help. The built-in `Shell`, `Git`, `Rg`, and `Mkdir` tools
36
- use it to kill a command that exceeds its `timeout`.
35
+ and `now` help. The built-in `Exec`, `Git`, `Rg`, `Mkdir`, and `Ruby`
36
+ tools use it to kill a command that exceeds its `timeout`.
37
37
 
38
38
  If
39
39
  [`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
@@ -44,22 +44,34 @@ also handle errors yourself inside
44
44
  by rescuing and returning a domain-specific error hash.
45
45
 
46
46
  ```ruby
47
- class Shell < LLM::Tool
48
- set name: "shell",
49
- description: "execute a shell command",
50
- parameters: [
51
- [:name, String, "the command's name", {required: true}],
52
- [:arguments, Array[String], "command args", {default: []}]
53
- ]
47
+ require "llm/tools/utils"
48
+
49
+ class Exec < LLM::Tool
50
+ include Utils
51
+
52
+ name "exec"
53
+ description "run a command without a shell"
54
+ parameter :name, String, "the command's name"
55
+ parameter :arguments, Array[String], "command args"
56
+ required %i[name]
57
+ defaults arguments: [], timeout: 60, max_bytes: :max_bytes
54
58
 
55
- def call(name:, arguments: [])
56
- out = `#{name.shellescape} #{arguments.map(&:shellescape).join(" ")}`
57
- {ok: $?.success?, out:}
59
+ def self.max_bytes(bytes = nil)
60
+ bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
61
+ end
62
+
63
+ def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
64
+ command = spawn(name:, arguments:, max_bytes:)
65
+ wait(command:, timeout:)
66
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
67
+ rescue LLM::Interrupt
68
+ command.kill! if command&.running?
69
+ raise
58
70
  end
59
71
  end
60
72
 
61
73
  llm = LLM.deepseek(key: ENV["KEY"])
62
- agent = LLM::Agent.new(llm, tools: [Shell], stream: $stdout)
74
+ agent = LLM::Agent.new(llm, tools: [Exec], stream: $stdout)
63
75
  agent.talk "What files are in the current working directory?"
64
76
  ```
65
77
 
@@ -165,17 +177,26 @@ message: "boom"}` to the model. You can also rescue inside
165
177
  and return your own error shape that gives the model more context.
166
178
 
167
179
  ```ruby
168
- class Shell < LLM::Tool
169
- set name: "shell",
170
- description: "run a shell command",
171
- parameters: [
172
- [:name, String, "the command name", {required: true}],
173
- [:arguments, Array[String], "command args", {default: []}]
174
- ]
180
+ require "llm/tools/utils"
175
181
 
176
- def call(name:, arguments: [])
177
- out = `#{name} #{arguments.join(" ")}`
178
- {ok: $?.success?, out:}
182
+ class Exec < LLM::Tool
183
+ include Utils
184
+
185
+ name "exec"
186
+ description "run a command without a shell"
187
+ parameter :name, String, "the command name"
188
+ parameter :arguments, Array[String], "command args"
189
+ required %i[name]
190
+ defaults arguments: [], timeout: 60, max_bytes: :max_bytes
191
+
192
+ def self.max_bytes(bytes = nil)
193
+ bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
194
+ end
195
+
196
+ def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
197
+ command = spawn(name:, arguments:, max_bytes:)
198
+ wait(command:, timeout:)
199
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
179
200
  rescue Errno::ENOENT
180
201
  {ok: false, error: "command not found: #{name}"}
181
202
  end
@@ -215,17 +236,28 @@ with a Hash. The keys match the individual method names. The
215
236
  individual `parameter` method does.
216
237
 
217
238
  ```ruby
218
- class Shell < LLM::Tool
219
- set name: "shell",
220
- description: "execute a shell command",
239
+ require "llm/tools/utils"
240
+
241
+ class Exec < LLM::Tool
242
+ include Utils
243
+
244
+ set name: "exec",
245
+ description: "run a command without a shell",
221
246
  parameters: [
222
247
  [:name, String, "the command's name", {required: true}],
223
- [:arguments, Array[String], "One or more arguments", {default: []}]
248
+ [:arguments, Array[String], "command args", {default: []}],
249
+ [:timeout, Integer, "timeout in seconds", {default: 60}],
250
+ [:max_bytes, Integer, "max bytes to emit", {default: 75_000}]
224
251
  ]
225
252
 
226
- def call(name:, arguments: [])
227
- out = `#{name.shellescape} #{arguments.map(&:shellescape).join(" ")}`
228
- {ok: $?.success?, out:}
253
+ def self.max_bytes(bytes = nil)
254
+ bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
255
+ end
256
+
257
+ def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
258
+ command = spawn(name:, arguments:, max_bytes:)
259
+ wait(command:, timeout:)
260
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
229
261
  end
230
262
  end
231
263
  ```
@@ -298,7 +330,7 @@ decide to retry or continue with the results it has.
298
330
 
299
331
  #### Overview
300
332
 
301
- llm.rb ships with twelve ready-made tools that cover filesystem,
333
+ llm.rb ships with thirteen ready-made tools that cover filesystem,
302
334
  search, and shell operations. Load them all with
303
335
  `require "llm/tools"`. Each tool is documented in the
304
336
  [built-in tools catalog](builtin_tools.md).
@@ -18,7 +18,7 @@ When you want to know what a conversation cost so far, call
18
18
  [`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
19
19
  (or
20
20
  [`LLM::Agent#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cost-instance_method))
21
- and read the breakdown. The REPL shows this live in its status bar
21
+ and read the breakdown. The console shows this live in its status bar
22
22
  after every turn:
23
23
 
24
24
  ```ruby
@@ -102,7 +102,7 @@ returns the total in a compact, human-friendly format, rounded to
102
102
  two decimals (`"0.01"`). Components that were not used are `0`, so
103
103
  you never need to guard against `nil` when aggregating.
104
104
 
105
- The REPL renders context usage as a proportion, not a cost.
105
+ The console renders context usage as a proportion, not a cost.
106
106
  [`LLM::Context#context_usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_usage-instance_method)
107
107
  returns a `Rational` of the tokens used over the context window
108
108
  (for example `Rational(100, 10_000)`), or `nil` when the window is
@@ -7,7 +7,7 @@
7
7
  The model registry is a catalog of every model each provider offers,
8
8
  shipped with llm.rb under `data/<provider>.json`. The data is sourced
9
9
  from [models.dev](https://models.dev) and powers cost estimation,
10
- context-window limits, and the `/model` auto-complete in the REPL.
10
+ context-window limits, and the `/model` auto-complete in the console.
11
11
 
12
12
  [`LLM::Registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html)
13
13
  exposes that catalog, and
@@ -210,7 +210,7 @@ require "llm"
210
210
  reg = LLM::Registry.for(:openai)
211
211
  list = reg.models.sort
212
212
  list.first.id # => "text-embedding-3-small"
213
- list.last.id # => "chatgpt-image-latest"
213
+ list.last.id # => "gpt-image-1-mini" (unpriced sorts last)
214
214
  ```
215
215
 
216
216
  #### Why would I use it?
@@ -28,7 +28,7 @@ provider or an agent:
28
28
 
29
29
  ```ruby
30
30
  llm = LLM.deepseek(key: ENV["KEY"])
31
- llm.tracer = LLM::Tracer::PrettyLogger.new(llm)
31
+ llm.tracer = LLM::Tracer.pretty_logger(llm)
32
32
  agent = LLM::Agent.new(llm)
33
33
  agent.talk "Hello"
34
34
  ```
@@ -59,12 +59,14 @@ request a provider makes. Three built-in tracers are available:
59
59
  [`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html)
60
60
  (OpenTelemetry).
61
61
 
62
- For a shorter way to build a
63
- [`LLM::Tracer::Logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html),
64
- use the [`LLM.logger`](https://r.uby.dev/api-docs/llm.rb/LLM.html#logger-class_method)
65
- convenience method. It takes a provider and forwards the options as
66
- a positional hash, so `LLM.logger(llm, io: $stdout)` is equivalent
67
- to `LLM::Tracer::Logger.new(llm, io: $stdout)`.
62
+ For a shorter way to build the built-in tracers, use the
63
+ [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method),
64
+ [`LLM::Tracer.pretty_logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method),
65
+ and
66
+ [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method)
67
+ convenience methods. Each takes a provider and forwards its options to
68
+ the matching tracer, so `LLM::Tracer.pretty_logger(llm, io: $stdout)`
69
+ forwards `io:` to the pretty logger.
68
70
 
69
71
  ### Provider
70
72
 
@@ -86,7 +88,7 @@ consistent observability without configuring each agent individually.
86
88
 
87
89
  ```ruby
88
90
  llm = LLM.deepseek(key: ENV["KEY"])
89
- llm.tracer = LLM::Tracer::Logger.new(llm, io: $stdout)
91
+ llm.tracer = LLM::Tracer.logger(llm, io: $stdout)
90
92
  ```
91
93
 
92
94
  #### Why would I use it?
@@ -118,7 +120,7 @@ same provider unaffected.
118
120
 
119
121
  ```ruby
120
122
  llm = LLM.deepseek(key: ENV["KEY"])
121
- agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, io: $stdout))
123
+ agent = LLM::Agent.new(llm, tracer: LLM::Tracer.logger(llm, io: $stdout))
122
124
  ```
123
125
 
124
126
  #### Why would I use it?
@@ -153,21 +155,21 @@ accepts an `io:` option to redirect output.
153
155
 
154
156
  ```ruby
155
157
  llm = LLM.deepseek(key: ENV["KEY"])
156
- llm.tracer = LLM::Tracer::PrettyLogger.new(llm)
158
+ llm.tracer = LLM::Tracer.pretty_logger(llm)
157
159
  ```
158
160
 
159
161
  ##### Agent-local
160
162
 
161
163
  ```ruby
162
164
  llm = LLM.deepseek(key: ENV["KEY"])
163
- agent = LLM::Agent.new(llm, tracer: LLM::Tracer::PrettyLogger.new(llm))
165
+ agent = LLM::Agent.new(llm, tracer: LLM::Tracer.pretty_logger(llm))
164
166
  ```
165
167
 
166
168
  ##### Custom output
167
169
 
168
170
  ```ruby
169
- tracer = LLM::Tracer::PrettyLogger.new(llm, io: $stdout)
170
- tracer = LLM::Tracer::PrettyLogger.new(llm, io: File.open("trace.log", "a"))
171
+ tracer = LLM::Tracer.pretty_logger(llm, io: $stdout)
172
+ tracer = LLM::Tracer.pretty_logger(llm, path: "trace.log")
171
173
  ```
172
174
 
173
175
  #### Why would I use it?
data/docs/deepdive.md CHANGED
@@ -37,7 +37,7 @@ code, `#### Why would I use it?` explains the use case, and
37
37
  #### Why would I use it?
38
38
 
39
39
  The deepdive documents everything there is to know about llm.rb.
40
- It includes the fundamnetals, advanced patterns, configuration
40
+ It includes the fundamentals, advanced patterns, configuration
41
41
  options, ORM support, protocol support, and edge cases. It is
42
42
  useful when you need to go beyond the basics.
43
43
 
@@ -69,7 +69,7 @@ is the best place to start if you are new to llm.rb.
69
69
  - [Concurrency](deepdive/features/concurrency.md)
70
70
  - [Embeddings](deepdive/features/embeddings.md)
71
71
  - [Database](deepdive/features/database.md)
72
- - [REPL](deepdive/features/repl.md)
72
+ - [Console](deepdive/features/console.md)
73
73
 
74
74
  ## Advanced
75
75
 
@@ -20,7 +20,10 @@ module LLM::ActiveRecord
20
20
  ##
21
21
  # @return [Class<LLM::Agent>]
22
22
  def agent
23
- @agent ||= Class.new(LLM::Agent)
23
+ return @agent if defined?(@agent)
24
+ @agent = Class.new(LLM::Agent)
25
+ @agent.name(self)
26
+ @agent
24
27
  end
25
28
 
26
29
  ##
@@ -112,11 +115,12 @@ module LLM::ActiveRecord
112
115
  # This method does not persist to the database,
113
116
  # but it can inspect and alter runtime state in
114
117
  # a way that is temporary.
115
- # @param (see LLM::Agent#repl)
116
- # @return (see LLM::Agent#repl)
117
- def repl(**params)
118
- ctx.repl(**params)
118
+ # @param (see LLM::Agent#console)
119
+ # @return (see LLM::Agent#console)
120
+ def console(**params)
121
+ ctx.console(**params)
119
122
  end
123
+ alias_method :repl, :console
120
124
 
121
125
  private
122
126
 
data/lib/llm/agent.rb CHANGED
@@ -128,7 +128,7 @@ module LLM
128
128
  # Set or get an agent's name
129
129
  # @note
130
130
  # This method serves as a self-documenting string
131
- # and it is used by {LLM::Repl LLM::Repl}. It is
131
+ # and it is used by {LLM::Console LLM::Console}. It is
132
132
  # optional but recommended.
133
133
  # @param [String] name
134
134
  # The agent name
@@ -143,7 +143,12 @@ module LLM
143
143
  @name
144
144
  end
145
145
  else
146
- @name = block || name
146
+ if Class === name
147
+ name = name.to_s.split("::").last
148
+ @name = name.gsub(CASE_PATTERN, "-").downcase
149
+ else
150
+ @name = block || name
151
+ end
147
152
  end
148
153
  end
149
154
 
@@ -292,7 +297,7 @@ module LLM
292
297
  #
293
298
  # @example
294
299
  # class Agent < LLM::Agent
295
- # tracer { LLM::Tracer::Logger.new(llm, io: $stdout) }
300
+ # tracer { LLM::Tracer.logger(llm, io: $stdout) }
296
301
  # end
297
302
  #
298
303
  # @param [LLM::Tracer, Proc, nil] tracer
@@ -372,7 +377,7 @@ module LLM
372
377
  # @return [Integer, nil]
373
378
  def self.retry_budget(budget = UNDEFINED)
374
379
  if budget.equal?(UNDEFINED)
375
- @retry_budget.nil? ? 5 : @retry_budget
380
+ @retry_budget.nil? ? UNDEFINED : @retry_budget
376
381
  else
377
382
  @retry_budget = budget
378
383
  end
@@ -404,7 +409,11 @@ module LLM
404
409
  @llm = llm
405
410
  fields, fields_ivar = FIELDS, IVARS
406
411
  fields.each do |field|
407
- resolvable = params.key?(field) ? params.delete(field) : (self.class.respond_to?(field) ? self.class.public_send(field) : nil)
412
+ resolvable = if params.key?(field)
413
+ params.delete(field)
414
+ else
415
+ (self.class.respond_to?(field) ? self.class.public_send(field) : nil)
416
+ end
408
417
  resolve_symbol = !%i[concurrency].include?(field)
409
418
  resolved = resolvable != nil ? resolve_option(self, resolvable, resolve_symbol:) : resolvable
410
419
  resolved = [*resolved].map(&:to_s) if field == :confirm && resolved
@@ -416,6 +425,13 @@ module LLM
416
425
  instance_variable_set(:"@#{field}", resolved)
417
426
  end
418
427
  end
428
+ ##
429
+ # Alibaba (token plan) will frequently issue rate
430
+ # limits or time outs that it recovers from. The
431
+ # higher retry count is to account for scenarios
432
+ # where it takes longer than expected to recover.
433
+ retry_budget = llm.name == :alibaba ? 8 : 5
434
+ params[:retry_budget] = retry_budget if params[:retry_budget].equal?(UNDEFINED)
419
435
  @ctx = LLM::Context.new(llm, {guard: LLM::Guard::Loop}.merge(params))
420
436
  @path and File.readable?(@path) ? @ctx.restore(path:) : nil
421
437
  end
@@ -486,6 +502,12 @@ module LLM
486
502
  @ctx.messages
487
503
  end
488
504
 
505
+ ##
506
+ # @return [Integer]
507
+ def retry_budget
508
+ @ctx.retry_budget
509
+ end
510
+
489
511
  ##
490
512
  # @return [Array<LLM::Function>]
491
513
  def pending_functions
@@ -644,14 +666,16 @@ module LLM
644
666
  end
645
667
 
646
668
  ##
647
- # Start a minimalist repl that can interact
669
+ # Start an agent console that can interact
648
670
  # with the agent and its current state. This
649
- # method requires the 'curses' gem to be installed
650
- # and available to require.
671
+ # method requires the following gems to be
672
+ # installed and available to require:
673
+ # 'unicode-display_width', 'kramdown', 'curses',
674
+ # 'test-cmd.rb', and 'xchan.rb'.
651
675
  #
652
676
  # @note
653
677
  # By default this method disables the tracer for
654
- # the duration of the repl session, and restores
678
+ # the duration of the console session, and restores
655
679
  # it afterwards.
656
680
  # @param [String] name
657
681
  # The agent's name.
@@ -660,14 +684,14 @@ module LLM
660
684
  # The path to a file where runtime state is read
661
685
  # from, and written to
662
686
  # @param [Array<LLM::Tool>] tools
663
- # Extra tools to attach for the repl session
687
+ # Extra tools to attach for the console session
664
688
  # @param [Array<String>] skills
665
- # Extra skills to attach for the repl session
689
+ # Extra skills to attach for the console session
666
690
  # @param [Boolean] tracer
667
691
  # When true, the tracer is kept alive during the
668
- # repl session. Default is false.
692
+ # console session. Default is false.
669
693
  # @return [void]
670
- def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
694
+ def console(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
671
695
  if trace != nil
672
696
  warn "llm.rb: trace option is deprecated, use tracer instead"
673
697
  tracer = trace
@@ -676,13 +700,14 @@ module LLM
676
700
  previous = self.tracer
677
701
  self.tracer = nil
678
702
  end
679
- require_relative "repl" unless defined?(::LLM::Repl)
680
- LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
703
+ require_relative "console" unless defined?(::LLM::Console)
704
+ LLM::Console.new(agent: self, name:, path:, tools:, skills:).start
681
705
  ensure
682
706
  if !tracer
683
707
  self.tracer = previous
684
708
  end
685
709
  end
710
+ alias_method :repl, :console
686
711
 
687
712
  ##
688
713
  # @see LLM::Context#params
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  ##
5
- # The {LLM::Repl::Bar LLM::Repl::Bar} class renders a
5
+ # The {LLM::Console::Bar LLM::Console::Bar} class renders a
6
6
  # small progress bar for the REPL. It is used to show
7
7
  # the remaining size of the model's context window in
8
8
  # a compact form near the input line.
@@ -21,7 +21,7 @@ class LLM::Repl
21
21
  # The fraction of the context window used, or nil when unknown
22
22
  # (eg after compaction).
23
23
  # @param [Integer] width
24
- # @return [LLM::Repl::Bar]
24
+ # @return [LLM::Console::Bar]
25
25
  def initialize(fraction:, width: 10)
26
26
  @width = width
27
27
  @label, @filled = remainder(fraction)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  ##
5
5
  # This class maintains conversation state that includes
6
6
  # the conversation itself, and metadata associated with
@@ -16,9 +16,9 @@ class LLM::Repl
16
16
  # row by overwriting its contents repeatedly.
17
17
  class Buffer
18
18
  ##
19
- # @param [LLM::Repl] repl
20
- # An instance of {LLM::Repl LLM::Repl}.
21
- # @return [LLM::Repl::Buffer]
19
+ # @param [LLM::Console] console
20
+ # An instance of {LLM::Console LLM::Console}.
21
+ # @return [LLM::Console::Buffer]
22
22
  def initialize(repl)
23
23
  @repl = repl
24
24
  @window = repl.window
@@ -147,15 +147,15 @@ class LLM::Repl
147
147
  if token == "\n"
148
148
  rows << []
149
149
  elsif token.match?(/\s/)
150
- if sum(rows.last) + token.length <= width
150
+ if sum(rows.last) + Node.width(token) <= width
151
151
  rows.last << Node.new(token, attrs)
152
152
  end
153
- elsif sum(rows.last) + token.length > width
153
+ elsif sum(rows.last) + Node.width(token) > width
154
154
  if sum(rows.last) == 0
155
155
  ##
156
156
  # A single word wider than the row: break it
157
- # every width characters.
158
- token.scan(/.{1,#{width}}/).each do |piece|
157
+ # every width columns.
158
+ slices(token, width).each do |piece|
159
159
  rows.last << Node.new(piece, attrs)
160
160
  if sum(rows.last) >= width
161
161
  rows << []
@@ -190,7 +190,22 @@ class LLM::Repl
190
190
  ##
191
191
  # @api private
192
192
  def sum(row)
193
- row.sum { _1[:text].to_s.length }
193
+ row.sum { _1.size }
194
+ end
195
+
196
+ ##
197
+ # Splits a single word into pieces that each fit within the
198
+ # buffer's width, measured in display columns.
199
+ # @api private
200
+ def slices(token, width)
201
+ pieces = []
202
+ remaining = token
203
+ until remaining.empty?
204
+ piece = Node.slice(remaining, width)
205
+ pieces << piece
206
+ remaining = remaining[piece.length..] || +""
207
+ end
208
+ pieces
194
209
  end
195
210
  end
196
211
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  ##
5
- # The {LLM::Repl::Color LLM::Repl::Color} module returns
5
+ # The {LLM::Console::Color LLM::Console::Color} module returns
6
6
  # bitmasks that are understood as colors by the Curses
7
7
  # library. They can be bitwise OR'ed with other attributes,
8
8
  # such as Curses::A_BOLD.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  ##
5
- # The {LLM::Repl::Command LLM::Repl::Command} class is the superclass
5
+ # The {LLM::Console::Command LLM::Console::Command} class is the superclass
6
6
  # of all read-eval-print loop commands. A command has a name, and a
7
7
  # description. This basic version does not implement parameters. A
8
8
  # command is accessible via the `/` prefix: eg `/exit`.
@@ -57,9 +57,9 @@ class LLM::Repl
57
57
  ##
58
58
  # Find a command by a name, or by an input string.
59
59
  # @example find by name
60
- # LLM::Repl::Command.find_by(name: "exit")
60
+ # LLM::Console::Command.find_by(name: "exit")
61
61
  # @example find by input string
62
- # LLM::Repl::Command.find_by(input: "/exit")
62
+ # LLM::Console::Command.find_by(input: "/exit")
63
63
  # @note
64
64
  # The input string must be prefixed with "/"
65
65
  # or it won't be matched. The match is made
@@ -68,7 +68,7 @@ class LLM::Repl
68
68
  # but "/exitnow" will not.
69
69
  # @param [String] input
70
70
  # @param [String] name
71
- # @return [LLM::Repl::Command, nil]
71
+ # @return [LLM::Console::Command, nil]
72
72
  def self.find_by(input: UNDEFINED, name: UNDEFINED)
73
73
  if input != UNDEFINED
74
74
  return nil unless input[0] == "/"
@@ -82,7 +82,7 @@ class LLM::Repl
82
82
  end
83
83
 
84
84
  ##
85
- # @param [LLM::Repl::Command] outer
85
+ # @param [LLM::Console::Command] outer
86
86
  # A new subclass
87
87
  # @return [void]
88
88
  def self.inherited(outer)
@@ -99,7 +99,7 @@ class LLM::Repl
99
99
  end
100
100
 
101
101
  ##
102
- # @return [Array<LLM::Repl::Command]
102
+ # @return [Array<LLM::Console::Command]
103
103
  def self.registry
104
104
  @registry.transform_keys(&:name)
105
105
  end
@@ -160,7 +160,7 @@ class LLM::Repl
160
160
  end
161
161
 
162
162
  ##
163
- # @return [LLM::Repl]
163
+ # @return [LLM::Console]
164
164
  attr_reader :repl
165
165
 
166
166
  ##
@@ -168,8 +168,8 @@ class LLM::Repl
168
168
  attr_reader :agent
169
169
 
170
170
  ##
171
- # @param [LLM::Repl] repl
172
- # @return [LLM::Repl::Command]
171
+ # @param [LLM::Console] console
172
+ # @return [LLM::Console::Command]
173
173
  def initialize(repl)
174
174
  @repl = repl
175
175
  @agent = repl.agent
@@ -235,7 +235,7 @@ class LLM::Repl
235
235
  self.class.parameters
236
236
  end
237
237
 
238
- require_relative "commands/compact"
238
+ require_relative "commands/keep"
239
239
  require_relative "commands/exit"
240
240
  require_relative "commands/help"
241
241
  require_relative "commands/model"
@@ -244,4 +244,4 @@ end
244
244
 
245
245
  ##
246
246
  # Convenience constant
247
- LLM::Command = LLM::Repl::Command
247
+ LLM::Command = LLM::Console::Command
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  ##
5
- # The 'exit' command exits the read-eval-print loop
6
- # by throwing. The {LLM::Repl LLM::Repl} class covers
5
+ # The 'exit' command exits the interactive console
6
+ # by throwing. The {LLM::Console LLM::Console} class covers
7
7
  # the loop with a catch that gracefully recovers and
8
8
  # exits the loop.
9
9
  class Command::Exit < Command
10
10
  name "exit"
11
- description "exits the repl"
11
+ description "exits the console"
12
12
 
13
13
  ##
14
14
  # @return [void]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Repl
3
+ class LLM::Console
4
4
  class Help < Command
5
5
  name "help"
6
6
  description "show help for a given command"