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
@@ -2,44 +2,24 @@
2
2
 
3
3
  module Pikuri
4
4
  class Agent
5
- # Prompt builder for the step-exhaustion rescue. When an
6
- # +Agent+'s {Control::StepLimit} trips with the +:synthesize+
7
- # policy, +Agent#run_loop+ runs this module's prompt on a
8
- # nested tools-free agent so the run still produces something
9
- # useful — an assistant turn that answers the user's question
10
- # from whatever evidence the failed agent collected before
11
- # running out of budget.
5
+ # Prompt builder for the step-exhaustion rescue. When an +Agent+'s
6
+ # {Control::StepLimit} trips with the +:synthesize+ policy, +Agent#run_loop+
7
+ # runs this prompt on a nested tools-free agent so the run still produces an
8
+ # answer from whatever evidence the failed agent gathered before running out
9
+ # of budget.
12
10
  #
13
- # == Why this exists
11
+ # The failure mode it salvages is the "wait, but what about X?" death-loop:
12
+ # the agent collects sound evidence in the first few rounds, then burns the
13
+ # rest of the budget second-guessing — by the cap, the answer is largely in
14
+ # the messages and just needs a tools-free pass to synthesize. Salvage is
15
+ # wrong for some agents (a coding agent's half-finished work can only be
16
+ # described, not completed), which is why the policy lives on
17
+ # {Control::StepLimit} and defaults to +:raise+.
14
18
  #
15
- # Without a rescue, a step-exhausted run just raises a stack
16
- # trace past +bin/pikuri-chat+ and the user gets nothing
17
- # despite the agent having gathered useful information in the
18
- # first N-1 steps. The observed failure mode is the "wait,
19
- # but what about X?" death-loop: the agent collects sound
20
- # evidence in the first few rounds, then spends the rest of
21
- # the budget second-guessing. By the time the cap trips, the
22
- # answer is largely in the messages — it just needs a
23
- # tools-free pass to synthesize.
24
- #
25
- # Salvage is the wrong move for some agents, which is why the
26
- # policy lives on {Control::StepLimit} and defaults to
27
- # +:raise+ — a coding agent's half-finished work can't be
28
- # completed by a tools-free pass, only described. See
29
- # {Control::StepLimit}'s class header.
30
- #
31
- # == Seam discipline
32
- #
33
- # This module is pure prompt construction — no chat handling,
34
- # no +RubyLLM.chat+ call, no event wiring. The execution side
35
- # (constructing the nested agent, sharing the parent's
36
- # listener stream and cancellable, capturing the answer) is
37
- # +Agent#run_synthesizer+'s job: the synth is a regular
38
- # tools-free +Agent+, the same construction shape the +agent+
39
- # tool from +pikuri-subagents+ uses for sub-agents. The only
40
- # +RubyLLM::*+ surface read here is the value-type
41
- # +RubyLLM::Message+ / +ToolCall+ passthrough (per the
42
- # value-type rule in CLAUDE.md).
19
+ # Pure prompt construction — no chat handling or +RubyLLM.chat+ call.
20
+ # {.run_synthesizer} owns the execution (a regular tools-free +Agent+, the
21
+ # same shape sub-agents use). The only +RubyLLM::*+ surface read here is the
22
+ # +RubyLLM::Message+/+ToolCall+ value-type passthrough.
43
23
  module Synthesizer
44
24
  # The synthesizer's system prompt. Strict and short: use
45
25
  # the evidence, don't apologize, admit gaps when present.
@@ -47,10 +27,8 @@ module Pikuri
47
27
  You are given evidence another agent collected before running out of steps. Answer the user's question using only this evidence. You have no tools. If the evidence is insufficient, state plainly what's missing and what partial answer you can give. Do not apologize or comment on the previous agent.
48
28
  PROMPT
49
29
 
50
- # Render the user's question plus an "Evidence gathered"
51
- # section built from +parent_messages+ as a single prompt
52
- # string. Pure function — no I/O, safe to test directly
53
- # with fixture messages.
30
+ # Render the question plus an "Evidence gathered" section from
31
+ # +parent_messages+. Pure — no I/O.
54
32
  #
55
33
  # @param parent_messages [Array<RubyLLM::Message>]
56
34
  # @param user_message [String]
@@ -60,14 +38,11 @@ module Pikuri
60
38
  "Question: #{user_message}\n\nEvidence gathered:\n#{transcript}"
61
39
  end
62
40
 
63
- # Walk the parent's message history and produce a paired
64
- # "Tool call:" / "Tool result:" log, preserving order. Tool
65
- # calls that have no matching +:tool+ message are dropped —
66
- # the call that tripped the step limit never executed, so
67
- # including it would mislead the synth into citing
68
- # nonexistent results. Non-empty assistant text content is
69
- # preserved as a "Note:" line, since the parent may have
70
- # summarized progress between tool calls.
41
+ # Walk the parent's history into a paired "Tool call:" / "Tool result:"
42
+ # log, in order. Tool calls with no matching +:tool+ message are dropped —
43
+ # the call that tripped the limit never executed, so citing its
44
+ # nonexistent result would mislead the synth. Non-empty assistant text
45
+ # becomes a "Note:" line.
71
46
  #
