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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to list all available capabilities in the registry.
|
|
6
|
+
# This helps POs discover what tools exist.
|
|
7
|
+
class ListCapabilities < Primitive
|
|
8
|
+
def name
|
|
9
|
+
"list_capabilities"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def description
|
|
13
|
+
"List all available capabilities (primitives and prompt objects) in the system. Useful for discovering what tools exist before creating new ones."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parameters
|
|
17
|
+
{
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
type: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["all", "primitives", "prompt_objects"],
|
|
23
|
+
description: "Filter by type. Default is 'all'."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: []
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def receive(message, context:)
|
|
31
|
+
filter = message[:type] || message["type"] || "all"
|
|
32
|
+
|
|
33
|
+
capabilities = case filter
|
|
34
|
+
when "primitives"
|
|
35
|
+
context.env.registry.primitives
|
|
36
|
+
when "prompt_objects"
|
|
37
|
+
context.env.registry.prompt_objects
|
|
38
|
+
else
|
|
39
|
+
context.env.registry.all
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if capabilities.empty?
|
|
43
|
+
return "No capabilities found."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
lines = capabilities.map do |cap|
|
|
47
|
+
type_label = cap.is_a?(PromptObject) ? "[PO]" : "[Primitive]"
|
|
48
|
+
"- #{cap.name} #{type_label}: #{cap.description}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
"Available capabilities:\n#{lines.join("\n")}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to list available primitives.
|
|
6
|
+
# Shows stdlib primitives, custom (environment) primitives, and which ones
|
|
7
|
+
# the current PO has active.
|
|
8
|
+
class ListPrimitives < Primitive
|
|
9
|
+
# Names of stdlib primitives (built into the framework)
|
|
10
|
+
STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
"list_primitives"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
"List available primitives (deterministic Ruby tools). Filter by type: stdlib (built-in), custom (environment-specific), active (on this PO), or available (all)."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parameters
|
|
21
|
+
{
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
filter: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["available", "active", "stdlib", "custom"],
|
|
27
|
+
description: "Filter primitives: 'available' (all), 'active' (currently on this PO), 'stdlib' (built-in), 'custom' (environment-specific). Default: 'available'"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: []
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def receive(message, context:)
|
|
35
|
+
filter = (message[:filter] || message["filter"] || "available").to_s
|
|
36
|
+
|
|
37
|
+
case filter
|
|
38
|
+
when "stdlib"
|
|
39
|
+
list_stdlib(context)
|
|
40
|
+
when "custom"
|
|
41
|
+
list_custom(context)
|
|
42
|
+
when "active"
|
|
43
|
+
list_active(context)
|
|
44
|
+
when "available"
|
|
45
|
+
list_available(context)
|
|
46
|
+
else
|
|
47
|
+
"Error: Unknown filter '#{filter}'. Use: available, active, stdlib, or custom."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def list_stdlib(context)
|
|
54
|
+
primitives = STDLIB_PRIMITIVES.filter_map do |name|
|
|
55
|
+
prim = context.env.registry.get(name)
|
|
56
|
+
prim if prim.is_a?(Primitive)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
format_list("Stdlib Primitives (built-in)", primitives)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def list_custom(context)
|
|
63
|
+
primitives = context.env.registry.primitives.reject do |prim|
|
|
64
|
+
STDLIB_PRIMITIVES.include?(prim.name) || universal_primitive?(prim)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if primitives.empty?
|
|
68
|
+
"No custom primitives found.\nCustom primitives are stored in: #{context.env.primitives_dir}"
|
|
69
|
+
else
|
|
70
|
+
format_list("Custom Primitives (environment-specific)", primitives)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def list_active(context)
|
|
75
|
+
caller = context.calling_po
|
|
76
|
+
unless caller
|
|
77
|
+
return "Error: No calling PO context. This filter shows primitives active on the current PO."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
po = context.env.registry.get(caller)
|
|
81
|
+
unless po.is_a?(PromptObject)
|
|
82
|
+
return "Error: Could not find calling PO '#{caller}'."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
capabilities = po.config["capabilities"] || []
|
|
86
|
+
active_primitives = capabilities.filter_map do |cap_name|
|
|
87
|
+
cap = context.env.registry.get(cap_name)
|
|
88
|
+
cap if cap.is_a?(Primitive) && !universal_primitive?(cap)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if active_primitives.empty?
|
|
92
|
+
"No primitives currently active on #{caller}.\nUse add_primitive to add primitives to your capabilities."
|
|
93
|
+
else
|
|
94
|
+
format_list("Active Primitives on #{caller}", active_primitives)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def list_available(context)
|
|
99
|
+
# All registered primitives except universal ones
|
|
100
|
+
primitives = context.env.registry.primitives.reject { |p| universal_primitive?(p) }
|
|
101
|
+
|
|
102
|
+
# Categorize them
|
|
103
|
+
stdlib = primitives.select { |p| STDLIB_PRIMITIVES.include?(p.name) }
|
|
104
|
+
custom = primitives.reject { |p| STDLIB_PRIMITIVES.include?(p.name) }
|
|
105
|
+
|
|
106
|
+
lines = []
|
|
107
|
+
|
|
108
|
+
unless stdlib.empty?
|
|
109
|
+
lines << "## Stdlib Primitives (built-in)"
|
|
110
|
+
stdlib.each { |p| lines << format_primitive(p) }
|
|
111
|
+
lines << ""
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
unless custom.empty?
|
|
115
|
+
lines << "## Custom Primitives (environment-specific)"
|
|
116
|
+
custom.each { |p| lines << format_primitive(p) }
|
|
117
|
+
lines << ""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if lines.empty?
|
|
121
|
+
"No primitives available."
|
|
122
|
+
else
|
|
123
|
+
lines.join("\n")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_list(title, primitives)
|
|
128
|
+
return "#{title}: (none)" if primitives.empty?
|
|
129
|
+
|
|
130
|
+
lines = ["## #{title}", ""]
|
|
131
|
+
primitives.each { |p| lines << format_primitive(p) }
|
|
132
|
+
lines.join("\n")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def format_primitive(primitive)
|
|
136
|
+
"- **#{primitive.name}**: #{primitive.description}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def universal_primitive?(primitive)
|
|
140
|
+
# Universal capabilities live in PromptObjects::Universal module
|
|
141
|
+
primitive.class.name&.start_with?("PromptObjects::Universal")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module Universal
|
|
7
|
+
# Universal capability to modify existing primitives.
|
|
8
|
+
# Allows POs to fix or improve their primitives.
|
|
9
|
+
class ModifyPrimitive < Primitive
|
|
10
|
+
# Stdlib primitives cannot be modified
|
|
11
|
+
STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
"modify_primitive"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def description
|
|
18
|
+
"Modify an existing primitive's code. Only custom (environment) primitives can be modified, not stdlib."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parameters
|
|
22
|
+
{
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
name: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Name of the primitive to modify"
|
|
28
|
+
},
|
|
29
|
+
code: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "New Ruby code for the primitive's receive method"
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Optional: Update the description"
|
|
36
|
+
},
|
|
37
|
+
parameters_schema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
description: "Optional: Update the parameters schema"
|
|
40
|
+
},
|
|
41
|
+
reason: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Brief explanation of why this change is needed (for commit message)"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: ["name", "code"]
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def receive(message, context:)
|
|
51
|
+
prim_name = message[:name] || message["name"]
|
|
52
|
+
new_code = message[:code] || message["code"]
|
|
53
|
+
new_description = message[:description] || message["description"]
|
|
54
|
+
new_params_schema = message[:parameters_schema] || message["parameters_schema"]
|
|
55
|
+
reason = message[:reason] || message["reason"] || "Updated primitive"
|
|
56
|
+
|
|
57
|
+
# Find the primitive
|
|
58
|
+
primitive = context.env.registry.get(prim_name)
|
|
59
|
+
unless primitive
|
|
60
|
+
return "Error: Primitive '#{prim_name}' not found."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unless primitive.is_a?(Primitive)
|
|
64
|
+
return "Error: '#{prim_name}' is not a primitive."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if it's a stdlib primitive
|
|
68
|
+
if STDLIB_PRIMITIVES.include?(prim_name)
|
|
69
|
+
return "Error: Cannot modify stdlib primitive '#{prim_name}'. Stdlib primitives are built into the framework."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if it's a universal capability
|
|
73
|
+
if universal_primitive?(primitive)
|
|
74
|
+
return "Error: Cannot modify universal capability '#{prim_name}'."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Find the primitive file
|
|
78
|
+
path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
|
|
79
|
+
unless File.exist?(path)
|
|
80
|
+
return "Error: Cannot find primitive file at #{path}. Only custom primitives can be modified."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Validate code syntax
|
|
84
|
+
syntax_error = validate_syntax(new_code)
|
|
85
|
+
return "Error: Invalid Ruby syntax - #{syntax_error}" if syntax_error
|
|
86
|
+
|
|
87
|
+
# Get current values if not provided
|
|
88
|
+
description = new_description || primitive.description
|
|
89
|
+
params_schema = new_params_schema || primitive.parameters
|
|
90
|
+
|
|
91
|
+
# Generate updated Ruby class
|
|
92
|
+
class_name = prim_name.split("_").map(&:capitalize).join
|
|
93
|
+
ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, new_code)
|
|
94
|
+
|
|
95
|
+
# Write the updated file
|
|
96
|
+
File.write(path, ruby_content, encoding: "UTF-8")
|
|
97
|
+
|
|
98
|
+
# Reload the primitive
|
|
99
|
+
begin
|
|
100
|
+
# Remove old constant to allow re-definition
|
|
101
|
+
if PromptObjects::Primitives.const_defined?(class_name)
|
|
102
|
+
PromptObjects::Primitives.send(:remove_const, class_name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
load(path)
|
|
106
|
+
klass = PromptObjects::Primitives.const_get(class_name)
|
|
107
|
+
new_instance = klass.new
|
|
108
|
+
|
|
109
|
+
# Re-register with the new instance
|
|
110
|
+
context.env.registry.register(new_instance)
|
|
111
|
+
|
|
112
|
+
# Auto-commit if in environment mode
|
|
113
|
+
if context.env.environment? && context.env.auto_commit
|
|
114
|
+
commit_message = "Modified primitive '#{prim_name}': #{reason}"
|
|
115
|
+
context.env.save(commit_message)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
"Modified primitive '#{prim_name}'. Changes saved to #{path}."
|
|
119
|
+
rescue SyntaxError => e
|
|
120
|
+
"Error: Invalid Ruby syntax in generated code - #{e.message}"
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
"Error reloading primitive: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def universal_primitive?(primitive)
|
|
129
|
+
primitive.class.name&.start_with?("PromptObjects::Universal")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_syntax(code)
|
|
133
|
+
eval("proc { #{code} }")
|
|
134
|
+
nil
|
|
135
|
+
rescue SyntaxError => e
|
|
136
|
+
e.message.sub(/^\(eval\):\d+: /, "")
|
|
137
|
+
rescue StandardError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def generate_ruby_class(class_name, prim_name, description, params_schema, code)
|
|
142
|
+
escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
143
|
+
|
|
144
|
+
<<~RUBY
|
|
145
|
+
# frozen_string_literal: true
|
|
146
|
+
# Auto-generated primitive: #{prim_name}
|
|
147
|
+
# Modified at #{Time.now.iso8601}
|
|
148
|
+
|
|
149
|
+
module PromptObjects
|
|
150
|
+
module Primitives
|
|
151
|
+
class #{class_name} < Primitive
|
|
152
|
+
def name
|
|
153
|
+
"#{prim_name}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def description
|
|
157
|
+
"#{escaped_desc}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parameters
|
|
161
|
+
#{params_schema.inspect}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def receive(message, context:)
|
|
165
|
+
#{indent_code(code, 10)}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
RUBY
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def indent_code(code, spaces)
|
|
174
|
+
code.lines.map.with_index do |line, i|
|
|
175
|
+
i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
|
|
176
|
+
end.join("\n")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to request a primitive from the human.
|
|
6
|
+
# Similar to ask_human but specifically for requesting new tools.
|
|
7
|
+
# The human can approve, modify, or reject the request.
|
|
8
|
+
class RequestPrimitive < Primitive
|
|
9
|
+
def name
|
|
10
|
+
"request_primitive"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def description
|
|
14
|
+
"Request a new primitive from the human. Use this when you need a tool that doesn't exist. The human can approve and create it, or reject the request."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parameters
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
name: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Suggested name for the primitive (lowercase, underscores)"
|
|
24
|
+
},
|
|
25
|
+
description: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "What this primitive should do"
|
|
28
|
+
},
|
|
29
|
+
reason: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Why you need this primitive"
|
|
32
|
+
},
|
|
33
|
+
suggested_code: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Optional: Your suggested Ruby implementation for the receive method"
|
|
36
|
+
},
|
|
37
|
+
parameters_schema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
description: "Optional: Suggested JSON Schema for parameters"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ["name", "description", "reason"]
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def receive(message, context:)
|
|
47
|
+
prim_name = message[:name] || message["name"]
|
|
48
|
+
description = message[:description] || message["description"]
|
|
49
|
+
reason = message[:reason] || message["reason"]
|
|
50
|
+
suggested_code = message[:suggested_code] || message["suggested_code"]
|
|
51
|
+
params_schema = message[:parameters_schema] || message["parameters_schema"]
|
|
52
|
+
|
|
53
|
+
# Validate name format
|
|
54
|
+
unless prim_name && prim_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
55
|
+
return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if already exists
|
|
59
|
+
if context.env.registry.exists?(prim_name)
|
|
60
|
+
return "Error: A capability named '#{prim_name}' already exists."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# In TUI mode, use the human queue
|
|
64
|
+
if context.tui_mode && context.human_queue
|
|
65
|
+
return receive_tui(prim_name, description, reason, suggested_code, params_schema, context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# REPL mode - use stdin directly
|
|
69
|
+
receive_repl(prim_name, description, reason, suggested_code, params_schema, context)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def receive_tui(prim_name, description, reason, suggested_code, params_schema, context)
|
|
75
|
+
# Build the question with all details
|
|
76
|
+
question = build_request_question(prim_name, description, reason, suggested_code)
|
|
77
|
+
|
|
78
|
+
# Queue the request
|
|
79
|
+
request = context.human_queue.enqueue(
|
|
80
|
+
capability: context.current_capability,
|
|
81
|
+
question: question,
|
|
82
|
+
options: ["approve", "reject"]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Log to message bus
|
|
86
|
+
context.bus.publish(
|
|
87
|
+
from: context.current_capability,
|
|
88
|
+
to: "human",
|
|
89
|
+
message: "[primitive request] #{prim_name}: #{description}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Wait for response
|
|
93
|
+
response = request.wait_for_response
|
|
94
|
+
|
|
95
|
+
# Handle the response
|
|
96
|
+
handle_response(response, prim_name, description, suggested_code, params_schema, context)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def receive_repl(prim_name, description, reason, suggested_code, params_schema, context)
|
|
100
|
+
puts
|
|
101
|
+
puts "┌─ Primitive Request ─────────────────────────────────────────┐"
|
|
102
|
+
puts "│"
|
|
103
|
+
puts "│ From: #{context.calling_po || context.current_capability}"
|
|
104
|
+
puts "│ Requested: #{prim_name}"
|
|
105
|
+
puts "│"
|
|
106
|
+
puts "│ Description: #{description}"
|
|
107
|
+
puts "│"
|
|
108
|
+
puts "│ Reason: #{reason}"
|
|
109
|
+
puts "│"
|
|
110
|
+
|
|
111
|
+
if suggested_code
|
|
112
|
+
puts "│ Suggested Code:"
|
|
113
|
+
suggested_code.lines.each { |line| puts "│ #{line}" }
|
|
114
|
+
puts "│"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
puts "├─────────────────────────────────────────────────────────────┤"
|
|
118
|
+
puts "│ [a] Approve (use suggested code)"
|
|
119
|
+
puts "│ [e] Edit (provide different code)"
|
|
120
|
+
puts "│ [r] Reject"
|
|
121
|
+
puts "└─────────────────────────────────────────────────────────────┘"
|
|
122
|
+
print "Your choice: "
|
|
123
|
+
|
|
124
|
+
choice = $stdin.gets&.chomp&.downcase
|
|
125
|
+
|
|
126
|
+
case choice
|
|
127
|
+
when "a", "approve"
|
|
128
|
+
if suggested_code
|
|
129
|
+
create_and_register(prim_name, description, suggested_code, params_schema, context)
|
|
130
|
+
else
|
|
131
|
+
puts "No suggested code provided. Please enter the code (end with 'END' on its own line):"
|
|
132
|
+
code = read_multiline_input
|
|
133
|
+
create_and_register(prim_name, description, code, params_schema, context)
|
|
134
|
+
end
|
|
135
|
+
when "e", "edit"
|
|
136
|
+
puts "Enter the Ruby code for the receive method (end with 'END' on its own line):"
|
|
137
|
+
code = read_multiline_input
|
|
138
|
+
create_and_register(prim_name, description, code, params_schema, context)
|
|
139
|
+
when "r", "reject"
|
|
140
|
+
"Request rejected by human."
|
|
141
|
+
else
|
|
142
|
+
"Request deferred."
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_request_question(prim_name, description, reason, suggested_code)
|
|
147
|
+
lines = []
|
|
148
|
+
lines << "**Primitive Request: #{prim_name}**"
|
|
149
|
+
lines << ""
|
|
150
|
+
lines << "**Description:** #{description}"
|
|
151
|
+
lines << ""
|
|
152
|
+
lines << "**Reason:** #{reason}"
|
|
153
|
+
|
|
154
|
+
if suggested_code
|
|
155
|
+
lines << ""
|
|
156
|
+
lines << "**Suggested Code:**"
|
|
157
|
+
lines << "```ruby"
|
|
158
|
+
lines << suggested_code
|
|
159
|
+
lines << "```"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
lines.join("\n")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def handle_response(response, prim_name, description, suggested_code, params_schema, context)
|
|
166
|
+
case response.to_s.downcase
|
|
167
|
+
when "approve", "approved", "yes", "y"
|
|
168
|
+
if suggested_code
|
|
169
|
+
create_and_register(prim_name, description, suggested_code, params_schema, context)
|
|
170
|
+
else
|
|
171
|
+
"Request approved but no code provided. Human should create the primitive manually."
|
|
172
|
+
end
|
|
173
|
+
when "reject", "rejected", "no", "n"
|
|
174
|
+
"Request rejected by human."
|
|
175
|
+
else
|
|
176
|
+
# Treat anything else as custom code provided by human
|
|
177
|
+
if response && !response.strip.empty?
|
|
178
|
+
create_and_register(prim_name, description, response, params_schema, context)
|
|
179
|
+
else
|
|
180
|
+
"Request deferred."
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def create_and_register(prim_name, description, code, params_schema, context)
|
|
186
|
+
params_schema ||= { type: "object", properties: {}, required: [] }
|
|
187
|
+
|
|
188
|
+
# Validate syntax
|
|
189
|
+
begin
|
|
190
|
+
eval("proc { #{code} }")
|
|
191
|
+
rescue SyntaxError => e
|
|
192
|
+
return "Error: Invalid Ruby syntax - #{e.message}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Generate and write the file
|
|
196
|
+
class_name = prim_name.split("_").map(&:capitalize).join
|
|
197
|
+
ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, code)
|
|
198
|
+
|
|
199
|
+
FileUtils.mkdir_p(context.env.primitives_dir)
|
|
200
|
+
path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
|
|
201
|
+
File.write(path, ruby_content, encoding: "UTF-8")
|
|
202
|
+
|
|
203
|
+
# Load and register
|
|
204
|
+
begin
|
|
205
|
+
load(path)
|
|
206
|
+
klass = PromptObjects::Primitives.const_get(class_name)
|
|
207
|
+
context.env.registry.register(klass.new)
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
File.delete(path) if File.exist?(path)
|
|
210
|
+
return "Error creating primitive: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Auto-add to the requesting PO
|
|
214
|
+
saved = false
|
|
215
|
+
if context.calling_po
|
|
216
|
+
caller_po = context.env.registry.get(context.calling_po)
|
|
217
|
+
if caller_po.is_a?(PromptObject)
|
|
218
|
+
caller_po.config["capabilities"] ||= []
|
|
219
|
+
unless caller_po.config["capabilities"].include?(prim_name)
|
|
220
|
+
caller_po.config["capabilities"] << prim_name
|
|
221
|
+
saved = caller_po.save
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Log to bus
|
|
227
|
+
context.bus.publish(
|
|
228
|
+
from: "human",
|
|
229
|
+
to: context.current_capability,
|
|
230
|
+
message: "[approved] Created primitive '#{prim_name}'"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
save_msg = saved ? " and saved to file" : ""
|
|
234
|
+
"Primitive '#{prim_name}' created and added to your capabilities#{save_msg}."
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def generate_ruby_class(class_name, prim_name, description, params_schema, code)
|
|
238
|
+
escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
239
|
+
|
|
240
|
+
<<~RUBY
|
|
241
|
+
# frozen_string_literal: true
|
|
242
|
+
# Requested primitive: #{prim_name}
|
|
243
|
+
# Created at #{Time.now.iso8601}
|
|
244
|
+
|
|
245
|
+
module PromptObjects
|
|
246
|
+
module Primitives
|
|
247
|
+
class #{class_name} < Primitive
|
|
248
|
+
def name
|
|
249
|
+
"#{prim_name}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def description
|
|
253
|
+
"#{escaped_desc}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def parameters
|
|
257
|
+
#{params_schema.inspect}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def receive(message, context:)
|
|
261
|
+
#{indent_code(code, 10)}
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
RUBY
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def indent_code(code, spaces)
|
|
270
|
+
code.lines.map.with_index do |line, i|
|
|
271
|
+
i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
|
|
272
|
+
end.join("\n")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def read_multiline_input
|
|
276
|
+
lines = []
|
|
277
|
+
loop do
|
|
278
|
+
line = $stdin.gets
|
|
279
|
+
break if line.nil? || line.strip == "END"
|
|
280
|
+
|
|
281
|
+
lines << line
|
|
282
|
+
end
|
|
283
|
+
lines.join
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|