pikuri-core 0.0.3 → 0.0.4
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 +10 -0
- data/lib/pikuri/agent/chat_transport.rb +6 -5
- data/lib/pikuri/agent/configurator.rb +50 -75
- data/lib/pikuri/agent/control/cancellable.rb +7 -17
- data/lib/pikuri/agent/control/interloper.rb +10 -21
- data/lib/pikuri/agent/control/step_limit.rb +0 -14
- data/lib/pikuri/agent/extension.rb +12 -14
- data/lib/pikuri/agent/listener/token_log.rb +20 -21
- data/lib/pikuri/agent/listener_list.rb +7 -5
- data/lib/pikuri/agent/synthesizer.rb +2 -2
- data/lib/pikuri/agent.rb +88 -96
- data/lib/pikuri/file_type.rb +237 -0
- data/lib/pikuri/subprocess.rb +9 -2
- data/lib/pikuri/tool/parameters.rb +64 -3
- data/lib/pikuri/tool.rb +15 -7
- data/lib/pikuri/version.rb +1 -1
- metadata +3 -3
- data/lib/pikuri/tool/sub_agent.rb +0 -150
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f26e9b56204d1fbbaf64390e643a26c4183b3c21d45e3dcc43984c677a09f400
|
|
4
|
+
data.tar.gz: e657171d9440ef19a53ae5ad9335c62ff34ea8db62c9753ce79352f699122f06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f53aef4566f6218750c58b0cac4d1015d9ae2d2a1d464481ff4eabae3d42eb560559517a105ccd6fb2128e0b8e1b9393fdea257757dd6c85af0dc7a47683ed3
|
|
7
|
+
data.tar.gz: ba4adbf32911a499111eaf26057622e94fa27511aaefb5f9fb705ac3471a20d860536d1717e6933ec3e2b55af152fbfb8b479f99e11e44ea9ad907dd1c666a66
|
data/README.md
CHANGED
|
@@ -65,3 +65,13 @@ agent.run_loop(user_message: 'What is 17 * 23?')
|
|
|
65
65
|
|
|
66
66
|
See `bin/pikuri-chat` for a worked example with REPL, signal
|
|
67
67
|
handling, and cancellation.
|
|
68
|
+
|
|
69
|
+
## Further reading
|
|
70
|
+
|
|
71
|
+
- **Narrative walkthrough:** [chapter 1 of the pikuri guide](../docs/guide/01-chat.md)
|
|
72
|
+
— install llama.cpp, start the server, run `pikuri-chat`, the
|
|
73
|
+
agentic loop, the four bundled tools, and search-provider
|
|
74
|
+
privacy postures.
|
|
75
|
+
- **API reference:** browse the YARD docs at
|
|
76
|
+
<https://rubydoc.info/gems/pikuri-core> (once published), or run
|
|
77
|
+
`bundle exec yard` in this directory for a local copy.
|
|
@@ -8,11 +8,12 @@ module Pikuri
|
|
|
8
8
|
#
|
|
9
9
|
# Bundling them is structural protection against a recurring bug
|
|
10
10
|
# class — every forwarding site (the synthesizer rescue in
|
|
11
|
-
# {Agent#run_loop},
|
|
12
|
-
# pass the three individually, and
|
|
13
|
-
# chat to a different server or
|
|
14
|
-
# on the unknown model id.
|
|
15
|
-
# can't silently miss a
|
|
11
|
+
# {Agent#run_loop}, the +agent+ tool from +pikuri-subagents+
|
|
12
|
+
# spawning a sub-agent) used to pass the three individually, and
|
|
13
|
+
# dropping one routed the spawned chat to a different server or
|
|
14
|
+
# raised +RubyLLM::ModelNotFoundError+ on the unknown model id.
|
|
15
|
+
# With a single value object the call site can't silently miss a
|
|
16
|
+
# field.
|
|
16
17
|
#
|
|
17
18
|
# Pure data carrier: no +RubyLLM+ references here, so the seam stays
|
|
18
19
|
# in {Agent}, +bin/pikuri-chat+, and {Tool}.
|
|
@@ -5,8 +5,9 @@ module Pikuri
|
|
|
5
5
|
# Build-time collector yielded into the +Pikuri::Agent.new+ block.
|
|
6
6
|
# Hosts and {Extension} implementations call its methods to declare
|
|
7
7
|
# additional tools, listeners, system-prompt snippets, +on_close+
|
|
8
|
-
# handlers,
|
|
9
|
-
# collected state into the agent's
|
|
8
|
+
# handlers, extension instances, and persona-flavored sub-agents;
|
|
9
|
+
# {Agent#initialize} drains the collected state into the agent's
|
|
10
|
+
# final wiring before returning.
|
|
10
11
|
#
|
|
11
12
|
# == Why this exists
|
|
12
13
|
#
|
|
@@ -15,10 +16,27 @@ module Pikuri
|
|
|
15
16
|
#
|
|
16
17
|
# Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
|
|
17
18
|
# c.add_listener Pikuri::Agent::Listener::Terminal.new
|
|
18
|
-
# c.add_tool Pikuri::Tool::
|
|
19
|
+
# c.add_tool Pikuri::Tool::WEB_SEARCH
|
|
20
|
+
# c.add_tool Pikuri::Tool::WEB_SCRAPE
|
|
21
|
+
# c.add_tool Pikuri::Tool::FETCH
|
|
19
22
|
# c.add_extension Pikuri::Skill::Extension.new(catalog: catalog)
|
|
20
23
|
# end
|
|
21
24
|
#
|
|
25
|
+
# == Two tool pools: regular vs. sub-agent-only
|
|
26
|
+
#
|
|
27
|
+
# {#add_tool} registers a tool the parent agent can call: it lands
|
|
28
|
+
# in {#tools} and gets handed to ruby_llm via +chat.with_tool+.
|
|
29
|
+
# {#add_sub_agent_tool} registers a tool the parent *cannot* call
|
|
30
|
+
# — it lands in {#sub_agent_tools} and is never sent to ruby_llm
|
|
31
|
+
# for the parent, but is visible to {Pikuri::SubAgent::Extension}'s
|
|
32
|
+
# persona-tool-name resolution. The use case is the lethal-trifecta
|
|
33
|
+
# defense in {Pikuri::Code::Bash::Sandbox} terms: keep network tools
|
|
34
|
+
# (+web_search+ / +web_scrape+ / +fetch+) off the parent so a prompt-
|
|
35
|
+
# injected file read cannot egress through the parent's own tools,
|
|
36
|
+
# while still letting the +researcher+ persona reach them via the
|
|
37
|
+
# +agent+ delegation tool. See SECURITY.md §"Defense: capability
|
|
38
|
+
# boundaries via sub-agents".
|
|
39
|
+
#
|
|
22
40
|
# Extensions implement their +configure(c)+ hook against the same
|
|
23
41
|
# type, so the call sites for "block users add stuff" and
|
|
24
42
|
# "extensions add stuff" share one API.
|
|
@@ -44,9 +62,9 @@ module Pikuri
|
|
|
44
62
|
# available for diagnostics.
|
|
45
63
|
attr_reader :system_prompt_base
|
|
46
64
|
|
|
47
|
-
# @return [String] this agent's identifier; empty for the
|
|
48
|
-
# agent,
|
|
49
|
-
attr_reader :
|
|
65
|
+
# @return [String] this agent's unique identifier; empty for the
|
|
66
|
+
# main agent, persona-rooted (e.g. +"researcher 0"+) for sub-agents.
|
|
67
|
+
attr_reader :id
|
|
50
68
|
|
|
51
69
|
# @return [Boolean] +true+ when the agent opted into chunk-level
|
|
52
70
|
# streaming.
|
|
@@ -68,9 +86,18 @@ module Pikuri
|
|
|
68
86
|
attr_reader :interloper
|
|
69
87
|
|
|
70
88
|
# @return [Array<Tool>] tools added via {#add_tool}, in
|
|
71
|
-
# declaration order. Drained by {Agent#initialize}
|
|
89
|
+
# declaration order. Drained by {Agent#initialize} and
|
|
90
|
+
# registered with ruby_llm so the parent LLM can call them.
|
|
72
91
|
attr_reader :tools
|
|
73
92
|
|
|
93
|
+
# @return [Array<Tool>] tools added via {#add_sub_agent_tool},
|
|
94
|
+
# in declaration order. Drained by {Agent#initialize} but
|
|
95
|
+
# *not* registered with ruby_llm — invisible to the parent
|
|
96
|
+
# LLM, available only to sub-agents through
|
|
97
|
+
# {Pikuri::SubAgent::Extension}'s persona-tool-name
|
|
98
|
+
# resolution. See the class header.
|
|
99
|
+
attr_reader :sub_agent_tools
|
|
100
|
+
|
|
74
101
|
# @return [Array<Listener::Base>] listeners added via
|
|
75
102
|
# {#add_listener}, in declaration order. Drained by
|
|
76
103
|
# {Agent#initialize}.
|
|
@@ -93,38 +120,25 @@ module Pikuri
|
|
|
93
120
|
# wiring is complete.
|
|
94
121
|
attr_reader :extensions
|
|
95
122
|
|
|
96
|
-
# @return [SubAgentRequest, nil] set when the block called
|
|
97
|
-
# {#allow_sub_agent}; +nil+ otherwise. The Agent ctor uses
|
|
98
|
-
# this to decide whether to create a {Tool::SubAgent}
|
|
99
|
-
# instance after the chat is wired (so the SubAgent tool's
|
|
100
|
-
# parent.tools snapshot doesn't include itself —
|
|
101
|
-
# recursion guard).
|
|
102
|
-
attr_reader :sub_agent_request
|
|
103
|
-
|
|
104
|
-
# Record holding the +max_steps+ value the host passed to
|
|
105
|
-
# {#allow_sub_agent}. The Agent ctor consumes one of these
|
|
106
|
-
# records to build a {Tool::SubAgent} keyed to that step
|
|
107
|
-
# budget.
|
|
108
|
-
SubAgentRequest = Data.define(:max_steps)
|
|
109
|
-
|
|
110
123
|
# @param transport [Agent::ChatTransport]
|
|
111
124
|
# @param system_prompt_base [String]
|
|
112
|
-
# @param
|
|
125
|
+
# @param id [String]
|
|
113
126
|
# @param streaming [Boolean]
|
|
114
127
|
# @param step_limit [Control::StepLimit, nil]
|
|
115
128
|
# @param cancellable [Control::Cancellable, nil]
|
|
116
129
|
# @param interloper [Control::Interloper, nil]
|
|
117
|
-
def initialize(transport:, system_prompt_base:,
|
|
130
|
+
def initialize(transport:, system_prompt_base:, id:, streaming:,
|
|
118
131
|
step_limit:, cancellable:, interloper:)
|
|
119
132
|
@transport = transport
|
|
120
133
|
@system_prompt_base = system_prompt_base
|
|
121
|
-
@
|
|
134
|
+
@id = id
|
|
122
135
|
@streaming = streaming
|
|
123
136
|
@step_limit = step_limit
|
|
124
137
|
@cancellable = cancellable
|
|
125
138
|
@interloper = interloper
|
|
126
139
|
|
|
127
140
|
@tools = []
|
|
141
|
+
@sub_agent_tools = []
|
|
128
142
|
@listeners = []
|
|
129
143
|
@system_prompt_additions = []
|
|
130
144
|
@on_close_handlers = []
|
|
@@ -142,8 +156,6 @@ module Pikuri
|
|
|
142
156
|
|
|
143
157
|
# Append several tools at once. Equivalent to calling {#add_tool}
|
|
144
158
|
# for each element of +tools+; declaration order is preserved.
|
|
145
|
-
# Sole intended caller today is {Tool::SubAgent}, which seeds a
|
|
146
|
-
# sub-agent's Configurator from the parent's tool snapshot.
|
|
147
159
|
#
|
|
148
160
|
# @param tools [Enumerable<Tool>]
|
|
149
161
|
# @return [void]
|
|
@@ -152,6 +164,18 @@ module Pikuri
|
|
|
152
164
|
nil
|
|
153
165
|
end
|
|
154
166
|
|
|
167
|
+
# Append a tool to the sub-agent-only pool. The parent LLM
|
|
168
|
+
# never sees it; only sub-agents whose persona +tool_names+
|
|
169
|
+
# include the tool's +name+ get it in their toolset. See the
|
|
170
|
+
# class header for the trifecta-defense rationale.
|
|
171
|
+
#
|
|
172
|
+
# @param tool [Tool]
|
|
173
|
+
# @return [void]
|
|
174
|
+
def add_sub_agent_tool(tool)
|
|
175
|
+
@sub_agent_tools << tool
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
155
179
|
# Append a listener to the agent's listener list.
|
|
156
180
|
#
|
|
157
181
|
# @param listener [Listener::Base]
|
|
@@ -163,9 +187,6 @@ module Pikuri
|
|
|
163
187
|
|
|
164
188
|
# Append several listeners at once. Accepts any enumerable
|
|
165
189
|
# (Array, {ListenerList}, …); declaration order is preserved.
|
|
166
|
-
# Sole intended caller today is {Tool::SubAgent}, which seeds a
|
|
167
|
-
# sub-agent's Configurator from the parent's listener list (run
|
|
168
|
-
# through {ListenerList#for_sub_agent}).
|
|
169
190
|
#
|
|
170
191
|
# @param listeners [Enumerable<Listener::Base>]
|
|
171
192
|
# @return [void]
|
|
@@ -207,26 +228,6 @@ module Pikuri
|
|
|
207
228
|
nil
|
|
208
229
|
end
|
|
209
230
|
|
|
210
|
-
# Retain a list of already-configured extensions for the
|
|
211
|
-
# bind sweep without re-running +configure+. Sole intended
|
|
212
|
-
# caller is {Tool::SubAgent}, which seeds a sub-agent's
|
|
213
|
-
# Configurator from the parent's extension list — the parent
|
|
214
|
-
# already drove +configure+ and its system-prompt snippets /
|
|
215
|
-
# static tools are inherited by the sub-agent verbatim
|
|
216
|
-
# through {#add_tools} + {#add_listeners} + the augmented
|
|
217
|
-
# +system_prompt+; re-running +configure+ on the sub-agent
|
|
218
|
-
# would double up those contributions. The +bind(sub_agent)+
|
|
219
|
-
# sweep in {Agent#initialize} still fires on each inherited
|
|
220
|
-
# extension so per-agent state (MCP's per-agent connect tool,
|
|
221
|
-
# for example) gets installed fresh on the sub-agent.
|
|
222
|
-
#
|
|
223
|
-
# @param extensions [Enumerable<Extension>]
|
|
224
|
-
# @return [void]
|
|
225
|
-
def inherit_extensions(extensions)
|
|
226
|
-
extensions.each { |ext| @extensions << ext }
|
|
227
|
-
nil
|
|
228
|
-
end
|
|
229
|
-
|
|
230
231
|
# Register a handler called by {Agent#close}. Handlers fire in
|
|
231
232
|
# LIFO order, each inside its own +rescue+ — same semantics as
|
|
232
233
|
# +ensure+-block cleanup discipline.
|
|
@@ -239,32 +240,6 @@ module Pikuri
|
|
|
239
240
|
@on_close_handlers << blk
|
|
240
241
|
nil
|
|
241
242
|
end
|
|
242
|
-
|
|
243
|
-
# Enable the +sub_agent+ tool on this agent. Records the
|
|
244
|
-
# +max_steps+ budget for sub-agent runs; the Agent ctor reads
|
|
245
|
-
# {#sub_agent_request} after the block returns and constructs
|
|
246
|
-
# the actual {Tool::SubAgent} so its snapshot of the parent's
|
|
247
|
-
# tool list doesn't include itself (recursion guard).
|
|
248
|
-
#
|
|
249
|
-
# Sub-agents inherit the parent's tool list (minus +sub_agent+
|
|
250
|
-
# itself), augmented system prompt, listeners (via
|
|
251
|
-
# +for_sub_agent+ per listener), controls (per the per-control
|
|
252
|
-
# rule), and the parent's extension list. Each inherited
|
|
253
|
-
# extension's +bind(sub_agent)+ fires during the sub-agent's
|
|
254
|
-
# construction — that's how MCP's per-agent connect tool ends
|
|
255
|
-
# up keyed to the sub-agent rather than the parent.
|
|
256
|
-
#
|
|
257
|
-
# @param max_steps [Integer] step budget for each sub-agent
|
|
258
|
-
# run, passed to {Tool::SubAgent#initialize}.
|
|
259
|
-
# @raise [RuntimeError] if called more than once on the same
|
|
260
|
-
# Configurator.
|
|
261
|
-
# @return [void]
|
|
262
|
-
def allow_sub_agent(max_steps: 10)
|
|
263
|
-
raise 'allow_sub_agent may only be called once per agent' if @sub_agent_request
|
|
264
|
-
|
|
265
|
-
@sub_agent_request = SubAgentRequest.new(max_steps: max_steps)
|
|
266
|
-
nil
|
|
267
|
-
end
|
|
268
243
|
end
|
|
269
244
|
end
|
|
270
245
|
end
|
|
@@ -40,13 +40,13 @@ module Pikuri
|
|
|
40
40
|
#
|
|
41
41
|
# == Sub-agent semantics
|
|
42
42
|
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
43
|
+
# Cancellation is a global "stop the whole tree" signal —
|
|
44
|
+
# the +agent+ tool from +pikuri-subagents+ shares the
|
|
45
|
+
# parent's +Cancellable+ by reference when spawning a child,
|
|
46
|
+
# so one {#cancel!} call stops the parent, every running
|
|
47
|
+
# sub-agent, and the synthesizer rescue. The sharing rule
|
|
48
|
+
# lives at the spawn site (sub-agent code), not on this
|
|
49
|
+
# class.
|
|
50
50
|
class Cancellable
|
|
51
51
|
# Raised by {#check!} once {#cancel!} has been called.
|
|
52
52
|
# Carries no fields; the cancellation reason ("the user
|
|
@@ -105,16 +105,6 @@ module Pikuri
|
|
|
105
105
|
@cancelled = false
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
# Sub-agent variant: the same instance, shared by
|
|
109
|
-
# reference, so a single {#cancel!} stops the parent,
|
|
110
|
-
# every running sub-agent, and the synthesizer rescue.
|
|
111
|
-
# See the class header for the rationale.
|
|
112
|
-
#
|
|
113
|
-
# @return [Cancellable] the receiver
|
|
114
|
-
def for_sub_agent(**)
|
|
115
|
-
self
|
|
116
|
-
end
|
|
117
|
-
|
|
118
108
|
# @return [String] short label for {Agent#to_s}; reflects
|
|
119
109
|
# the current flag state so a startup banner or debug
|
|
120
110
|
# print can tell an armed token apart from one that has
|
|
@@ -70,16 +70,16 @@ module Pikuri
|
|
|
70
70
|
#
|
|
71
71
|
# == Sub-agent semantics
|
|
72
72
|
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# contrasts with {Control::Cancellable}, which
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
73
|
+
# Sub-agents are private to the parent agent; the host has
|
|
74
|
+
# no handle to them, so a child +Interloper+ would be
|
|
75
|
+
# unreachable. The +agent+ tool from +pikuri-subagents+
|
|
76
|
+
# therefore omits the kwarg when spawning a child, leaving
|
|
77
|
+
# the sub-agent's +interloper+ at its default +nil+. The
|
|
78
|
+
# behavior contrasts with {Control::Cancellable}, which is
|
|
79
|
+
# shared by reference so the parent's signal propagates to
|
|
80
|
+
# children — cancellation is a global "stop the whole tree"
|
|
81
|
+
# event, whereas injection is a directed "talk to the main
|
|
82
|
+
# agent" event.
|
|
83
83
|
class Interloper
|
|
84
84
|
def initialize
|
|
85
85
|
@mutex = Mutex.new
|
|
@@ -143,17 +143,6 @@ module Pikuri
|
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
# Sub-agent variant: +nil+, signalling to {Agent} (and
|
|
147
|
-
# transitively to {Tool::SubAgent}) that no +Interloper+
|
|
148
|
-
# should be wired on a spawned sub-agent. See the class
|
|
149
|
-
# header for the "host has no handle to sub-agents"
|
|
150
|
-
# rationale.
|
|
151
|
-
#
|
|
152
|
-
# @return [nil]
|
|
153
|
-
def for_sub_agent(**)
|
|
154
|
-
nil
|
|
155
|
-
end
|
|
156
|
-
|
|
157
146
|
# @return [String] short label for {Agent#to_s}; reflects
|
|
158
147
|
# the pending-count so a debug print or banner can tell
|
|
159
148
|
# an idle interloper apart from one with queued items
|
|
@@ -69,20 +69,6 @@ module Pikuri
|
|
|
69
69
|
# can introspect it (and so tests can assert it)
|
|
70
70
|
attr_reader :step
|
|
71
71
|
|
|
72
|
-
# Sub-agent variant: a fresh +StepLimit+ at the
|
|
73
|
-
# caller-supplied +max_steps:+, or — when the key is
|
|
74
|
-
# absent — at the receiver's own cap. The mutable counter
|
|
75
|
-
# is per-chat, so the parent's instance cannot govern a
|
|
76
|
-
# sub-agent's chat; every sub-agent needs its own.
|
|
77
|
-
#
|
|
78
|
-
# @param max_steps [Integer] positive step cap for the
|
|
79
|
-
# sub-agent; defaults to the receiver's current cap
|
|
80
|
-
# @return [StepLimit]
|
|
81
|
-
# @raise [ArgumentError] if +max_steps+ is non-positive
|
|
82
|
-
def for_sub_agent(max_steps: @max)
|
|
83
|
-
self.class.new(max: max_steps)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
72
|
# @return [String] short config dump for {Agent#to_s}
|
|
87
73
|
def to_s
|
|
88
74
|
"StepLimit(max=#{@max})"
|
|
@@ -57,22 +57,20 @@ module Pikuri
|
|
|
57
57
|
|
|
58
58
|
# Called by {Agent#initialize} after the block returns and the
|
|
59
59
|
# chat is fully wired, with the live {Agent} as the argument.
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# typically do here:
|
|
60
|
+
# Fires once per agent the extension was registered to via
|
|
61
|
+
# {Configurator#add_extension} — in the typical setup that's
|
|
62
|
+
# the parent agent only, since sub-agents do not inherit
|
|
63
|
+
# extensions. The default is a no-op; override when you need
|
|
64
|
+
# to install state keyed to the live agent object. Things
|
|
65
|
+
# you typically do here:
|
|
67
66
|
#
|
|
68
|
-
# * register
|
|
69
|
-
# {
|
|
70
|
-
#
|
|
71
|
-
#
|
|
67
|
+
# * register dynamic tools via {Agent#internal_add_tool}
|
|
68
|
+
# (used by {Pikuri::Mcp::Extension} for +mcp_connect+,
|
|
69
|
+
# whose +execute+ closure needs the live agent so
|
|
70
|
+
# activations register on the right chat)
|
|
71
|
+
# * register +on_close+ handlers via {Agent#on_close}
|
|
72
72
|
# * stash an +@agent+ reference if the extension's tools need
|
|
73
|
-
# to act on this specific agent later
|
|
74
|
-
# fires and wants to register more tools on its owning
|
|
75
|
-
# chat)
|
|
73
|
+
# to act on this specific agent later
|
|
76
74
|
#
|
|
77
75
|
# @param agent [Agent] the live agent, fully wired
|
|
78
76
|
# @return [void]
|
|
@@ -41,10 +41,10 @@ module Pikuri
|
|
|
41
41
|
#
|
|
42
42
|
# msg #1: ctx=6.8k/32.0k Δ+6.8k ↑6.8k ↓0.0k
|
|
43
43
|
#
|
|
44
|
-
# When the owning {Agent} has a non-empty {Agent#
|
|
45
|
-
#
|
|
44
|
+
# When the owning {Agent} has a non-empty {Agent#id} (i.e. a
|
|
45
|
+
# sub-agent), the line is prefixed with +[id] +:
|
|
46
46
|
#
|
|
47
|
-
# [
|
|
47
|
+
# [researcher 0] msg #1: ctx=4.2k Δ+4.2k ↑4.2k ↓0.0k
|
|
48
48
|
#
|
|
49
49
|
# +ctx+ is the snapshot
|
|
50
50
|
# (+input + cached + cache_creation + output+; see
|
|
@@ -89,16 +89,15 @@ module Pikuri
|
|
|
89
89
|
# @return [Integer, nil]
|
|
90
90
|
attr_accessor :context_window_cap
|
|
91
91
|
|
|
92
|
-
# @return [String] owning agent's
|
|
92
|
+
# @return [String] owning agent's id ({Agent#id}). Empty by
|
|
93
93
|
# default (main agent); set by {#for_sub_agent} from the
|
|
94
|
-
# sub-agent's generated
|
|
95
|
-
# prefixed with +[<
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
attr_reader :name
|
|
94
|
+
# sub-agent's generated id so the log lines can be
|
|
95
|
+
# prefixed with +[<id>] +. Read-only — for a sub-agent's
|
|
96
|
+
# listener you get a fresh instance via {#for_sub_agent}.
|
|
97
|
+
attr_reader :id
|
|
99
98
|
|
|
100
99
|
# The most recent log line, in the exact format written to
|
|
101
|
-
# {LOGGER} (including any +[<
|
|
100
|
+
# {LOGGER} (including any +[<id>] + prefix). Empty until
|
|
102
101
|
# the first {Event::Tokens} has been processed. Hosts that
|
|
103
102
|
# want to surface the current context-window snapshot in
|
|
104
103
|
# their own UI (e.g. a TUI status footer) read this
|
|
@@ -113,12 +112,12 @@ module Pikuri
|
|
|
113
112
|
# @return [String]
|
|
114
113
|
attr_reader :status_line
|
|
115
114
|
|
|
116
|
-
# @param
|
|
117
|
-
# log line as +[<
|
|
118
|
-
#
|
|
119
|
-
def initialize(
|
|
115
|
+
# @param id [String] owning agent's id, prepended to each
|
|
116
|
+
# log line as +[<id>] + when non-empty. Defaults to +""+
|
|
117
|
+
# for the main agent.
|
|
118
|
+
def initialize(id: '')
|
|
120
119
|
super()
|
|
121
|
-
@
|
|
120
|
+
@id = id
|
|
122
121
|
@msg = 0
|
|
123
122
|
@context_window_size = 0
|
|
124
123
|
@context_window_cap = nil
|
|
@@ -128,17 +127,17 @@ module Pikuri
|
|
|
128
127
|
# Sub-agent variant: a fresh +TokenLog+ with a zeroed
|
|
129
128
|
# snapshot so the sub-agent's context-window readings
|
|
130
129
|
# track its own +RubyLLM::Chat+ rather than continuing the
|
|
131
|
-
# parent's. Picks the sub-agent's +
|
|
132
|
-
# forwarded params so its log lines carry the +[<
|
|
130
|
+
# parent's. Picks the sub-agent's +id:+ out of the
|
|
131
|
+
# forwarded params so its log lines carry the +[<id>] +
|
|
133
132
|
# prefix; defaults to +""+ when absent. The cap is left
|
|
134
133
|
# +nil+ here; the sub-agent's {Agent#initialize} emits a
|
|
135
134
|
# fresh {Event::ContextCap} immediately after construction
|
|
136
135
|
# and this listener picks it up off the stream.
|
|
137
136
|
#
|
|
138
|
-
# @param
|
|
137
|
+
# @param id [String] sub-agent's id
|
|
139
138
|
# @return [TokenLog]
|
|
140
|
-
def for_sub_agent(
|
|
141
|
-
self.class.new(
|
|
139
|
+
def for_sub_agent(id: '', **)
|
|
140
|
+
self.class.new(id: id)
|
|
142
141
|
end
|
|
143
142
|
|
|
144
143
|
# @param event [Agent::Event]
|
|
@@ -184,7 +183,7 @@ module Pikuri
|
|
|
184
183
|
|
|
185
184
|
def format_line(input, output, delta)
|
|
186
185
|
sign = delta.negative? ? '-' : '+'
|
|
187
|
-
prefix = @
|
|
186
|
+
prefix = @id.empty? ? '' : "[#{@id}] "
|
|
188
187
|
"#{prefix}msg ##{@msg}: ctx=#{format_ctx} Δ#{sign}#{format_k(delta.abs)} ↑#{format_k(input)} ↓#{format_k(output)}"
|
|
189
188
|
end
|
|
190
189
|
|
|
@@ -35,8 +35,9 @@ module Pikuri
|
|
|
35
35
|
|
|
36
36
|
# Iterate over the wrapped listeners in registration order. The
|
|
37
37
|
# method exists so a ListenerList can be passed directly to
|
|
38
|
-
# {Configurator#add_listeners} (used by
|
|
39
|
-
# seeding a sub-agent's Configurator
|
|
38
|
+
# {Configurator#add_listeners} (used by the +agent+ tool from
|
|
39
|
+
# +pikuri-subagents+ when seeding a sub-agent's Configurator
|
|
40
|
+
# from the parent's list).
|
|
40
41
|
#
|
|
41
42
|
# @yield [listener]
|
|
42
43
|
# @yieldparam listener [Listener::Base]
|
|
@@ -59,13 +60,14 @@ module Pikuri
|
|
|
59
60
|
# change this class — see {Listener::Terminal#for_sub_agent}
|
|
60
61
|
# (fresh padded instance) and
|
|
61
62
|
# {Listener::TokenLog#for_sub_agent} (fresh, zeroed snapshot
|
|
62
|
-
# with the forwarded +
|
|
63
|
+
# with the forwarded +id:+).
|
|
63
64
|
#
|
|
64
65
|
# +params+ is a flat hash forwarded as kwargs to every
|
|
65
66
|
# listener's hook; each listener picks the keys it cares about
|
|
66
67
|
# and ignores the rest. The only key currently consumed by
|
|
67
|
-
# bundled listeners is +
|
|
68
|
-
#
|
|
68
|
+
# bundled listeners is +id:+ (used by {Listener::TokenLog} to
|
|
69
|
+
# prefix its log lines with the sub-agent's id). Calling with
|
|
70
|
+
# no params is always valid.
|
|
69
71
|
#
|
|
70
72
|
# @param params [Hash{Symbol => Object}]
|
|
71
73
|
# @return [ListenerList]
|
|
@@ -45,7 +45,7 @@ module Pikuri
|
|
|
45
45
|
# reasoning and answer flow through the same listener
|
|
46
46
|
# surface the parent agent uses — terminal renders them
|
|
47
47
|
# inline (padded under sub-agent), an in-memory recorder
|
|
48
|
-
# picks them up, a TokenLog tags them with the synth
|
|
48
|
+
# picks them up, a TokenLog tags them with the synth id.
|
|
49
49
|
#
|
|
50
50
|
# @param chat [RubyLLM::Chat] a *fresh* chat with no tools.
|
|
51
51
|
# The caller is responsible for constructing it with the
|
|
@@ -58,7 +58,7 @@ module Pikuri
|
|
|
58
58
|
# @param listeners [Agent::ListenerList] listeners to wire
|
|
59
59
|
# the synth chat into. Typically the parent agent's list
|
|
60
60
|
# run through {ListenerList#for_sub_agent} with the
|
|
61
|
-
# synth's +
|
|
61
|
+
# synth's +id:+ so any +TokenLog+ tags its lines with
|
|
62
62
|
# the synth bracket and any +Terminal+ pads its output.
|
|
63
63
|
# @param step_limit [Control::StepLimit, nil] defensive
|
|
64
64
|
# step budget. The synth has no tools so it should never
|