72
47
  # @param messages [Array<RubyLLM::Message>]
73
48
  # @return [String]
@@ -97,33 +72,25 @@ module Pikuri
97
72
  end
98
73
  private_class_method :format_evidence
99
74
 
100
- # The +:synthesize+ arm of the step-exhaustion policy (see the
101
- # class header). Runs the {Synthesizer} prompt over the
102
- # exhausted chat's history on a nested tools-free +Agent+ —
103
- # the same construction shape the +agent+ tool from
104
- # +pikuri-subagents+ uses for sub-agents, so the synth gets
105
- # listener propagation, transport / context-window-cap /
106
- # streaming inheritance, and teardown via +close+ for free.
107
- # The synth's answer is returned.
75
+ # The +:synthesize+ arm of the step-exhaustion policy. Runs the
76
+ # {Synthesizer} prompt over the exhausted chat's history on a nested
77
+ # tools-free +Agent+ (the sub-agent construction shape, so it inherits
78
+ # listener propagation, transport/cap/streaming, and +close+ teardown).
108
79
  #
109
80
  # @param ctx [ExtensionContext]
110
- # @param chat_messages [Array<RubyLLM::Message>] the
111
- # exhausted chat's full message history, the evidence
112
- # {.build_prompt} renders
81
+ # @param chat_messages [Array<RubyLLM::Message>] the exhausted chat's
82
+ # history, the evidence {.build_prompt} renders
113
83
  # @param user_message [String] the user's original question
114
- # from the turn that exhausted
115
- # @raise [Control::Cancellable::Cancelled] when a cancel
116
- # landed between the budget tripping and this rescue —
117
- # cancellation wins over salvage
84
+ # @raise [Control::Cancellable::Cancelled] when a cancel landed between
85
+ # the budget tripping and this rescue — cancellation wins over salvage
118
86
  # @return [String] the synth answer
119
87
  def self.run_synthesizer(ctx, chat_messages, user_message)
120
- # Check the cancel flag *before* constructing the synth: the
121
- # nested run_loop resets the shared cancellable at its turn
122
- # boundary, which would erase a cancel requested in this
123
- # window. The raise propagates without a parent-side
124
- # {Event::Cancelled} — a cancel *during* synthesis emits it
125
- # from the synth's own rescue (on the derived listener list)
126
- # instead, so either way the stream sees at most one.
88
+ # Check the cancel flag *before* constructing the synth: the nested
89
+ # run_loop resets the shared cancellable at its turn boundary, which
90
+ # would erase a cancel requested in this window. The raise propagates
91
+ # without a parent-side {Event::Cancelled} — a cancel *during* synthesis
92
+ # emits it from the synth's own rescue instead, so the stream sees at
93
+ # most one.
127
94
  ctx.agent.cancellable&.check!
128
95
 
129
96
  ctx.emit_event(Event::FallbackNotice.new(
@@ -131,27 +98,18 @@ module Pikuri
131
98
  'synthesizing answer from gathered evidence'
132
99
  ))
133
100
 
134
- # Synth runs under this agent's identity but with a
135
- # different system prompt, so it gets a distinct
136
- # +_synthesizer+ suffix on the id — same +_+ separator the
137
- # sub-agent generator uses, so main becomes +"synthesizer"+
138
- # and a sub-agent +"researcher 0"+ becomes
139
- # +"researcher 0_synthesizer"+. Any +TokenLog+ in the list
140
- # tags the synth's prompt under that bracket so it's
141
- # obvious from the log which turns were the rescue rather
142
- # than the original loop.
101
+ # Synth runs under this agent's identity with a distinct +_synthesizer+
102
+ # id suffix (same +_+ separator the sub-agent generator uses), so a
103
+ # +TokenLog+ tags its turns as the rescue, not the loop.
143
104
  synth_id = ctx.agent.id.empty? ? 'synthesizer' : "#{ctx.agent.id}_synthesizer"
144
105
  synth = Agent.new(
145
- # Carry the parent's resolved cap on the transport so the synth
146
- # reuses it without a re-probe — the cap rides {ChatTransport}
147
- # now, not an +Agent.new(context_window:)+ kwarg.
106
+ # Carry the parent's resolved cap on the transport so the synth reuses
107
+ # it without a re-probe (the cap rides {ChatTransport}).
148
108
  transport: ctx.agent.transport.with(context_window: ctx.agent.context_window_cap),
149
109
  system_prompt: Synthesizer::SYSTEM_PROMPT,
150
- # Defensive budget with the default :raise policy: the
151
- # synth has no tools so it should never tick, but a buggy
152
- # provider that somehow returns a tool call must not loop
153
- # forever — and a synth that needs its own synth is a bug,
154
- # not a rescue.
110
+ # Defensive :raise budget: the synth has no tools so should never
111
+ # tick, but a buggy provider returning a tool call must not loop — a
112
+ # synth that needs its own synth is a bug, not a rescue.
155
113
  step_limit: Control::StepLimit.new(max: 1),
156
114
  cancellable: ctx.agent.cancellable,
157
115
  id: synth_id,