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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/pikuri/agent/chat_transport.rb +73 -93
  4. data/lib/pikuri/agent/configurator.rb +46 -106
  5. data/lib/pikuri/agent/context_window_detector.rb +44 -85
  6. data/lib/pikuri/agent/control/cancellable.rb +87 -66
  7. data/lib/pikuri/agent/control/interloper.rb +127 -105
  8. data/lib/pikuri/agent/control/step_limit.rb +25 -41
  9. data/lib/pikuri/agent/control.rb +14 -34
  10. data/lib/pikuri/agent/event.rb +123 -188
  11. data/lib/pikuri/agent/extension.rb +118 -94
  12. data/lib/pikuri/agent/extension_context.rb +50 -77
  13. data/lib/pikuri/agent/history.rb +653 -0
  14. data/lib/pikuri/agent/listener/rate_limited.rb +40 -66
  15. data/lib/pikuri/agent/listener/terminal.rb +143 -117
  16. data/lib/pikuri/agent/listener/token_log.rb +101 -140
  17. data/lib/pikuri/agent/listener.rb +23 -43
  18. data/lib/pikuri/agent/listener_list.rb +26 -47
  19. data/lib/pikuri/agent/synthesizer.rb +45 -87
  20. data/lib/pikuri/agent.rb +816 -474
  21. data/lib/pikuri/bundler_env.rb +68 -0
  22. data/lib/pikuri/extractor/html.rb +63 -110
  23. data/lib/pikuri/extractor/passthrough.rb +20 -30
  24. data/lib/pikuri/extractor.rb +93 -154
  25. data/lib/pikuri/file_type.rb +63 -135
  26. data/lib/pikuri/finalizers.rb +32 -47
  27. data/lib/pikuri/paths.rb +104 -13
  28. data/lib/pikuri/ruby_llm_patches.rb +106 -0
  29. data/lib/pikuri/sanitizer.rb +45 -67
  30. data/lib/pikuri/subprocess.rb +75 -119
  31. data/lib/pikuri/testing.rb +296 -0
  32. data/lib/pikuri/tool/calculator.rb +56 -66
  33. data/lib/pikuri/tool/execute_context.rb +42 -0
  34. data/lib/pikuri/tool/fetch.rb +51 -77
  35. data/lib/pikuri/tool/parameters.rb +21 -29
  36. data/lib/pikuri/tool/scraper.rb +55 -97
  37. data/lib/pikuri/tool/search/brave.rb +52 -80
  38. data/lib/pikuri/tool/search/duckduckgo.rb +59 -91
  39. data/lib/pikuri/tool/search/engines.rb +230 -97
  40. data/lib/pikuri/tool/search/exa.rb +56 -90
  41. data/lib/pikuri/tool/search/rate_limiter.rb +61 -38
  42. data/lib/pikuri/tool/search/result.rb +10 -15
  43. data/lib/pikuri/tool/trifecta_legs.rb +217 -0
  44. data/lib/pikuri/tool/web_scrape.rb +38 -54
  45. data/lib/pikuri/tool/web_search.rb +100 -24
  46. data/lib/pikuri/tool.rb +140 -65
  47. data/lib/pikuri/trifecta/contribution.rb +43 -0
  48. data/lib/pikuri/trifecta/node.rb +47 -0
  49. data/lib/pikuri/trifecta/report.rb +230 -0
  50. data/lib/pikuri/trifecta.rb +127 -0
  51. data/lib/pikuri/url_cache.rb +33 -49
  52. data/lib/pikuri/version.rb +1 -1
  53. data/lib/pikuri-core.rb +72 -88
  54. data/prompts/agent-loop.txt +5 -0
  55. data/prompts/pikuri-chat.txt +3 -12
  56. metadata +14 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b034cfd9a32f43694e474444155ea4e18c0c728449f50f33755a789b4879ab6
4
- data.tar.gz: 0fdff7d50e07f52f9063b240db87e67d8bb93ac85a09cbb56b3caace3232478c
3
+ metadata.gz: f2703004f3ce0c935f4ee932493db6231109f4ecde7bcfe0ec1481dfecc21001
4
+ data.tar.gz: adf19fd1af6df3aad4ef58d10d5845ce38932566ba5c9d33a7f45791c34287a3
5
5
  SHA512:
