pocketrb 0.1.0 → 0.3.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +98 -17
  4. data/lib/pocketrb/agent/compaction.rb +235 -22
  5. data/lib/pocketrb/agent/context.rb +26 -3
  6. data/lib/pocketrb/agent/loop.rb +53 -5
  7. data/lib/pocketrb/agent/spawn_tool.rb +14 -0
  8. data/lib/pocketrb/agent/subagent_manager.rb +24 -2
  9. data/lib/pocketrb/bus/events.rb +42 -1
  10. data/lib/pocketrb/bus/message_bus.rb +37 -0
  11. data/lib/pocketrb/channels/base.rb +13 -0
  12. data/lib/pocketrb/channels/cli.rb +16 -1
  13. data/lib/pocketrb/channels/telegram.rb +70 -4
  14. data/lib/pocketrb/channels/whatsapp.rb +49 -7
  15. data/lib/pocketrb/cli/chat.rb +7 -0
  16. data/lib/pocketrb/cli/config.rb +4 -0
  17. data/lib/pocketrb/cli/cron.rb +6 -0
  18. data/lib/pocketrb/cli.rb +0 -1
  19. data/lib/pocketrb/config.rb +55 -6
  20. data/lib/pocketrb/cron/job.rb +41 -0
  21. data/lib/pocketrb/cron/service.rb +44 -15
  22. data/lib/pocketrb/heartbeat/service.rb +21 -0
  23. data/lib/pocketrb/mcp/client.rb +41 -3
  24. data/lib/pocketrb/mcp/memory_tool.rb +33 -0
  25. data/lib/pocketrb/media/processor.rb +32 -1
  26. data/lib/pocketrb/memory.rb +12 -3
  27. data/lib/pocketrb/planning/manager.rb +29 -1
  28. data/lib/pocketrb/planning/plan.rb +52 -0
  29. data/lib/pocketrb/planning/tool.rb +45 -0
  30. data/lib/pocketrb/providers/anthropic.rb +113 -9
  31. data/lib/pocketrb/providers/base.rb +39 -4
  32. data/lib/pocketrb/providers/claude_cli.rb +79 -7
  33. data/lib/pocketrb/providers/claude_max_proxy.rb +72 -6
  34. data/lib/pocketrb/providers/openai_compatible.rb +79 -0
  35. data/lib/pocketrb/providers/openrouter.rb +72 -12
  36. data/lib/pocketrb/providers/registry.rb +24 -0
  37. data/lib/pocketrb/providers/ruby_llm_provider.rb +168 -54
  38. data/lib/pocketrb/providers/types.rb +51 -1
  39. data/lib/pocketrb/session/manager.rb +34 -2
  40. data/lib/pocketrb/session/session.rb +53 -11
  41. data/lib/pocketrb/skills/builtin/tmux/SKILL.md +9 -9
  42. data/lib/pocketrb/skills/create_tool.rb +28 -0
  43. data/lib/pocketrb/skills/loader.rb +22 -1
  44. data/lib/pocketrb/skills/modify_tool.rb +22 -0
  45. data/lib/pocketrb/skills/skill.rb +19 -3
  46. data/lib/pocketrb/tools/background_job_manager.rb +29 -0
  47. data/lib/pocketrb/tools/base.rb +16 -1
  48. data/lib/pocketrb/tools/browser.rb +40 -0
  49. data/lib/pocketrb/tools/browser_advanced.rb +94 -4
  50. data/lib/pocketrb/tools/browser_session.rb +18 -0
  51. data/lib/pocketrb/tools/cron.rb +46 -0
  52. data/lib/pocketrb/tools/edit_file.rb +16 -0
  53. data/lib/pocketrb/tools/exec.rb +46 -0
  54. data/lib/pocketrb/tools/jobs.rb +26 -2
  55. data/lib/pocketrb/tools/list_dir.rb +19 -0
  56. data/lib/pocketrb/tools/memory.rb +34 -0
  57. data/lib/pocketrb/tools/message.rb +15 -0
  58. data/lib/pocketrb/tools/para_memory.rb +47 -0
  59. data/lib/pocketrb/tools/read_file.rb +11 -0
  60. data/lib/pocketrb/tools/registry.rb +12 -4
  61. data/lib/pocketrb/tools/send_file.rb +29 -0
  62. data/lib/pocketrb/tools/think.rb +9 -0
  63. data/lib/pocketrb/tools/web_fetch.rb +58 -3
  64. data/lib/pocketrb/tools/web_search.rb +25 -0
  65. data/lib/pocketrb/tools/write_file.rb +10 -0
  66. data/lib/pocketrb/version.rb +2 -1
  67. data/lib/pocketrb.rb +4 -1
  68. data/pocketrb.gemspec +6 -8
  69. metadata +24 -44
