riffer-rig 0.5.0 → 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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +43 -6
  4. data/lib/riffer/rig/cli.rb +46 -41
  5. data/lib/riffer/rig/credentials/resolution.rb +16 -0
  6. data/lib/riffer/rig/credentials.rb +151 -60
  7. data/lib/riffer/rig/env.rb +29 -0
  8. data/lib/riffer/rig/events/command_output.rb +27 -0
  9. data/lib/riffer/rig/events/event.rb +30 -0
  10. data/lib/riffer/rig/events/notify.rb +27 -0
  11. data/lib/riffer/rig/events/session_end.rb +24 -0
  12. data/lib/riffer/rig/events/session_start.rb +27 -0
  13. data/lib/riffer/rig/events/skill_activated.rb +24 -0
  14. data/lib/riffer/rig/events/turn_end.rb +32 -0
  15. data/lib/riffer/rig/events.rb +4 -0
  16. data/lib/riffer/rig/extension.rb +0 -4
  17. data/lib/riffer/rig/hosts/base.rb +36 -0
  18. data/lib/riffer/rig/hosts/mirror.rb +60 -0
  19. data/lib/riffer/rig/hosts/null.rb +36 -0
  20. data/lib/riffer/rig/provider_setup/field.rb +24 -0
  21. data/lib/riffer/rig/provider_setup.rb +84 -0
  22. data/lib/riffer/rig/registrar.rb +14 -2
  23. data/lib/riffer/rig/repl.rb +7 -8
  24. data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
  25. data/lib/riffer/rig/runtime.rb +104 -41
  26. data/lib/riffer/rig/settings/document.rb +6 -1
  27. data/lib/riffer/rig/settings/pricing.rb +0 -1
  28. data/lib/riffer/rig/settings.rb +47 -50
  29. data/lib/riffer/rig/token_tally.rb +0 -20
  30. data/lib/riffer/rig/tools/bash.rb +32 -11
  31. data/lib/riffer/rig/ui/animator.rb +2 -9
  32. data/lib/riffer/rig/ui/cursor.rb +0 -2
  33. data/lib/riffer/rig/ui/renderer.rb +9 -24
  34. data/lib/riffer/rig/ui/smoother.rb +11 -19
  35. data/lib/riffer/rig/ui/theme.rb +3 -5
  36. data/lib/riffer/rig/version.rb +1 -1
  37. data/lib/riffer/rig.rb +2 -7
  38. data/sig/generated/riffer/rig/cli.rbs +20 -12
  39. data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
  40. data/sig/generated/riffer/rig/credentials.rbs +70 -41
  41. data/sig/generated/riffer/rig/env.rbs +20 -0
  42. data/sig/generated/riffer/rig/events/command_output.rbs +19 -0
  43. data/sig/generated/riffer/rig/events/event.rbs +20 -0
  44. data/sig/generated/riffer/rig/events/notify.rbs +19 -0
  45. data/sig/generated/riffer/rig/events/session_end.rbs +16 -0
  46. data/sig/generated/riffer/rig/events/session_start.rbs +19 -0
  47. data/sig/generated/riffer/rig/events/skill_activated.rbs +16 -0
  48. data/sig/generated/riffer/rig/events/turn_end.rbs +22 -0
  49. data/sig/generated/riffer/rig/events.rbs +4 -0
  50. data/sig/generated/riffer/rig/extension.rbs +0 -4
  51. data/sig/generated/riffer/rig/hosts/base.rbs +26 -0
  52. data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
  53. data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
  54. data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
  55. data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
  56. data/sig/generated/riffer/rig/registrar.rbs +10 -2
  57. data/sig/generated/riffer/rig/repl.rbs +0 -8
  58. data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
  59. data/sig/generated/riffer/rig/runtime.rbs +58 -39
  60. data/sig/generated/riffer/rig/settings/document.rbs +5 -1
  61. data/sig/generated/riffer/rig/settings/pricing.rbs +0 -1
  62. data/sig/generated/riffer/rig/settings.rbs +25 -46
  63. data/sig/generated/riffer/rig/token_tally.rbs +0 -19
  64. data/sig/generated/riffer/rig/tools/bash.rbs +13 -1
  65. data/sig/generated/riffer/rig/ui/animator.rbs +0 -8
  66. data/sig/generated/riffer/rig/ui/cursor.rbs +0 -2
  67. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -24
  68. data/sig/generated/riffer/rig/ui/smoother.rbs +5 -18
  69. data/sig/generated/riffer/rig/ui/theme.rbs +2 -4
  70. data/sig/generated/riffer/rig.rbs +0 -7
  71. data/sig/manual/riffer/rig/cli.rbs +2 -4
  72. data/sig/manual/riffer/rig/credentials.rbs +2 -4
  73. data/sig/manual/riffer/rig/hosts.rbs +5 -0
  74. data/sig/manual/riffer/rig/settings.rbs +2 -4
  75. data/sig/manual/riffer/rig/ui/banner.rbs +2 -4
  76. metadata +38 -13
  77. data/lib/riffer/rig/host.rb +0 -52
  78. data/sig/generated/riffer/rig/host.rbs +0 -42
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SkillActivated < Riffer::Rig::Events::Event
4
+ # @dynamic name
5
+ attr_reader :name #: String
6
+
7
+ # @rbs name: String
8
+ # @rbs return: void
9
+ def initialize(name)
10
+ super()
11
+ @name = name
12
+ freeze
13
+ end
14
+
15
+ # @rbs return: Symbol
16
+ def type
17
+ :skill_activated
18
+ end
19
+
20
+ # @rbs return: Hash[Symbol, untyped]
21
+ def to_h
22
+ { type: type, name: name }
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::TurnEnd < Riffer::Rig::Events::Event
4
+ # @dynamic stop_reason, usage
5
+ attr_reader :stop_reason #: Symbol
6
+ attr_reader :usage #: ::Riffer::Providers::TokenUsage?
7
+
8
+ # @rbs stop_reason: Symbol
9
+ # @rbs usage: ::Riffer::Providers::TokenUsage?
10
+ # @rbs return: void
11
+ def initialize(stop_reason, usage)
12
+ super()
13
+ @stop_reason = stop_reason
14
+ @usage = usage
15
+ freeze
16
+ end
17
+
18
+ # @rbs return: Float?
19
+ def cost
20
+ usage&.cost
21
+ end
22
+
23
+ # @rbs return: Symbol
24
+ def type
25
+ :turn_end
26
+ end
27
+
28
+ # @rbs return: Hash[Symbol, untyped]
29
+ def to_h
30
+ { type: type, stop_reason: stop_reason, usage: usage }
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Events
4
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # An extension recorded by <tt>Riffer::Rig.extension</tt>: a name, an optional
4
- # Gem::Requirement on riffer-rig, and the block that runs against a Runtime's
5
- # registrar. The process registry holds these objects; Runtime.new runs their
6
- # blocks.
7
3
  class Riffer::Rig::Extension
