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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +43 -6
- data/lib/riffer/rig/cli.rb +46 -41
- data/lib/riffer/rig/credentials/resolution.rb +16 -0
- data/lib/riffer/rig/credentials.rb +151 -60
- data/lib/riffer/rig/env.rb +29 -0
- data/lib/riffer/rig/events/command_output.rb +27 -0
- data/lib/riffer/rig/events/event.rb +30 -0
- data/lib/riffer/rig/events/notify.rb +27 -0
- data/lib/riffer/rig/events/session_end.rb +24 -0
- data/lib/riffer/rig/events/session_start.rb +27 -0
- data/lib/riffer/rig/events/skill_activated.rb +24 -0
- data/lib/riffer/rig/events/turn_end.rb +32 -0
- data/lib/riffer/rig/events.rb +4 -0
- data/lib/riffer/rig/extension.rb +0 -4
- data/lib/riffer/rig/hosts/base.rb +36 -0
- data/lib/riffer/rig/hosts/mirror.rb +60 -0
- data/lib/riffer/rig/hosts/null.rb +36 -0
- data/lib/riffer/rig/provider_setup/field.rb +24 -0
- data/lib/riffer/rig/provider_setup.rb +84 -0
- data/lib/riffer/rig/registrar.rb +14 -2
- data/lib/riffer/rig/repl.rb +7 -8
- data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
- data/lib/riffer/rig/runtime.rb +104 -41
- data/lib/riffer/rig/settings/document.rb +6 -1
- data/lib/riffer/rig/settings/pricing.rb +0 -1
- data/lib/riffer/rig/settings.rb +47 -50
- data/lib/riffer/rig/token_tally.rb +0 -20
- data/lib/riffer/rig/tools/bash.rb +32 -11
- data/lib/riffer/rig/ui/animator.rb +2 -9
- data/lib/riffer/rig/ui/cursor.rb +0 -2
- data/lib/riffer/rig/ui/renderer.rb +9 -24
- data/lib/riffer/rig/ui/smoother.rb +11 -19
- data/lib/riffer/rig/ui/theme.rb +3 -5
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +2 -7
- data/sig/generated/riffer/rig/cli.rbs +20 -12
- data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
- data/sig/generated/riffer/rig/credentials.rbs +70 -41
- data/sig/generated/riffer/rig/env.rbs +20 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +19 -0
- data/sig/generated/riffer/rig/events/event.rbs +20 -0
- data/sig/generated/riffer/rig/events/notify.rbs +19 -0
- data/sig/generated/riffer/rig/events/session_end.rbs +16 -0
- data/sig/generated/riffer/rig/events/session_start.rbs +19 -0
- data/sig/generated/riffer/rig/events/skill_activated.rbs +16 -0
- data/sig/generated/riffer/rig/events/turn_end.rbs +22 -0
- data/sig/generated/riffer/rig/events.rbs +4 -0
- data/sig/generated/riffer/rig/extension.rbs +0 -4
- data/sig/generated/riffer/rig/hosts/base.rbs +26 -0
- data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
- data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
- data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
- data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
- data/sig/generated/riffer/rig/registrar.rbs +10 -2
- data/sig/generated/riffer/rig/repl.rbs +0 -8
- data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
- data/sig/generated/riffer/rig/runtime.rbs +58 -39
- data/sig/generated/riffer/rig/settings/document.rbs +5 -1
- data/sig/generated/riffer/rig/settings/pricing.rbs +0 -1
- data/sig/generated/riffer/rig/settings.rbs +25 -46
- data/sig/generated/riffer/rig/token_tally.rbs +0 -19
- data/sig/generated/riffer/rig/tools/bash.rbs +13 -1
- data/sig/generated/riffer/rig/ui/animator.rbs +0 -8
- data/sig/generated/riffer/rig/ui/cursor.rbs +0 -2
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -24
- data/sig/generated/riffer/rig/ui/smoother.rbs +5 -18
- data/sig/generated/riffer/rig/ui/theme.rbs +2 -4
- data/sig/generated/riffer/rig.rbs +0 -7
- data/sig/manual/riffer/rig/cli.rbs +2 -4
- data/sig/manual/riffer/rig/credentials.rbs +2 -4
- data/sig/manual/riffer/rig/hosts.rbs +5 -0
- data/sig/manual/riffer/rig/settings.rbs +2 -4
- data/sig/manual/riffer/rig/ui/banner.rbs +2 -4
- metadata +38 -13
- data/lib/riffer/rig/host.rb +0 -52
- 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
|
data/lib/riffer/rig/extension.rb
CHANGED
|
@@ -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
|
data/lib/riffer/rig/registrar.rb
CHANGED
|
@@ -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
|
data/lib/riffer/rig/repl.rb
CHANGED
|
@@ -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
|
data/lib/riffer/rig/runtime.rb
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
-
#
|
|
37
|
-
#
|
|
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
|
-
|
|
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:
|
|
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 #:
|
|
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:
|
|
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[
|
|
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::
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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(
|
|
160
|
-
|
|
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
|