@@ -4,6 +4,7 @@ module Pocketrb
4
4
  module Agent
5
5
  # Builds context for LLM requests
6
6
  class Context
7
+ # Tool usage guidelines for LLM system prompts
7
8
  TOOL_GUIDELINES = <<~PROMPT
8
9
  ## Tool Usage Guidelines
9
10
 
@@ -18,12 +19,17 @@ module Pocketrb
18
19
  - Search memory when the user asks about something you may have learned before
19
20
  PROMPT
20
21
 
22
+ # Default agent identity for system prompts
21
23
  DEFAULT_IDENTITY = <<~PROMPT
22
24
  You are Pocketrb, an AI assistant with access to tools for interacting with files, executing commands, and searching the web.
23
25
  PROMPT
24
26
 
25
27
  attr_reader :system_prompt, :workspace, :skills_summary
26
28
 
29
+ # Initialize a new context builder
30
+ # @param workspace [Pathname, nil] Path to workspace directory for loading context files
31
+ # @param system_prompt [String, nil] Custom system prompt (defaults to generated base prompt)
32
+ # @param skills_summary [String, nil] Summary of available skills to include in prompts
27
33
  def initialize(workspace: nil, system_prompt: nil, skills_summary: nil)
28
34
  @workspace = workspace
29
35
  @skills_summary = skills_summary
@@ -32,6 +38,8 @@ module Pocketrb
32
38
 
33
39
  private
34
40
 
41
+ # Assemble the base system prompt from identity, tool guidelines, and memory files
42
+ # @return [String] combined system prompt text
35
43
  def build_base_prompt
36
44
  parts = []
37
45
 
@@ -49,6 +57,9 @@ module Pocketrb
49
57
  parts.join("\n\n")
50
58
  end
51
59
 
60
+ # Read a file from the workspace directory, returning nil if missing or empty
61
+ # @param filename [String] name of the file relative to workspace root
62
+ # @return [String, nil] file contents or nil when unavailable
52
63
  def load_workspace_file(filename)
53
64
  return nil unless @workspace
54
65
 
@@ -68,7 +79,7 @@ module Pocketrb
68
79
  # @param history [Array<Message>] Conversation history
69
80
  # @param current [String] Current user message
70
81
  # @param media [Array<Bus::Media>] Media attachments
71
- # @param memory_context [String|nil] Memory context from Memory system
82
+ # @param memory_context [String, nil] Memory context from Memory system
72
83
  # @return [Array<Message>]
73
84
  def build_messages(history:, current:, media: nil, memory_context: nil)
74
85
  messages = []
@@ -87,6 +98,7 @@ module Pocketrb
87
98
 
88
99
  # Build messages for continuing after tool execution
89
100
  # @param history [Array<Message>] Full history including tool results
101
+ # @param memory_context [String, nil] Memory context from Memory system
90
102
  # @return [Array<Message>]
91
103
  def build_continuation(history:, memory_context: nil)
92
104
  messages = []
@@ -96,24 +108,32 @@ module Pocketrb
96
108
  end
97
109
 
98
110
  # Update system prompt
111
+ # @param prompt [String] New system prompt to use
112
+ # @return [void]
99
113
  def update_system_prompt(prompt)
100
114
  @system_prompt = prompt
101
115
  end
102
116
 
103
117
  # Add to system prompt
118
+ # @param content [String] Additional content to append to system prompt
119
+ # @return [void]
104
120
  def append_to_system_prompt(content)
105
121
  @system_prompt = "#{@system_prompt}\n\n#{content}"
106
122
  end
107
123
 
108
124
  # Update skills summary
125
+ # @param summary [String] New skills summary describing available tools/capabilities
126
+ # @return [void]
109
127
  def update_skills_summary(summary)
110
128
  @skills_summary = summary
111
129
  end
112
130
 
113
131
  private
114
132
 
115
- # Strip media from history messages to prevent context bloat
116
- # Only the current message should include images
133
+ # Strip media from history messages to prevent context bloat.
134
+ # Only the current message should include images.
135
+ # @param history [Array<Message>] past conversation messages potentially containing media blocks
136
+ # @return [Array<Message>] new messages with media replaced by text placeholders
117
137
  def strip_media_from_history(history)