8
4
  # @rbs @name: String
9
5
  # @rbs @requires: Gem::Requirement?
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Base
4
+ # @rbs return: Set[Symbol]
5
+ def capabilities
6
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
7
+ end
8
+
9
+ # @rbs question: String?
10
+ # @rbs options: Array[String]?
11
+ # @rbs secret: bool
12
+ # @rbs return: String?
13
+ def ask(question = nil, options: nil, secret: false)
14
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
15
+ end
16
+
17
+ # @rbs question: String?
18
+ # @rbs return: bool
19
+ def confirm(question = nil)
20
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
21
+ end
22
+
23
+ # @rbs message: String?
24
+ # @rbs level: Symbol
25
+ # @rbs return: void
26
+ def notify(message = nil, level: :info)
27
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
28
+ end
29
+
30
+ # @rbs label: String?
31
+ # @rbs &: ? () -> void
32
+ # @rbs return: void
33
+ def progress(label = nil, &)
34
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Mirror < Riffer::Rig::Hosts::Base
4
+ # @rbs @host: Riffer::Rig::Hosts::Base
5
+ # @rbs @queue: Array[Riffer::Rig::Events::Event]
6
+
7
+ # @dynamic capabilities
8
+ attr_reader :capabilities #: Set[Symbol]
9
+
10
+ # @rbs host: Riffer::Rig::Hosts::Base
11
+ # @rbs return: void
12
+ def initialize(host)
13
+ super()
14
+ @host = host
15
+ @capabilities = host.capabilities
16
+ @queue = []
17
+ end
18
+
19
+ # @rbs question: String?
20
+ # @rbs options: Array[String]?
21
+ # @rbs secret: bool
22
+ # @rbs return: String?
23
+ def ask(question = nil, options: nil, secret: false)
24
+ @host.ask(question, options: options, secret: secret)
25
+ end
26
+
27
+ # @rbs question: String?
28
+ # @rbs return: bool
29
+ def confirm(question = nil)
30
+ @host.confirm(question)
31
+ end
32
+
33
+ # @rbs message: String?
34
+ # @rbs level: Symbol
35
+ # @rbs return: void
36
+ def notify(message = nil, level: :info)
37
+ queue(Riffer::Rig::Events::Notify.new(message, level))
38
+ @host.notify(message, level: level)
39
+ end
40
+
41
+ # @rbs label: String?
42
+ # @rbs &block: ? () -> void
43
+ # @rbs return: void
44
+ def progress(label = nil, &block)
45
+ @host.progress(label) { block&.call }
46
+ end
47
+
48
+ # @rbs event: Riffer::Rig::Events::Event
49
+ # @rbs return: void
50
+ def queue(event)
51
+ @queue << event
52
+ end
53
+
54
+ # @rbs return: Array[Riffer::Rig::Events::Event]
55
+ def drain
56
+ queued = @queue
57
+ @queue = []
58
+ queued
59
+ end
60
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Null < Riffer::Rig::Hosts::Base
4
+ # @rbs return: Set[Symbol]
5
+ def capabilities
6
+ Set.new.freeze
7
+ end
8
+
9
+ # @rbs question: String?
10
+ # @rbs options: Array[String]?
11
+ # @rbs secret: bool
12
+ # @rbs return: String?
13
+ def ask(question = nil, options: nil, secret: false)
14
+ nil
15
+ end
16
+
17
+ # @rbs question: String?
18
+ # @rbs return: bool
19
+ def confirm(question = nil)
20
+ false
21
+ end
22
+
23
+ # @rbs message: String?
24
+ # @rbs level: Symbol
25
+ # @rbs return: void
26
+ def notify(message = nil, level: :info)
27
+ nil
28
+ end
29
+
30
+ # @rbs label: String?
31
+ # @rbs &block: ? () -> void
32
+ # @rbs return: void
33
+ def progress(label = nil, &block)
34
+ block&.call
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::ProviderSetup::Field
4
+ # @dynamic name, env, secret, required, fallback
5
+ attr_reader :name #: Symbol
6
+ attr_reader :env #: Array[String]
7
+ attr_reader :secret, :required #: bool
8
+ attr_reader :fallback #: (^() -> String?)?
9
+
10
+ # @rbs name: Symbol
11
+ # @rbs env: Array[String]
12
+ # @rbs secret: bool
13
+ # @rbs required: bool
14
+ # @rbs fallback: (^() -> String?)?
15
+ # @rbs return: void
16
+ def initialize(name:, env:, secret:, required:, fallback: nil)
17
+ @name = name
18
+ @env = env.freeze
19
+ @secret = secret
20
+ @required = required
21
+ @fallback = fallback
22
+ freeze
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::ProviderSetup
4
+ # @dynamic url, chain, fields
5
+ attr_reader :url #: String?
6
+ attr_reader :chain #: bool
7
+ attr_reader :fields #: Array[Riffer::Rig::ProviderSetup::Field]
8
+
9
+ # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
10
+ # @rbs url: String?
11
+ # @rbs chain: bool
12
+ # @rbs return: void
13
+ def initialize(fields:, url: nil, chain: false)
14
+ @url = url
15
+ @chain = chain
16
+ @fields = fields.freeze
17
+ freeze
18
+ end
19
+
20
+ AWS_SHARED_CONFIG_REGION = lambda do
21
+ require 'aws-sdk-core'
22
+ Object.const_get(:Aws).shared_config.region
23
+ rescue LoadError
24
+ nil
25
+ end #: ^() -> String?
26
+
27
+ # Upstream candidate: each riffer provider could declare its own credential
28
+ # fields, and this table would go.
29
+ TABLE = {
30
+ anthropic: new(
31
+ url: 'https://console.anthropic.com/settings/keys',
32
+ fields: [Field.new(name: :api_key, env: ['ANTHROPIC_API_KEY'], secret: true, required: true)]
33
+ ),
34
+ openai: new(
35
+ url: 'https://platform.openai.com/api-keys',
36
+ fields: [
37
+ Field.new(name: :api_key, env: ['OPENAI_API_KEY'], secret: true, required: true),
38
+ Field.new(name: :base_url, env: ['OPENAI_BASE_URL'], secret: false, required: false)
39
+ ]
40
+ ),
41
+ gemini: new(
42
+ url: 'https://aistudio.google.com/app/apikey',
43
+ fields: [Field.new(name: :api_key, env: ['GEMINI_API_KEY'], secret: true, required: true)]
44
+ ),
45
+ openrouter: new(
46
+ url: 'https://openrouter.ai/keys',
47
+ fields: [Field.new(name: :api_key, env: ['OPENROUTER_API_KEY'], secret: true, required: true)]
48
+ ),
49
+ azure_openai: new(
50
+ url: 'https://portal.azure.com',
51
+ fields: [
52
+ Field.new(name: :endpoint, env: ['AZURE_OPENAI_ENDPOINT'], secret: false, required: true),
53
+ Field.new(name: :api_key, env: ['AZURE_OPENAI_API_KEY'], secret: true, required: true)
54
+ ]
55
+ ),
56
+ amazon_bedrock: new(
57
+ url: 'https://console.aws.amazon.com/bedrock',
58
+ chain: true,
59
+ fields: [
60
+ Field.new(
61
+ name: :region,
62
+ env: %w[AWS_REGION AWS_DEFAULT_REGION],
63
+ secret: false,
64
+ required: true,
65
+ fallback: AWS_SHARED_CONFIG_REGION
66
+ ),
67
+ Field.new(name: :api_token, env: ['AWS_BEARER_TOKEN_BEDROCK'], secret: true, required: false)
68
+ ]
69
+ )
70
+ }.freeze #: Hash[Symbol, Riffer::Rig::ProviderSetup]
71
+
72
+ # @rbs identifier: String | Symbol
73
+ # @rbs return: Riffer::Rig::ProviderSetup?
74
+ def self.[](identifier)
75
+ TABLE[identifier.to_sym]
76
+ end
77
+
78
+ # @rbs identifier: String | Symbol
79
+ # @rbs return: Riffer::Rig::ProviderSetup
80
+ def self.for(identifier)
81
+ self[identifier] ||
82
+ new(fields: [Field.new(name: :api_key, env: ["#{identifier.to_s.upcase}_API_KEY"], secret: true, required: true)])
83
+ end
84
+ end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The per-Runtime registrar extension blocks run against. Seams are added one
4
- # at a time; today only #tool exists.
5
3
  class Riffer::Rig::Registrar
