phronomy 0.10.0 → 0.11.1
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 +41 -1
- data/README.md +35 -24
- data/benchmark/bench_agent_invoke.rb +1 -0
- data/lib/phronomy/agent/base.rb +223 -95
- data/lib/phronomy/agent/concerns/error_translation.rb +1 -1
- data/lib/phronomy/agent/concerns/filterable.rb +184 -0
- data/lib/phronomy/agent/concerns/retryable.rb +9 -5
- data/lib/phronomy/agent/context/capability/base.rb +2 -2
- data/lib/phronomy/agent/invocation_context.rb +171 -0
- data/lib/phronomy/agent/invocation_session.rb +337 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +189 -0
- data/lib/phronomy/agent/suspended_session_registry.rb +54 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +26 -0
- data/lib/phronomy/configuration.rb +0 -6
- data/lib/phronomy/{concurrency → engine/concurrency}/async_queue.rb +68 -5
- data/lib/phronomy/{concurrency → engine/concurrency}/blocking_adapter_pool.rb +9 -3
- data/lib/phronomy/{event_loop.rb → engine/event_loop.rb} +71 -37
- data/lib/phronomy/engine/fsm_session.rb +248 -0
- data/lib/phronomy/{runtime → engine/runtime}/deterministic_scheduler.rb +28 -1
- data/lib/phronomy/{runtime → engine/runtime}/fake_scheduler.rb +1 -1
- data/lib/phronomy/{runtime.rb → engine/runtime.rb} +2 -4
- data/lib/phronomy/{task → engine/task}/backend.rb +2 -2
- data/lib/phronomy/engine/task/deferred_backend.rb +73 -0
- data/lib/phronomy/{task → engine/task}/fiber_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/immediate_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/mapped_backend.rb +15 -3
- data/lib/phronomy/{task → engine/task}/thread_backend.rb +1 -1
- data/lib/phronomy/{task.rb → engine/task.rb} +19 -7
- data/lib/phronomy/eval/scorer/llm_judge.rb +1 -1
- data/lib/phronomy/filter/base.rb +56 -0
- data/lib/phronomy/{guardrail/prompt_injection_guardrail.rb → filter/prompt_injection_filter.rb} +20 -13
- data/lib/phronomy/filter.rb +5 -0
- data/lib/phronomy/generator_verifier.rb +21 -13
- data/lib/phronomy/multi_agent/orchestrator.rb +4 -4
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +3 -3
- data/lib/phronomy/multi_agent/team_coordinator.rb +1 -2
- data/lib/phronomy/tools/mcp.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +24 -13
- data/lib/phronomy/workflow.rb +6 -3
- data/lib/phronomy/workflow_context.rb +7 -5
- data/lib/phronomy/workflow_runner.rb +77 -27
- data/lib/phronomy.rb +24 -13
- data/scripts/api_snapshot.rb +2 -2
- metadata +42 -44
- data/lib/phronomy/agent/checkpoint.rb +0 -208
- data/lib/phronomy/agent/checkpoint_store.rb +0 -97
- data/lib/phronomy/agent/concerns/guardrailable.rb +0 -45
- data/lib/phronomy/agent/concerns/suspendable.rb +0 -190
- data/lib/phronomy/agent/suspend_signal.rb +0 -37
- data/lib/phronomy/context.rb +0 -12
- data/lib/phronomy/guardrail/base.rb +0 -45
- data/lib/phronomy/guardrail/input_guardrail.rb +0 -19
- data/lib/phronomy/guardrail/output_guardrail.rb +0 -19
- data/lib/phronomy/guardrail.rb +0 -7
- data/lib/phronomy/tool.rb +0 -9
- data/lib/phronomy/workflow/fsm_session.rb +0 -249
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_scope.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_token.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/concurrency_gate.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/deadline.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/gate_registry.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/pool_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/runtime_metrics.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler_timer_adapter.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/task_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/thread_scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_queue.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_service.rb +0 -0
- /data/lib/phronomy/{task_group.rb → engine/task_group.rb} +0 -0
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# Default in-memory idempotency store for {Checkpoint} resume operations.
|
|
6
|
-
#
|
|
7
|
-
# Tracks consumed checkpoint IDs so that calling {Agent::Base#resume} twice
|
|
8
|
-
# with the same checkpoint raises {Phronomy::CheckpointAlreadyResumedError}
|
|
9
|
-
# instead of silently executing the approved tool a second time.
|
|
10
|
-
#
|
|
11
|
-
# This implementation is *not thread-safe*. It assumes a single agent instance
|
|
12
|
-
# is accessed from only one thread at a time, which is the expected usage pattern.
|
|
13
|
-
# Agent instances themselves are not thread-safe (state like +@messages+, +@config+
|
|
14
|
-
# is not protected), so concurrent calls to the same agent instance are unsupported.
|
|
15
|
-
#
|
|
16
|
-
# Each agent instance gets its own store by default, so no sharing occurs unless
|
|
17
|
-
# the caller explicitly assigns the same store object to multiple agents.
|
|
18
|
-
#
|
|
19
|
-
# For distributed environments (multiple processes or background jobs), swap this
|
|
20
|
-
# for a custom implementation backed by Redis, ActiveRecord, or another shared store.
|
|
21
|
-
# *Your custom store implementation is responsible for ensuring thread-safety* if
|
|
22
|
-
# your application shares the same store instance across multiple threads.
|
|
23
|
-
#
|
|
24
|
-
# @example Plugging in a custom store
|
|
25
|
-
# agent = MyAgent.new
|
|
26
|
-
# agent.checkpoint_store = MyRedis::CheckpointStore.new
|
|
27
|
-
#
|
|
28
|
-
# @example Duck-type contract required by any replacement
|
|
29
|
-
# # consumed?(checkpoint_id) => Boolean
|
|
30
|
-
# # consume!(checkpoint_id) => void; raises CheckpointAlreadyResumedError if duplicate
|
|
31
|
-
# # cleanup!(checkpoint_id) => void (optional); removes tracking for the checkpoint
|
|
32
|
-
# # clear! => void (optional); removes all tracked checkpoints
|
|
33
|
-
#
|
|
34
|
-
# @api public
|
|
35
|
-
class CheckpointStore
|
|
36
|
-
def initialize
|
|
37
|
-
@consumed = Set.new
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Returns +true+ if the given checkpoint ID has already been consumed.
|
|
41
|
-
#
|
|
42
|
-
# @param checkpoint_id [String]
|
|
43
|
-
# @return [Boolean]
|
|
44
|
-
# @api public
|
|
45
|
-
def consumed?(checkpoint_id)
|
|
46
|
-
@consumed.include?(checkpoint_id)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Marks +checkpoint_id+ as consumed, or raises if it was already consumed.
|
|
50
|
-
#
|
|
51
|
-
# @param checkpoint_id [String]
|
|
52
|
-
# @raise [Phronomy::CheckpointAlreadyResumedError]
|
|
53
|
-
# @return [void]
|
|
54
|
-
# @api public
|
|
55
|
-
def consume!(checkpoint_id)
|
|
56
|
-
if @consumed.include?(checkpoint_id)
|
|
57
|
-
raise Phronomy::CheckpointAlreadyResumedError,
|
|
58
|
-
"checkpoint #{checkpoint_id} has already been resumed"
|
|
59
|
-
end
|
|
60
|
-
@consumed.add(checkpoint_id)
|
|
61
|
-
nil
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Removes tracking for a specific checkpoint ID.
|
|
65
|
-
#
|
|
66
|
-
# Use this to explicitly discard a checkpoint when the application
|
|
67
|
-
# determines it is no longer needed (e.g., user abandons an approval
|
|
68
|
-
# workflow).
|
|
69
|
-
#
|
|
70
|
-
# This method is optional in the duck-type contract. Custom store
|
|
71
|
-
# implementations may choose not to implement it.
|
|
72
|
-
#
|
|
73
|
-
# @param checkpoint_id [String]
|
|
74
|
-
# @return [void]
|
|
75
|
-
# @api public
|
|
76
|
-
def cleanup!(checkpoint_id)
|
|
77
|
-
@consumed.delete(checkpoint_id)
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Removes all tracked checkpoint IDs.
|
|
82
|
-
#
|
|
83
|
-
# Use this for test cleanup, periodic maintenance, or application
|
|
84
|
-
# shutdown.
|
|
85
|
-
#
|
|
86
|
-
# This method is optional in the duck-type contract. Custom store
|
|
87
|
-
# implementations may choose not to implement it.
|
|
88
|
-
#
|
|
89
|
-
# @return [void]
|
|
90
|
-
# @api public
|
|
91
|
-
def clear!
|
|
92
|
-
@consumed.clear
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Concerns
|
|
6
|
-
# Adds input and output guardrail support to an agent.
|
|
7
|
-
#
|
|
8
|
-
# Included in {Phronomy::Agent::Base}. Guardrails are run on the raw
|
|
9
|
-
# input string before the LLM is called, and on the raw output string
|
|
10
|
-
# before the result is returned to the caller.
|
|
11
|
-
# @api private
|
|
12
|
-
module Guardrailable
|
|
13
|
-
# Attach a guardrail that validates input before every #invoke call.
|
|
14
|
-
# @param guardrail [Phronomy::Guardrail::InputGuardrail]
|
|
15
|
-
# @return [self]
|
|
16
|
-
# @api private
|
|
17
|
-
def add_input_guardrail(guardrail)
|
|
18
|
-
@input_guardrails ||= []
|
|
19
|
-
@input_guardrails << guardrail
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Attach a guardrail that validates output before it is returned.
|
|
24
|
-
# @param guardrail [Phronomy::Guardrail::OutputGuardrail]
|
|
25
|
-
# @return [self]
|
|
26
|
-
# @api private
|
|
27
|
-
def add_output_guardrail(guardrail)
|
|
28
|
-
@output_guardrails ||= []
|
|
29
|
-
@output_guardrails << guardrail
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def run_input_guardrails!(input)
|
|
36
|
-
(@input_guardrails || []).each { |g| g.run!(input) }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def run_output_guardrails!(output)
|
|
40
|
-
(@output_guardrails || []).each { |g| g.run!(output) }
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module Agent
|
|
7
|
-
module Concerns
|
|
8
|
-
# Adds suspend/resume and tool-approval support to an agent.
|
|
9
|
-
#
|
|
10
|
-
# Included in {Phronomy::Agent::Base}. When a tool decorated with
|
|
11
|
-
# +requires_approval true+ is called and no synchronous approval handler
|
|
12
|
-
# has been registered, the invocation is suspended and a
|
|
13
|
-
# @api private
|
|
14
|
-
# {Phronomy::Agent::Checkpoint} is returned so the caller can resume later.
|
|
15
|
-
module Suspendable
|
|
16
|
-
# Registers a callback that is invoked before executing any tool that has
|
|
17
|
-
# +requires_approval true+ set. The block receives the tool name (String)
|
|
18
|
-
# and the arguments Hash, and must return a truthy value to allow execution.
|
|
19
|
-
# Returning a falsy value causes the tool to return a denial message instead
|
|
20
|
-
# of executing.
|
|
21
|
-
#
|
|
22
|
-
# When no handler is registered and a tool with +requires_approval+ is
|
|
23
|
-
# called, #invoke returns a suspended result hash containing a
|
|
24
|
-
# {Phronomy::Agent::Checkpoint}. Call #resume to continue execution after
|
|
25
|
-
# obtaining an approval decision from the user or an external system.
|
|
26
|
-
#
|
|
27
|
-
# @example Synchronous handler
|
|
28
|
-
# agent = MyAgent.new
|
|
29
|
-
# agent.on_approval_required { |tool_name, args| prompt_user(tool_name, args) }
|
|
30
|
-
# @return [self]
|
|
31
|
-
# @api private
|
|
32
|
-
def on_approval_required(&block)
|
|
33
|
-
@approval_handler = block
|
|
34
|
-
self
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Registers a scope policy callable for this agent instance.
|
|
38
|
-
#
|
|
39
|
-
# The callable receives +(tool_class, scope, agent)+ and must return
|
|
40
|
-
# +:allow+, +:reject+, or +:approve+.
|
|
41
|
-
#
|
|
42
|
-
# @example Reject all write-scoped tools
|
|
43
|
-
# agent.scope_policy = ->(_tc, scope, _agent) { scope == :write ? :reject : :allow }
|
|
44
|
-
#
|
|
45
|
-
# @param policy [#call]
|
|
46
|
-
# @return [void]
|
|
47
|
-
# @api public
|
|
48
|
-
def scope_policy=(policy)
|
|
49
|
-
@scope_policy = policy
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Sets the idempotency store used to guard against duplicate resumes.
|
|
53
|
-
#
|
|
54
|
-
# The store must respond to:
|
|
55
|
-
# - +consumed?(checkpoint_id)+ ⇒ Boolean
|
|
56
|
-
# - +consume!(checkpoint_id)+ ⇒ void; raises {Phronomy::CheckpointAlreadyResumedError} on duplicate
|
|
57
|
-
#
|
|
58
|
-
# Defaults to a per-instance {Phronomy::Agent::CheckpointStore} (in-memory, not thread-safe).
|
|
59
|
-
# Assign a shared persistent store when resuming across processes (e.g. Redis-backed).
|
|
60
|
-
# Custom stores are responsible for ensuring thread-safety if shared across threads.
|
|
61
|
-
#
|
|
62
|
-
# @param store [#consumed?, #consume!]
|
|
63
|
-
# @return [void]
|
|
64
|
-
# @api public
|
|
65
|
-
def checkpoint_store=(store)
|
|
66
|
-
@checkpoint_store = store
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Resumes a previously suspended invocation from a {Phronomy::Agent::Checkpoint}.
|
|
70
|
-
#
|
|
71
|
-
# This method reconstructs the conversation state captured at suspension
|
|
72
|
-
# time, injects the tool result (executed or denied), and continues the
|
|
73
|
-
# LLM loop until it produces a final answer.
|
|
74
|
-
#
|
|
75
|
-
# @param checkpoint [Phronomy::Agent::Checkpoint] the checkpoint returned by
|
|
76
|
-
# the suspended #invoke call
|
|
77
|
-
# @param approved [Boolean] +true+ to execute the pending tool; +false+
|
|
78
|
-
# to inject a denial message and let the LLM handle it gracefully
|
|
79
|
-
# @param config [Hash] same runtime options as #invoke
|
|
80
|
-
# @return [Hash] +{ output: String, suspended: false, messages: Array, usage: Phronomy::TokenUsage }+
|
|
81
|
-
# or +{ output: nil, suspended: true, checkpoint: Phronomy::Agent::Checkpoint, messages: Array }+
|
|
82
|
-
# when a second approval-required tool is encountered during continuation
|
|
83
|
-
# @raise [Phronomy::GuardrailError] when an output guardrail rejects the value
|
|
84
|
-
# @raise [Phronomy::CheckpointAlreadyResumedError] when the checkpoint has already been consumed
|
|
85
|
-
# @api private
|
|
86
|
-
def resume(checkpoint, approved:, config: {})
|
|
87
|
-
# Guard against duplicate resumes using the idempotency store.
|
|
88
|
-
_checkpoint_store.consume!(checkpoint.checkpoint_id)
|
|
89
|
-
# Build a fresh chat with all tools registered.
|
|
90
|
-
chat = build_chat
|
|
91
|
-
|
|
92
|
-
# Re-apply system instructions and register tools so the LLM has the
|
|
93
|
-
# same persona/context as the original invocation. build_context
|
|
94
|
-
# includes all tool classes (static + handoff) via add_capability.
|
|
95
|
-
context = build_context(checkpoint.original_input, messages: [])
|
|
96
|
-
apply_instructions(chat, context[:system]) if context[:system]
|
|
97
|
-
(context[:tool_classes] || []).each { |tc| chat.with_tool(prepare_tool_class(tc)) }
|
|
98
|
-
|
|
99
|
-
# Restore the full conversation (history + user + assistant with tool call).
|
|
100
|
-
checkpoint.messages.each { |msg| chat.messages << msg }
|
|
101
|
-
|
|
102
|
-
# Determine the tool result: execute it or inject a denial string.
|
|
103
|
-
tool_result =
|
|
104
|
-
if approved
|
|
105
|
-
tool_instance = chat.tools[checkpoint.pending_tool_name.to_sym]
|
|
106
|
-
tool_instance ? tool_instance.call(checkpoint.pending_tool_args) : "Tool not found."
|
|
107
|
-
else
|
|
108
|
-
"Tool execution denied."
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Inject the tool result so the LLM can continue.
|
|
112
|
-
chat.add_message(
|
|
113
|
-
role: :tool,
|
|
114
|
-
content: tool_result.to_s,
|
|
115
|
-
tool_call_id: checkpoint.pending_tool_call_id
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# Re-register the suspension hook so that any further requires_approval
|
|
119
|
-
# tools encountered during continuation are intercepted rather than
|
|
120
|
-
# executed without approval (cascading / chained approval scenario).
|
|
121
|
-
_register_suspension_hook!(chat)
|
|
122
|
-
|
|
123
|
-
# Continue the LLM loop. Rescue SuspendSignal so that a second
|
|
124
|
-
# approval-required tool produces a new checkpoint instead of running
|
|
125
|
-
# without consent.
|
|
126
|
-
begin
|
|
127
|
-
response = chat.complete
|
|
128
|
-
rescue SuspendSignal => signal
|
|
129
|
-
new_checkpoint = Checkpoint.new(
|
|
130
|
-
checkpoint_id: SecureRandom.uuid,
|
|
131
|
-
agent_class: self.class.name,
|
|
132
|
-
requested_at: Time.now.utc,
|
|
133
|
-
thread_id: checkpoint.thread_id,
|
|
134
|
-
original_input: checkpoint.original_input,
|
|
135
|
-
messages: chat.messages.dup,
|
|
136
|
-
pending_tool_name: signal.tool_name,
|
|
137
|
-
pending_tool_args: signal.args,
|
|
138
|
-
pending_tool_call_id: signal.tool_call_id
|
|
139
|
-
)
|
|
140
|
-
return {output: nil, suspended: true, checkpoint: new_checkpoint, messages: chat.messages}
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
output = response.content
|
|
144
|
-
usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
145
|
-
|
|
146
|
-
run_output_guardrails!(output)
|
|
147
|
-
|
|
148
|
-
{output: output, suspended: false, messages: chat.messages, usage: usage}
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
# Registers an on_tool_call hook on the chat object that raises SuspendSignal
|
|
154
|
-
# when an approval-required tool is about to be executed and no synchronous
|
|
155
|
-
# on_approval_required handler has been registered.
|
|
156
|
-
#
|
|
157
|
-
# Does nothing when:
|
|
158
|
-
# - a synchronous handler is already registered (@approval_handler is set), or
|
|
159
|
-
# - none of the agent's tools have requires_approval set.
|
|
160
|
-
#
|
|
161
|
-
# @param chat [RubyLLM::Chat]
|
|
162
|
-
# @api private
|
|
163
|
-
def _register_suspension_hook!(chat)
|
|
164
|
-
return if @approval_handler
|
|
165
|
-
return if self.class.tools.none? { |tc| tc.requires_approval }
|
|
166
|
-
|
|
167
|
-
chat.on_tool_call do |tool_call|
|
|
168
|
-
tool_instance = chat.tools[tool_call.name.to_sym]
|
|
169
|
-
if tool_instance&.requires_approval
|
|
170
|
-
raise SuspendSignal.new(
|
|
171
|
-
tool_name: tool_call.name,
|
|
172
|
-
args: tool_call.arguments,
|
|
173
|
-
tool_call_id: tool_call.id
|
|
174
|
-
)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Returns the checkpoint idempotency store for this instance, lazily
|
|
180
|
-
# initialising a default in-memory {Phronomy::Agent::CheckpointStore}.
|
|
181
|
-
#
|
|
182
|
-
# @return [#consumed?, #consume!]
|
|
183
|
-
# @api private
|
|
184
|
-
def _checkpoint_store
|
|
185
|
-
@checkpoint_store ||= CheckpointStore.new
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# Raised internally inside the on_tool_call hook when an approval-required
|
|
6
|
-
# tool is encountered and no synchronous on_approval_required handler has
|
|
7
|
-
# been registered. Caught by Agent::Base#invoke_once to produce a
|
|
8
|
-
# suspended result hash containing a Checkpoint.
|
|
9
|
-
#
|
|
10
|
-
# This class is intentionally NOT part of the public API. Callers should
|
|
11
|
-
# @api private
|
|
12
|
-
# inspect the +:suspended+ key in the result hash returned by #invoke.
|
|
13
|
-
#
|
|
14
|
-
# @api private
|
|
15
|
-
class SuspendSignal < StandardError
|
|
16
|
-
# @return [String] the name of the tool that triggered the suspension
|
|
17
|
-
attr_reader :tool_name
|
|
18
|
-
|
|
19
|
-
# @return [Hash] the arguments the LLM passed to the tool
|
|
20
|
-
attr_reader :args
|
|
21
|
-
|
|
22
|
-
# @return [String] the tool_call_id from the LLM response
|
|
23
|
-
attr_reader :tool_call_id
|
|
24
|
-
|
|
25
|
-
# @param tool_name [String]
|
|
26
|
-
# @param args [Hash]
|
|
27
|
-
# @param tool_call_id [String]
|
|
28
|
-
# @api private
|
|
29
|
-
def initialize(tool_name:, args:, tool_call_id:)
|
|
30
|
-
super("Agent suspended waiting for approval of tool: #{tool_name}")
|
|
31
|
-
@tool_name = tool_name
|
|
32
|
-
@args = args
|
|
33
|
-
@tool_call_id = tool_call_id
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
data/lib/phronomy/context.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
# Context management utilities: token estimation, budget calculation, and
|
|
5
|
-
# context assembly.
|
|
6
|
-
#
|
|
7
|
-
# Sub-modules are auto-loaded by Zeitwerk:
|
|
8
|
-
# Phronomy::LlmContextWindow::TokenEstimator
|
|
9
|
-
# Phronomy::LlmContextWindow::TokenBudget
|
|
10
|
-
module Context
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Abstract base class for all guardrails.
|
|
6
|
-
#
|
|
7
|
-
# Subclasses override #check to validate input or output.
|
|
8
|
-
# Call #fail! inside #check to reject with a reason.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# class NoPIIGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
12
|
-
# def check(input)
|
|
13
|
-
# fail!("PII detected") if input.to_s.match?(/\d{3}-\d{2}-\d{4}/)
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
class Base
|
|
17
|
-
# Validate the value. Subclasses must implement this method.
|
|
18
|
-
# @param value [Object] the input or output being checked
|
|
19
|
-
# @raise [Phronomy::GuardrailError] if the guardrail rejects the value
|
|
20
|
-
# @api public
|
|
21
|
-
def check(value)
|
|
22
|
-
raise NotImplementedError, "#{self.class}#check is not implemented"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Run the check, raising GuardrailError on failure.
|
|
26
|
-
# @param value [Object]
|
|
27
|
-
# @return [Object] the original value (unchanged) when the check passes
|
|
28
|
-
# @api public
|
|
29
|
-
def run!(value)
|
|
30
|
-
check(value)
|
|
31
|
-
value
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
protected
|
|
35
|
-
|
|
36
|
-
# Call inside #check to reject the value.
|
|
37
|
-
# @param reason [String] human-readable rejection reason
|
|
38
|
-
# @raise [Phronomy::GuardrailError]
|
|
39
|
-
# @api public
|
|
40
|
-
def fail!(reason)
|
|
41
|
-
raise Phronomy::GuardrailError.new(reason, guardrail: self)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Guardrail applied to agent/chain input before it reaches the LLM.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# class NoCreditCardGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
9
|
-
# def check(input)
|
|
10
|
-
# fail!("Credit card numbers are not allowed") if input.to_s.match?(/\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}/)
|
|
11
|
-
# end
|
|
12
|
-
# end
|
|
13
|
-
#
|
|
14
|
-
# agent = MyAgent.new
|
|
15
|
-
# agent.add_input_guardrail(NoCreditCardGuardrail.new)
|
|
16
|
-
class InputGuardrail < Base
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Guardrail
|
|
5
|
-
# Guardrail applied to agent/chain output before it is returned to the caller.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# class NoSecretsGuardrail < Phronomy::Guardrail::OutputGuardrail
|
|
9
|
-
# def check(output)
|
|
10
|
-
# fail!("Response contains a secret key") if output.to_s.match?(/sk-[A-Za-z0-9]{32,}/)
|
|
11
|
-
# end
|
|
12
|
-
# end
|
|
13
|
-
#
|
|
14
|
-
# agent = MyAgent.new
|
|
15
|
-
# agent.add_output_guardrail(NoSecretsGuardrail.new)
|
|
16
|
-
class OutputGuardrail < Base
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/lib/phronomy/guardrail.rb
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Convenience require for Guardrail sub-classes.
|
|
4
|
-
# Zeitwerk auto-loads individual files; this is only needed for explicit requires.
|
|
5
|
-
require_relative "guardrail/base"
|
|
6
|
-
require_relative "guardrail/input_guardrail"
|
|
7
|
-
require_relative "guardrail/output_guardrail"
|
data/lib/phronomy/tool.rb
DELETED