pikuri-core 0.0.6 → 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 +6 -4
  3. data/lib/pikuri/agent/chat_transport.rb +128 -24
  4. data/lib/pikuri/agent/configurator.rb +47 -107
  5. data/lib/pikuri/agent/context_window_detector.rb +80 -70
  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 +46 -30
  9. data/lib/pikuri/agent/control.rb +14 -34
  10. data/lib/pikuri/agent/event.rb +125 -163
  11. data/lib/pikuri/agent/extension.rb +121 -83
  12. data/lib/pikuri/agent/extension_context.rb +120 -0
  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 +147 -110
  16. data/lib/pikuri/agent/listener/token_log.rb +116 -108
  17. data/lib/pikuri/agent/listener.rb +23 -36
  18. data/lib/pikuri/agent/listener_list.rb +26 -57
  19. data/lib/pikuri/agent/synthesizer.rb +76 -92
  20. data/lib/pikuri/agent.rb +939 -642
  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 +157 -0
  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 +70 -15
  36. data/lib/pikuri/tool/scraper.rb +55 -97
  37. data/lib/pikuri/tool/search/brave.rb +70 -84
  38. data/lib/pikuri/tool/search/duckduckgo.rb +65 -86
  39. data/lib/pikuri/tool/search/engines.rb +249 -93
  40. data/lib/pikuri/tool/search/exa.rb +75 -97
  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 +121 -26
  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 -86
  54. data/prompts/agent-loop.txt +5 -0
  55. data/prompts/pikuri-chat.txt +3 -12
  56. metadata +18 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac822a7bd46228f2eea2994c2e1428e3aa90c269e6ebafd603474fb630ba34ee
4
- data.tar.gz: 00c69d139bc38c1a881bf87980970672517db51f59469aef266009a803a874db
3
+ metadata.gz: f2703004f3ce0c935f4ee932493db6231109f4ecde7bcfe0ec1481dfecc21001
4
+ data.tar.gz: adf19fd1af6df3aad4ef58d10d5845ce38932566ba5c9d33a7f45791c34287a3
5
5
  SHA512:
6
- metadata.gz: d148d78b2027d747ef10f4dd7a19252f66bb1b5f99e8eef763149bf3e93ae608a3f7cf739b02ed4d92ab3ab39d8fe6888615a5acaf90dcb44ca783058a95a716
7
- data.tar.gz: 32ef75bbd6d825970e5a1e6b5e27cf0fc812980e87e5ddd59b14f2c10772c91cf93003c2ebe2c9501f5e9682e726f77c4c387c7c83b467b9aa57dc91a9a178f0
6
+ metadata.gz: e7475e59d9dc941226a77c875a7236c6649f678e933938bc7d9bbe18a76cbb1b8b492c3d471d94db1e68dc242f2030e25a48c236b3690cb8191a77d403467bae
7
+ data.tar.gz: 28345b25378c2ae0dabe37d58b8cc592d2f45c4a634d57f134ecd0c15bad7b157c912aa09f9f93dab46a8e78e035400faf69f0e6a2e4222874ffe88e1c73503f
data/README.md CHANGED
@@ -12,8 +12,10 @@ AI-assistant toolkit:
12
12
  token accounting, and structured capture.
13
13
  - Controls (`StepLimit`, `Cancellable`, `Interloper`) for budget
14
14
  enforcement + cancellation.
15
- - Four stateless bundled tools: `CALCULATOR`, `WEB_SEARCH`,
16
- `WEB_SCRAPE`, `FETCH`.
15
+ - Three stateless bundled tools (`CALCULATOR`, `WEB_SCRAPE`,
16
+ `FETCH`) plus a host-configured web-search tool built via
17
+ `Pikuri::Tool::WebSearch.build` (provider keys passed in, never
18
+ read from the environment).
17
19
  - A demo binary, `bin/pikuri-chat`.
18
20
 
19
21
  Extensions (skills, MCP, workspace, coding stack, named-agent
@@ -57,7 +59,7 @@ agent = Pikuri::Agent.new(
57
59
  step_limit: Pikuri::Agent::Control::StepLimit.new(max: 20)
58
60
  ) do |c|
59
61
  c.add_tool Pikuri::Tool::CALCULATOR
60
- c.add_tool Pikuri::Tool::WEB_SEARCH
62
+ c.add_tool Pikuri::Tool::WebSearch.build
61
63
  c.add_listener Pikuri::Agent::Listener::Terminal.new
62
64
  end
63
65
  agent.run_loop(user_message: 'What is 17 * 23?')
@@ -68,7 +70,7 @@ handling, and cancellation.
68
70
 
69
71
  ## Further reading
70
72
 
71
- - **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)
72
74
  — install llama.cpp, start the server, run `pikuri-chat`, the