6
4
  # @rbs @tools: Array[singleton(Riffer::Tool)]
5
+ # @rbs @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
7
6
 
8
7
  # @rbs return: void
9
8
  def initialize
10
9
  @tools = []
10
+ @prompts = {}
11
11
  end
12
12
 
13
13
  # @rbs klass: singleton(Riffer::Tool)
@@ -16,8 +16,20 @@ class Riffer::Rig::Registrar
16
16
  @tools << klass
17
17
  end
18
18
 
19
+ # @rbs name: Symbol
20
+ # @rbs &block: (Riffer::Rig::Runtime) -> String?
21
+ # @rbs return: void
22
+ def prompt(name, &block)
23
+ @prompts[name] = block
24
+ end
25
+
19
26
  # @rbs return: Array[singleton(Riffer::Tool)]
20
27
  def tools
21
28
  @tools.dup
22
29
  end
30
+
31
+ # @rbs return: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
32
+ def prompts
33
+ @prompts.dup
34
+ end
23
35
  end
@@ -73,6 +73,8 @@ class Riffer::Rig::REPL
73
73
  # @rbs prompt: String
74
74
  # @rbs return: void
75
75
  def run_turn(prompt)
76
+ # A hidden cursor can't flicker against the animator's erase-and-redraw
77
+ # churn.
76
78
  @cursor.hide
