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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +108 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +231 -0
  5. data/IMPLEMENTATION_PLAN.md +1073 -0
  6. data/LICENSE +21 -0
  7. data/README.md +73 -0
  8. data/Rakefile +27 -0
  9. data/design-doc-v2.md +1232 -0
  10. data/exe/prompt_objects +572 -0
  11. data/exe/prompt_objects_mcp +34 -0
  12. data/frontend/.gitignore +3 -0
  13. data/frontend/index.html +13 -0
  14. data/frontend/package-lock.json +4417 -0
  15. data/frontend/package.json +32 -0
  16. data/frontend/postcss.config.js +6 -0
  17. data/frontend/src/App.tsx +95 -0
  18. data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
  19. data/frontend/src/components/ChatPanel.tsx +251 -0
  20. data/frontend/src/components/Dashboard.tsx +83 -0
  21. data/frontend/src/components/Header.tsx +141 -0
  22. data/frontend/src/components/MarkdownMessage.tsx +153 -0
  23. data/frontend/src/components/MessageBus.tsx +55 -0
  24. data/frontend/src/components/ModelSelector.tsx +112 -0
  25. data/frontend/src/components/NotificationPanel.tsx +134 -0
  26. data/frontend/src/components/POCard.tsx +56 -0
  27. data/frontend/src/components/PODetail.tsx +117 -0
  28. data/frontend/src/components/PromptPanel.tsx +51 -0
  29. data/frontend/src/components/SessionsPanel.tsx +174 -0
  30. data/frontend/src/components/ThreadsSidebar.tsx +119 -0
  31. data/frontend/src/components/index.ts +11 -0
  32. data/frontend/src/hooks/useWebSocket.ts +363 -0
  33. data/frontend/src/index.css +37 -0
  34. data/frontend/src/main.tsx +10 -0
  35. data/frontend/src/store/index.ts +246 -0
  36. data/frontend/src/types/index.ts +146 -0
  37. data/frontend/tailwind.config.js +25 -0
  38. data/frontend/tsconfig.json +30 -0
  39. data/frontend/vite.config.ts +29 -0
  40. data/lib/prompt_objects/capability.rb +46 -0
  41. data/lib/prompt_objects/cli.rb +431 -0
  42. data/lib/prompt_objects/connectors/base.rb +73 -0
  43. data/lib/prompt_objects/connectors/mcp.rb +524 -0
  44. data/lib/prompt_objects/environment/exporter.rb +83 -0
  45. data/lib/prompt_objects/environment/git.rb +118 -0
  46. data/lib/prompt_objects/environment/importer.rb +159 -0
  47. data/lib/prompt_objects/environment/manager.rb +401 -0
  48. data/lib/prompt_objects/environment/manifest.rb +218 -0
  49. data/lib/prompt_objects/environment.rb +283 -0
  50. data/lib/prompt_objects/human_queue.rb +144 -0
  51. data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
  52. data/lib/prompt_objects/llm/factory.rb +84 -0
  53. data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
  54. data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
  55. data/lib/prompt_objects/llm/response.rb +61 -0
  56. data/lib/prompt_objects/loader.rb +32 -0
  57. data/lib/prompt_objects/mcp/server.rb +167 -0
  58. data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
  59. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
  60. data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
  61. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
  62. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
  63. data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
  64. data/lib/prompt_objects/message_bus.rb +97 -0
  65. data/lib/prompt_objects/primitive.rb +13 -0
  66. data/lib/prompt_objects/primitives/http_get.rb +72 -0
  67. data/lib/prompt_objects/primitives/list_files.rb +95 -0
  68. data/lib/prompt_objects/primitives/read_file.rb +81 -0
  69. data/lib/prompt_objects/primitives/write_file.rb +73 -0
  70. data/lib/prompt_objects/prompt_object.rb +415 -0
  71. data/lib/prompt_objects/registry.rb +88 -0
  72. data/lib/prompt_objects/server/api/routes.rb +297 -0
  73. data/lib/prompt_objects/server/app.rb +174 -0
  74. data/lib/prompt_objects/server/file_watcher.rb +113 -0
  75. data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
  76. data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
  77. data/lib/prompt_objects/server/public/index.html +14 -0
  78. data/lib/prompt_objects/server/websocket_handler.rb +619 -0
  79. data/lib/prompt_objects/server.rb +166 -0
  80. data/lib/prompt_objects/session/store.rb +826 -0
  81. data/lib/prompt_objects/universal/add_capability.rb +74 -0
  82. data/lib/prompt_objects/universal/add_primitive.rb +113 -0
  83. data/lib/prompt_objects/universal/ask_human.rb +109 -0
  84. data/lib/prompt_objects/universal/create_capability.rb +219 -0
  85. data/lib/prompt_objects/universal/create_primitive.rb +170 -0
  86. data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
  87. data/lib/prompt_objects/universal/list_primitives.rb +145 -0
  88. data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
  89. data/lib/prompt_objects/universal/request_primitive.rb +287 -0
  90. data/lib/prompt_objects/universal/think.rb +41 -0
  91. data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
  92. data/lib/prompt_objects.rb +62 -0
  93. data/objects/coordinator.md +48 -0
  94. data/objects/greeter.md +30 -0
  95. data/objects/reader.md +33 -0
  96. data/prompt_objects.gemspec +50 -0
  97. data/templates/basic/.gitignore +2 -0
  98. data/templates/basic/manifest.yml +7 -0
  99. data/templates/basic/objects/basic.md +32 -0
  100. data/templates/developer/.gitignore +5 -0
  101. data/templates/developer/manifest.yml +17 -0
  102. data/templates/developer/objects/code_reviewer.md +33 -0
  103. data/templates/developer/objects/coordinator.md +39 -0
  104. data/templates/developer/objects/debugger.md +35 -0
  105. data/templates/empty/.gitignore +5 -0
  106. data/templates/empty/manifest.yml +14 -0
  107. data/templates/empty/objects/.gitkeep +0 -0
  108. data/templates/empty/objects/assistant.md +41 -0
  109. data/templates/minimal/.gitignore +5 -0
  110. data/templates/minimal/manifest.yml +7 -0
  111. data/templates/minimal/objects/assistant.md +41 -0
  112. data/templates/writer/.gitignore +5 -0
  113. data/templates/writer/manifest.yml +17 -0
  114. data/templates/writer/objects/coordinator.md +33 -0
  115. data/templates/writer/objects/editor.md +33 -0
  116. data/templates/writer/objects/researcher.md +34 -0
  117. metadata +343 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to add capabilities to a prompt object at runtime.