6
- metadata.gz: 25ac79d6951ce4574d6400742bec3603306c72ca334786a13c34c3a3a5ab48de2735be5b5f51b1b6a25aa647c8f3d36da2f53d3bc97072ea276528464d6d4e16
7
- data.tar.gz: 60100a4ab334e08066b29057d0eea4003a6c0db67947cf17a81484308dcd305ef08869b740965d6e81be246fd3ca964ad5afb2ca668bc101eaa867ed63c1bc02
6
+ metadata.gz: e7475e59d9dc941226a77c875a7236c6649f678e933938bc7d9bbe18a76cbb1b8b492c3d471d94db1e68dc242f2030e25a48c236b3690cb8191a77d403467bae
7
+ data.tar.gz: 28345b25378c2ae0dabe37d58b8cc592d2f45c4a634d57f134ecd0c15bad7b157c912aa09f9f93dab46a8e78e035400faf69f0e6a2e4222874ffe88e1c73503f
data/README.md CHANGED
@@ -70,7 +70,7 @@ handling, and cancellation.
70
70
 
71
71
  ## Further reading
72
72
 
73
- - **Narrative walkthrough:** [chapter 1 of the pikuri guide](../docs/guide/01-chat.md)
73
+ - **Narrative walkthrough:** [the first-conversation chapter of the pikuri guide](../book/chat.md)
74
74
  — install llama.cpp, start the server, run `pikuri-chat`, the
75
75
  agentic loop, the four bundled tools, and search-provider
76
76
  privacy postures.
@@ -2,106 +2,86 @@
2
2
 
3
3
  module Pikuri
4
4
  class Agent
5
- # Everything that has to travel together for a chat to resolve to
6
- # the same model *on the same server* on every construction: the
7
- # model id, the provider hint, the registry-bypass flag, and — when
8
- # the model lives on a server other than the process-global
9
- # +RubyLLM.config+ default that server's base URL and API key.
10
- #
11
- # Bundling them is structural protection against a recurring bug
12
- # class — every forwarding site (the synthesizer rescue in
13
- # {Agent#run_loop}, the +agent+ tool from +pikuri-subagents+
14
- # spawning a sub-agent, a mid-conversation model switch) used to
15
- # pass the resolution fields individually, and dropping one routed
16
- # the chat to a different server or raised
17
- # +RubyLLM::ModelNotFoundError+ on the unknown model id. With a
18
- # single value object the call site can't silently miss a field.
5
+ # Everything that must travel together for a chat to resolve to the same
6
+ # model *on the same server* on every construction: model id, provider
7
+ # hint, registry-bypass flag, and — when the model lives off the
8
+ # process-global +RubyLLM.config+ default that server's base URL and API
9
+ # key. One value object so a forwarding site (the synthesizer, a sub-agent
10
+ # spawn, a mid-conversation switch) can't silently drop a field and route
11
+ # the chat to the wrong server or raise +RubyLLM::ModelNotFoundError+.
19
12
  #
20
13
  # == Why +api_base+ / +api_key+ live here
21
14
  #
22
- # +RubyLLM::Chat#with_model+ swaps only the model/provider against
23
- # the chat's *existing* connection config, so switching to a model
24
- # on a different server (a small local llama.cpp vs a big cloud
25
- # model) needs the connection to travel with the model otherwise
26
- # the new model id is sent to the old server's URL with the old
27
- # key. {Agent} maps these two generic fields onto the provider's
28
- # ruby_llm config slots (+#{provider}_api_base+ /
29
- # +#{provider}_api_key+) via a per-chat +RubyLLM::Context+; both are
30
- # +nil+ for a transport that rides the process-global config.
15
+ # +RubyLLM::Chat#with_model+ swaps only model/provider against the chat's
16
+ # *existing* connection, so switching to a model on a different server
17
+ # needs the connection to travel with the model else the new id is sent
18
+ # to the old server's URL with the old key. {Agent} maps these two generic
19
+ # fields onto the provider's config slots (+#{provider}_api_base+ /
20
+ # +#{provider}_api_key+) via a per-chat +RubyLLM::Context+; both +nil+ for a
21
+ # transport riding the global config.
31
22
  #