77
79
  start_animator
78
80
  @smoother.start
@@ -154,13 +156,11 @@ class Riffer::Rig::REPL
154
156
  "<skill name=\"#{name}\">\n#{body}\n</skill>"
155
157
  end
156
158
 
157
- # Chrome lines that never pass through the renderer (errors, exit line) end
158
- # their line with a newline but leave the renderer's tool-group state alone —
159
- # they can only follow prose, never mid-group.
160
- #
161
159
  # @rbs &block: () -> String
162
160
  # @rbs return: void
163
161
  def print_block(&)
162
+ # Bypasses the renderer's tool-group state: these lines (errors, the exit
163
+ # line) can only follow prose, never land mid-group.
164
164
  @output.puts
165
165
  @output.puts(yield)
166
166
  @output.flush
@@ -176,13 +176,12 @@ class Riffer::Rig::REPL
176
176
  start_animator
177
177
  end
178
178
 
179
- # Every animator start sits behind a smoother drain: the smoother's backlog
180
- # may still be trickling out from streamed prose, and spinner frames drawn
181
- # mid-drain would carve `\r…\e[K` through a half-printed sentence.
182
- #
183
179
  # @rbs mode: Symbol
184
180
  # @rbs return: void
185
181
  def start_animator(mode = :neutral)