6
+ # This allows dynamic extension of POs after primitives are created.
7
+ class AddCapability < Primitive
8
+ def name
9
+ "add_capability"
10
+ end
11
+
12
+ def description
13
+ "Add a capability to a prompt object, allowing it to use new tools. Can target self or another PO."
14
+ end
15
+
16
+ def parameters
17
+ {
18
+ type: "object",
19
+ properties: {
20
+ target: {
21
+ type: "string",
22
+ description: "Name of the prompt object to add the capability to. Use 'self' for the current PO."
23
+ },
24
+ capability: {
25
+ type: "string",
26
+ description: "Name of the capability to add (must already exist in the registry)"
27
+ }
28
+ },
29
+ required: ["target", "capability"]
30
+ }
31
+ end
32
+
33
+ def receive(message, context:)
34
+ target = message[:target] || message["target"]
35
+ capability = message[:capability] || message["capability"]
36
+
37
+ # Resolve 'self' to the calling PO (not current_capability which is this tool)
38
+ target = context.calling_po if target == "self"
39
+
40
+ # Find the target PO
41
+ target_po = context.env.registry.get(target)
42
+ unless target_po
43
+ return "Error: Prompt object '#{target}' not found"
44
+ end
45
+
46
+ unless target_po.is_a?(PromptObject)
47
+ return "Error: '#{target}' is not a prompt object (can only add capabilities to POs)"
48
+ end
49
+
50
+ # Check if capability exists
51
+ unless context.env.registry.exists?(capability)
52
+ return "Error: Capability '#{capability}' does not exist"
53
+ end
54
+
55
+ # Check if already has it
56
+ current_caps = target_po.config["capabilities"] || []
57
+ if current_caps.include?(capability)
58
+ return "'#{target}' already has the '#{capability}' capability"
59
+ end
60
+
61
+ # Add the capability
62
+ target_po.config["capabilities"] ||= []
63
+ target_po.config["capabilities"] << capability
64
+
65
+ # Persist to file so it's available on restart
66
+ if target_po.save
67
+ "Added '#{capability}' to '#{target}' and saved to file. It can now use this capability."
68
+ else
69
+ "Added '#{capability}' to '#{target}' (in-memory only, could not save to file). It can now use this capability."
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to add primitives to the current PO.
6
+ # Focused version of add_capability specifically for primitives with better UX.
7
+ class AddPrimitive < Primitive
8
+ # Names of stdlib primitives (built into the framework)
9
+ STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
10
+
11
+ def name
12
+ "add_primitive"
13
+ end
14
+
15
+ def description
16
+ "Add a primitive (deterministic Ruby tool) to your capabilities. Use list_primitives first to see what's available."
17
+ end
18
+
19
+ def parameters
20
+ {
21
+ type: "object",
22
+ properties: {
23
+ primitive: {
24
+ type: "string",
25
+ description: "Name of the primitive to add (e.g., 'read_file', 'write_file')"
26
+ }
27
+ },
28
+ required: ["primitive"]
29
+ }
30
+ end
31
+
32
+ def receive(message, context:)
33
+ primitive_name = message[:primitive] || message["primitive"]
34
+
35
+ # Find the calling PO
36
+ caller = context.calling_po
37
+ unless caller
38
+ return "Error: No calling PO context. This capability adds primitives to the current PO."
39
+ end
40
+
41
+ target_po = context.env.registry.get(caller)
42
+ unless target_po.is_a?(PromptObject)
43
+ return "Error: Could not find calling PO '#{caller}'."
44
+ end
45
+
46
+ # Check if primitive exists
47
+ primitive = context.env.registry.get(primitive_name)
48
+ unless primitive
49
+ return suggest_primitives(primitive_name, context)
50
+ end
51
+
52
+ unless primitive.is_a?(Primitive)
53
+ return "Error: '#{primitive_name}' is not a primitive (it's a prompt object). Use add_capability for POs."
54
+ end
55
+
56
+ # Check if it's a universal capability (shouldn't be added explicitly)
57
+ if universal_primitive?(primitive)
58
+ return "Error: '#{primitive_name}' is a universal capability and is already available to all POs."
59
+ end
60
+
61
+ # Check if already has it
62
+ current_caps = target_po.config["capabilities"] || []
63
+ if current_caps.include?(primitive_name)
64
+ return "You already have the '#{primitive_name}' primitive."
65
+ end
66
+
67
+ # Add the primitive
68
+ target_po.config["capabilities"] ||= []
69
+ target_po.config["capabilities"] << primitive_name
70
+
71
+ # Persist to file so it's available on restart
72
+ if target_po.save
73
+ "Added '#{primitive_name}' to your capabilities and saved to file. You can now use it."
74
+ else
75
+ "Added '#{primitive_name}' to your capabilities (in-memory only). You can now use it."
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def suggest_primitives(name, context)
82
+ # Get all available primitives (excluding universal ones)
83
+ available = context.env.registry.primitives
84
+ .reject { |p| universal_primitive?(p) }
85
+ .map(&:name)
86
+
87
+ # Find similar names
88
+ suggestions = available.select { |n| n.include?(name) || name.include?(n) || levenshtein_similar?(n, name) }
89
+
90
+ if suggestions.any?
91
+ "Error: Primitive '#{name}' not found. Did you mean: #{suggestions.join(', ')}?"
92
+ else
93
+ "Error: Primitive '#{name}' not found. Available primitives: #{available.join(', ')}"
94
+ end
95
+ end
96
+
97
+ def universal_primitive?(primitive)
98
+ primitive.class.name&.start_with?("PromptObjects::Universal")
99
+ end
100
+
101
+ # Simple check for similar strings (within 2 edits)
102
+ def levenshtein_similar?(a, b)
103
+ return true if a == b
104
+ return false if (a.length - b.length).abs > 2
105
+
106
+ # Simple approximation: check if they share most characters
107
+ common = (a.chars & b.chars).length
108
+ max_len = [a.length, b.length].max
109
+ common.to_f / max_len > 0.6
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to pause and ask the human a question.
6
+ # For now this is synchronous (blocking). In Phase 5 with the TUI,
7
+ # this will become async with a notification queue.
8
+ class AskHuman < Primitive
9
+ def name
10
+ "ask_human"
11
+ end
12
+
13
+ def description
14
+ "Pause and ask the human a question. Use this when you need confirmation, clarification, or input."
15
+ end
16
+
17
+ def parameters
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ question: {
22
+ type: "string",
23
+ description: "The question to ask the human"
24
+ },
25
+ options: {
26
+ type: "array",
27
+ items: { type: "string" },
28
+ description: "Optional list of choices to present"
29
+ }
30
+ },
31
+ required: ["question"]
32
+ }
33
+ end
34
+
35
+ def receive(message, context:)
36
+ question = message[:question] || message["question"]
37
+ options = message[:options] || message["options"]
38
+
39
+ # In TUI mode, use the human queue (non-blocking for UI)
40
+ if context.tui_mode && context.human_queue
41
+ return receive_tui(question, options, context)
42
+ end
43
+
44
+ # REPL mode - use stdin directly
45
+ receive_repl(question, options, context)
46
+ end
47
+
48
+ private
49
+
50
+ def receive_tui(question, options, context)
51
+ # Queue the request and wait for response
52
+ request = context.human_queue.enqueue(
53
+ capability: context.current_capability,
54
+ question: question,
55
+ options: options
56
+ )
57
+
58
+ # Log to message bus
59
+ context.bus.publish(
60
+ from: context.current_capability,
61
+ to: "human",
62
+ message: "[waiting] #{question}"
63
+ )
64
+
65
+ # Block this thread until human responds via UI
66
+ response = request.wait_for_response
67
+
68
+ # Log the response
69
+ context.bus.publish(
70
+ from: "human",
71
+ to: context.current_capability,
72
+ message: response
73
+ )
74
+
75
+ response
76
+ end
77
+
78
+ def receive_repl(question, options, context)
79
+ puts
80
+ puts "┌─ #{context.current_capability} asks ──────────────────────────────────┐"
81
+ puts "│"
82
+ puts "│ #{question}"
83
+ puts "│"
84
+
85
+ if options && !options.empty?
86
+ options.each_with_index do |opt, i|
87
+ puts "│ [#{i + 1}] #{opt}"
88
+ end
89
+ puts "│"
90
+ puts "└──────────────────────────────────────────────────────────────┘"
91
+ print "Your choice (1-#{options.length}): "
92
+
93
+ choice = $stdin.gets&.chomp
94
+ index = choice.to_i - 1
95
+
96
+ if index >= 0 && index < options.length
97
+ options[index]
98
+ else
99
+ choice # Return raw input if not a valid index
100
+ end
101
+ else
102
+ puts "└──────────────────────────────────────────────────────────────┘"
103
+ print "Your answer: "
104
+ $stdin.gets&.chomp || ""
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module PromptObjects
6
+ module Universal
7
+ # Universal capability to create new capabilities at runtime.
8
+ # Can create both prompt objects (markdown) and primitives (Ruby code).
9
+ # This enables self-modification - the system can create new specialists and tools.
10
+ class CreateCapability < Primitive
11
+ def name
12
+ "create_capability"
13
+ end
14
+
15
+ def description
16
+ "Create a new capability. Use type='prompt_object' for LLM-backed specialists, or type='primitive' for deterministic Ruby tools."
17
+ end
18
+
19
+ def parameters
20
+ {
21
+ type: "object",
22
+ properties: {
23
+ type: {
24
+ type: "string",
25
+ enum: ["prompt_object", "primitive"],
26
+ description: "Type of capability: 'prompt_object' for LLM specialists, 'primitive' for Ruby tools"
27
+ },
28
+ name: {
29
+ type: "string",
30
+ description: "Name for the capability (lowercase, underscores allowed)"
31
+ },
32
+ description: {
33
+ type: "string",
34
+ description: "Brief description of what this capability does"
35
+ },
36
+ # For prompt_objects:
37
+ capabilities: {
38
+ type: "array",
39
+ items: { type: "string" },
40
+ description: "(prompt_object only) List of capabilities this PO can use"
41
+ },
42
+ identity: {
43
+ type: "string",
44
+ description: "(prompt_object only) Who is this prompt object? Their personality and role."
45
+ },
46
+ behavior: {
47
+ type: "string",
48
+ description: "(prompt_object only) How should this prompt object behave?"
49
+ },
50
+ # For primitives:
51
+ parameters_schema: {
52
+ type: "object",
53
+ description: "(primitive only) JSON Schema for the parameters this primitive accepts"
54
+ },
55
+ ruby_code: {
56
+ type: "string",
57
+ description: "(primitive only) Ruby code for the receive method body. Has access to 'message' (Hash) and 'context'."
58
+ }
59
+ },
60
+ required: ["type", "name", "description"]
61
+ }
62
+ end
63
+
64
+ def receive(message, context:)
65
+ type = message[:type] || message["type"]
66
+ cap_name = message[:name] || message["name"]
67
+
68
+ # Validate name
69
+ unless cap_name && cap_name.match?(/\A[a-z][a-z0-9_]*\z/)
70
+ return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
71
+ end
72
+
73
+ # Check if already exists
74
+ if context.env.registry.exists?(cap_name)
75
+ return "Error: A capability named '#{cap_name}' already exists."
76
+ end
77
+
78
+ result = case type
79
+ when "prompt_object"
80
+ create_prompt_object(message, context)
81
+ when "primitive"
82
+ create_primitive(message, context)
83
+ else
84
+ return "Error: type must be 'prompt_object' or 'primitive'"
85
+ end
86
+
87
+ # If creation succeeded, add the new capability to the creating PO
88
+ if result && !result.start_with?("Error:")
89
+ added_msg = add_to_creator(cap_name, context)
90
+ result = "#{result} #{added_msg}" if added_msg
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ private
97
+
98
+ # Add the newly created capability to the PO that created it
99
+ # Returns a message if added, nil otherwise
100
+ def add_to_creator(cap_name, context)
101
+ creator_name = context.calling_po
102
+ return nil unless creator_name
103
+
104
+ creator_po = context.env.registry.get(creator_name)
105
+ return nil unless creator_po.is_a?(PromptObject)
106
+
107
+ # Add to the creator's capabilities if not already present
108
+ creator_po.config["capabilities"] ||= []
109
+ unless creator_po.config["capabilities"].include?(cap_name)
110
+ creator_po.config["capabilities"] << cap_name
111
+ # Persist the change to file
112
+ saved = creator_po.save ? " and saved" : ""
113
+ return "Also added '#{cap_name}' to #{creator_name}'s capabilities#{saved}."
114
+ end
115
+
116
+ nil
117
+ end
118
+
119
+ def create_prompt_object(message, context)
120
+ cap_name = message[:name] || message["name"]
121
+ description = message[:description] || message["description"]
122
+ capabilities = message[:capabilities] || message["capabilities"] || []
123
+ identity = message[:identity] || message["identity"] || "You are a helpful assistant."
124
+ behavior = message[:behavior] || message["behavior"] || "Help the user with their request."
125
+
126
+ # Build the markdown content
127
+ frontmatter = {
128
+ "name" => cap_name,
129
+ "description" => description,
130
+ "capabilities" => capabilities
131
+ }
132
+
133
+ body = <<~MARKDOWN
134
+ # #{cap_name.split('_').map(&:capitalize).join(' ')}
135
+
136
+ ## Identity
137
+
138
+ #{identity}
139
+
140
+ ## Behavior
141
+
142
+ #{behavior}
143
+ MARKDOWN
144
+
145
+ # to_yaml already includes leading "---\n", so we just need the closing ---
146
+ content = "#{frontmatter.to_yaml}---\n\n#{body}"
147
+
148
+ # Write to file
149
+ path = File.join(context.env.objects_dir, "#{cap_name}.md")
150
+ File.write(path, content, encoding: "UTF-8")
151
+
152
+ # Load into environment
153
+ context.env.load_prompt_object(path)
154
+
155
+ "Created prompt object '#{cap_name}' with capabilities: #{capabilities.join(', ')}. It's now available."
156
+ end
157
+
158
+ def create_primitive(message, context)
159
+ cap_name = message[:name] || message["name"]
160
+ description = message[:description] || message["description"]
161
+ params_schema = message[:parameters_schema] || message["parameters_schema"] || {}
162
+ ruby_code = message[:ruby_code] || message["ruby_code"]
163
+
164
+ unless ruby_code
165
+ return "Error: ruby_code is required for primitives"
166
+ end
167
+
168
+ # Generate the Ruby class
169
+ class_name = cap_name.split('_').map(&:capitalize).join
170
+
171
+ ruby_content = <<~RUBY
172
+ # frozen_string_literal: true
173
+ # Auto-generated primitive: #{cap_name}
174
+
175
+ module PromptObjects
176
+ module Primitives
177
+ class #{class_name} < Primitive
178
+ def name
179
+ "#{cap_name}"
180
+ end
181
+
182
+ def description
183
+ "#{description.gsub('"', '\\"')}"
184
+ end
185
+
186
+ def parameters
187
+ #{params_schema.inspect}
188
+ end
189
+
190
+ def receive(message, context:)
191
+ #{ruby_code.gsub(/^/, ' ').strip}
192
+ end
193
+ end
194
+ end
195
+ end
196
+ RUBY
197
+
198
+ # Write to primitives directory (uses env.primitives_dir for sandbox support)
199
+ FileUtils.mkdir_p(context.env.primitives_dir)
200
+ path = File.join(context.env.primitives_dir, "#{cap_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
+ "Created primitive '#{cap_name}'. It's now available. File: #{path}"
209
+ rescue SyntaxError => e
210
+ File.delete(path) if File.exist?(path)
211
+ "Error: Invalid Ruby syntax - #{e.message}"
212
+ rescue StandardError => e
213
+ File.delete(path) if File.exist?(path)
214
+ "Error creating primitive: #{e.message}"
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module PromptObjects
6
+ module Universal
7
+ # Universal capability to create new primitives (deterministic Ruby code).
8
+ # This enables POs to create their own tools at runtime.
9
+ class CreatePrimitive < Primitive
10
+ def name
11
+ "create_primitive"
12
+ end
13
+
14
+ def description
15
+ "Create a new primitive (deterministic Ruby tool). The primitive will be saved to the environment and added to your capabilities."
16
+ end
17
+
18
+ def parameters
19
+ {
20
+ type: "object",
21
+ properties: {
22
+ name: {
23
+ type: "string",
24
+ description: "Name for the primitive (lowercase, underscores allowed, e.g., 'parse_json')"
25
+ },
26
+ description: {
27
+ type: "string",
28
+ description: "Brief description of what this primitive does"
29
+ },
30
+ code: {
31
+ type: "string",
32
+ description: "Ruby code for the primitive's receive method. Has access to 'message' (Hash with parameters) and 'context'. Should return a String result."
33
+ },
34
+ parameters_schema: {
35
+ type: "object",
36
+ description: "Optional JSON Schema for the parameters this primitive accepts"
37
+ }
38
+ },
39
+ required: ["name", "description", "code"]
40
+ }
41
+ end
42
+
43
+ def receive(message, context:)
44
+ prim_name = message[:name] || message["name"]
45
+ description = message[:description] || message["description"]
46
+ code = message[:code] || message["code"]
47
+ params_schema = message[:parameters_schema] || message["parameters_schema"] || default_params_schema
48
+
49
+ # Validate name
50
+ unless prim_name && prim_name.match?(/\A[a-z][a-z0-9_]*\z/)
51
+ return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
52
+ end
53
+
54
+ # Check if already exists
55
+ if context.env.registry.exists?(prim_name)
56
+ return "Error: A capability named '#{prim_name}' already exists. Use modify_primitive to update it."
57
+ end
58
+
59
+ # Validate code syntax
60
+ syntax_error = validate_syntax(code)
61
+ return "Error: Invalid Ruby syntax - #{syntax_error}" if syntax_error
62
+
63
+ # Generate the Ruby class
64
+ class_name = prim_name.split("_").map(&:capitalize).join
65
+ ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, code)
66
+
67
+ # Write to primitives directory
68
+ FileUtils.mkdir_p(context.env.primitives_dir)
69
+ path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
70
+ File.write(path, ruby_content, encoding: "UTF-8")
71
+
72
+ # Load and register
73
+ begin
74
+ load(path)
75
+ klass = PromptObjects::Primitives.const_get(class_name)
76
+ context.env.registry.register(klass.new)
77
+ rescue SyntaxError => e
78
+ File.delete(path) if File.exist?(path)
79
+ return "Error: Invalid Ruby syntax in generated code - #{e.message}"
80
+ rescue StandardError => e
81
+ File.delete(path) if File.exist?(path)
82
+ return "Error creating primitive: #{e.message}"
83
+ end
84
+
85
+ # Auto-add to the creating PO
86
+ added_msg = add_to_caller(prim_name, context)
87
+
88
+ result = "Created primitive '#{prim_name}' at #{path}."
89
+ result += " #{added_msg}" if added_msg
90
+ result
91
+ end
92
+
93
+ private
94
+
95
+ def default_params_schema
96
+ {
97
+ type: "object",
98
+ properties: {},
99
+ required: []
100
+ }
101
+ end
102
+
103
+ def validate_syntax(code)
104
+ # Try to parse the code to check for syntax errors
105
+ eval("proc { #{code} }")
106
+ nil
107
+ rescue SyntaxError => e
108
+ e.message.sub(/^\(eval\):\d+: /, "")
109
+ rescue StandardError
110
+ nil # Other errors are OK at this stage
111
+ end
112
+
113
+ def generate_ruby_class(class_name, prim_name, description, params_schema, code)
114
+ # Escape description for string literal
115
+ escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
116
+
117
+ <<~RUBY
118
+ # frozen_string_literal: true
119
+ # Auto-generated primitive: #{prim_name}
120
+ # Created by PO at #{Time.now.iso8601}
121
+
122
+ module PromptObjects
123
+ module Primitives
124
+ class #{class_name} < Primitive
125
+ def name
126
+ "#{prim_name}"
127
+ end
128
+
129
+ def description
130
+ "#{escaped_desc}"
131
+ end
132
+
133
+ def parameters
134
+ #{params_schema.inspect}
135
+ end
136
+
137
+ def receive(message, context:)
138
+ #{indent_code(code, 10)}
139
+ end
140
+ end
141
+ end
142
+ end
143
+ RUBY
144
+ end
145
+
146
+ def indent_code(code, spaces)
147
+ code.lines.map.with_index do |line, i|
148
+ i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
149
+ end.join("\n")
150
+ end
151
+
152
+ def add_to_caller(prim_name, context)
153
+ caller_name = context.calling_po
154
+ return nil unless caller_name
155
+
156
+ caller_po = context.env.registry.get(caller_name)
157
+ return nil unless caller_po.is_a?(PromptObject)
158
+
159
+ caller_po.config["capabilities"] ||= []
160
+ unless caller_po.config["capabilities"].include?(prim_name)
161
+ caller_po.config["capabilities"] << prim_name
162
+ saved = caller_po.save ? " and saved to file" : ""
163
+ return "Added '#{prim_name}' to your capabilities#{saved}."
164
+ end
165
+
166
+ nil
167
+ end
168
+ end
169
+ end
170
+ end