118
138
  history.map do |msg|
119
139
  content = msg.content
@@ -149,6 +169,9 @@ module Pocketrb
149
169
  end
150
170
  end
151
171
 
172
+ # Construct the system message combining prompt, workspace info, skills, and memory
173
+ # @param memory_context [String, nil] retrieved memory facts to inject into the system message
174
+ # @return [Message] system-role message for LLM requests
152
175
  def build_system_message(memory_context = nil)
153
176
  parts = [@system_prompt]
154
177
 
@@ -10,6 +10,19 @@ module Pocketrb
10
10
  attr_reader :bus, :provider, :tools, :sessions, :context, :model, :max_iterations, :workspace, :memory_dir,
11
11
  :memory, :compaction
12
12
 
13
+ # Initialize the agent loop
14
+ # @param bus [Bus::MessageBus] Message bus for communication
15
+ # @param provider [Object] LLM provider instance
16
+ # @param workspace [String, Pathname] Working directory path
17
+ # @param memory_dir [String, Pathname, nil] Directory for memory and session storage (defaults to workspace)
18
+ # @param model [String, nil] Model name to use (defaults to provider default)
19
+ # @param max_iterations [Integer] Maximum tool execution iterations per message
20
+ # @param system_prompt [String, nil] Custom system prompt (defaults to auto-generated)
21
+ # @param mcp_endpoint [String, nil] MCP server endpoint URL (currently unused)
22
+ # @param enable_compaction [Boolean] Whether to enable context compaction
23
+ # @param compaction_threshold [Integer, nil] Message count threshold for compaction
24
+ # @param compaction_model [String, nil] Model to use for compaction summaries (defaults to main model)
25
+ # @param compaction_pressure [Float, nil] Context pressure threshold for compaction (0.0-1.0)
13
26
  def initialize(
14
27
  bus:,
15
28
  provider:,
@@ -20,7 +33,9 @@ module Pocketrb
20
33
  system_prompt: nil,
21
34
  mcp_endpoint: nil,
22
35
  enable_compaction: true,
23
- compaction_threshold: nil
36
+ compaction_threshold: nil,
37
+ compaction_model: nil,
38
+ compaction_pressure: nil
24
39
  )
25
40
  @bus = bus
26
41
  @provider = provider
@@ -53,8 +68,10 @@ module Pocketrb
53
68
  @compaction = if enable_compaction
54
69
  Compaction.new(
55
70
  provider: @provider,
56
- model: @model,
57
- message_threshold: compaction_threshold
71
+ model: compaction_model || @model,
72
+ message_threshold: compaction_threshold,
73
+ context_window: @provider.context_window(model: @model),
74
+ context_pressure: compaction_pressure
58
75
  )
59
76
  end
60
77
 
@@ -83,12 +100,13 @@ module Pocketrb
83
100
  # Stop the agent loop
84
101
  def stop
85
102
  @running = false
103
+ @compaction&.wait_for_compaction(timeout: 10)
86
104
  Pocketrb.logger.info("Agent loop stopping")
87
105
  end
88
106
 
89
107
  # Process a single message (synchronous, for direct use)
90
108
  # @param msg [Bus::InboundMessage]
91
- # @return [Bus::OutboundMessage|nil]
109
+ # @return [Bus::OutboundMessage, nil]
92
110
  def process_message(msg)
93
111
  # Update tools context with current channel info for message/send_file tools