182
+ # The smoother's backlog may still be trickling out from streamed prose,
183
+ # and spinner frames drawn mid-drain would carve `\r…\e[K` through a
184
+ # half-printed sentence.
186
185
  @smoother.drain
187
186
  @animator.start(mode)
188
187
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Runtime::CancelFlag
4
+ # @rbs @mutex: Thread::Mutex
5
+ # @rbs @set: bool
6
+
7
+ # @rbs return: void
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @set = false
11
+ end
12
+
13
+ # @rbs return: void
14
+ def set
15
+ @mutex.synchronize { @set = true }
16
+ end
17
+
18
+ # @rbs return: void
19
+ def clear
20
+ @mutex.synchronize { @set = false }
21
+ end
22
+
23
+ # @rbs return: bool
24
+ def set?
25
+ @mutex.synchronize { @set }
26
+ end
27
+ end
@@ -1,25 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'date'
4
+ require 'securerandom'
4
5
 
5
- # One Runtime builds its own Riffer::Agent from a per-instance
6
- # Riffer::Agent::Config and runs a prompt: streamed as events, or run to
7
- # completion and returned as riffer's Agent::Response.
8
- #
9
- # runtime = Riffer::Rig::Runtime.new('mock/x')
10
- # runtime.prompt('hello') { |event| ... } # yields riffer StreamEvents
11
- # runtime.prompt('hello').each { |event| } # an Enumerator without a block
12
- # response = runtime.ask('hello') # a Riffer::Agent::Response
13
- #
14
- # Two Runtimes in one process share nothing but the process-wide extension
15
- # registry and riffer's provider repository. One prompt runs at a time; a
16
- # second while one is running raises Riffer::Rig::Runtime::BusyError. The
17
- # Runtime never renders, never prints, never reads the filesystem.
6
+ # Never renders, prints or reads the filesystem: anything a host needs is a
7
+ # Runtime feature, so embedders get it too.
18
8
  class Riffer::Rig::Runtime