32
- # Pure data carrier: no +RubyLLM+ references here, so the seam stays
33
- # in {Agent}, +bin/pikuri-chat+, and {Tool}.
23
+ # Pure data carrier no +RubyLLM+ references here, so the seam stays in
24
+ # {Agent}, +bin/pikuri-chat+, and {Tool}.
34
25
  #
35
26
  # @!attribute [r] model
36
- # @return [String, nil] LLM identifier; +nil+ defers to
37
- # +RubyLLM.config.default_model+ at {Agent} construction time
27
+ # @return [String, nil] LLM id; +nil+ defers to
28
+ # +RubyLLM.config.default_model+ at {Agent} construction.
38
29
  # @!attribute [r] provider
39
- # @return [Symbol, nil] forwarded to +RubyLLM.chat+. Required
40
- # together with +assume_model_exists+ when pointing at a local
41
- # OpenAI-compatible server (llama.cpp, gpustack, ...) whose model
42
- # ids are not in ruby_llm's bundled registry; required whenever
43
- # +api_base+ / +api_key+ is set (it names the config slots).
30
+ # @return [Symbol, nil] forwarded to +RubyLLM.chat+. Required with
31
+ # +assume_model_exists+ for a local OpenAI-compatible server whose ids
32
+ # aren't in ruby_llm's registry, and whenever +api_base+/+api_key+ is
33
+ # set (it names the config slots).
44
34
  # @!attribute [r] assume_model_exists
45
- # @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips
46
- # ruby_llm's registry lookup and trusts the supplied model id.
47
- # Requires +provider+.
35
+ # @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips the registry
36
+ # lookup and trusts the model id. Requires +provider+.
48
37
  # @!attribute [r] api_base
49
- # @return [String, nil] connection base URL for this model's
50
- # server (e.g. +http://localhost:8080/v1+). +nil+ rides the
51
- # process-global +RubyLLM.config+ base. Mapped to the provider's
52
- # +#{provider}_api_base+ slot by {Agent}.
38
+ # @return [String, nil] this server's base URL (e.g.
39
+ # +http://localhost:8080/v1+); +nil+ rides the global config. Mapped to
40
+ # +#{provider}_api_base+ by {Agent}.
53
41
  # @!attribute [r] api_key
54
- # @return [String, nil] API key for this model's server. +nil+
55
- # rides the process-global config key. Mapped to the provider's
56
- # +#{provider}_api_key+ slot by {Agent}. Redacted in {#inspect}
57
- # so it never leaks into a log line or backtrace.
42
+ # @return [String, nil] API key for this server; +nil+ rides the global
43
+ # config. Mapped to +#{provider}_api_key+ by {Agent}. Redacted in
44
+ # {#inspect}.
58
45
  # @!attribute [r] context_window
59
46
  # @return [Integer, nil] explicit context-window cap for this
60
- # model on this server, or +nil+ to defer to
61
- # {ContextWindowDetector}'s probe. Travels with the model
62
- # because the cap *is* a per-model-per-server property: a
63
- # {Ctrl+P}-style switch to a different transport must carry its
64
- # own cap, not inherit the previous model's. Never sent to
65
- # ruby_llm (it is neither a {#chat_kwargs} entry nor a
66
- # connection slot) — pure pikuri metadata read by
67
- # {Agent#detect_and_emit_context_cap!}. The cap-inheritance
68
- # channel too: the +agent+ tool from +pikuri-subagents+ and the
69
- # synthesizer hand a spawned agent +parent.transport.with(
70
- # context_window: parent.context_window_cap)+ so the parent's
71
- # *resolved* cap (explicit or probed) rides along without a
47
+ # model-on-server, or +nil+ to defer to {ContextWindowDetector}. Travels
48
+ # with the model because the cap *is* per-model-per-server. Never sent to
49
+ # ruby_llm pure pikuri metadata read by
50
+ # {Agent#detect_and_emit_context_cap!}, and the cap-inheritance channel:
51
+ # a spawned agent gets +parent.transport.with(context_window:
52
+ # parent.context_window_cap)+ so the resolved cap rides along without a
72
53
  # re-probe.