94
112
  @tools.update_context(
@@ -103,7 +121,14 @@ module Pocketrb
103
121
 
104
122
  # Compact session history if needed (before building messages)
105
123
  if @compaction&.needs_compaction?(session.messages)
106
- @compaction.compact_session!(session)
124
+ @compaction.schedule_compaction(session)
125
+
126
+ # Wait for compaction to finish; fall back to synchronous if timeout
127
+ unless @compaction.wait_for_compaction(timeout: 30)
128
+ Pocketrb.logger.warn("Background compaction timed out, falling back to synchronous")
129
+ @compaction.compact_session!(session)
130
+ end
131
+
107
132
  @sessions.save(session)
108
133
  end
109
134
 
@@ -165,17 +190,26 @@ module Pocketrb
165
190
  end
166
191
 
167
192
  # Register an additional tool
193
+ # @param tool [Tools::Base] Tool instance to add to the registry
194
+ # @return [void]
168
195
  def register_tool(tool)
169
196
  @tools.register(tool)
170
197
  end
171
198
 
172
199
  # Update the system prompt
200
+ # @param prompt [String] New system prompt to use for subsequent messages
201
+ # @return [void]
173
202
  def update_system_prompt(prompt)
174
203
  @context.update_system_prompt(prompt)
175
204
  end
176
205
 
177
206
  private
178
207
 
208
+ # Execute all tool calls from an LLM response and append results to the message list
209
+ # @param session [Session::Session] active session to persist tool results into
210
+ # @param messages [Array<Message>] current message list to extend with assistant and tool messages
211
+ # @param response [Providers::LLMResponse] LLM response containing tool calls to execute
212
+ # @return [Array<Message>] updated messages with tool call results appended
179
213
  def execute_tool_calls(session, messages, response)
180
214
  # Add assistant message with tool calls
181
215
  assistant_msg = Providers::Message.assistant(
@@ -231,6 +265,12 @@ module Pocketrb
231
265
  messages
232
266
  end
233
267
 
268
+ # Publish a tool execution event to the message bus
269
+ # @param tool_call [Providers::ToolCall] the tool call that was executed
270
+ # @param result [String, nil] successful execution output, nil on failure
271
+ # @param error [String, nil] error message if execution failed, nil on success
272
+ # @param duration_ms [Integer] wall-clock execution time in milliseconds
273
+ # @return [void]
234
274
  def publish_tool_event(tool_call, result, error, duration_ms)
235
275
  event = Bus::ToolExecution.new(
236
276
  tool_call_id: tool_call.id,
@@ -243,6 +283,12 @@ module Pocketrb
243
283
  @bus.publish_tool_event(event)
244
284
  end
245
285
 
286
+ # Publish a session state transition event to the message bus
287
+ # @param session_key [String] identifier of the session changing state
288
+ # @param from [Symbol] previous state (e.g. :idle, :processing)
289
+ # @param to [Symbol] new state being transitioned to
290
+ # @param reason [String, nil] optional explanation for the state change
291
+ # @return [void]
246
292
  def publish_state_change(session_key, from, to, reason = nil)
247
293
  event = Bus::StateChange.new(
248
294
  session_key: session_key,
@@ -265,6 +311,8 @@ module Pocketrb
265
311
  end
266
312
 
267
313
  # Load skills triggered by a message (for context-aware skill loading)
314
+ # @param text [String] user message text to match against skill trigger patterns
315
+ # @return [String] concatenated prompt content from matched skills, empty if none match
268
316
  def load_triggered_skills(text)
269
317
  triggered = @skills_loader.get_triggered_skills(text)
270
318
  return "" if triggered.empty?
@@ -4,14 +4,20 @@ module Pocketrb
4
4
  module Agent
5
5
  # Tool for spawning subagents
6
6
  class SpawnTool < Tools::Base
7
+ # Tool name
8
+ # @return [String] Tool name identifier
7
9
  def name
8
10
  "spawn"
9
11
  end
10
12
 
13
+ # Tool description
14
+ # @return [String] Human-readable description
11
15
  def description
12
16
  "Spawn a subagent to work on a specific task in the background. Useful for delegating independent work or parallel processing."
13
17
  end
14
18
 
19
+ # Tool parameters schema
20
+ # @return [Hash] JSON schema for tool parameters
15
21
  def parameters
16
22
  {
17
23
  type: "object",
@@ -38,10 +44,18 @@ module Pocketrb
38
44
  }
39
45
  end
40
46
 
47
+ # Check if tool is available (requires subagent manager)
48
+ # @return [Boolean] True if subagent manager is available
41
49
  def available?
42
50
  @context[:subagent_manager] != nil
43
51
  end
44
52
 
53
+ # Execute the spawn tool
54
+ # @param task [String] Task description for the subagent to complete
55
+ # @param skills [Array<String>] List of skill names to load for the subagent
56
+ # @param wait [Boolean] Whether to block until subagent completes
57
+ # @param timeout [Integer] Maximum wait time in seconds when waiting
58
+ # @return [String] Success or error message
45
59
  def execute(task:, skills: [], wait: false, timeout: 300)
46
60
  manager = @context[:subagent_manager]
47
61
  return error("Subagent spawning not available") unless manager
@@ -8,6 +8,8 @@ module Pocketrb
8
8
  class SubagentManager
9
9
  attr_reader :active_agents
10
10
 
11
+ # Initialize the subagent manager
12
+ # @param parent_loop [Loop] Parent agent loop instance that spawns subagents
11
13
  def initialize(parent_loop:)
12
14
  @parent_loop = parent_loop
13
15
  @active_agents = {}
@@ -51,7 +53,7 @@ module Pocketrb
51
53
 
52
54
  # Get status of a subagent
53
55
  # @param agent_id [String]
54
- # @return [Hash|nil]
56
+ # @return [Hash, nil]
55
57
  def get_status(agent_id)
56
58
  @mutex.synchronize { @active_agents[agent_id]&.dup }
57
59
  end
@@ -75,7 +77,7 @@ module Pocketrb
75
77
  # Wait for a subagent to complete
76
78
  # @param agent_id [String]
77
79
  # @param timeout [Integer] Timeout in seconds
78
- # @return [String|nil] Result
80
+ # @return [String, nil] Result
79
81
  def wait_for(agent_id, timeout: 300)
80
82
  deadline = Time.now + timeout
81
83
 
@@ -100,6 +102,10 @@ module Pocketrb
100
102
 
101
103
  private
102
104
 
105
+ # Execute a subagent lifecycle: create an isolated loop, process the task, and report the result
106
+ # @param agent_id [String]
107
+ # @param info [Hash] agent configuration containing :task, :model, :skills, and origin details
108
+ # @return [void]
103
109
  def run_agent(agent_id, info)
104
110
  update_status(agent_id, :running)
105
111
 
@@ -144,6 +150,11 @@ module Pocketrb
144
150
  announce_result(info, "Subagent failed: #{e.message}")
145
151
  end
146
152
 
153
+ # Update the tracked status and optional result for a subagent
154
+ # @param agent_id [String]
155
+ # @param status [Symbol] new status (e.g. :running, :completed, :failed)
156
+ # @param result [String, nil] output or error message to store
157
+ # @return [void]
147
158
  def update_status(agent_id, status, result: nil)
148
159
  @mutex.synchronize do
149
160
  if @active_agents[agent_id]
@@ -154,6 +165,9 @@ module Pocketrb
154
165
  end
155
166
  end
156
167
 
168
+ # Generate a focused system prompt instructing the subagent on its assigned task
169
+ # @param info [Hash] agent configuration containing the :task description
170
+ # @return [String] system prompt for the subagent
157
171
  def build_subagent_prompt(info)
158
172
  <<~PROMPT
159
173
  You are a specialized subagent spawned to complete a specific task.
@@ -169,6 +183,10 @@ module Pocketrb
169
183
  PROMPT
170
184
  end
171
185
 
186
+ # Load named skills into a subagent loop's system prompt
187
+ # @param loop [Loop] subagent loop instance to receive skill prompts
188
+ # @param skill_names [Array<String>] skill names to look up and inject
189
+ # @return [void]
172
190
  def load_skills(loop, skill_names)
173
191
  return if skill_names.empty?
174
192
 
@@ -181,6 +199,10 @@ module Pocketrb
181
199
  loop.context.append_to_system_prompt(skill_content) unless skill_content.empty?
182
200
  end
183
201
 
202
+ # Send the subagent's result back to the originating channel via the parent message bus
203
+ # @param info [Hash] agent configuration with :origin_channel, :origin_chat_id, and :id
204
+ # @param result [String] completion message to deliver
205
+ # @return [void]
184
206
  def announce_result(info, result)
185
207
  outbound = Bus::OutboundMessage.new(
186
208
  channel: info[:origin_channel],
@@ -12,6 +12,13 @@ module Pocketrb
12
12
  :media, # Array<Media> - attached media (images, files, etc.)
13
13
  :metadata # Hash - channel-specific metadata
14
14
  ) do
15
+ # Initialize an inbound message
16
+ # @param channel [Symbol] Channel identifier (e.g., :cli, :telegram, :discord)
17
+ # @param sender_id [String] User or system identifier that sent this message
18
+ # @param chat_id [String] Conversation or thread identifier for session tracking
19
+ # @param content [String] Text content of the message
20
+ # @param media [Array<Media>] Array of media attachments (defaults to empty)
21
+ # @param metadata [Hash] Channel-specific metadata (defaults to empty hash)
15
22
  def initialize(channel:, sender_id:, chat_id:, content:, media: [], metadata: {})
16
23
  super
17
24
  end
@@ -22,8 +29,9 @@ module Pocketrb
22
29
  end
23
30
 
24
31
  # Check if message has media attachments
32
+ # @return [Boolean] true if media is present
25
33
  def has_media?
26
- media && !media.empty?
34
+ !media.nil? && !media.empty?
27
35
  end
28
36
  end
29
37
 
@@ -36,6 +44,13 @@ module Pocketrb
36
44
  :reply_to, # String|nil - message ID to reply to
37
45
  :metadata # Hash - channel-specific options
38
46
  ) do
47
+ # Initialize an outbound message
48
+ # @param channel [Symbol] Target channel identifier
49
+ # @param chat_id [String] Target chat or conversation identifier
50
+ # @param content [String] Text content to send
51
+ # @param media [Array<Media>] Array of media to attach (defaults to empty)
52
+ # @param reply_to [String, nil] Message ID to reply to (optional)
53
+ # @param metadata [Hash] Channel-specific options (defaults to empty hash)
39
54
  def initialize(channel:, chat_id:, content:, media: [], reply_to: nil, metadata: {})
40
55
  super
41
56
  end
@@ -49,14 +64,24 @@ module Pocketrb
49
64
  :filename, # String|nil - original filename
50
65
  :data # String|nil - base64 encoded data (for inline media)
51
66
  ) do
67
+ # Initialize a media attachment
68
+ # @param type [Symbol] Media type (:image, :file, :audio, or :video)
69
+ # @param path [String] File system path or URL to the media
70
+ # @param mime_type [String] MIME type of the media (e.g., "image/png")
71
+ # @param filename [String, nil] Original filename (optional)
72
+ # @param data [String, nil] Base64 encoded data for inline media (optional)
52
73
  def initialize(type:, path:, mime_type:, filename: nil, data: nil)
53
74
  super
54
75
  end
55
76
 
77
+ # Check if media is an image type
78
+ # @return [Boolean] true if type is :image
56
79
  def image?
57
80
  type == :image
58
81
  end
59
82
 
83
+ # Check if media is a file type
84
+ # @return [Boolean] true if type is :file
60
85
  def file?
61
86
  type == :file
62
87
  end
@@ -71,14 +96,25 @@ module Pocketrb
71
96
  :error, # String|nil - error message if failed
72
97
  :duration_ms # Integer|nil - execution time in milliseconds
73
98
  ) do
99
+ # Initialize a tool execution event
100
+ # @param tool_call_id [String] LLM-generated identifier linking this execution to the request
101
+ # @param name [String] Name of the tool being executed
102
+ # @param arguments [Hash] Arguments passed to the tool
103
+ # @param result [String, nil] Execution result if successful (optional)
104
+ # @param error [String, nil] Error message if execution failed (optional)
105
+ # @param duration_ms [Integer, nil] Execution duration in milliseconds (optional)
74
106
  def initialize(tool_call_id:, name:, arguments:, result: nil, error: nil, duration_ms: nil)
75
107
  super
76
108
  end
77
109
 
110
+ # Check if tool execution succeeded
111
+ # @return [Boolean] true if no error occurred
78
112
  def success?
79
113
  error.nil?
80
114
  end
81
115
 
116
+ # Check if tool execution failed
117
+ # @return [Boolean] true if an error occurred
82
118
  def failed?
83
119
  !success?
84
120
  end
@@ -91,6 +127,11 @@ module Pocketrb
91
127
  :to_state, # Symbol - new state
92
128
  :reason # String|nil - reason for change
93
129
  ) do