19
- # Raised when a second prompt, ask or registrar build runs while one is
20
- # already running on this Runtime.
21
9
  class BusyError < StandardError; end
22
10
 
11
+ class ClosedError < StandardError; end
12
+
23
13
  BASE_PROMPT_TEMPLATE = <<~TEXT
24
14
  You are %<name>s, a general-purpose agent. You work by using the tools you have
25
15
  been given; each tool describes what it does and when to use it.
@@ -33,41 +23,42 @@ class Riffer::Rig::Runtime
33
23
 
34
24
  DEFAULT_NAME = 'riffer'
35
25
 
36
- # The legacy CodingAgent's default: an unlimited agent loop. riffer's own
37
- # default (16) is too small for a general-purpose harness.
26
+ # Unlimited: riffer's own default (16) is too small for a general-purpose
27
+ # harness.
38
28
  DEFAULT_MAX_STEPS = nil #: Integer?
39
29
 
40
- # @rbs!
41
- # interface _Host
42
- # def ask: (?String, ?options: Array[String]?, ?secret: bool) -> String?
43
- # def confirm: (?String) -> bool
44
- # def notify: (?String, ?level: Symbol) -> void
45
- # def progress: (?String) { () -> void } -> void
46
- # def capabilities: () -> Set[Symbol]
47
- # end
30
+ INTERRUPT_CANCELLED = :cancelled #: Symbol
48
31
 
49
32
  # @rbs @agent: Riffer::Agent
33
+ # @rbs @base_prompt: String
34
+ # @rbs @cancel_flag: Riffer::Rig::Runtime::CancelFlag
35
+ # @rbs @credentials: Hash[Symbol, Hash[Symbol, String]]
50
36
  # @rbs @cwd: String
51
- # @rbs @host: _Host
37
+ # @rbs @host: Riffer::Rig::Hosts::Mirror
38
+ # @rbs @id: String
52
39
  # @rbs @settings: Hash[Symbol, untyped]
53
40
  # @rbs @busy: bool
41
+ # @rbs @closed: bool
42
+ # @rbs @session_start_pending: bool
54
43
  # @rbs @registrar: Riffer::Rig::Registrar
55
44
 
56
- # @dynamic agent, cwd, host, settings
45
+ # @dynamic agent, credentials, cwd, host, id, settings
57
46
  attr_reader :agent #: Riffer::Agent
47
+ attr_reader :credentials #: Hash[Symbol, Hash[Symbol, String]]
58
48
  attr_reader :cwd #: String
59
- attr_reader :host #: _Host
49
+ attr_reader :host #: Riffer::Rig::Hosts::Mirror
50
+ attr_reader :id #: String
60
51
  attr_reader :settings #: Hash[Symbol, untyped]
61
52
 
62
53
  # @rbs model: String
63
54
  # @rbs extensions: Array[Riffer::Rig::Extension]
64
55
  # @rbs tools: Array[String]?
65
56
  # @rbs settings: Hash[Symbol, untyped]
66
- # @rbs host: _Host
57
+ # @rbs host: Riffer::Rig::Hosts::Base
67
58
  # @rbs cwd: String?
68
59
  # @rbs name: String
69
60
  # @rbs instructions: String?
70
- # @rbs credentials: Hash[String, String]
61
+ # @rbs credentials: Hash[Symbol, Hash[Symbol, String]]
71
62
  # @rbs pricing: Hash[String, Riffer::Rig::Settings::Pricing]
72
63
  # @rbs max_steps: Integer?
73
64
  # @rbs snapshot: Hash[Symbol, untyped]?
@@ -77,7 +68,7 @@ class Riffer::Rig::Runtime
77
68
  extensions: [],
78
69
  tools: nil,
79
70
  settings: {},