73
- class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window)
74
- # Build an +:openai+-provider transport for an OpenAI-compatible
75
- # server (a local llama.cpp, a cloud endpoint, ...), carrying that
76
- # server's connection so the agent rides a per-chat
77
- # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+.
78
- # This is the host-boot factory the +bin/pikuri-*+ demos use in
79
- # place of +RubyLLM.configure+ one isolated connection per agent,
80
- # so several agents pointed at different servers (and different
81
- # keys) don't stomp a shared global.
54
+ # @!attribute [r] faraday_adapter
55
+ # @return [Class, Symbol, nil] a custom Faraday adapter to install on this
56
+ # agent's per-chat +RubyLLM::Context+ (via +config.faraday_adapter=+),
57
+ # or +nil+ for ruby_llm's default (+:net_http+). A generic HTTP seam
58
+ # inject a VCR / logging / *scripted-fake* adapter without touching the
59
+ # global +RubyLLM.config+. Its worked use is
60
+ # {Pikuri::Testing.fake_transport}, which bakes an anonymous scripted
61
+ # adapter here so the real ruby_llm loop runs against canned HTTP. Only
62
+ # honoured on the connection-override path ({#connection_overrides?}),
63
+ # which any adapter-carrying transport is on (it also sets +api_base+).
64
+ class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key,
65
+ :context_window, :faraday_adapter)
66
+ # Build an +:openai+-provider transport for an OpenAI-compatible server
67
+ # (local llama.cpp, cloud endpoint, ...), carrying its connection so the
68
+ # agent rides a per-chat +RubyLLM::Context+ instead of the global config.
69
+ # The +bin/pikuri-*+ host-boot factory in place of +RubyLLM.configure+ —
70
+ # one isolated connection per agent.
82
71
  #
83
- # +server+ is the bare server origin; a trailing +/v1+ (the
84
- # OpenAI-compatible suffix ruby_llm appends to reach
85
- # +/v1/chat/completions+) is stripped and re-appended exactly once,
86
- # so +https://api.x.ai+, +https://api.x.ai/v1+, and
87
- # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base.
88
- # Without this, a +server+ value that already ended in +/v1+ would
89
- # double to +/v1/v1+ and every request would 404.
72
+ # A trailing +/v1+ on +server+ is stripped and re-appended exactly once,
73
+ # so +https://api.x.ai+, +.../v1+, and +.../v1/+ all normalize to the same
74
+ # +.../v1+ base without which a +/v1+-terminated value would double to
75
+ # +/v1/v1+ and 404.
90
76
  #
91
- # @param server [String] server origin, with or without a trailing
92
- # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+
77
+ # @param server [String] server origin, with or without a trailing +/v1+
93
78
  # @param model [String] model id served there, trusted verbatim
94
- # (+assume_model_exists+ is +true+, so it need not appear in
95
- # ruby_llm's bundled registry)
96
- # @param api_key [String] API key for the server; the conventional
97
- # +"not-needed"+ placeholder for a keyless local server
98
- # @param context_window [Integer, nil] explicit context-window cap
99
- # for this model, or +nil+ to defer to {ContextWindowDetector}'s
100
- # +/props+ probe (the right default for a local llama.cpp, which
101
- # reports its launched +n_ctx+; the right *override* for a cloud
102
- # server the probe can't reach, e.g. a 2M-window model on x.ai)
103
- # @return [ChatTransport] a transport whose +api_base+ is the
104
- # normalized +.../v1+ URL and whose +api_key+ is +api_key+
79
+ # @param api_key [String] API key; the +"not-needed"+ placeholder for a
80
+ # keyless local server
81
+ # @param context_window [Integer, nil] explicit cap, or +nil+ to defer to
82
+ # {ContextWindowDetector}'s +/props+ probe (right for local llama.cpp;
83
+ # an override for a cloud server the probe can't reach)
84
+ # @return [ChatTransport]
105
85
  def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil)
106
86
  base = server.to_s.strip.chomp('/').delete_suffix('/v1')