130
+ # Initialize a state change event
131
+ # @param session_key [String] Session identifier for the agent
132
+ # @param from_state [Symbol] Previous state the agent was in
133
+ # @param to_state [Symbol] New state the agent is transitioning to
134
+ # @param reason [String, nil] Optional reason for the state change
94
135
  def initialize(session_key:, from_state:, to_state:, reason: nil)
95
136
  super
96
137
  end
@@ -21,6 +21,9 @@ module Pocketrb
21
21
  end
22
22
 
23
23
  # Publish an inbound message from a channel
24
+ # @param message [InboundMessage] Message received from a channel
25
+ # @return [void]
26
+ # @raise [ArgumentError] if message is not an InboundMessage
24
27
  def publish_inbound(message)
25
28
  raise ArgumentError, "Expected InboundMessage" unless message.is_a?(InboundMessage)
26
29
 
@@ -35,6 +38,9 @@ module Pocketrb
35
38
  end
36
39
 
37
40
  # Publish an outbound message to a channel
41
+ # @param message [OutboundMessage] Message to send to a channel
42
+ # @return [void]
43
+ # @raise [ArgumentError] if message is not an OutboundMessage
38
44
  def publish_outbound(message)
39
45
  raise ArgumentError, "Expected OutboundMessage" unless message.is_a?(OutboundMessage)