80
- host: Riffer::Rig::Host.new,
71
+ host: Riffer::Rig::Hosts::Null.new,
81
72
  cwd: nil,
82
73
  name: DEFAULT_NAME,
83
74
  instructions: nil,
@@ -86,37 +77,48 @@ class Riffer::Rig::Runtime
86
77
  max_steps: DEFAULT_MAX_STEPS,
87
78
  snapshot: nil
88
79
  )
89
- @host = host
80
+ # Doubles as the snapshot id and ACP sessionId.
81
+ @id = ::SecureRandom.uuid_v7
82
+ @host = Riffer::Rig::Hosts::Mirror.new(host)
90
83
  @cwd = cwd || Dir.pwd
91
84
  @settings = settings
85
+ @credentials = credentials
92
86
 
93
87
  @busy = false
88
+ @closed = false
89
+ @cancel_flag = Riffer::Rig::Runtime::CancelFlag.new
90
+ @session_start_pending = true
94
91
  @registrar = build_registrar(extensions)
95
92
  tool_classes = select_tools(@registrar.tools, tools)
96
- base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
93
+ @base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
97
94
 
98
95
  @agent = Riffer::Agent.new(
96
+ context: { cancel_flag: @cancel_flag },
99
97
  config: Riffer::Agent::Config.new(
100
98
  model: model,
101
- instructions: system_prompt(base_prompt),
99
+ instructions: system_prompt([]),
102
100
  tools_config: tool_classes,
103
101
  max_steps: max_steps
104
102
  )
105
103
  )
104
+ @agent.session.on_message { |_message| interrupt_if_cancelled }
106
105
  end
107
106
 
108
107
  # @rbs text: String
109
- # @rbs &block: ?(Riffer::StreamEvents::Base) -> void
110
- # @rbs return: (nil | Enumerator[Riffer::StreamEvents::Base, void])
108
+ # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event) -> void
109
+ # @rbs return: (nil | Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event, Riffer::Agent::Response])
111
110
  def prompt(text, &block)
112
111
  raise BusyError, 'a prompt is already running on this Runtime' if @busy
112
+ raise ClosedError, 'this Runtime is closed' if @closed
113
113
 
114
114
  @busy = true
115
+ @cancel_flag.clear
116
+ refresh_system_message
115
117
  if block
116
- @agent.stream(text).each(&block)
118
+ wrap_stream(@agent.stream(text)).each(&block)
117
119
  nil
118
120
  else
119
- @agent.stream(text)
121
+ wrap_stream(@agent.stream(text))
120
122
  end
121
123
  ensure
122
124
  @busy = false
@@ -126,15 +128,63 @@ class Riffer::Rig::Runtime
126
128
  # @rbs return: Riffer::Agent::Response
127
129
  def ask(text)
128
130
  raise BusyError, 'a prompt is already running on this Runtime' if @busy
131
+ raise ClosedError, 'this Runtime is closed' if @closed
129
132
 
130
133
  @busy = true
134
+ @cancel_flag.clear
135
+ refresh_system_message
131
136
  @agent.stream(text).each { |event| event }
132
137
  ensure
133
138
  @busy = false
134
139
  end
135
140
 
141
+ # @rbs return: nil
142
+ def cancel
143
+ @cancel_flag.set
144
+ nil
145
+ end
146
+
147
+ # @rbs return: void
148
+ def close
149
+ # TODO: emit Riffer::Rig::Events::SessionEnd once the rebuild ticket settles
150
+ # the stream's session_end reasons.
151
+ @closed = true
152
+ end
153
+
136
154
  private
137
155
 
