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
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ class Agent
5
+ # Capability facade handed to {Extension#bind} and
6
+ # {Extension#on_user_message} — the runtime counterpart of {Configurator}.
7
+ # Where the Configurator collects *build-time* declarations, this grants
8
+ # the *runtime* capabilities an extension needs once the agent is wired:
9
+ # emitting domain events, registering raw per-agent tools, deriving
10
+ # sub-agent listener lists.
11
+ #
12
+ # == Why a handed object, not a getter on Agent
13
+ #
14
+ # {Agent} exposes NO public path to these capabilities (no +listeners+ /
15
+ # +chat+ reader, no emit method): holding an agent grants read access to
16
+ # its config and nothing more. The only way to obtain this object is to be
17
+ # an {Extension} receiving +bind+ / +on_user_message+ — or to be handed it
18
+ # onward by one ({Pikuri::SubAgent::SubAgentTool} and
19
+ # {Pikuri::Mcp::Servers::Connect} both capture the context their +bind+
20
+ # received). Capabilities flow by explicit handoff, never by fetching from
21
+ # a globally reachable object. The boundary is the API contract, not a
22
+ # mechanical seal — same as every seam in CLAUDE.md.
23
+ #
24
+ # == Boundary rule
25
+ #
26
+ # Operations that *act on* the live agent's wiring live here; passive
27
+ # readers of constructor-given config (+transport+, +id+, +tools+, ...)
28
+ # stay on {Agent}, reachable via {#agent}. Don't move readers in; don't add
29
+ # capabilities to Agent.
30
+ #
31
+ # == Audit
32
+ #
33
+ # One context per agent, built by {Agent#initialize} before the +bind+
34
+ # sweep. {ListenerList#emit} has exactly two callers: {Agent} (loop
35
+ # narration) and this class (domain events). Capability-user roster:
36
+ # +grep -rn 'emit_event\|add_raw_tool\|sub_agent_listeners' pikuri-*/lib/+.
37
+ class ExtensionContext
38
+ # @param agent [Agent] the live, fully wired agent.
39
+ # @param chat [RubyLLM::Chat] the agent's underlying chat —
40
+ # target of {#add_raw_tool}.
41
+ # @param listeners [ListenerList] the agent's listener list —
42
+ # target of {#emit_event} / {#sub_agent_listeners}.
43
+ # @param on_close_sink [Array<Proc>] the agent's live
44
+ # +@on_close_handlers+ array, which {#on_close} appends to —
45
+ # same live-sink shape as {Configurator}'s +on_close_sink:+.
46
+ def initialize(agent:, chat:, listeners:, on_close_sink:)
47
+ @agent = agent
48
+ @chat = chat
49
+ @listeners = listeners
50
+ @on_close_handlers = on_close_sink
51
+ end
52
+
53
+ # @return [Agent] the live agent, for read access to its
54
+ # configuration (tools, transport, id, streaming, ...).
55
+ attr_reader :agent
56
+
57
+ # Emit a domain event onto the agent's listener stream. Core {Event}
58
+ # variants narrate the loop ({Agent}-emitted); gems define their own
59
+ # (e.g. +Pikuri::Tasks::ListChanged+) and emit them here. Listeners must
60
+ # no-op on variants they don't recognize (the {Listener::Base#on_event}
61
+ # default gives that for free).
62
+ #
63
+ # Called on the agent's thread (typically inside a tool's +execute+,
64
+ # landing between {Event::ToolCall} and {Event::ToolResult}); listeners
65
+ # doing cross-thread handoff snapshot inside +on_event+.
66
+ #
67
+ # @param event [Object] an immutable event value (by convention a +Data+)
68
+ # @return [void]
69
+ def emit_event(event)
70
+ @listeners.emit(event)
71
+ nil
72
+ end
73
+
74
+ # Register a raw +RubyLLM::Tool+ subclass on the agent's chat, bypassing
75
+ # the {Pikuri::Tool} strict-validation seam — hence "raw" (native tools
76
+ # go through {Configurator#add_tool} for {Tool::Parameters} validation).
77
+ # Callers: {Pikuri::Mcp::Servers} (MCP tools deliberately bypass) and
78
+ # {Pikuri::SubAgent::Extension} (the +agent+ tool registers after the
79
+ # parent's tool list is final).
80
+ #
81
+ # The tool does NOT enter {Agent#tools}, only the chat's list — so
82
+ # sub-agents can't snapshot it, which is the point: activation is strictly
83
+ # per-agent.
84
+ #
85
+ # @param ruby_llm_tool [Class] subclass of +RubyLLM::Tool+
86
+ # @return [void]
87
+ def add_raw_tool(ruby_llm_tool)
88
+ @chat.with_tool(ruby_llm_tool)
89
+ nil
90
+ end
91
+
92
+ # Derive a listener list for a spawned sub-agent via
93
+ # {ListenerList#for_sub_agent}. Sole caller:
94
+ # {Pikuri::SubAgent::SubAgentTool}, once per spawn. The derived list
95
+ # aliases the parent's listener *instances* where a listener opts to
96
+ # share by reference — see {ListenerList#for_sub_agent}.
97
+ #
98
+ # @param params [Hash{Symbol => Object}] forwarded to each listener's
99
+ # +for_sub_agent+ hook (currently +id:+).
100
+ # @return [ListenerList]
101
+ def sub_agent_listeners(**params)
102
+ @listeners.for_sub_agent(**params)
103
+ end
104
+
105
+ # Register a handler called by {Agent#close} — symmetric to
106
+ # {Configurator#on_close} (LIFO, per-handler rescue, idempotent) but
107
+ # available post-construction, so an {Extension}'s +bind+ can install
108
+ # per-agent cleanup (Memory arms its recorder's bounded flush here).
109
+ #
110
+ # @yield called with no arguments at close time
111
+ # @return [void]
112
+ def on_close(&blk)
113
+ raise ArgumentError, 'on_close requires a block' unless block_given?
114
+
115
+ @on_close_handlers << blk
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end