pikuri-core 0.0.7 → 0.1.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/README.md +1 -1
- data/lib/pikuri/agent/chat_transport.rb +73 -93
- data/lib/pikuri/agent/configurator.rb +46 -106
- data/lib/pikuri/agent/context_window_detector.rb +44 -85
- data/lib/pikuri/agent/control/cancellable.rb +87 -66
- data/lib/pikuri/agent/control/interloper.rb +127 -105
- data/lib/pikuri/agent/control/step_limit.rb +25 -41
- data/lib/pikuri/agent/control.rb +14 -34
- data/lib/pikuri/agent/event.rb +123 -188
- data/lib/pikuri/agent/extension.rb +118 -94
- data/lib/pikuri/agent/extension_context.rb +50 -77
- data/lib/pikuri/agent/history.rb +653 -0
- data/lib/pikuri/agent/listener/rate_limited.rb +40 -66
- data/lib/pikuri/agent/listener/terminal.rb +143 -117
- data/lib/pikuri/agent/listener/token_log.rb +101 -140
- data/lib/pikuri/agent/listener.rb +23 -43
- data/lib/pikuri/agent/listener_list.rb +26 -47
- data/lib/pikuri/agent/synthesizer.rb +45 -87
- data/lib/pikuri/agent.rb +816 -474
- data/lib/pikuri/bundler_env.rb +68 -0
- data/lib/pikuri/extractor/html.rb +63 -110
- data/lib/pikuri/extractor/passthrough.rb +20 -30
- data/lib/pikuri/extractor.rb +93 -154
- data/lib/pikuri/file_type.rb +63 -135
- data/lib/pikuri/finalizers.rb +32 -47
- data/lib/pikuri/paths.rb +104 -13
- data/lib/pikuri/ruby_llm_patches.rb +106 -0
- data/lib/pikuri/sanitizer.rb +45 -67
- data/lib/pikuri/subprocess.rb +75 -119
- data/lib/pikuri/testing.rb +296 -0
- data/lib/pikuri/tool/calculator.rb +56 -66
- data/lib/pikuri/tool/execute_context.rb +42 -0
- data/lib/pikuri/tool/fetch.rb +51 -77
- data/lib/pikuri/tool/parameters.rb +21 -29
- data/lib/pikuri/tool/scraper.rb +55 -97
- data/lib/pikuri/tool/search/brave.rb +52 -80
- data/lib/pikuri/tool/search/duckduckgo.rb +59 -91
- data/lib/pikuri/tool/search/engines.rb +230 -97
- data/lib/pikuri/tool/search/exa.rb +56 -90
- data/lib/pikuri/tool/search/rate_limiter.rb +61 -38
- data/lib/pikuri/tool/search/result.rb +10 -15
- data/lib/pikuri/tool/trifecta_legs.rb +217 -0
- data/lib/pikuri/tool/web_scrape.rb +38 -54
- data/lib/pikuri/tool/web_search.rb +100 -24
- data/lib/pikuri/tool.rb +140 -65
- data/lib/pikuri/trifecta/contribution.rb +43 -0
- data/lib/pikuri/trifecta/node.rb +47 -0
- data/lib/pikuri/trifecta/report.rb +230 -0
- data/lib/pikuri/trifecta.rb +127 -0
- data/lib/pikuri/url_cache.rb +33 -49
- data/lib/pikuri/version.rb +1 -1
- data/lib/pikuri-core.rb +72 -88
- data/prompts/agent-loop.txt +5 -0
- data/prompts/pikuri-chat.txt +3 -12
- metadata +14 -3
|
@@ -3,161 +3,183 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
class Agent
|
|
5
5
|
module Control
|
|
6
|
-
# Mid-loop
|
|
7
|
-
# constructs an +Interloper+,
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# ruby_llm's loop where the conversation state is
|
|
13
|
-
# consistent — and emits each item into the chat history
|
|
14
|
-
# plus the listener stream. The agent's next round-trip
|
|
15
|
-
# then sees the injected user message and reacts to it on
|
|
16
|
-
# its own.
|
|
6
|
+
# Mid-loop host→agent injection queue. A host (TUI, web client)
|
|
7
|
+
# constructs an +Interloper+, passes it to {Agent#initialize} via
|
|
8
|
+
# +interloper:+, and enqueues from any thread while the agent runs. The
|
|
9
|
+
# +Agent+ drains it once the running tool batch is answered — the only
|
|
10
|
+
# point in ruby_llm's loop where conversation state is consistent — and
|
|
11
|
+
# the next round-trip reacts to what was injected.
|
|
17
12
|
#
|
|
18
|
-
#
|
|
19
|
-
# the bundled +bin/pikuri-*+ entry-point scripts do *not*
|
|
20
|
-
# wire one up, since they keep stdin synchronous and have
|
|
21
|
-
# no way for a user to type while a turn is in flight.
|
|
22
|
-
# Downstream hosts that *do* run the agent on a worker
|
|
23
|
-
# thread can wire one in with no other changes to pikuri.
|
|
13
|
+
# == Two kinds
|
|
24
14
|
#
|
|
25
|
-
#
|
|
15
|
+
# Each item carries a {Item#kind}, driving how {Agent#drain_interloper}
|
|
16
|
+
# lands it:
|
|
26
17
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
18
|
+
# - +:user+ ({#inject_user_message}) — a mid-loop user turn: appended
|
|
19
|
+
# verbatim, emits +Event::UserTurn(mid_loop: true)+, and runs the
|
|
20
|
+
# {Extension#on_user_message} dispatch (prefetch/record like an initial
|
|
21
|
+
# turn).
|
|
22
|
+
# - +:system+ ({#inject_system_message}) — host-supplied reference text
|
|
23
|
+
# (a loaded skill, retrieved context): appended wrapped in
|
|
24
|
+
# +<system-reminder>+, emits +Event::SystemInjected+, and runs *no*
|
|
25
|
+
# +on_user_message+ dispatch and no +UserTurn+ (it's not a user turn).
|
|
30
26
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# through the listener stream so other listeners
|
|
35
|
-
# (Terminal renderer, in-memory recorder, future logging)
|
|
36
|
-
# see the injection as a normal +UserTurn+ event with the
|
|
37
|
-
# +mid_loop:+ flag set.
|
|
27
|
+
# Both reach the wire as +role: :user+ — the kinds differ in dispatch and
|
|
28
|
+
# envelope, not in role. +:system+ names the *role the text plays*, and
|
|
29
|
+
# {Agent#append_reference_block} says why the wire role can't match it.
|
|
38
30
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# injection — so the "cancel-then-inject" hazard and the
|
|
43
|
-
# "refresh-budget-by-injecting" hazard cannot arise.
|
|
31
|
+
# Groundwork for downstream TUI/web hosts: the bundled +bin/pikuri-*+ do
|
|
32
|
+
# not wire one (stdin is synchronous). A host running the agent on a
|
|
33
|
+
# worker thread wires one in with no other changes.
|
|
44
34
|
#
|
|
45
|
-
# ==
|
|
35
|
+
# == Delivery boundary
|
|
46
36
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
37
|
+
# Delivery is at the end of a tool batch, *not* the LLM HTTP call: an
|
|
38
|
+
# injection made mid-response takes effect on the *next* round-trip, once
|
|
39
|
+
# the model has committed to the current response's tool calls. Never
|
|
40
|
+
# *inside* a batch — an assistant message carrying tool_calls and the
|
|
41
|
+
# +:tool+ messages answering it are one unit on the wire, and a message
|
|
42
|
+
# appended between them is rejected or mangled (see
|
|
43
|
+
# {Agent#tool_batch_complete?}). Same
|
|
44
|
+
# "gentle" semantic {Control::Cancellable} promises — the cleanest
|
|
45
|
+
# cross-provider point (no in-flight subprocess, no half-built response).
|
|
46
|
+
# Controls are reset only at turn start, never on a mid-loop injection,
|
|
47
|
+
# so "cancel-then-inject" and "refresh-budget-by-injecting" can't arise.
|
|
58
48
|
#
|
|
59
|
-
# ==
|
|
49
|
+
# == Change notification
|
|
60
50
|
#
|
|
61
|
-
#
|
|
62
|
-
# {#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
51
|
+
# A host rendering the pending count reactively (a "3 waiting" badge)
|
|
52
|
+
# sets {#on_change} instead of polling {#size}. It fires whenever the
|
|
53
|
+
# count changes — once per {#inject_user_message}, once per {#drain!}
|
|
54
|
+
# that empties a non-empty queue (an empty-queue drain, the hot path,
|
|
55
|
+
# stays silent) — with the new count, outside the lock. A host-side hook,
|
|
56
|
+
# not an {Agent} listener event: the count changes on the injecting
|
|
57
|
+
# thread, off the agent-thread-confined stream. Mirrors
|
|
58
|
+
# {Pikuri::Tasks::List#on_change} but pushes the count (the interloper
|
|
59
|
+
# exposes no race-free snapshot to re-read).
|
|
70
60
|
#
|
|
71
61
|
# == Sub-agent semantics
|
|
72
62
|
#
|
|
73
|
-
# Sub-agents are private to the parent
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
63
|
+
# Sub-agents are private to the parent; the host has no handle to them,
|
|
64
|
+
# so the +agent+ tool omits the kwarg, leaving a child's +interloper+
|
|
65
|
+
# +nil+. Contrast {Control::Cancellable}, shared by reference so the
|
|
66
|
+
# parent's stop propagates to the whole tree — cancellation is global,
|
|
67
|
+
# injection is directed at the main agent.
|
|
68
|
+
#
|
|
69
|
+
# Thread-safe: {#inject_user_message}, {#inject_system_message}, {#peek},
|
|
70
|
+
# {#pending?}, {#size}, and {#drain!} are a +Mutex+-guarded +Array+
|
|
71
|
+
# (chosen over +Thread::Queue+, which exposes no snapshot for {#peek}).
|
|
72
|
+
# {#on_change} fires outside the lock so a re-entrant or blocking callback
|
|
73
|
+
# can't deadlock.
|
|
83
74
|
class Interloper
|
|
75
|
+
# A queued injection: +kind+ (+:user+ or +:system+) selects the drain
|
|
76
|
+
# path; +content+ is the message text. Producers are the two
|
|
77
|
+
# +inject_*+ methods; the consumer is {Agent#drain_interloper}.
|
|
78
|
+
Item = Data.define(:kind, :content)
|
|
79
|
+
|
|
84
80
|
def initialize
|
|
85
81
|
@mutex = Mutex.new
|
|
86
82
|
@items = []
|
|
83
|
+
@on_change = nil
|
|
87
84
|
end
|
|
88
85
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
86
|
+
# Optional host callback invoked with the new pending count whenever it
|
|
87
|
+
# changes (after {#inject_user_message}, and after a {#drain!} that
|
|
88
|
+
# empties a non-empty queue). +nil+ (default) disables it. Runs
|
|
89
|
+
# *outside* the queue lock, on the mutating thread, so a callback
|
|
90
|
+
# feeding another thread should hand off only immutable data.
|
|
91
|
+
#
|
|
92
|
+
# @return [Proc, nil]
|
|
93
|
+
attr_accessor :on_change
|
|
94
|
+
|
|
95
|
+
# Enqueue a +:user+ mid-loop turn (any thread; +Mutex+-guarded). Fires
|
|
96
|
+
# {#on_change} with the new count, outside the lock.
|
|
91
97
|
#
|
|
92
98
|
# @param content [String] non-blank user-supplied text
|
|
93
|
-
# @raise [ArgumentError] if +content+ is +nil
|
|
94
|
-
#
|
|
95
|
-
# +user_message:+ argument, since an empty injection
|
|
96
|
-
# would poison the chat history just as a blank turn
|
|
97
|
-
# would
|
|
99
|
+
# @raise [ArgumentError] if +content+ is +nil+/blank — a blank
|
|
100
|
+
# injection would poison the chat history like a blank turn
|
|
98
101
|
# @return [void]
|
|
99
102
|
def inject_user_message(content)
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
enqueue(:user, content)
|
|
104
|
+
end
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
# Enqueue a +:system+ reference block — host-loaded skill text or
|
|
107
|
+
# other retrieved context injected on the host's own initiative, not a
|
|
108
|
+
# user turn (any thread; +Mutex+-guarded). Fires {#on_change}.
|
|
109
|
+
#
|
|
110
|
+
# @param content [String] non-blank reference text (e.g. a rendered
|
|
111
|
+
# skill block); an own semantic tag is kept, the +<system-reminder>+
|
|
112
|
+
# envelope nests around it
|
|
113
|
+
# @raise [ArgumentError] if +content+ is +nil+/blank
|
|
114
|
+
# @return [void]
|
|
115
|
+
def inject_system_message(content)
|
|
116
|
+
enqueue(:system, content)
|
|
105
117
|
end
|
|
106
118
|
|
|
107
|
-
# Non-destructive snapshot of the queue
|
|
108
|
-
#
|
|
109
|
-
# "ongoing / pending" UI affordance ("3 messages waiting
|
|
110
|
-
# to deliver") in parallel with the agent's progress
|
|
111
|
-
# stream. Safe to call from any thread.
|
|
119
|
+
# Non-destructive snapshot of the queue in delivery order, for a host
|
|
120
|
+
# rendering a "pending" affordance. Safe from any thread.
|
|
112
121
|
#
|
|
113
|
-
# @return [Array<
|
|
114
|
-
#
|
|
122
|
+
# @return [Array<Item>] copy of the pending items; never shares state
|
|
123
|
+
# with the internal buffer
|
|
115
124
|
def peek
|
|
116
125
|
@mutex.synchronize { @items.dup }
|
|
117
126
|
end
|
|
118
127
|
|
|
119
|
-
# @return [Boolean] whether the queue
|
|
120
|
-
#
|
|
121
|
-
# thread
|
|
128
|
+
# @return [Boolean] whether the queue holds a pending injection;
|
|
129
|
+
# observable from any thread
|
|
122
130
|
def pending?
|
|
123
131
|
@mutex.synchronize { !@items.empty? }
|
|
124
132
|
end
|
|
125
133
|
|
|
126
|
-
# @return [Integer] number of pending injections;
|
|
127
|
-
#
|
|
128
|
-
# any thread — by the time the caller reads it the
|
|
129
|
-
# queue may already have drained
|
|
134
|
+
# @return [Integer] number of pending injections; a snapshot from any
|
|
135
|
+
# thread (may have drained by the time the caller reads it)
|
|
130
136
|
def size
|
|
131
137
|
@mutex.synchronize { @items.size }
|
|
132
138
|
end
|
|
133
139
|
|
|
134
|
-
# Atomically take and remove all pending items. Called by
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
140
|
+
# Atomically take and remove all pending items. Called by {Agent}'s
|
|
141
|
+
# +after_message+ wiring, once a tool batch is complete. Returns +[]+ when empty (the hot path) —
|
|
142
|
+
# an empty drain does *not* fire {#on_change}; a drain that empties a
|
|
143
|
+
# non-empty queue fires it with +0+, outside the lock.
|
|
138
144
|
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# @return [Array<String>] items in delivery order; empty
|
|
143
|
-
# when the queue is empty
|
|
145
|
+
# @return [Array<Item>] items in delivery order; empty when the queue
|
|
146
|
+
# is empty
|
|
144
147
|
def drain!
|
|
145
|
-
@mutex.synchronize do
|
|
148
|
+
items = @mutex.synchronize do
|
|
146
149
|
next [] if @items.empty?
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
taken = @items.dup
|
|
149
152
|
@items.clear
|
|
150
|
-
|
|
153
|
+
taken
|
|
151
154
|
end
|
|
155
|
+
@on_change&.call(0) unless items.empty?
|
|
156
|
+
items
|
|
152
157
|
end
|
|
153
158
|
|
|
154
|
-
# @return [String] short label for {Agent#to_s}
|
|
155
|
-
#
|
|
156
|
-
# an idle interloper apart from one with queued items
|
|
159
|
+
# @return [String] short label for {Agent#to_s}, reflecting the
|
|
160
|
+
# pending count.
|
|
157
161
|
def to_s
|
|
158
162
|
pending = size
|
|
159
163
|
pending.zero? ? 'Interloper' : "Interloper(#{pending} pending)"
|
|
160
164
|
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Shared enqueue for both +inject_*+ methods: validate, push a typed
|
|
169
|
+
# {Item}, fire {#on_change} outside the lock.
|
|
170
|
+
#
|
|
171
|
+
# @param kind [Symbol] +:user+ or +:system+
|
|
172
|
+
# @param content [String]
|
|
173
|
+
# @raise [ArgumentError] if +content+ is +nil+/blank
|
|
174
|
+
# @return [void]
|
|
175
|
+
def enqueue(kind, content)
|
|
176
|
+
raise ArgumentError, "content must not be blank, got #{content.inspect}" \
|
|
177
|
+
if content.nil? || content.to_s.strip.empty?
|
|
178
|
+
|
|
179
|
+
count = @mutex.synchronize { @items << Item.new(kind: kind, content: content); @items.size }
|
|
180
|
+
@on_change&.call(count)
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
161
183
|
end
|
|
162
184
|
end
|
|
163
185
|
end
|
|
@@ -3,35 +3,27 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
class Agent
|
|
5
5
|
module Control
|
|
6
|
-
# Caps the number of tool calls per {Agent#run_loop}
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# {Exceeded} and the +Agent+ applies the {#on_exhausted}
|
|
12
|
-
# policy: re-raise to the host (the default), or run the
|
|
13
|
-
# step-exhaustion synthesizer to salvage a partial answer.
|
|
6
|
+
# Caps the number of tool calls per {Agent#run_loop}. ruby_llm has no
|
|
7
|
+
# step budget; the +Agent+ pokes {#tick!} on every +before_tool_call+ and
|
|
8
|
+
# {#reset!} at turn start. Once the count exceeds the cap, {#tick!} raises
|
|
9
|
+
# {Exceeded} and the +Agent+ applies {#on_exhausted}: re-raise to the host
|
|
10
|
+
# (default), or run the synthesizer to salvage a partial answer.
|
|
14
11
|
#
|
|
15
12
|
# == Why the policy lives here, not on +Agent+
|
|
16
13
|
#
|
|
17
|
-
# Synthesis can only
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# the
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# gathered so far); a coding agent wants the default
|
|
26
|
-
# +:raise+ (a tools-free pass can't finish writing code —
|
|
27
|
-
# stop, let the user say "continue"; {#reset!} at the next
|
|
28
|
-
# turn boundary refreshes the budget).
|
|
14
|
+
# Synthesis can only fire off a tripped step limit, so an
|
|
15
|
+
# +Agent.new(synthesize: ...)+ kwarg would be meaningless when
|
|
16
|
+
# +step_limit:+ is +nil+ — an invalid combination. Attaching the policy to
|
|
17
|
+
# the budget makes "what happens when it runs out" travel with the budget,
|
|
18
|
+
# and that nonsense state unrepresentable. Hosts pick per wiring: a Q&A
|
|
19
|
+
# REPL wants +:synthesize+ (salvage from evidence so far); a coding agent
|
|
20
|
+
# wants +:raise+ (a tools-free pass can't finish code — stop, let the user
|
|
21
|
+
# say "continue"; {#reset!} refreshes the budget next turn).
|
|
29
22
|
class StepLimit
|
|
30
23
|
# Valid {#on_exhausted} policies.
|
|
31
24
|
ON_EXHAUSTED = %i[raise synthesize].freeze
|
|
32
|
-
# Raised by {#tick!} once tool-call count exceeds +max
|
|
33
|
-
#
|
|
34
|
-
# can include it in user-facing messages.
|
|
25
|
+
# Raised by {#tick!} once the tool-call count exceeds +max+; carries the
|
|
26
|
+
# tripped budget for user-facing messages.
|
|
35
27
|
class Exceeded < StandardError
|
|
36
28
|
# @return [Integer]
|
|
37
29
|
attr_reader :max_steps
|
|
@@ -68,39 +60,31 @@ module Pikuri
|
|
|
68
60
|
@step = 0
|
|
69
61
|
end
|
|
70
62
|
|
|
71
|
-
# Increment the
|
|
72
|
-
#
|
|
73
|
-
# +before_tool_call+ wiring.
|
|
63
|
+
# Increment the counter; raise {Exceeded} once it crosses {#max}. Called
|
|
64
|
+
# from {Agent}'s +before_tool_call+ wiring.
|
|
74
65
|
#
|
|
75
66
|
# @return [void]
|
|
76
|
-
# @raise [Exceeded] when the counter
|
|
77
|
-
# {#max}
|
|
67
|
+
# @raise [Exceeded] when the counter now exceeds {#max}
|
|
78
68
|
def tick!
|
|
79
69
|
@step += 1
|
|
80
70
|
raise Exceeded, @max if @step > @max
|
|
81
71
|
end
|
|
82
72
|
|
|
83
|
-
# Reset the counter
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# {Control::Interloper} injections deliberately do *not*
|
|
88
|
-
# trigger a reset — those are additional context for the
|
|
89
|
-
# same turn, not a fresh one, and a chatty user could
|
|
90
|
-
# otherwise refresh the budget forever by injecting.
|
|
73
|
+
# Reset the counter to zero. Called by {Agent} at each turn start so one
|
|
74
|
+
# instance governs many turns. Mid-loop {Control::Interloper} injections
|
|
75
|
+
# deliberately do *not* reset — they're context for the same turn, and a
|
|
76
|
+
# chatty user could otherwise refresh the budget forever by injecting.
|
|
91
77
|
#
|
|
92
78
|
# @return [void]
|
|
93
79
|
def reset!
|
|
94
80
|
@step = 0
|
|
95
81
|
end
|
|
96
82
|
|
|
97
|
-
# @return [Integer] current step count
|
|
98
|
-
# can introspect it (and so tests can assert it)
|
|
83
|
+
# @return [Integer] current step count.
|
|
99
84
|
attr_reader :step
|
|
100
85
|
|
|
101
|
-
# @return [String] short config dump for {Agent#to_s}
|
|
102
|
-
#
|
|
103
|
-
# +:synthesize+, so existing banner output is unchanged.
|
|
86
|
+
# @return [String] short config dump for {Agent#to_s}; the policy
|
|
87
|
+
# renders only when the non-default +:synthesize+.
|
|
104
88
|
def to_s
|
|
105
89
|
policy = @on_exhausted == :raise ? '' : ", on_exhausted=#{@on_exhausted}"
|
|
106
90
|
"StepLimit(max=#{@max}#{policy})"
|
data/lib/pikuri/agent/control.rb
CHANGED
|
@@ -2,43 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
class Agent
|
|
5
|
-
# Namespace for the +Agent+'s host-facing controls:
|
|
6
|
-
# {
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# no events and never appear in a {ListenerList}.
|
|
5
|
+
# Namespace for the +Agent+'s host-facing controls: {StepLimit},
|
|
6
|
+
# {Cancellable}, and {Interloper}. Each is a small value-holder the +Agent+
|
|
7
|
+
# reads from (or pokes into the event stream from) at fixed points in
|
|
8
|
+
# ruby_llm's callback cycle. They are *not* listeners — they receive no
|
|
9
|
+
# events and never appear in a {ListenerList}.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# The +Agent+ is the only entity that emits events, the only
|
|
17
|
-
# entity that ticks the step counter, the only entity that
|
|
18
|
-
# checks the cancellation flag, and the only entity that drains
|
|
19
|
-
# the interloper queue. "What fires when" is a single grep for
|
|
11
|
+
# The split: listeners are pure consumers of the event stream; controls are
|
|
12
|
+
# host-facing signal holders the +Agent+ reads. The +Agent+ is the only
|
|
13
|
+
# entity that emits events, ticks the step counter, checks the cancel flag,
|
|
14
|
+
# and drains the interloper queue — "what fires when" is one grep for
|
|
20
15
|
# +@listeners.emit+ in +agent.rb+.
|
|
21
16
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# * {
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# when over budget) and {StepLimit#reset!} at the start of
|
|
28
|
-
# each turn. Sub-agents get their own counter.
|
|
29
|
-
# * {Cancellable} — cooperative cancellation flag. The host
|
|
30
|
-
# calls {Cancellable#cancel!} from any thread; the +Agent+
|
|
31
|
-
# calls {Cancellable#check!} at +before_tool_call+ (raising
|
|
32
|
-
# {Cancellable::Cancelled} when the flag is set) and
|
|
33
|
-
# {Cancellable#reset!} at the start of each turn. The same
|
|
34
|
-
# instance is shared by reference across the parent, every
|
|
35
|
-
# sub-agent, and the synthesizer rescue.
|
|
36
|
-
# * {Interloper} — mid-loop user-input queue. The host calls
|
|
37
|
-
# {Interloper#inject_user_message} from any thread; the
|
|
38
|
-
# +Agent+ drains the queue at +after_tool_result+,
|
|
39
|
-
# appending each item as a user-role message and emitting
|
|
40
|
-
# {Event::UserTurn} with +mid_loop: true+. Not propagated to
|
|
41
|
-
# sub-agents (the host has no handle to them).
|
|
17
|
+
# * {StepLimit} — caps tool calls per {Agent#run_loop}; sub-agents get their
|
|
18
|
+
# own counter.
|
|
19
|
+
# * {Cancellable} — cooperative cancel flag, shared by reference across the
|
|
20
|
+
# parent, sub-agents, and the synthesizer.
|
|
21
|
+
# * {Interloper} — mid-loop user-input queue; not propagated to sub-agents.
|
|
42
22
|
module Control
|
|
43
23
|
end
|
|
44
24
|
end
|