107
87
  new(
@@ -120,11 +100,12 @@ module Pikuri
120
100
  # @param api_base [String, nil]
121
101
  # @param api_key [String, nil]
122
102
  # @param context_window [Integer, nil]
103
+ # @param faraday_adapter [Class, Symbol, nil]
123
104
  # @raise [ArgumentError] if +api_base+ or +api_key+ is set without
124
105
  # a +provider+ (the provider names the config slots the
125
106
  # connection overrides map onto)
126
107
  def initialize(model:, provider: nil, assume_model_exists: false,
127
- api_base: nil, api_key: nil, context_window: nil)
108
+ api_base: nil, api_key: nil, context_window: nil, faraday_adapter: nil)
128
109
  if (api_base || api_key) && provider.nil?
129
110
  raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}"
130
111
  end
@@ -132,10 +113,9 @@ module Pikuri
132
113
  super
133
114
  end
134
115
 
135
- # The model-resolution kwargs to spread into +RubyLLM.chat+ /
136
- # +RubyLLM::Context#chat+. Excludes the connection fields those
137
- # configure the +Context+ the chat is built from, not the +chat+
138
- # call itself.
116
+ # Model-resolution kwargs to spread into +RubyLLM.chat+ /
117
+ # +RubyLLM::Context#chat+. Excludes the connection fields (those configure
118
+ # the +Context+, not the +chat+ call).
139
119
  #
140
120
  # @return [Hash{Symbol => String, Symbol, Boolean, nil}]
141
121
  def chat_kwargs
@@ -150,15 +130,15 @@ module Pikuri
150
130
  !api_base.nil? || !api_key.nil?
151
131
  end
152
132
 
153
- # Default +Data#inspect+ would print +api_key+ verbatim, leaking
154
- # the secret into any log line, +to_s+ interpolation, or backtrace
155
- # that touches the transport. Redact it.
133
+ # Default +Data#inspect+ would print +api_key+ verbatim into any log line
134
+ # or backtrace. Redact it.
156
135
  #
157
136
  # @return [String]
158
137
  def inspect
159
138
  "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \
160
139
  "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \
161
- "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>"
140
+ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect} " \
141
+ "faraday_adapter=#{faraday_adapter.inspect}>"
162
142
  end
163
143
  alias to_s inspect
164
144
  end
@@ -2,64 +2,40 @@
2
2
 
3
3
  module Pikuri
4
4
  class Agent
5
- # Build-time collector yielded into the +Pikuri::Agent.new+ block.
6
- # Hosts and {Extension} implementations call its methods to declare
7
- # additional tools, listeners, system-prompt snippets, +on_close+
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.
11
- #
12
- # == Why this exists
13
- #
14
- # Splits "configure the agent" from "the agent's runtime state".
15
- # Hosts can write a block that reads cleanly:
5
+ # Build-time collector yielded into the +Pikuri::Agent.new+ block. Hosts
6
+ # and {Extension}s call its methods to declare tools, listeners,
7
+ # +on_close+ handlers, and extension instances; {Agent#initialize} drains
8
+ # the collected state into the agent's wiring before returning, then
9
+ # discards the Configurator (it carries no runtime state). System-prompt
10
+ # contributions are *not* collected here — the Agent pulls them from each
11
+ # extension's {Extension#system_prompt_snippets} at assembly time.
16
12
  #
17
13
  # Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
18
14
  # c.add_listener Pikuri::Agent::Listener::Terminal.new
19
15
  # c.add_tool Pikuri::Tool::WebSearch.build
20
- # c.add_tool Pikuri::Tool::WEB_SCRAPE
21
- # c.add_tool Pikuri::Tool::FETCH
22
- # c.add_extension Pikuri::Skill::Extension.new(catalog: catalog)
16
+ # c.add_extension Pikuri::Skill::Extension.new(catalog: catalog, root: project_root)
23
17
  # end
24
18
  #
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".
19
+ # Extensions implement +configure(c)+ against this same type, so
20
+ # block-users and extensions share one API.
39
21
  #
40
- # Extensions implement their +configure(c)+ hook against the same
41
- # type, so the call sites for "block users add stuff" and
42
- # "extensions add stuff" share one API.
43
- #
44
- # == Lifecycle
22
+ # == Two tool pools: regular vs. sub-agent-only
45
23
  #
