prompt_objects 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.
- checksums.yaml +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module MCP
|
|
7
|
+
# MCP Server exposing PromptObjects functionality
|
|
8
|
+
# This allows any MCP client (Claude Desktop, Go TUI, etc.) to interact with POs
|
|
9
|
+
class Server
|
|
10
|
+
attr_reader :env, :mcp_server
|
|
11
|
+
|
|
12
|
+
def initialize(objects_dir: "objects", primitives_dir: nil)
|
|
13
|
+
@objects_dir = objects_dir
|
|
14
|
+
@primitives_dir = primitives_dir
|
|
15
|
+
@env = nil
|
|
16
|
+
@context = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
setup_environment
|
|
21
|
+
setup_mcp_server
|
|
22
|
+
run_stdio_transport
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def setup_environment
|
|
28
|
+
@env = Environment.new(
|
|
29
|
+
objects_dir: @objects_dir,
|
|
30
|
+
primitives_dir: @primitives_dir
|
|
31
|
+
)
|
|
32
|
+
@context = @env.context(tui_mode: true)
|
|
33
|
+
|
|
34
|
+
load_all_objects
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_all_objects
|
|
38
|
+
return unless Dir.exist?(@objects_dir)
|
|
39
|
+
|
|
40
|
+
Dir.glob(File.join(@objects_dir, "*.md")).each do |path|
|
|
41
|
+
@env.load_prompt_object(path)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
warn "Failed to load #{path}: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@env.registry.prompt_objects.each do |po|
|
|
47
|
+
@env.load_dependencies(po)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def setup_mcp_server
|
|
52
|
+
@mcp_server = ::MCP::Server.new(
|
|
53
|
+
name: "prompt_objects",
|
|
54
|
+
version: "0.1.0",
|
|
55
|
+
tools: build_tools,
|
|
56
|
+
server_context: { env: @env, context: @context }
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
setup_resource_handlers
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_tools
|
|
63
|
+
[
|
|
64
|
+
Tools::ListPromptObjects,
|
|
65
|
+
Tools::SendMessage,
|
|
66
|
+
Tools::GetConversation,
|
|
67
|
+
Tools::GetPendingRequests,
|
|
68
|
+
Tools::RespondToRequest,
|
|
69
|
+
Tools::InspectPO
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def setup_resource_handlers
|
|
74
|
+
@mcp_server.resources_read_handler do |params|
|
|
75
|
+
handle_resource_read(params)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_resource_read(params)
|
|
80
|
+
uri = params[:uri]
|
|
81
|
+
|
|
82
|
+
case uri
|
|
83
|
+
when %r{^po://([^/]+)/conversation$}
|
|
84
|
+
po_name = ::Regexp.last_match(1)
|
|
85
|
+
read_conversation(po_name)
|
|
86
|
+
when %r{^po://([^/]+)/config$}
|
|
87
|
+
po_name = ::Regexp.last_match(1)
|
|
88
|
+
read_config(po_name)
|
|
89
|
+
when %r{^po://([^/]+)/prompt$}
|
|
90
|
+
po_name = ::Regexp.last_match(1)
|
|
91
|
+
read_prompt(po_name)
|
|
92
|
+
when "bus://messages"
|
|
93
|
+
read_bus_messages
|
|
94
|
+
else
|
|
95
|
+
[{ uri: uri, mimeType: "text/plain", text: "Unknown resource: #{uri}" }]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_conversation(po_name)
|
|
100
|
+
po = @env.registry.get(po_name)
|
|
101
|
+
return [{ uri: "po://#{po_name}/conversation", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
102
|
+
|
|
103
|
+
history = po.history.map do |msg|
|
|
104
|
+
{ role: msg[:role].to_s, content: msg[:content] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[{
|
|
108
|
+
uri: "po://#{po_name}/conversation",
|
|
109
|
+
mimeType: "application/json",
|
|
110
|
+
text: JSON.pretty_generate(history)
|
|
111
|
+
}]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_config(po_name)
|
|
115
|
+
po = @env.registry.get(po_name)
|
|
116
|
+
return [{ uri: "po://#{po_name}/config", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
117
|
+
|
|
118
|
+
[{
|
|
119
|
+
uri: "po://#{po_name}/config",
|
|
120
|
+
mimeType: "application/json",
|
|
121
|
+
text: JSON.pretty_generate(po.config)
|
|
122
|
+
}]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read_prompt(po_name)
|
|
126
|
+
po = @env.registry.get(po_name)
|
|
127
|
+
return [{ uri: "po://#{po_name}/prompt", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
128
|
+
|
|
129
|
+
[{
|
|
130
|
+
uri: "po://#{po_name}/prompt",
|
|
131
|
+
mimeType: "text/markdown",
|
|
132
|
+
text: po.body
|
|
133
|
+
}]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def read_bus_messages
|
|
137
|
+
entries = @env.bus.recent(50).map do |entry|
|
|
138
|
+
{
|
|
139
|
+
from: entry[:from],
|
|
140
|
+
to: entry[:to],
|
|
141
|
+
message: entry[:message],
|
|
142
|
+
timestamp: entry[:timestamp]&.iso8601
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
[{
|
|
147
|
+
uri: "bus://messages",
|
|
148
|
+
mimeType: "application/json",
|
|
149
|
+
text: JSON.pretty_generate(entries)
|
|
150
|
+
}]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def run_stdio_transport
|
|
154
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(@mcp_server)
|
|
155
|
+
transport.open
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Require tools
|
|
162
|
+
require_relative "tools/list_prompt_objects"
|
|
163
|
+
require_relative "tools/send_message"
|
|
164
|
+
require_relative "tools/get_conversation"
|
|
165
|
+
require_relative "tools/get_pending_requests"
|
|
166
|
+
require_relative "tools/respond_to_request"
|
|
167
|
+
require_relative "tools/inspect_po"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Get conversation history for a prompt object
|
|
7
|
+
class GetConversation < ::MCP::Tool
|
|
8
|
+
tool_name "get_conversation"
|
|
9
|
+
description "Get the conversation history for a prompt object"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
po_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Name of the prompt object"
|
|
17
|
+
},
|
|
18
|
+
limit: {
|
|
19
|
+
type: "integer",
|
|
20
|
+
description: "Maximum number of messages to return (default: all)"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: %w[po_name]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(po_name:, limit: nil, server_context:)
|
|
27
|
+
env = server_context[:env]
|
|
28
|
+
|
|
29
|
+
po = env.registry.get(po_name)
|
|
30
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
31
|
+
return ::MCP::Tool::Response.new([{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
34
|
+
}])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
history = po.history.map do |msg|
|
|
38
|
+
{
|
|
39
|
+
role: msg[:role].to_s,
|
|
40
|
+
content: msg[:content],
|
|
41
|
+
from: msg[:from],
|
|
42
|
+
tool_calls: msg[:tool_calls]&.map { |tc| { name: tc.name, arguments: tc.arguments } }
|
|
43
|
+
}.compact
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
history = history.last(limit) if limit
|
|
47
|
+
|
|
48
|
+
::MCP::Tool::Response.new([{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: JSON.pretty_generate({
|
|
51
|
+
po_name: po_name,
|
|
52
|
+
message_count: history.length,
|
|
53
|
+
history: history
|
|
54
|
+
})
|
|
55
|
+
}])
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Get all pending human requests from the queue
|
|
7
|
+
class GetPendingRequests < ::MCP::Tool
|
|
8
|
+
tool_name "get_pending_requests"
|
|
9
|
+
description "Get all pending human requests across all prompt objects. These are questions POs have asked that await human response."
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
po_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Optional: filter to requests from a specific PO"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: []
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def self.call(po_name: nil, server_context:)
|
|
23
|
+
env = server_context[:env]
|
|
24
|
+
queue = env.human_queue
|
|
25
|
+
|
|
26
|
+
requests = if po_name
|
|
27
|
+
queue.pending_for(po_name)
|
|
28
|
+
else
|
|
29
|
+
queue.all_pending
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
formatted = requests.map do |req|
|
|
33
|
+
{
|
|
34
|
+
id: req.id,
|
|
35
|
+
capability: req.capability,
|
|
36
|
+
question: req.question,
|
|
37
|
+
options: req.options,
|
|
38
|
+
age: req.age_string,
|
|
39
|
+
created_at: req.created_at.iso8601
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
::MCP::Tool::Response.new([{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.pretty_generate({
|
|
46
|
+
count: formatted.length,
|
|
47
|
+
requests: formatted
|
|
48
|
+
})
|
|
49
|
+
}])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Get detailed information about a prompt object
|
|
7
|
+
class InspectPO < ::MCP::Tool
|
|
8
|
+
tool_name "inspect_po"
|
|
9
|
+
description "Get detailed information about a prompt object including its configuration, capabilities, and prompt body"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
po_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Name of the prompt object to inspect"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: %w[po_name]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def self.call(po_name:, server_context:)
|
|
23
|
+
env = server_context[:env]
|
|
24
|
+
|
|
25
|
+
po = env.registry.get(po_name)
|
|
26
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
27
|
+
return ::MCP::Tool::Response.new([{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
30
|
+
}])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Categorize capabilities
|
|
34
|
+
declared_caps = po.config["capabilities"] || []
|
|
35
|
+
universal_caps = PromptObjects::UNIVERSAL_CAPABILITIES
|
|
36
|
+
|
|
37
|
+
# Resolve which are POs vs primitives
|
|
38
|
+
delegates = []
|
|
39
|
+
primitives = []
|
|
40
|
+
|
|
41
|
+
declared_caps.each do |cap_name|
|
|
42
|
+
cap = env.registry.get(cap_name)
|
|
43
|
+
if cap.is_a?(PromptObjects::PromptObject)
|
|
44
|
+
delegates << cap_name
|
|
45
|
+
elsif cap.is_a?(PromptObjects::Primitive)
|
|
46
|
+
primitives << cap_name
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
info = {
|
|
51
|
+
name: po.name,
|
|
52
|
+
description: po.description,
|
|
53
|
+
state: po.state || :idle,
|
|
54
|
+
config: po.config,
|
|
55
|
+
capabilities: {
|
|
56
|
+
universal: universal_caps,
|
|
57
|
+
primitives: primitives,
|
|
58
|
+
delegates: delegates,
|
|
59
|
+
all_declared: declared_caps
|
|
60
|
+
},
|
|
61
|
+
prompt_body: po.body,
|
|
62
|
+
history_length: po.history.length
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
::MCP::Tool::Response.new([{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.pretty_generate(info)
|
|
68
|
+
}])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# List all loaded prompt objects with their current state
|
|
7
|
+
class ListPromptObjects < ::MCP::Tool
|
|
8
|
+
tool_name "list_prompt_objects"
|
|
9
|
+
description "List all loaded prompt objects with their names, descriptions, and current state"
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {},
|
|
14
|
+
required: []
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def self.call(server_context:, **_args)
|
|
18
|
+
env = server_context[:env]
|
|
19
|
+
|
|
20
|
+
pos = env.registry.prompt_objects.map do |po|
|
|
21
|
+
{
|
|
22
|
+
name: po.name,
|
|
23
|
+
description: po.description,
|
|
24
|
+
state: po.state || :idle,
|
|
25
|
+
capabilities: po.config["capabilities"] || []
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
::MCP::Tool::Response.new([{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: JSON.pretty_generate(pos)
|
|
32
|
+
}])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Respond to a pending human request
|
|
7
|
+
class RespondToRequest < ::MCP::Tool
|
|
8
|
+
tool_name "respond_to_request"
|
|
9
|
+
description "Respond to a pending human request from a prompt object. This unblocks the PO that asked the question."
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
request_id: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "The ID of the pending request (from get_pending_requests)"
|
|
17
|
+
},
|
|
18
|
+
response: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Your response to the question"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: %w[request_id response]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(request_id:, response:, server_context:)
|
|
27
|
+
env = server_context[:env]
|
|
28
|
+
queue = env.human_queue
|
|
29
|
+
|
|
30
|
+
# Find the request first to give better error messages
|
|
31
|
+
request = queue.all_pending.find { |r| r.id == request_id }
|
|
32
|
+
|
|
33
|
+
unless request
|
|
34
|
+
return ::MCP::Tool::Response.new([{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: JSON.generate({
|
|
37
|
+
error: "Request not found",
|
|
38
|
+
request_id: request_id,
|
|
39
|
+
hint: "Use get_pending_requests to see available requests"
|
|
40
|
+
})
|
|
41
|
+
}])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Respond (this unblocks the waiting thread)
|
|
45
|
+
queue.respond(request_id, response)
|
|
46
|
+
|
|
47
|
+
# Log to message bus
|
|
48
|
+
env.bus.publish(
|
|
49
|
+
from: "mcp_client",
|
|
50
|
+
to: request.capability,
|
|
51
|
+
message: "[response to ask_human] #{response}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
::MCP::Tool::Response.new([{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: JSON.generate({
|
|
57
|
+
success: true,
|
|
58
|
+
request_id: request_id,
|
|
59
|
+
capability: request.capability,
|
|
60
|
+
question: request.question,
|
|
61
|
+
response: response
|
|
62
|
+
})
|
|
63
|
+
}])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Send a message to a prompt object and get its response
|
|
7
|
+
class SendMessage < ::MCP::Tool
|
|
8
|
+
tool_name "send_message"
|
|
9
|
+
description "Send a message to a prompt object. The PO will process it (potentially calling tools) and return a response."
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
po_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Name of the prompt object to message"
|
|
17
|
+
},
|
|
18
|
+
message: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The message to send"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: %w[po_name message]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(po_name:, message:, server_context:)
|
|
27
|
+
env = server_context[:env]
|
|
28
|
+
context = server_context[:context]
|
|
29
|
+
|
|
30
|
+
po = env.registry.get(po_name)
|
|
31
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
32
|
+
return ::MCP::Tool::Response.new([{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: JSON.generate({ error: "Prompt object '#{po_name}' not found" })
|
|
35
|
+
}])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Log to message bus
|
|
39
|
+
env.bus.publish(from: "mcp_client", to: po_name, message: message)
|
|
40
|
+
|
|
41
|
+
# Set context for this interaction
|
|
42
|
+
context.current_capability = po_name
|
|
43
|
+
po.state = :working
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
response = po.receive(message, context: context)
|
|
47
|
+
po.state = :idle
|
|
48
|
+
|
|
49
|
+
# Log response
|
|
50
|
+
env.bus.publish(from: po_name, to: "mcp_client", message: response)
|
|
51
|
+
|
|
52
|
+
::MCP::Tool::Response.new([{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: JSON.generate({
|
|
55
|
+
po_name: po_name,
|
|
56
|
+
response: response,
|
|
57
|
+
history_length: po.history.length
|
|
58
|
+
})
|
|
59
|
+
}])
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
po.state = :idle
|
|
62
|
+
::MCP::Tool::Response.new([{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: JSON.generate({ error: e.message, backtrace: e.backtrace.first(5) })
|
|
65
|
+
}])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
# Message bus for routing and logging all inter-capability communication.
|
|
5
|
+
# This makes the semantic binding visible - you can see natural language
|
|
6
|
+
# being transformed into capability calls.
|
|
7
|
+
class MessageBus
|
|
8
|
+
attr_reader :log
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@log = []
|
|
12
|
+
@subscribers = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Log a message between capabilities.
|
|
16
|
+
# @param from [String] Source capability name
|
|
17
|
+
# @param to [String] Destination capability name
|
|
18
|
+
# @param message [String, Hash] The message content
|
|
19
|
+
# @return [Hash] The log entry
|
|
20
|
+
def publish(from:, to:, message:)
|
|
21
|
+
entry = {
|
|
22
|
+
timestamp: Time.now,
|
|
23
|
+
from: from,
|
|
24
|
+
to: to,
|
|
25
|
+
message: truncate_message(message)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@log << entry
|
|
29
|
+
notify_subscribers(entry)
|
|
30
|
+
entry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Subscribe to message events.
|
|
34
|
+
# @yield [Hash] Called with each new message entry
|
|
35
|
+
def subscribe(&block)
|
|
36
|
+
@subscribers << block
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Unsubscribe from message events.
|
|
40
|
+
# @param block [Proc] The subscriber to remove
|
|
41
|
+
def unsubscribe(block)
|
|
42
|
+
@subscribers.delete(block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get recent log entries.
|
|
46
|
+
# @param count [Integer] Number of entries to return
|
|
47
|
+
# @return [Array<Hash>]
|
|
48
|
+
def recent(count = 20)
|
|
49
|
+
@log.last(count)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear the log.
|
|
53
|
+
def clear
|
|
54
|
+
@log.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Format log entries for display.
|
|
58
|
+
# @param count [Integer] Number of entries to format
|
|
59
|
+
# @return [String]
|
|
60
|
+
def format_log(count = 20)
|
|
61
|
+
recent(count).map do |entry|
|
|
62
|
+
time = entry[:timestamp].strftime("%H:%M:%S")
|
|
63
|
+
from = entry[:from]
|
|
64
|
+
to = entry[:to]
|
|
65
|
+
msg = entry[:message]
|
|
66
|
+
|
|
67
|
+
"#{time} #{from} → #{to}: #{msg}"
|
|
68
|
+
end.join("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def notify_subscribers(entry)
|
|
74
|
+
@subscribers.each { |s| s.call(entry) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def truncate_message(message, max_length = 100)
|
|
78
|
+
str = case message
|
|
79
|
+
when Hash
|
|
80
|
+
message.to_json
|
|
81
|
+
when String
|
|
82
|
+
message
|
|
83
|
+
else
|
|
84
|
+
message.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Remove newlines for cleaner log display
|
|
88
|
+
str = str.gsub(/\s+/, " ").strip
|
|
89
|
+
|
|
90
|
+
if str.length > max_length
|
|
91
|
+
str[0, max_length] + "..."
|
|
92
|
+
else
|
|
93
|
+
str
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
# Base class for primitive capabilities.
|
|
5
|
+
# Primitives are deterministic Ruby implementations (no LLM interpretation).
|
|
6
|
+
class Primitive < Capability
|
|
7
|
+
# Primitives are always "idle" in terms of state since they execute synchronously
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
@state = :idle
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|