40
46
 
@@ -49,6 +55,9 @@ module Pocketrb
49
55
  end
50
56
 
51
57
  # Publish a tool execution event
58
+ # @param event [ToolExecution] Tool execution event to publish
59
+ # @return [void]
60
+ # @raise [ArgumentError] if event is not a ToolExecution
52
61
  def publish_tool_event(event)
53
62
  raise ArgumentError, "Expected ToolExecution" unless event.is_a?(ToolExecution)
54
63
 
@@ -63,6 +72,9 @@ module Pocketrb
63
72
  end
64
73
 
65
74
  # Publish a state change event
75
+ # @param event [StateChange] State change event to publish
76
+ # @return [void]
77
+ # @raise [ArgumentError] if event is not a StateChange
66
78
  def publish_state_event(event)
67
79
  raise ArgumentError, "Expected StateChange" unless event.is_a?(StateChange)
68
80
 
@@ -72,6 +84,11 @@ module Pocketrb
72
84
  end
73
85
 
74
86
  # Subscribe to events
87
+ # @param type [Symbol] Event type to subscribe to (:inbound, :outbound, :tool, or :state)
88
+ # @param block [Proc] Block to call when events of this type are published
89
+ # @yieldparam event [Object] Event object passed to the block
90
+ # @return [void]
91
+ # @raise [ArgumentError] if type is not a recognized event type
75
92
  def subscribe(type, &block)