46
- # One +Configurator+ per +Agent.new+ invocation. The Configurator
47
- # is constructed inside {Agent#initialize}, yielded to the block
48
- # (if any), then its collected state is drained by the Agent body.
49
- # The Configurator instance is discarded once +Agent.new+ returns
50
- # it carries no runtime state.
24
+ # {#add_tool} registers a tool the parent agent can call (lands in {#tools},
25
+ # handed to ruby_llm). {#add_sub_agent_tool} registers one the parent
26
+ # *cannot* call (lands in {#sub_agent_tools}, never sent to ruby_llm for the
27
+ # parent) but which {Pikuri::SubAgent::Extension}'s persona-tool-name
28
+ # resolution can reach. The use case is the lethal-trifecta defense: keep
29
+ # network tools (+web_search+ / +web_scrape+ / +fetch+) off the parent so a
30
+ # prompt-injected file read can't egress through it, while the +researcher+
31
+ # persona still reaches them via the +agent+ tool. See SECURITY.md.
51
32
  class Configurator
52
- # @return [Agent::ChatTransport] same transport the Agent will
53
- # use. Extensions read this to wire helpers consistently
54
- # (e.g. an MCP description-synthesizer that calls the same
55
- # model the agent itself uses).
33
+ # @return [Agent::ChatTransport] same transport the Agent will use, so an
34
+ # extension can wire helpers against the same model.
56
35
  attr_reader :transport
57
36
 
58
- # @return [String] the +system_prompt:+ kwarg passed to
59
- # {Agent#initialize}, untouched. Extensions append to the
60
- # prompt via {#append_system_prompt} rather than mutating
61
- # this; the attribute exists so a peek at the base is
62
- # available for diagnostics.
37
+ # @return [String] the +system_prompt:+ kwarg, untouched — the base the
38
+ # Agent prepends to the pulled {Extension#system_prompt_snippets}.
63
39
  attr_reader :system_prompt_base
64
40
 
65
41
  # @return [String] this agent's unique identifier; empty for the
@@ -74,28 +50,22 @@ module Pikuri
74
50
  # to the Agent ctor, or +nil+.
75
51
  attr_reader :step_limit
76
52
 
77
- # @return [Control::Cancellable, nil] cancellation control
78
- # passed to the Agent ctor, or +nil+. Extensions that run
79
- # sub-LLM calls during +configure+ (e.g. an MCP description
80
- # synthesizer) share this so a user cancel during boot
81
- # propagates correctly.
53
+ # @return [Control::Cancellable, nil] cancellation control, or +nil+.
54
+ # Extensions running sub-LLM calls in +configure+ share it so a user
55
+ # cancel during boot propagates.
82
56
  attr_reader :cancellable
83
57
 
84
58
  # @return [Control::Interloper, nil] mid-loop user-input queue
85
59
  # passed to the Agent ctor, or +nil+.
86
60
  attr_reader :interloper
87
61
 
88
- # @return [Array<Tool>] tools added via {#add_tool}, in
89
- # declaration order. Drained by {Agent#initialize} and
90
- # registered with ruby_llm so the parent LLM can call them.
62
+ # @return [Array<Tool>] tools added via {#add_tool}, in declaration order;
63
+ # drained by {Agent#initialize} and registered with ruby_llm.
91
64
  attr_reader :tools
92
65
 
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.
66
+ # @return [Array<Tool>] tools added via {#add_sub_agent_tool}, in
67
+ # declaration order; drained but *not* registered with ruby_llm —
68
+ # available only to sub-agents (see the class header).
99
69
  attr_reader :sub_agent_tools
100
70
 
101
71
  # @return [Array<Listener::Base>] listeners added via
@@ -103,21 +73,14 @@ module Pikuri
103
73
  # {Agent#initialize}.
104
74
  attr_reader :listeners
105
75
 
106
- # @return [Array<String>] system-prompt snippets added via
107
- # {#append_system_prompt}, in declaration order. Joined with
108
- # double-newline separators between the base prompt and each
109
- # snippet by {Agent#initialize}.
110
- attr_reader :system_prompt_additions
111
-
112
76
  # @return [Array<Proc>] +on_close+ handlers added via
113
77
  # {#on_close}, in declaration order. Fired by {Agent#close}
114
78
  # in LIFO order with per-handler rescue.
115
79
  attr_reader :on_close_handlers
116
80
 
117
81
  # @return [Array<#configure>] extension instances added via
