riffer 0.47.2 → 0.49.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/.claude/rules/comments.md +2 -4
- data/.claude/rules/rbs-inline.md +2 -12
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +42 -0
- data/docs/AGENTS.md +39 -7
- data/docs/AGENT_LIFECYCLE.md +14 -16
- data/docs/CONFIGURATION.md +23 -34
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +8 -4
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +2 -2
- data/docs/providers/AMAZON_BEDROCK.md +32 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +56 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/docs/providers/OPENROUTER.md +18 -1
- data/docs-site/build.rb +0 -6
- data/docs-site/check.rb +1 -5
- data/lib/riffer/agent/config.rb +27 -44
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +4 -35
- data/lib/riffer/agent/run.rb +32 -48
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +26 -38
- data/lib/riffer/agent/structured_output/result.rb +0 -8
- data/lib/riffer/agent/structured_output.rb +0 -7
- data/lib/riffer/agent.rb +31 -127
- data/lib/riffer/config/amazon_bedrock.rb +30 -0
- data/lib/riffer/config/anthropic.rb +21 -0
- data/lib/riffer/config/azure_open_ai.rb +30 -0
- data/lib/riffer/config/evals.rb +18 -0
- data/lib/riffer/config/files.rb +61 -0
- data/lib/riffer/config/gemini.rb +21 -0
- data/lib/riffer/config/mcp.rb +31 -0
- data/lib/riffer/config/open_ai.rb +30 -0
- data/lib/riffer/config/open_router.rb +21 -0
- data/lib/riffer/config/pricing/rates.rb +36 -0
- data/lib/riffer/config/pricing.rb +64 -0
- data/lib/riffer/config/skills.rb +38 -0
- data/lib/riffer/config/tracing.rb +43 -0
- data/lib/riffer/config.rb +3 -362
- data/lib/riffer/evals/evaluator.rb +22 -23
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +9 -11
- data/lib/riffer/evals/result.rb +1 -11
- data/lib/riffer/evals/run_result.rb +0 -12
- data/lib/riffer/evals/scenario_result.rb +0 -19
- data/lib/riffer/files/downloader.rb +2 -3
- data/lib/riffer/files/resolver.rb +5 -10
- data/lib/riffer/guardrail.rb +2 -22
- data/lib/riffer/guardrails/modification.rb +0 -8
- data/lib/riffer/guardrails/result.rb +0 -17
- data/lib/riffer/guardrails/runner.rb +2 -11
- data/lib/riffer/guardrails/tripwire.rb +0 -8
- data/lib/riffer/guardrails.rb +0 -2
- data/lib/riffer/helpers/boolean.rb +0 -4
- data/lib/riffer/helpers/call_or_value.rb +0 -3
- data/lib/riffer/helpers/deep_dup.rb +37 -0
- data/lib/riffer/helpers/dependencies.rb +0 -4
- data/lib/riffer/helpers/identifier.rb +3 -12
- data/lib/riffer/helpers/validate.rb +42 -0
- data/lib/riffer/mcp/authenticated_tool.rb +4 -12
- data/lib/riffer/mcp/client.rb +0 -7
- data/lib/riffer/mcp/manifest.rb +3 -7
- data/lib/riffer/mcp/registration.rb +0 -10
- data/lib/riffer/mcp/registry.rb +0 -9
- data/lib/riffer/mcp/search_tool.rb +0 -4
- data/lib/riffer/mcp/tool.rb +1 -4
- data/lib/riffer/mcp/tool_factory.rb +3 -6
- data/lib/riffer/mcp.rb +2 -23
- data/lib/riffer/messages/assistant/reasoning_part.rb +73 -0
- data/lib/riffer/messages/assistant/tool_call.rb +55 -0
- data/lib/riffer/messages/assistant.rb +43 -15
- data/lib/riffer/messages/base.rb +5 -33
- data/lib/riffer/messages/system.rb +8 -1
- data/lib/riffer/messages/tool.rb +15 -13
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +16 -40
- data/lib/riffer/messages/user.rb +11 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +15 -31
- data/lib/riffer/params.rb +15 -39
- data/lib/riffer/providers/amazon_bedrock.rb +104 -44
- data/lib/riffer/providers/anthropic.rb +11 -26
- data/lib/riffer/providers/azure_open_ai.rb +3 -8
- data/lib/riffer/providers/base.rb +30 -32
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +6 -12
- data/lib/riffer/providers/mock.rb +18 -27
- data/lib/riffer/providers/open_ai.rb +12 -21
- data/lib/riffer/providers/open_router.rb +109 -33
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +19 -12
- data/lib/riffer/registrable.rb +11 -45
- data/lib/riffer/runner/fibers.rb +1 -6
- data/lib/riffer/runner/sequential.rb +0 -1
- data/lib/riffer/runner/threaded.rb +0 -3
- data/lib/riffer/runner.rb +0 -3
- data/lib/riffer/skills/activate_tool.rb +0 -3
- data/lib/riffer/skills/adapter.rb +0 -8
- data/lib/riffer/skills/backend.rb +2 -7
- data/lib/riffer/skills/config.rb +13 -17
- data/lib/riffer/skills/context.rb +0 -29
- data/lib/riffer/skills/filesystem_backend.rb +0 -7
- data/lib/riffer/skills/frontmatter.rb +2 -16
- data/lib/riffer/skills/markdown_adapter.rb +3 -6
- data/lib/riffer/skills/xml_adapter.rb +0 -3
- data/lib/riffer/stream_events/base.rb +0 -3
- data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
- data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
- data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
- data/lib/riffer/stream_events/interrupt.rb +2 -12
- data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
- data/lib/riffer/stream_events/reasoning_done.rb +6 -8
- data/lib/riffer/stream_events/skill_activation.rb +0 -3
- data/lib/riffer/stream_events/text_delta.rb +0 -2
- data/lib/riffer/stream_events/text_done.rb +0 -2
- data/lib/riffer/stream_events/token_usage_done.rb +0 -2
- data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
- data/lib/riffer/stream_events/tool_call_done.rb +0 -5
- data/lib/riffer/stream_events/web_search_done.rb +0 -3
- data/lib/riffer/stream_events/web_search_status.rb +1 -5
- data/lib/riffer/testing/minitest.rb +4 -5
- data/lib/riffer/testing.rb +5 -38
- data/lib/riffer/tool.rb +2 -28
- data/lib/riffer/tools/response.rb +3 -32
- data/lib/riffer/tools/runtime/fibers.rb +0 -6
- data/lib/riffer/tools/runtime/inline.rb +0 -1
- data/lib/riffer/tools/runtime/threaded.rb +0 -6
- data/lib/riffer/tools/runtime.rb +8 -30
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +5 -9
- data/lib/riffer/tracing/no_op.rb +0 -7
- data/lib/riffer/tracing/otel.rb +7 -16
- data/lib/riffer/tracing/stream_recorder.rb +0 -7
- data/lib/riffer/tracing.rb +4 -27
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -30
- data/sig/generated/riffer/agent/config.rbs +24 -46
- data/sig/generated/riffer/agent/context.rbs +2 -26
- data/sig/generated/riffer/agent/outcome.rbs +0 -20
- data/sig/generated/riffer/agent/response.rbs +4 -27
- data/sig/generated/riffer/agent/run.rbs +12 -31
- data/sig/generated/riffer/agent/serializer.rbs +2 -27
- data/sig/generated/riffer/agent/session/repair.rbs +2 -10
- data/sig/generated/riffer/agent/session.rbs +10 -36
- data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
- data/sig/generated/riffer/agent/structured_output.rbs +0 -7
- data/sig/generated/riffer/agent.rbs +25 -125
- data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
- data/sig/generated/riffer/config/anthropic.rbs +15 -0
- data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
- data/sig/generated/riffer/config/evals.rbs +13 -0
- data/sig/generated/riffer/config/files.rbs +43 -0
- data/sig/generated/riffer/config/gemini.rbs +15 -0
- data/sig/generated/riffer/config/mcp.rbs +19 -0
- data/sig/generated/riffer/config/open_ai.rbs +21 -0
- data/sig/generated/riffer/config/open_router.rbs +15 -0
- data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
- data/sig/generated/riffer/config/pricing.rbs +31 -0
- data/sig/generated/riffer/config/skills.rbs +19 -0
- data/sig/generated/riffer/config/tracing.rbs +25 -0
- data/sig/generated/riffer/config.rbs +2 -307
- data/sig/generated/riffer/evals/evaluator.rbs +12 -21
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +4 -8
- data/sig/generated/riffer/evals/result.rbs +1 -11
- data/sig/generated/riffer/evals/run_result.rbs +0 -12
- data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
- data/sig/generated/riffer/files/resolver.rbs +6 -13
- data/sig/generated/riffer/guardrail.rbs +2 -22
- data/sig/generated/riffer/guardrails/modification.rbs +0 -6
- data/sig/generated/riffer/guardrails/result.rbs +0 -15
- data/sig/generated/riffer/guardrails/runner.rbs +0 -11
- data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
- data/sig/generated/riffer/guardrails.rbs +0 -2
- data/sig/generated/riffer/helpers/boolean.rbs +0 -4
- data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
- data/sig/generated/riffer/helpers/deep_dup.rbs +13 -0
- data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
- data/sig/generated/riffer/helpers/identifier.rbs +0 -12
- data/sig/generated/riffer/helpers/validate.rbs +21 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
- data/sig/generated/riffer/mcp/client.rbs +0 -7
- data/sig/generated/riffer/mcp/manifest.rbs +3 -7
- data/sig/generated/riffer/mcp/registration.rbs +0 -10
- data/sig/generated/riffer/mcp/registry.rbs +0 -9
- data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
- data/sig/generated/riffer/mcp.rbs +2 -22
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +45 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +34 -0
- data/sig/generated/riffer/messages/assistant.rbs +25 -31
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +4 -1
- data/sig/generated/riffer/messages/tool.rbs +4 -10
- data/sig/generated/riffer/messages/user/file_part.rbs +76 -0
- data/sig/generated/riffer/messages/user.rbs +7 -5
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +7 -31
- data/sig/generated/riffer/params.rbs +8 -38
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +33 -29
- data/sig/generated/riffer/providers/anthropic.rbs +2 -12
- data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
- data/sig/generated/riffer/providers/base.rbs +18 -34
- data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
- data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
- data/sig/generated/riffer/providers/gemini.rbs +4 -10
- data/sig/generated/riffer/providers/mock.rbs +6 -27
- data/sig/generated/riffer/providers/open_ai.rbs +6 -13
- data/sig/generated/riffer/providers/open_router.rbs +37 -18
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +9 -12
- data/sig/generated/riffer/registrable.rbs +0 -45
- data/sig/generated/riffer/runner/fibers.rbs +0 -6
- data/sig/generated/riffer/runner/sequential.rbs +0 -1
- data/sig/generated/riffer/runner/threaded.rbs +0 -3
- data/sig/generated/riffer/runner.rbs +0 -3
- data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
- data/sig/generated/riffer/skills/adapter.rbs +0 -8
- data/sig/generated/riffer/skills/backend.rbs +2 -7
- data/sig/generated/riffer/skills/config.rbs +6 -17
- data/sig/generated/riffer/skills/context.rbs +0 -29
- data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
- data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
- data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
- data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
- data/sig/generated/riffer/stream_events/base.rbs +0 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
- data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
- data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
- data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
- data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +4 -6
- data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
- data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
- data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
- data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
- data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
- data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
- data/sig/generated/riffer/testing.rbs +0 -38
- data/sig/generated/riffer/tool.rbs +0 -27
- data/sig/generated/riffer/tools/response.rbs +3 -29
- data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
- data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
- data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
- data/sig/generated/riffer/tools/runtime.rbs +4 -26
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +6 -9
- data/sig/generated/riffer/tracing/no_op.rbs +0 -7
- data/sig/generated/riffer/tracing/otel.rbs +7 -16
- data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
- data/sig/generated/riffer/tracing.rbs +4 -23
- data/sig/generated/riffer.rbs +2 -29
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- data/sig/manual/riffer/helpers/validate.rbs +5 -0
- metadata +39 -3
- data/sig/generated/riffer/messages/file_part.rbs +0 -101
data/lib/riffer/agent/context.rb
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Typed value object wrapping the runtime context Hash held by a Riffer::Agent.
|
|
5
|
-
# Exposes typed +skills+, +token_usage+, +mcp_progressive_tools+, and
|
|
6
|
-
# +discovered_tools+ accessors while preserving +#[]+ / +#dig+ for caller-provided keys.
|
|
7
4
|
class Riffer::Agent::Context
|
|
8
5
|
# @rbs @data: Hash[Symbol, untyped]
|
|
9
6
|
|
|
10
7
|
RESERVED_KEYS = %i[skills token_usage mcp_progressive_tools discovered_tools].freeze #: Array[Symbol]
|
|
11
8
|
|
|
12
|
-
#
|
|
13
|
-
# don't leak in. Raises Riffer::ArgumentError if it contains a reserved key.
|
|
9
|
+
# Raises Riffer::ArgumentError if +data+ contains a reserved key.
|
|
14
10
|
#--
|
|
15
11
|
#: (?Hash[Symbol, untyped]) -> void
|
|
16
12
|
def initialize(data = {})
|
|
@@ -27,17 +23,12 @@ class Riffer::Agent::Context
|
|
|
27
23
|
@data[:discovered_tools] = nil
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
# The agent's resolved +Riffer::Skills::Context+, or +nil+ when skills
|
|
31
|
-
# are not configured.
|
|
32
|
-
#
|
|
33
26
|
#--
|
|
34
27
|
#: () -> Riffer::Skills::Context?
|
|
35
28
|
def skills
|
|
36
29
|
@data[:skills]
|
|
37
30
|
end
|
|
38
31
|
|
|
39
|
-
# Sets the resolved skills context. Raises Riffer::ArgumentError on an
|
|
40
|
-
# invalid value.
|
|
41
32
|
#--
|
|
42
33
|
#: (Riffer::Skills::Context?) -> Riffer::Skills::Context?
|
|
43
34
|
def skills=(value)
|
|
@@ -48,17 +39,13 @@ class Riffer::Agent::Context
|
|
|
48
39
|
@data[:skills] = value
|
|
49
40
|
end
|
|
50
41
|
|
|
51
|
-
#
|
|
52
|
-
# or +nil+ before the first response is recorded.
|
|
53
|
-
#
|
|
42
|
+
# Cumulative across every run on this agent.
|
|
54
43
|
#--
|
|
55
44
|
#: () -> Riffer::Providers::TokenUsage?
|
|
56
45
|
def token_usage
|
|
57
46
|
@data[:token_usage]
|
|
58
47
|
end
|
|
59
48
|
|
|
60
|
-
# Sets the cumulative token usage. Raises Riffer::ArgumentError on an invalid
|
|
61
|
-
# value.
|
|
62
49
|
#--
|
|
63
50
|
#: (Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
|
|
64
51
|
def token_usage=(value)
|
|
@@ -69,22 +56,18 @@ class Riffer::Agent::Context
|
|
|
69
56
|
@data[:token_usage] = value
|
|
70
57
|
end
|
|
71
58
|
|
|
72
|
-
# Hash-style read, preserved so tools can pull caller-provided keys via
|
|
73
|
-
# <tt>context[:agent]</tt>.
|
|
74
59
|
#--
|
|
75
60
|
#: (Symbol) -> untyped
|
|
76
61
|
def [](key)
|
|
77
62
|
@data[key]
|
|
78
63
|
end
|
|
79
64
|
|
|
80
|
-
# Auth-wrapped MCP tool classes for progressive discovery, or +nil+.
|
|
81
65
|
#--
|
|
82
66
|
#: () -> Array[singleton(Riffer::Tool)]?
|
|
83
67
|
def mcp_progressive_tools
|
|
84
68
|
@data[:mcp_progressive_tools]
|
|
85
69
|
end
|
|
86
70
|
|
|
87
|
-
# Sets progressive MCP tools. Raises Riffer::ArgumentError on an invalid value.
|
|
88
71
|
#--
|
|
89
72
|
#: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
|
|
90
73
|
def mcp_progressive_tools=(value)
|
|
@@ -99,15 +82,12 @@ class Riffer::Agent::Context
|
|
|
99
82
|
@data[:mcp_progressive_tools] = value
|
|
100
83
|
end
|
|
101
84
|
|
|
102
|
-
# MCP tool classes discovered during progressive search. Accumulates across
|
|
103
|
-
# +generate+ calls and is merged into the active tool list on every LLM call.
|
|
104
85
|
#--
|
|
105
86
|
#: () -> Array[singleton(Riffer::Tool)]?
|
|
106
87
|
def discovered_tools
|
|
107
88
|
@data[:discovered_tools]
|
|
108
89
|
end
|
|
109
90
|
|
|
110
|
-
# Sets the discovered tools array. Raises Riffer::ArgumentError on an invalid value.
|
|
111
91
|
#--
|
|
112
92
|
#: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
|
|
113
93
|
def discovered_tools=(value)
|
|
@@ -122,8 +102,6 @@ class Riffer::Agent::Context
|
|
|
122
102
|
@data[:discovered_tools] = value
|
|
123
103
|
end
|
|
124
104
|
|
|
125
|
-
# Accumulates newly discovered MCP tool classes, deduplicating by name.
|
|
126
|
-
# Each call extends the existing set; calling multiple times is safe.
|
|
127
105
|
#--
|
|
128
106
|
#: (Array[singleton(Riffer::Tool)]) -> Array[singleton(Riffer::Tool)]
|
|
129
107
|
def discover_tools(tools)
|
|
@@ -137,8 +115,6 @@ class Riffer::Agent::Context
|
|
|
137
115
|
@data.dig(*keys)
|
|
138
116
|
end
|
|
139
117
|
|
|
140
|
-
# Returns a copy of the underlying Hash; mutating it does not affect this
|
|
141
|
-
# context.
|
|
142
118
|
#--
|
|
143
119
|
#: () -> Hash[Symbol, untyped]
|
|
144
120
|
def to_h
|
data/lib/riffer/agent/outcome.rb
CHANGED
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# How a run ended — the single place to read whether the agent completed
|
|
5
|
-
# normally and, if not, why. +detail+ carries the specifics when there are any:
|
|
6
|
-
# the tripwire reason, the interrupt reason, the provider's raw finish value,
|
|
7
|
-
# or the structured output parse/validation error.
|
|
8
|
-
#
|
|
9
|
-
# response = agent.generate("Analyze this")
|
|
10
|
-
# case response.outcome.reason
|
|
11
|
-
# when :completed then puts response.structured_output
|
|
12
|
-
# when :invalid_structured_output then warn response.outcome.detail
|
|
13
|
-
# end
|
|
14
4
|
class Riffer::Agent::Outcome
|
|
15
|
-
# Finish reasons that end a turn normally; every other finish reason means the
|
|
16
|
-
# provider cut the turn short and surfaces as the run's outcome verbatim.
|
|
17
5
|
NORMAL_FINISH_REASONS = %i[stop tool_calls].freeze #: Array[Symbol]
|
|
18
6
|
|
|
19
|
-
# Derived from the provider vocabulary so a new finish reason becomes an
|
|
20
|
-
# outcome without a second list to update.
|
|
21
7
|
PROVIDER_STOP_REASONS = (Riffer::Providers::FinishReason::VALUES - NORMAL_FINISH_REASONS).freeze #: Array[Symbol]
|
|
22
8
|
|
|
23
|
-
# The vocabulary every run ends in.
|
|
24
9
|
VALUES = (%i[completed guardrail_blocked interrupted max_steps invalid_structured_output] +
|
|
25
10
|
PROVIDER_STOP_REASONS).freeze #: Array[Symbol]
|
|
26
11
|
|
|
27
|
-
# Why the run ended.
|
|
28
12
|
attr_reader :reason #: Symbol # @dynamic reason
|
|
29
13
|
|
|
30
|
-
# Human-readable specifics for +reason+, when there are any.
|
|
31
14
|
attr_reader :detail #: String? # @dynamic detail
|
|
32
15
|
|
|
33
|
-
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
34
16
|
#--
|
|
35
17
|
#: (reason: Symbol, ?detail: String?) -> void
|
|
36
18
|
def initialize(reason:, detail: nil)
|
|
@@ -42,8 +24,6 @@ class Riffer::Agent::Outcome
|
|
|
42
24
|
@detail = detail
|
|
43
25
|
end
|
|
44
26
|
|
|
45
|
-
# Returns true when the run completed normally.
|
|
46
|
-
#
|
|
47
27
|
#--
|
|
48
28
|
#: () -> bool
|
|
49
29
|
def success?
|
|
@@ -1,55 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Wraps an agent generation response. +outcome+ says how the run ended; when a
|
|
5
|
-
# guardrail blocks execution, +content+ is empty and +tripwire+ carries the
|
|
6
|
-
# block details.
|
|
7
|
-
#
|
|
8
|
-
# response = agent.generate("Hello")
|
|
9
|
-
# if response.outcome.success?
|
|
10
|
-
# puts response.content
|
|
11
|
-
# else
|
|
12
|
-
# puts "#{response.outcome.reason}: #{response.outcome.detail}"
|
|
13
|
-
# end
|
|
14
4
|
class Riffer::Agent::Response
|
|
15
|
-
# The response content.
|
|
16
5
|
attr_reader :content #: String # @dynamic content
|
|
17
|
-
|
|
18
|
-
# How the run ended.
|
|
19
6
|
attr_reader :outcome #: Riffer::Agent::Outcome # @dynamic outcome
|
|
20
|
-
|
|
21
|
-
# The tripwire if execution was blocked.
|
|
22
7
|
attr_reader :tripwire #: Riffer::Guardrails::Tripwire? # @dynamic tripwire
|
|
23
|
-
|
|
24
|
-
# The modifications made by guardrails during processing.
|
|
25
8
|
attr_reader :modifications #: Array[Riffer::Guardrails::Modification] # @dynamic modifications
|
|
26
|
-
|
|
27
|
-
# The parsed structured output, if structured output was configured.
|
|
9
|
+
attr_reader :reasoning #: Array[Riffer::Messages::Assistant::ReasoningPart] # @dynamic reasoning
|
|
28
10
|
attr_reader :structured_output #: Hash[Symbol, untyped]? # @dynamic structured_output
|
|
29
|
-
|
|
30
|
-
# The aggregate token usage across this run's LLM calls, if any was reported.
|
|
31
11
|
attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
|
|
32
|
-
|
|
33
|
-
# The number of LLM calls made during this run (0 when a before-guardrail
|
|
34
|
-
# blocks before any call). Distinct from the session's cumulative step count.
|
|
35
12
|
attr_reader :steps #: Integer # @dynamic steps
|
|
36
|
-
|
|
37
|
-
# The full message history from the agent conversation.
|
|
38
13
|
attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
|
|
39
14
|
|
|
40
|
-
# Call ids of tool_use blocks riffer filled with placeholder results this
|
|
41
|
-
# turn (when an interrupt left them unanswered and history healing is on).
|
|
42
|
-
attr_reader :healed_tool_call_ids #: Array[String] # @dynamic healed_tool_call_ids
|
|
43
|
-
|
|
44
15
|
#--
|
|
45
16
|
#: (
|
|
46
17
|
# String,
|
|
47
18
|
# outcome: Riffer::Agent::Outcome,
|
|
48
19
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
49
20
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
21
|
+
# ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
|
|
50
22
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
51
23
|
# ?messages: Array[Riffer::Messages::Base],
|
|
52
|
-
# ?healed_tool_call_ids: Array[String],
|
|
53
24
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
54
25
|
# ?steps: Integer
|
|
55
26
|
# ) -> void
|
|
@@ -58,9 +29,9 @@ class Riffer::Agent::Response
|
|
|
58
29
|
outcome:,
|
|
59
30
|
tripwire: nil,
|
|
60
31
|
modifications: [],
|
|
32
|
+
reasoning: [],
|
|
61
33
|
structured_output: nil,
|
|
62
34
|
messages: [],
|
|
63
|
-
healed_tool_call_ids: [],
|
|
64
35
|
token_usage: nil,
|
|
65
36
|
steps: 0
|
|
66
37
|
)
|
|
@@ -68,15 +39,13 @@ class Riffer::Agent::Response
|
|
|
68
39
|
@outcome = outcome
|
|
69
40
|
@tripwire = tripwire
|
|
70
41
|
@modifications = modifications
|
|
42
|
+
@reasoning = reasoning
|
|
71
43
|
@structured_output = structured_output
|
|
72
44
|
@messages = messages
|
|
73
|
-
@healed_tool_call_ids = healed_tool_call_ids
|
|
74
45
|
@token_usage = token_usage
|
|
75
46
|
@steps = steps
|
|
76
47
|
end
|
|
77
48
|
|
|
78
|
-
# Returns true if any guardrail modified data during processing.
|
|
79
|
-
#
|
|
80
49
|
#--
|
|
81
50
|
#: () -> bool
|
|
82
51
|
def modified?
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# The generation loop — a pure module of functions over an +agent+, which owns
|
|
5
|
-
# every per-call value; Run just orchestrates.
|
|
6
4
|
module Riffer::Agent::Run
|
|
7
5
|
extend self
|
|
8
6
|
|
|
9
|
-
# Runs the generate loop for the given agent. See Riffer::Agent#generate
|
|
10
|
-
# for prompt/files semantics.
|
|
11
|
-
#
|
|
12
7
|
#--
|
|
13
|
-
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
8
|
+
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
14
9
|
def generate(agent:, prompt: nil, files: nil, tags: {})
|
|
15
10
|
append_user_message(agent, prompt, files: files)
|
|
16
11
|
run_loop(agent, tags: tags)
|
|
17
12
|
end
|
|
18
13
|
|
|
19
|
-
# Runs the streaming loop for the given agent. See Riffer::Agent#stream
|
|
20
|
-
# for prompt/files semantics.
|
|
21
|
-
#
|
|
22
14
|
#--
|
|
23
|
-
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
|
|
15
|
+
#: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
|
|
24
16
|
def stream(agent:, prompt: nil, files: nil, tags: {})
|
|
25
17
|
append_user_message(agent, prompt, files: files)
|
|
26
18
|
# The enumerator body runs in its own fiber, where the fiber-local OTEL
|
|
27
19
|
# context is empty — capture here so the run span parents to the caller's
|
|
28
|
-
# trace.
|
|
29
|
-
# cross the fiber boundary without any re-propagation.
|
|
20
|
+
# trace.
|
|
30
21
|
trace_context = Riffer::Tracing.current_context
|
|
31
22
|
Enumerator.new do |stream_yielder|
|
|
32
23
|
Riffer::Tracing.with_context(trace_context) { run_loop(agent, tags: tags, stream_yielder: stream_yielder) }
|
|
@@ -35,17 +26,12 @@ module Riffer::Agent::Run
|
|
|
35
26
|
|
|
36
27
|
private
|
|
37
28
|
|
|
38
|
-
# Both +generate+ and +stream+ funnel here, so this is the single place raw
|
|
39
|
-
# +tags+ are normalized. The clean <tt>String => String</tt> map is then
|
|
40
|
-
# threaded to every span builder in the run as +riffer.tag.*+ and to each
|
|
41
|
-
# provider call (via +merged_model_options+) for native request-metadata
|
|
42
|
-
# mapping.
|
|
43
29
|
#--
|
|
44
30
|
#: (Riffer::Agent, ?tags: Hash[(String | Symbol), untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
|
|
45
31
|
def run_loop(agent, tags: {}, stream_yielder: nil)
|
|
46
|
-
tags = normalize_tags(tags)
|
|
32
|
+
tags = default_tags(agent).merge(normalize_tags(tags))
|
|
47
33
|
Riffer::Tracing.in_span(
|
|
48
|
-
"invoke_agent #{agent.
|
|
34
|
+
"invoke_agent #{agent.identifier}",
|
|
49
35
|
attributes: run_span_attributes(agent, tags),
|
|
50
36
|
kind: :internal,
|
|
51
37
|
) do |span|
|
|
@@ -124,20 +110,12 @@ module Riffer::Agent::Run
|
|
|
124
110
|
return final_response(agent, all_modifications, token_usage: run_usage, steps: run_steps)
|
|
125
111
|
end
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
agent.session.set(new_messages)
|
|
129
|
-
if stream_yielder
|
|
130
|
-
stream_yielder << Riffer::StreamEvents::Interrupt.new(
|
|
131
|
-
reason: reason,
|
|
132
|
-
healed_tool_call_ids: filled,
|
|
133
|
-
)
|
|
134
|
-
end
|
|
113
|
+
stream_yielder << Riffer::StreamEvents::Interrupt.new(reason: reason) if stream_yielder
|
|
135
114
|
final_response(
|
|
136
115
|
agent,
|
|
137
116
|
all_modifications,
|
|
138
117
|
interrupted: true,
|
|
139
118
|
interrupt_reason: reason,
|
|
140
|
-
healed_tool_call_ids: filled,
|
|
141
119
|
token_usage: run_usage,
|
|
142
120
|
steps: run_steps,
|
|
143
121
|
)
|
|
@@ -153,6 +131,7 @@ module Riffer::Agent::Run
|
|
|
153
131
|
def accumulate_streamed_response(agent, stream_yielder, tags = {})
|
|
154
132
|
accumulated_content = +""
|
|
155
133
|
accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
134
|
+
accumulated_reasoning = [] #: Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
156
135
|
accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
|
|
157
136
|
accumulated_finish_reason = nil #: Symbol?
|
|
158
137
|
accumulated_finish_reason_raw = nil #: String?
|
|
@@ -162,12 +141,12 @@ module Riffer::Agent::Run
|
|
|
162
141
|
|
|
163
142
|
case event
|
|
164
143
|
when Riffer::StreamEvents::TextDelta
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
# owned buffer; replace (not =) on TextDone keeps it that way so a later
|
|
168
|
-
# delta's << can never mutate the string held by a TextDone event.
|
|
144
|
+
# << rather than +=, which copies the whole buffer per delta (O(n^2)
|
|
145
|
+
# over a stream).
|
|
169
146
|
accumulated_content << event.content
|
|
170
147
|
when Riffer::StreamEvents::TextDone
|
|
148
|
+
# replace, not =, so a later delta's << can never mutate the string
|
|
149
|
+
# this event holds.
|
|
171
150
|
accumulated_content.replace(event.content)
|
|
172
151
|
when Riffer::StreamEvents::ToolCallDone
|
|
173
152
|
accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
@@ -175,6 +154,8 @@ module Riffer::Agent::Run
|
|
|
175
154
|
name: event.name,
|
|
176
155
|
arguments: event.arguments,
|
|
177
156
|
)
|
|
157
|
+
when Riffer::StreamEvents::ReasoningDone
|
|
158
|
+
accumulated_reasoning << event.part
|
|
178
159
|
when Riffer::StreamEvents::TokenUsageDone
|
|
179
160
|
accumulated_token_usage = event.token_usage
|
|
180
161
|
when Riffer::StreamEvents::FinishReasonDone
|
|
@@ -186,6 +167,7 @@ module Riffer::Agent::Run
|
|
|
186
167
|
Riffer::Messages::Assistant.new(
|
|
187
168
|
accumulated_content,
|
|
188
169
|
tool_calls: accumulated_tool_calls,
|
|
170
|
+
reasoning: accumulated_reasoning,
|
|
189
171
|
token_usage: accumulated_token_usage,
|
|
190
172
|
finish_reason: accumulated_finish_reason,
|
|
191
173
|
finish_reason_raw: accumulated_finish_reason_raw,
|
|
@@ -228,19 +210,18 @@ module Riffer::Agent::Run
|
|
|
228
210
|
message&.content || "",
|
|
229
211
|
outcome: final_outcome(message, result, interrupted: interrupted, interrupt_reason: interrupt_reason),
|
|
230
212
|
modifications: all_modifications,
|
|
213
|
+
reasoning: message&.reasoning || [],
|
|
231
214
|
structured_output: result&.object,
|
|
232
215
|
**extra,
|
|
233
216
|
)
|
|
234
217
|
end
|
|
235
218
|
|
|
236
|
-
# Checked in the order things happened. The loop being stopped (max_steps or
|
|
237
|
-
# an interrupt) beats the provider's finish reason, which beats riffer's own
|
|
238
|
-
# validation of the content. A truncated response that also fails the schema
|
|
239
|
-
# therefore reports :length, not :invalid_structured_output.
|
|
240
219
|
#--
|
|
241
220
|
#: (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
|
|
242
221
|
def final_outcome(message, result, interrupted:, interrupt_reason:)
|
|
243
222
|
finish_reason = message&.finish_reason
|
|
223
|
+
# Precedence follows the order things happened: a stopped loop beats the
|
|
224
|
+
# provider's finish reason, which beats schema validation of the content.
|
|
244
225
|
if interrupted && interrupt_reason == Riffer::Agent::INTERRUPT_MAX_STEPS
|
|
245
226
|
Riffer::Agent::Outcome.new(reason: :max_steps)
|
|
246
227
|
elsif interrupted
|
|
@@ -368,19 +349,23 @@ module Riffer::Agent::Run
|
|
|
368
349
|
discovered.empty? ? agent.tools : agent.tools + discovered
|
|
369
350
|
end
|
|
370
351
|
|
|
371
|
-
# +tags+ rides in the options hash as a curated key the providers extract for
|
|
372
|
-
# native request-metadata mapping (alongside +:structured_output+); it never
|
|
373
|
-
# reaches an SDK call verbatim. Span tagging is threaded separately to each
|
|
374
|
-
# builder.
|
|
375
352
|
#--
|
|
376
353
|
#: (Riffer::Agent, ?Hash[String, String]) -> Hash[Symbol, untyped]
|
|
377
354
|
def merged_model_options(agent, tags = {})
|
|
378
355
|
opts = agent.config.model_options.dup
|
|
379
356
|
opts[:structured_output] = agent.structured_output if agent.structured_output
|
|
357
|
+
# Providers extract :tags for native request metadata; it never reaches an
|
|
358
|
+
# SDK call verbatim.
|
|
380
359
|
opts[:tags] = tags unless tags.empty?
|
|
381
360
|
opts
|
|
382
361
|
end
|
|
383
362
|
|
|
363
|
+
#--
|
|
364
|
+
#: (Riffer::Agent) -> Hash[String, String]
|
|
365
|
+
def default_tags(agent)
|
|
366
|
+
{ "kind" => "agent", "agent" => agent.identifier }
|
|
367
|
+
end
|
|
368
|
+
|
|
384
369
|
#--
|
|
385
370
|
#: (
|
|
386
371
|
# Riffer::Agent,
|
|
@@ -388,8 +373,8 @@ module Riffer::Agent::Run
|
|
|
388
373
|
# outcome: Riffer::Agent::Outcome,
|
|
389
374
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
390
375
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
376
|
+
# ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
|
|
391
377
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
392
|
-
# ?healed_tool_call_ids: Array[String],
|
|
393
378
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
394
379
|
# ?steps: Integer
|
|
395
380
|
# ) -> Riffer::Agent::Response
|
|
@@ -399,8 +384,8 @@ module Riffer::Agent::Run
|
|
|
399
384
|
outcome:,
|
|
400
385
|
tripwire: nil,
|
|
401
386
|
modifications: [],
|
|
387
|
+
reasoning: [],
|
|
402
388
|
structured_output: nil,
|
|
403
|
-
healed_tool_call_ids: [],
|
|
404
389
|
token_usage: nil,
|
|
405
390
|
steps: 0
|
|
406
391
|
)
|
|
@@ -410,23 +395,22 @@ module Riffer::Agent::Run
|
|
|
410
395
|
outcome: outcome,
|
|
411
396
|
tripwire: tripwire,
|
|
412
397
|
modifications: modifications,
|
|
398
|
+
reasoning: reasoning,
|
|
413
399
|
structured_output: structured_output,
|
|
414
400
|
messages: messages.frozen? ? messages : messages.dup.freeze,
|
|
415
|
-
healed_tool_call_ids: healed_tool_call_ids,
|
|
416
401
|
token_usage: token_usage,
|
|
417
402
|
steps: steps,
|
|
418
403
|
)
|
|
419
404
|
end
|
|
420
405
|
|
|
421
|
-
# Raises when +files+ are supplied without a +prompt+ — the provider needs
|
|
422
|
-
# text to anchor the attachments.
|
|
423
406
|
#--
|
|
424
|
-
#: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> void
|
|
407
|
+
#: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> void
|
|
425
408
|
def append_user_message(agent, prompt, files: nil)
|
|
409
|
+
# The provider needs text to anchor the attachments.
|
|
426
410
|
raise Riffer::ArgumentError, "files: requires a prompt" if files && !files.empty? && prompt.nil?
|
|
427
411
|
return unless prompt
|
|
428
412
|
|
|
429
|
-
file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
413
|
+
file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
430
414
|
agent.session.add(Riffer::Messages::User.new(prompt, files: file_parts), silent: true)
|
|
431
415
|
end
|
|
432
416
|
|
|
@@ -451,7 +435,7 @@ module Riffer::Agent::Run
|
|
|
451
435
|
def run_span_attributes(agent, tags = {})
|
|
452
436
|
{
|
|
453
437
|
"gen_ai.operation.name" => "invoke_agent",
|
|
454
|
-
"gen_ai.agent.name" => agent.
|
|
438
|
+
"gen_ai.agent.name" => agent.identifier,
|
|
455
439
|
"gen_ai.provider.name" => agent.provider.class.semconv_provider_name,
|
|
456
440
|
"gen_ai.request.model" => agent.model_name,
|
|
457
441
|
}.merge(tag_attributes(tags))
|
|
@@ -3,32 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
# Turns a resolved agent into a self-contained, provider-neutral data hash and
|
|
7
|
-
# back into a runnable agent, behind the +Riffer::Agent#to_h+ /
|
|
8
|
-
# +Riffer::Agent.from_h+ delegators.
|
|
9
|
-
#
|
|
10
|
-
# hash = Riffer::Agent::Serializer.to_h(agent: agent)
|
|
11
|
-
# rebuilt = Riffer::Agent::Serializer.from_h(hash, context: {tenant: "acme"})
|
|
12
6
|
module Riffer::Agent::Serializer
|
|
13
7
|
extend self
|
|
14
8
|
|
|
15
|
-
#
|
|
16
|
-
# shape; +from_h+ refuses any other version.
|
|
9
|
+
# Bump only on an incompatible change to the hash shape.
|
|
17
10
|
SCHEMA_VERSION = 1 #: Integer
|
|
18
11
|
|
|
19
|
-
# Raised by +from_h+ when the hash's +schema_version+ is unsupported.
|
|
20
12
|
class VersionError < Riffer::ArgumentError; end
|
|
21
13
|
|
|
22
|
-
# The default +tool_resolver+: synthesizes a body-less tool shell from a
|
|
23
|
-
# descriptor. Its +#call+ raises — route shells through a remote runtime.
|
|
24
14
|
DEFAULT_TOOL_RESOLVER = ->(descriptor) { build_tool_shell(descriptor) } #: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool)
|
|
25
15
|
|
|
26
|
-
# Snapshots a resolved agent into a self-contained wire hash. Proc-based
|
|
27
|
-
# settings are already evaluated against the agent's context, so the hash
|
|
28
|
-
# carries plain data, never Procs.
|
|
29
16
|
#--
|
|
30
17
|
#: (agent: Riffer::Agent) -> Hash[Symbol, untyped]
|
|
31
18
|
def to_h(agent:)
|
|
19
|
+
# Already resolved against the agent's context, so the hash carries plain data, never Procs.
|
|
32
20
|
config = agent.config
|
|
33
21
|
{
|
|
34
22
|
schema_version: SCHEMA_VERSION,
|
|
@@ -43,17 +31,11 @@ module Riffer::Agent::Serializer
|
|
|
43
31
|
}
|
|
44
32
|
end
|
|
45
33
|
|
|
46
|
-
#
|
|
47
|
-
# tool dispatch (not used to re-resolve the already-resolved config);
|
|
48
|
-
# +session+ seeds conversation history (the hash carries the agent definition,
|
|
49
|
-
# not its history). Raises Riffer::Agent::Serializer::VersionError on an
|
|
50
|
-
# unsupported +schema_version+.
|
|
51
|
-
#
|
|
34
|
+
# Raises Riffer::Agent::Serializer::VersionError on an unsupported +schema_version+.
|
|
52
35
|
#--
|
|
53
36
|
#: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
|
|
54
37
|
def from_h(hash, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
|
|
55
|
-
#
|
|
56
|
-
# decoder) is how a future breaking change keeps older hashes readable.
|
|
38
|
+
# One arm per supported version keeps older hashes decodable after a breaking change.
|
|
57
39
|
case hash[:schema_version]
|
|
58
40
|
when SCHEMA_VERSION
|
|
59
41
|
decode_v1(hash, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
|
|
@@ -63,15 +45,12 @@ module Riffer::Agent::Serializer
|
|
|
63
45
|
end
|
|
64
46
|
end
|
|
65
47
|
|
|
66
|
-
# Snapshots a resolved agent to a JSON string.
|
|
67
48
|
#--
|
|
68
49
|
#: (agent: Riffer::Agent) -> String
|
|
69
50
|
def to_json(agent:)
|
|
70
51
|
JSON.generate(to_h(agent: agent))
|
|
71
52
|
end
|
|
72
53
|
|
|
73
|
-
# Reconstructs a runnable agent from a JSON string produced by +to_json+. See
|
|
74
|
-
# +from_h+ for the arguments.
|
|
75
54
|
#--
|
|
76
55
|
#: (String, ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
|
|
77
56
|
def from_json(json, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
|
|
@@ -100,14 +79,11 @@ module Riffer::Agent::Serializer
|
|
|
100
79
|
max_steps: decode_max_steps(hash),
|
|
101
80
|
tools_config: tools,
|
|
102
81
|
} #: Hash[Symbol, untyped]
|
|
103
|
-
# tool_runtime= rejects nil
|
|
104
|
-
# Config default (Riffer.config.tool_runtime) applies.
|
|
82
|
+
# Config#tool_runtime= rejects nil.
|
|
105
83
|
config_args[:tool_runtime] = tool_runtime if tool_runtime
|
|
106
84
|
|
|
107
|
-
# +session+ is
|
|
108
|
-
#
|
|
109
|
-
# resume persisted history. The hash never carries history (see "What does
|
|
110
|
-
# not transfer"), so this is the only seam for rehydrating a conversation.
|
|
85
|
+
# The hash never carries history, so +session+ is the only seam for rehydrating a conversation.
|
|
86
|
+
# +context+ feeds tool dispatch only; the config was resolved before serialization.
|
|
111
87
|
Riffer::Agent.new(config: Riffer::Agent::Config.new(**config_args), context: context, session: session)
|
|
112
88
|
end
|
|
113
89
|
|
|
@@ -119,19 +95,17 @@ module Riffer::Agent::Serializer
|
|
|
119
95
|
Riffer::Params.from_json_schema(schema)
|
|
120
96
|
end
|
|
121
97
|
|
|
122
|
-
# Encodes unlimited steps (+nil+ in the DSL) as +-1+ on the wire, where a
|
|
123
|
-
# JSON +null+ is awkward across transports (e.g. proto3).
|
|
124
98
|
#--
|
|
125
99
|
#: (Numeric?) -> Numeric
|
|
126
100
|
def encode_max_steps(value)
|
|
101
|
+
# A JSON null is awkward across transports (e.g. proto3), so unlimited travels as -1.
|
|
127
102
|
value.nil? ? -1 : value
|
|
128
103
|
end
|
|
129
104
|
|
|
130
|
-
# Reverses +encode_max_steps+; a missing key falls back to the default so a
|
|
131
|
-
# partial hash can't become an unbounded loop.
|
|
132
105
|
#--
|
|
133
106
|
#: (Hash[Symbol, untyped]) -> Numeric?
|
|
134
107
|
def decode_max_steps(hash)
|
|
108
|
+
# A partial hash must not become an unbounded loop.
|
|
135
109
|
return Riffer::Agent::Config::DEFAULT_MAX_STEPS unless hash.key?(:max_steps)
|
|
136
110
|
|
|
137
111
|
hash[:max_steps] == -1 ? nil : hash[:max_steps]
|
|
@@ -151,10 +125,7 @@ module Riffer::Agent::Serializer
|
|
|
151
125
|
schema = descriptor[:parameters_schema]
|
|
152
126
|
tool_timeout = descriptor[:timeout]
|
|
153
127
|
|
|
154
|
-
#
|
|
155
|
-
# from data — the tool DSL is class-level, so there is no value-level
|
|
156
|
-
# builder to type against. Same approach as Riffer::Mcp::ToolFactory;
|
|
157
|
-
# steep can't type the dynamic class body, hence the ignore block.
|
|
128
|
+
# The tool DSL is class-level, so there is no value-level builder to synthesize a tool from data.
|
|
158
129
|
Class.new(Riffer::Tool) do
|
|
159
130
|
# steep:ignore:start
|
|
160
131
|
identifier tool_name
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# +Riffer.config.experimental_history_healing+ is off.
|
|
4
|
+
# Maintains the invariant that every +tool_use+ has a matching +tool_result+
|
|
5
|
+
# and every +tool_result+ has a parent +tool_use+.
|
|
7
6
|
module Riffer::Agent::Session::Repair
|
|
8
7
|
extend self
|
|
9
8
|
|
|
10
|
-
# Placeholder response filled in for an orphaned +tool_use+ on interrupt.
|
|
11
9
|
ORPHAN_PLACEHOLDER = lambda { |_tool_call|
|
|
12
10
|
Riffer::Tools::Response.error("Tool call interrupted before completion.", type: :interrupted)
|
|
13
11
|
} #: ^(Riffer::Messages::Assistant::ToolCall) -> Riffer::Tools::Response
|
|
14
12
|
|
|
15
|
-
# Fills each orphaned +tool_use+ in +messages+ with an +ORPHAN_PLACEHOLDER+
|
|
16
|
-
# result inserted after its parent. Returns +[new_messages, filled_call_ids]+.
|
|
17
13
|
#--
|
|
18
14
|
#: (Array[Riffer::Messages::Base]) -> [Array[Riffer::Messages::Base], Array[String]]
|
|
19
15
|
def fill_orphans(messages)
|
|
20
|
-
return [messages, []] unless Riffer.config.experimental_history_healing
|
|
21
|
-
|
|
22
16
|
result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
|
|
23
17
|
filled = [] #: Array[String]
|
|
24
18
|
new_messages = [] #: Array[Riffer::Messages::Base]
|
|
@@ -45,15 +39,9 @@ module Riffer::Agent::Session::Repair
|
|
|
45
39
|
[new_messages, filled]
|
|
46
40
|
end
|
|
47
41
|
|
|
48
|
-
# Prunes a seeded message array to the invariant — dropping orphaned tool
|
|
49
|
-
# exchanges and parentless Tool messages, but preserving the pending
|
|
50
|
-
# tool_calls on the resume boundary (the last assistant) for
|
|
51
|
-
# +execute_pending_tool_calls+. Returns a new array.
|
|
52
42
|
#--
|
|
53
43
|
#: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
|
|
54
44
|
def prune_orphans(messages)
|
|
55
|
-
return messages unless Riffer.config.experimental_history_healing
|
|
56
|
-
|
|
57
45
|
resume_boundary = (messages.length - 1).downto(0).find do |idx|
|
|
58
46
|
m = messages[idx]
|
|
59
47
|
m.is_a?(Riffer::Messages::Assistant) &&
|
|
@@ -67,7 +55,7 @@ module Riffer::Agent::Session::Repair
|
|
|
67
55
|
|
|
68
56
|
strip_offenders = messages.each_with_index.flat_map do |m, idx|
|
|
69
57
|
next [] unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
|
|
70
|
-
next [] if idx == resume_boundary #
|
|
58
|
+
next [] if idx == resume_boundary # execute_pending_tool_calls still runs these
|
|
71
59
|
next [] if m.tool_calls.all? { |tc| result_ids.include?(tc.call_id) }
|
|
72
60
|
|
|
73
61
|
m.tool_calls.map(&:call_id)
|