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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +98 -17
- data/lib/pocketrb/agent/compaction.rb +235 -22
- data/lib/pocketrb/agent/context.rb +26 -3
- data/lib/pocketrb/agent/loop.rb +53 -5
- data/lib/pocketrb/agent/spawn_tool.rb +14 -0
- data/lib/pocketrb/agent/subagent_manager.rb +24 -2
- data/lib/pocketrb/bus/events.rb +42 -1
- data/lib/pocketrb/bus/message_bus.rb +37 -0
- data/lib/pocketrb/channels/base.rb +13 -0
- data/lib/pocketrb/channels/cli.rb +16 -1
- data/lib/pocketrb/channels/telegram.rb +70 -4
- data/lib/pocketrb/channels/whatsapp.rb +49 -7
- data/lib/pocketrb/cli/chat.rb +7 -0
- data/lib/pocketrb/cli/config.rb +4 -0
- data/lib/pocketrb/cli/cron.rb +6 -0
- data/lib/pocketrb/cli.rb +0 -1
- data/lib/pocketrb/config.rb +55 -6
- data/lib/pocketrb/cron/job.rb +41 -0
- data/lib/pocketrb/cron/service.rb +44 -15
- data/lib/pocketrb/heartbeat/service.rb +21 -0
- data/lib/pocketrb/mcp/client.rb +41 -3
- data/lib/pocketrb/mcp/memory_tool.rb +33 -0
- data/lib/pocketrb/media/processor.rb +32 -1
- data/lib/pocketrb/memory.rb +12 -3
- data/lib/pocketrb/planning/manager.rb +29 -1
- data/lib/pocketrb/planning/plan.rb +52 -0
- data/lib/pocketrb/planning/tool.rb +45 -0
- data/lib/pocketrb/providers/anthropic.rb +113 -9
- data/lib/pocketrb/providers/base.rb +39 -4
- data/lib/pocketrb/providers/claude_cli.rb +79 -7
- data/lib/pocketrb/providers/claude_max_proxy.rb +72 -6
- data/lib/pocketrb/providers/openai_compatible.rb +79 -0
- data/lib/pocketrb/providers/openrouter.rb +72 -12
- data/lib/pocketrb/providers/registry.rb +24 -0
- data/lib/pocketrb/providers/ruby_llm_provider.rb +168 -54
- data/lib/pocketrb/providers/types.rb +51 -1
- data/lib/pocketrb/session/manager.rb +34 -2
- data/lib/pocketrb/session/session.rb +53 -11
- data/lib/pocketrb/skills/builtin/tmux/SKILL.md +9 -9
- data/lib/pocketrb/skills/create_tool.rb +28 -0
- data/lib/pocketrb/skills/loader.rb +22 -1
- data/lib/pocketrb/skills/modify_tool.rb +22 -0
- data/lib/pocketrb/skills/skill.rb +19 -3
- data/lib/pocketrb/tools/background_job_manager.rb +29 -0
- data/lib/pocketrb/tools/base.rb +16 -1
- data/lib/pocketrb/tools/browser.rb +40 -0
- data/lib/pocketrb/tools/browser_advanced.rb +94 -4
- data/lib/pocketrb/tools/browser_session.rb +18 -0
- data/lib/pocketrb/tools/cron.rb +46 -0
- data/lib/pocketrb/tools/edit_file.rb +16 -0
- data/lib/pocketrb/tools/exec.rb +46 -0
- data/lib/pocketrb/tools/jobs.rb +26 -2
- data/lib/pocketrb/tools/list_dir.rb +19 -0
- data/lib/pocketrb/tools/memory.rb +34 -0
- data/lib/pocketrb/tools/message.rb +15 -0
- data/lib/pocketrb/tools/para_memory.rb +47 -0
- data/lib/pocketrb/tools/read_file.rb +11 -0
- data/lib/pocketrb/tools/registry.rb +12 -4
- data/lib/pocketrb/tools/send_file.rb +29 -0
- data/lib/pocketrb/tools/think.rb +9 -0
- data/lib/pocketrb/tools/web_fetch.rb +58 -3
- data/lib/pocketrb/tools/web_search.rb +25 -0
- data/lib/pocketrb/tools/write_file.rb +10 -0
- data/lib/pocketrb/version.rb +2 -1
- data/lib/pocketrb.rb +4 -1
- data/pocketrb.gemspec +6 -8
- 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
|
|
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
|
|
data/lib/pocketrb/agent/loop.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
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],
|
data/lib/pocketrb/bus/events.rb
CHANGED
|
@@ -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
|