118
- # {#add_extension}, in declaration order. The Agent ctor
119
- # walks this list after wiring is complete and calls +bind+
120
- # on each with the agent's {ExtensionContext}.
82
+ # {#add_extension}, in declaration order. The Agent calls +bind+ on
83
+ # each after wiring is complete.
121
84
  attr_reader :extensions
122
85
 
123
86
  # @param transport [Agent::ChatTransport]
@@ -127,15 +90,14 @@ module Pikuri
127
90
  # @param step_limit [Control::StepLimit, nil]
128
91
  # @param cancellable [Control::Cancellable, nil]
129
92
  # @param interloper [Control::Interloper, nil]
130
- # @param on_close_sink [Array<Proc>, nil] array that {#on_close}
131
- # appends to. {Agent#initialize} passes its own live
132
- # +@on_close_handlers+ so a handler an extension arms via
133
- # +c.on_close+ is reachable the instant it's registered which
134
- # is what lets the constructor close a half-built agent if a
135
- # later extension's +configure+ raises. Defaults to a fresh
136
- # array for standalone use (e.g. specs).
93
+ # @param on_close_sink [Array<Proc>, nil] array {#on_close} appends to.
94
+ # {Agent#initialize} passes its live +@on_close_handlers+ so a handler
95
+ # armed via +c.on_close+ is reachable instantly what lets the
96
+ # constructor close a half-built agent if a later +configure+ raises.
97
+ # Defaults to a fresh array (standalone/spec use).
137
98
  def initialize(transport:, system_prompt_base:, id:, streaming:,
138
- step_limit:, cancellable:, interloper:, on_close_sink: nil)
99
+ step_limit:, cancellable:, interloper:,
100
+ on_close_sink: nil)
139
101
  @transport = transport
140
102
  @system_prompt_base = system_prompt_base
141
103
  @id = id
@@ -147,7 +109,6 @@ module Pikuri
147
109
  @tools = []
148
110
  @sub_agent_tools = []
149
111
  @listeners = []
150
- @system_prompt_additions = []
151
112
  @on_close_handlers = on_close_sink || []
152
113
  @extensions = []
153
114
  end
@@ -202,30 +163,10 @@ module Pikuri
202
163
  nil
203
164
  end
204
165
 
205
- # Append a snippet to the system prompt. Snippets are joined
206
- # to the base prompt with double-newline separators.
207
- #
208
- # Used by extensions to register +<available_skills>+,
209
- # +<available_mcps>+, and similar advertisement blocks. Block
210
- # users typically pass the full system prompt via the ctor's
211
- # +system_prompt:+ kwarg instead.
212
- #
213
- # @param snippet [String]
214
- # @return [void]
215
- def append_system_prompt(snippet)
216
- @system_prompt_additions << snippet
217
- nil
218
- end
219
-
220
- # Register an extension. The extension's +configure(self)+ is
221
- # called immediately so source-order matches execution-order.
222
- # The instance is also retained for the +bind(ctx)+ sweep
223
- # that runs at the end of {Agent#initialize}.
224
- #
225
- # Extensions must implement both +configure+ and +bind+. The
226
- # easy way is to +include Pikuri::Agent::Extension+ — that
227
- # mixes in empty defaults for both, so an extension overrides
228
- # only what it cares about and leaves the other as a no-op.
166
+ # Register an extension: its +configure(self)+ runs immediately (so
167
+ # source order matches execution order), and the instance is retained for
168
+ # the +bind(ctx)+ sweep at the end of {Agent#initialize}. Both hooks are
169
+ # required +include Pikuri::Agent::Extension+ for no-op defaults.
229
170
  #
230
171
  # @param extension [Extension, #configure, #bind] extension instance
231
172
  # @return [void]
@@ -235,9 +176,8 @@ module Pikuri
235
176
  nil
236
177
  end
237
178
 
238
- # Register a handler called by {Agent#close}. Handlers fire in
239
- # LIFO order, each inside its own +rescue+ — same semantics as
240
- # +ensure+-block cleanup discipline.
179
+ # Register a handler called by {Agent#close} (LIFO, each inside its own
180
+ # +rescue+).
241
181
  #
242
182
  # @yield called with no arguments at close time
243
183
  # @return [void]