76
93
  raise ArgumentError, "Unknown event type: #{type}" unless @subscribers.key?(type)
77
94
 
@@ -81,6 +98,9 @@ module Pocketrb
81
98
  end
82
99
 
83
100
  # Unsubscribe from events
101
+ # @param type [Symbol] Event type to unsubscribe from
102
+ # @param block [Proc] Block to remove from subscribers
103
+ # @return [void]
84
104
  def unsubscribe(type, block)
85
105
  @mutex.synchronize do
86
106
  @subscribers[type].delete(block)
@@ -88,10 +108,13 @@ module Pocketrb
88
108
  end
89
109
 
90
110
  # Check if queues have pending messages
111
+ # @return [Boolean] true if the inbound queue is not empty
91
112
  def pending_inbound?
92
113
  !@inbound.empty?
93
114
  end
94
115
 
116
+ # Check if there are pending outbound messages
117
+ # @return [Boolean] True if outbound queue is not empty
95
118
  def pending_outbound?
96
119
  !@outbound.empty?
97
120
  end
@@ -107,6 +130,10 @@ module Pocketrb
107
130
 
108
131
  private
109
132
 
133
+ # Notify all subscribers of a given event type
134
+ # @param type [Symbol] event type (:inbound, :outbound, :tool, :state)
135
+ # @param event [Object] event object to broadcast
136
+ # @return [void]
110
137
  def notify_subscribers(type, event)
111
138
  subscribers = @mutex.synchronize { @subscribers[type].dup }
112
139
  subscribers.each do |handler|
@@ -125,20 +152,30 @@ module Pocketrb
125
152
  @mutex = Mutex.new
126
153
  end
127
154
 
155
+ # Increment a statistic counter
156
+ # @param key [Symbol] Counter key to increment
157
+ # @return [void]
128
158
  def increment(key)
129
159
  @mutex.synchronize { @data[key] += 1 }
130
160
  end
131
161
 
162
+ # Retrieve a statistic value
163
+ # @param key [Symbol] Counter key to retrieve
164
+ # @return [Integer] Current counter value
132
165
  def [](key)
133
166
  @mutex.synchronize { @data[key] }
134
167
  end
135
168
 
169
+ # Reset all statistics
170
+ # @return [void]
136
171
  def reset!
137
172
  @mutex.synchronize do
138
173
  @data.transform_values! { 0 }
139
174
  end
140
175
  end
141
176
 
177
+ # Convert stats to hash
178
+ # @return [Hash] Stats as hash
142
179
  def to_h
143
180
  @mutex.synchronize { @data.dup }
144
181
  end
@@ -6,6 +6,9 @@ module Pocketrb
6
6
  class Base
7
7
  attr_reader :bus, :name
8
8
 