156
+ # @rbs stream: Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
157
+ # @rbs return: Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event, Riffer::Agent::Response]
158
+ def wrap_stream(stream)
159
+ Enumerator.new do |yielder|
160
+ yielder << Riffer::Rig::Events::SessionStart.new(@id, :new) if @session_start_pending
161
+ @session_start_pending = false
162
+ @host.drain.each { |event| yielder << event }
163
+ response = stream.each { |event| yielder << event }
164
+ yielder << Riffer::Rig::Events::TurnEnd.new(stop_reason(response.outcome), response.token_usage)
165
+ response
166
+ end
167
+ end
168
+
169
+ # @rbs return: void
170
+ def interrupt_if_cancelled
171
+ return unless @cancel_flag.set?
172
+
173
+ @agent.session.discard_pending_tool_calls
174
+ # Upstream candidate: a cancel token on riffer's run loop. Until then the
175
+ # loop can only be stopped from inside, at a message boundary.
176
+ @agent.interrupt!(INTERRUPT_CANCELLED)
177
+ end
178
+
179
+ # @rbs outcome: Riffer::Agent::Outcome
180
+ # @rbs return: Symbol
181
+ def stop_reason(outcome)
182
+ # Upstream candidate: riffer's outcome vocabulary is closed, so a cancel
183
+ # reaches us as :interrupted with the reason in detail.
184
+ cancelled = outcome.reason == :interrupted && outcome.detail == INTERRUPT_CANCELLED.to_s
185
+ cancelled ? INTERRUPT_CANCELLED : outcome.reason
186
+ end
187
+
138
188
  # @rbs extensions: Array[Riffer::Rig::Extension]
139
189
  # @rbs return: Riffer::Rig::Registrar
140
190
  def build_registrar(extensions)
@@ -154,9 +204,22 @@ class Riffer::Rig::Runtime
154
204
  registered.select { |klass| allowlist.include?(klass.name) }
155
205
  end
156
206
 
157
- # @rbs base_prompt: String
207
+ # @rbs return: void
208
+ def refresh_system_message
209
+ # Upstream candidate: riffer resolves `instructions` once, in Agent.new, so
210
+ # a per-turn system message has to be swapped into the session by hand.
211
+ session = @agent.session
212
+ session.set([Riffer::Messages::System.new(system_prompt(rendered_sections)), *session.messages.drop(1)])
213
+ end
214
+
215
+ # @rbs return: Array[String]
216
+ def rendered_sections
217
+ @registrar.prompts.each_value.map { |section| section.call(self).to_s }.reject(&:empty?)
218
+ end
219
+
220
+ # @rbs sections: Array[String]
158
221
  # @rbs return: String
159
- def system_prompt(base_prompt)
160
- "#{base_prompt}\n\nCurrent date: #{Date.today}\nCurrent working directory: #{@cwd}"
222
+ def system_prompt(sections)
223
+ [@base_prompt, *sections, "Current date: #{Date.today}\nCurrent working directory: #{@cwd}"].join("\n\n")
161
224
  end
162
225
  end
@@ -4,11 +4,13 @@ class Riffer::Rig::Settings::Document
4
4
  # @rbs @model: String?
5
5
  # @rbs @reasoning: String?
6
6
  # @rbs @models: Hash[String, Riffer::Rig::Settings::Pricing]
7
+ # @rbs @providers: Hash[String, Hash[String, String]]
7
8
 
8
- # @dynamic model, reasoning, models
9
+ # @dynamic model, reasoning, models, providers
9
10
  attr_reader :model #: String?
10
11
  attr_reader :reasoning #: String?
11
12
  attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
13
+ attr_reader :providers #: Hash[String, Hash[String, String]]
12
14
 
13
15
  # @rbs source: untyped
14
16
  # @rbs return: void
@@ -19,5 +21,8 @@ class Riffer::Rig::Settings::Document
19
21
  @models = entries.filter_map do |name, entry|
20
22
  [name, Riffer::Rig::Settings::Pricing.from(entry)] if name.is_a?(String) && entry.is_a?(Hash)
21
23
  end.to_h
24
+ blocks = source['providers'].is_a?(Hash) ? source['providers'] : {} #: Hash[String, untyped]
25
+ @providers = blocks.select { |_identifier, fields| fields.is_a?(Hash) }
26
+ .transform_values { |fields| fields.select { |_name, value| value.is_a?(String) } }
22
27
  end
23
28
  end