73
75
  agentic loop, the four bundled tools, and search-provider
74
76
  privacy postures.
@@ -2,41 +2,145 @@
2
2
 
3
3
  module Pikuri
4
4
  class Agent
5
- # The trio of arguments that has to travel together to +RubyLLM.chat+
6
- # for model resolution to come out the same on every construction:
7
- # the model id, the provider hint, and the registry-bypass flag.
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+.
8
12
  #
9
- # Bundling them is structural protection against a recurring bug
10
- # class — every forwarding site (the synthesizer rescue in
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.
13
+ # == Why +api_base+ / +api_key+ live here
17
14
  #
18
- # Pure data carrier: no +RubyLLM+ references here, so the seam stays
19
- # in {Agent}, +bin/pikuri-chat+, and {Tool}.
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.
22
+ #
23
+ # Pure data carrier — no +RubyLLM+ references here, so the seam stays in
24
+ # {Agent}, +bin/pikuri-chat+, and {Tool}.
20
25
  #
21
26
  # @!attribute [r] model
22
- # @return [String, nil] LLM identifier; +nil+ defers to
23
- # +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.
24
29
  # @!attribute [r] provider
25
- # @return [Symbol, nil] forwarded to +RubyLLM.chat+. Required
26
- # together with +assume_model_exists+ when pointing at a local
27
- # OpenAI-compatible server (llama.cpp, gpustack, ...) whose model
28
- # ids are not in ruby_llm's bundled registry.
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).
29
34
  # @!attribute [r] assume_model_exists
30
- # @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips
31
- # ruby_llm's registry lookup and trusts the supplied model id.
32
- # Requires +provider+.
33
- class ChatTransport < Data.define(:model, :provider, :assume_model_exists)
35
+ # @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips the registry
36
+ # lookup and trusts the model id. Requires +provider+.
37
+ # @!attribute [r] api_base
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}.
41
+ # @!attribute [r] api_key
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}.
45
+ # @!attribute [r] context_window
46
+ # @return [Integer, nil] explicit context-window cap for this
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
53
+ # re-probe.
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.
71
+ #
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.
76
+ #
77
+ # @param server [String] server origin, with or without a trailing +/v1+
78
+ # @param model [String] model id served there, trusted verbatim
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]
85
+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil)
86
+ base = server.to_s.strip.chomp('/').delete_suffix('/v1')
87
+ new(
88
+ model: model,
89
+ provider: :openai,
90
+ assume_model_exists: true,
91
+ api_base: "#{base}/v1",
92
+ api_key: api_key,
93
+ context_window: context_window
94
+ )
95
+ end
96
+
34
97
  # @param model [String, nil]
35
98
  # @param provider [Symbol, nil]
36
99
  # @param assume_model_exists [Boolean]
37
- def initialize(model:, provider: nil, assume_model_exists: false)
100
+ # @param api_base [String, nil]
101
+ # @param api_key [String, nil]
102
+ # @param context_window [Integer, nil]
103
+ # @param faraday_adapter [Class, Symbol, nil]
104
+ # @raise [ArgumentError] if +api_base+ or +api_key+ is set without
105
+ # a +provider+ (the provider names the config slots the
106
+ # connection overrides map onto)
107
+ def initialize(model:, provider: nil, assume_model_exists: false,
108
+ api_base: nil, api_key: nil, context_window: nil, faraday_adapter: nil)
109
+ if (api_base || api_key) && provider.nil?
110
+ raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}"
111
+ end
112
+
38
113
  super
39
114
  end
115
+
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).
119
+ #
120
+ # @return [Hash{Symbol => String, Symbol, Boolean, nil}]
121
+ def chat_kwargs
122
+ { model: model, provider: provider, assume_model_exists: assume_model_exists }
123
+ end
124
+
125
+ # Whether this transport overrides the process-global connection
126
+ # (and so needs a dedicated +RubyLLM::Context+).
127
+ #
128
+ # @return [Boolean]
129
+ def connection_overrides?
130
+ !api_base.nil? || !api_key.nil?
131
+ end
132
+
133
+ # Default +Data#inspect+ would print +api_key+ verbatim into any log line
134
+ # or backtrace. Redact it.
135
+ #
136
+ # @return [String]
137
+ def inspect
138
+ "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \
139
+ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \
140
+ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect} " \
141
+ "faraday_adapter=#{faraday_adapter.inspect}>"
142
+ end
143
+ alias to_s inspect
40
144
  end
41
145
  end
42
146
  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
- # c.add_tool Pikuri::Tool::WEB_SEARCH
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)
15
+ # c.add_tool Pikuri::Tool::WebSearch.build
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 and calls +bind(self)+ on each after
120
- # wiring is complete.
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(agent)+ 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]