9
+ # Initialize channel adapter
10
+ # @param bus [Bus::MessageBus] Message bus for publishing and consuming messages
11
+ # @param name [Symbol, nil] Channel name (defaults to class name)
9
12
  def initialize(bus:, name: nil)
10
13
  @bus = bus
11
14
  @name = name || self.class.name.split("::").last.downcase.to_sym
@@ -25,6 +28,7 @@ module Pocketrb
25
28
  end
26
29
 
27
30
  # Check if channel is running
31
+ # @return [Boolean] true if the channel is running
28
32
  def running?
29
33
  @running
30
34
  end
@@ -37,12 +41,14 @@ module Pocketrb
37
41
  end
38
42
 
39
43
  # Override in subclasses to send outbound messages
44
+ # @param message [Bus::OutboundMessage] outbound message to deliver
40
45
  def send_message(message)
41
46
  raise NotImplementedError
42
47
  end
43
48
 
44
49
  private
45
50
 
51
+ # Consume outbound messages from the bus and dispatch to send_message
46
52
  def start_outbound_consumer
47
53
  Async do
48
54
  while @running
@@ -54,6 +60,13 @@ module Pocketrb
54
60
  end
55
61
  end
56
62
 
63
+ # Build an inbound message struct for publishing to the bus
64
+ # @param sender_id [String]
65
+ # @param chat_id [String] conversation or chat identifier
66
+ # @param content [String] text content of the message
67
+ # @param media [Array<Media::Item>] attached media items
68
+ # @param metadata [Hash] extra channel-specific metadata
69
+ # @return [Bus::InboundMessage]
57
70
  def create_inbound_message(sender_id:, chat_id:, content:, media: [], metadata: {})
58
71
  Bus::InboundMessage.new(
59
72
  channel: @name,
@@ -5,6 +5,9 @@ module Pocketrb
5
5
  module Channels
6
6
  # Interactive CLI channel
7
7
  class CLI < Base
8
+ # Initialize CLI channel
9
+ # @param bus [Bus::MessageBus] Message bus for publishing and consuming messages
10
+ # @param prompt [String] Command prompt string (defaults to "> ")
8
11
  def initialize(bus:, prompt: "> ")
9
12
  super(bus: bus, name: :cli)
10
13
  @prompt = prompt
@@ -13,6 +16,7 @@ module Pocketrb
13
16
 
14
17
  protected
15
18
 
19
+ # Read user input from stdin and publish inbound messages
16
20
  def run_inbound_loop
17
21
  Async do
18
22
  while @running
@@ -37,6 +41,8 @@ module Pocketrb
37
41
  end
38
42
  end
39
43
 
44
+ # Print the outbound message content to stdout
45
+ # @param message [Bus::OutboundMessage] outbound message to display
40
46
  def send_message(message)
41
47
  @output_mutex.synchronize do
42
48
  puts "\n#{format_output(message.content)}\n"
@@ -45,6 +51,8 @@ module Pocketrb
45
51
 
46
52
  private
47
53
 
54
+ # Read a single line from stdin, returning nil on EOF or interrupt
55
+ # @return [String, nil]
48
56
  def read_input
49
57
  line = $stdin.gets
50
58
  return nil if line.nil?
@@ -54,6 +62,9 @@ module Pocketrb
54
62
  nil
55
63
  end
56
64
 
65
+ # Process built-in CLI commands (exit, help, clear, stats)
66
+ # @param input [String] raw user input to check for commands
67
+ # @return [Boolean] true if input was a recognized command
57
68
  def handle_command(input)
58
69
  case input.downcase
59
70
  when "exit", "quit", "/exit", "/quit"
@@ -74,6 +85,7 @@ module Pocketrb
74
85
  end
75
86
  end
76
87
 
88
+ # Display available CLI commands
77
89
  def print_help
78
90
  puts <<~HELP
79
91
 
@@ -86,6 +98,7 @@ module Pocketrb
86
98
  HELP
87
99
  end
88
100
 
101
+ # Display message bus statistics
89
102
  def print_stats
90
103
  stats = @bus.stats.to_h
91
104
  puts <<~STATS
@@ -98,10 +111,12 @@ module Pocketrb
98
111
  STATS
99
112
  end
100
113
 
114
+ # Format message content for terminal display
115
+ # @param content [String, nil] raw message content
116
+ # @return [String] formatted output string
101
117
  def format_output(content)
102
118
  return "" if content.nil? || content.empty?
103
119
 
104
- # Simple formatting - could be enhanced with tty-markdown
105
120
  content
106
121
  end
107
122
  end