prompt_objects 0.3.1 → 0.5.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 +32 -0
- data/CLAUDE.md +112 -44
- data/Gemfile.lock +31 -29
- data/README.md +5 -0
- data/frontend/index.html +5 -1
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +70 -71
- data/frontend/src/canvas/CanvasView.tsx +113 -0
- data/frontend/src/canvas/ForceLayout.ts +115 -0
- data/frontend/src/canvas/SceneManager.ts +587 -0
- data/frontend/src/canvas/canvasStore.ts +47 -0
- data/frontend/src/canvas/constants.ts +95 -0
- data/frontend/src/canvas/controls/CameraControls.ts +98 -0
- data/frontend/src/canvas/edges/MessageArc.ts +149 -0
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
- data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
- data/frontend/src/canvas/nodes/PONode.ts +249 -0
- data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
- data/frontend/src/canvas/types.ts +24 -0
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/Inspector.tsx +232 -0
- data/frontend/src/components/MarkdownMessage.tsx +22 -20
- data/frontend/src/components/MethodList.tsx +90 -0
- data/frontend/src/components/ModelSelector.tsx +13 -14
- data/frontend/src/components/NotificationPanel.tsx +29 -33
- data/frontend/src/components/ObjectList.tsx +78 -0
- data/frontend/src/components/PaneSlot.tsx +30 -0
- data/frontend/src/components/SourcePane.tsx +202 -0
- data/frontend/src/components/SystemBar.tsx +74 -0
- data/frontend/src/components/Transcript.tsx +76 -0
- data/frontend/src/components/UsagePanel.tsx +27 -27
- data/frontend/src/components/Workspace.tsx +260 -0
- data/frontend/src/components/index.ts +10 -9
- data/frontend/src/hooks/useResize.ts +55 -0
- data/frontend/src/hooks/useWebSocket.ts +274 -189
- data/frontend/src/index.css +69 -3
- data/frontend/src/store/index.ts +23 -0
- data/frontend/src/types/index.ts +5 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +5 -22
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +150 -6
- data/lib/prompt_objects/server/api/routes.rb +3 -48
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +23 -100
- data/lib/prompt_objects/server.rb +6 -62
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
- data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
- data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
- metadata +26 -14
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -288
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -141
- data/frontend/src/components/MessageBus.tsx +0 -56
- data/frontend/src/components/POCard.tsx +0 -56
- data/frontend/src/components/PODetail.tsx +0 -124
- data/frontend/src/components/PromptPanel.tsx +0 -156
- data/frontend/src/components/SessionsPanel.tsx +0 -174
- data/frontend/src/components/ThreadsSidebar.tsx +0 -163
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
data/frontend/tailwind.config.js
CHANGED
|
@@ -7,18 +7,37 @@ export default {
|
|
|
7
7
|
theme: {
|
|
8
8
|
extend: {
|
|
9
9
|
colors: {
|
|
10
|
-
// Custom colors for PromptObjects
|
|
11
10
|
po: {
|
|
12
|
-
bg: '#
|
|
13
|
-
surface: '#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
bg: '#1a1918',
|
|
12
|
+
surface: '#222120',
|
|
13
|
+
'surface-2': '#2c2a28',
|
|
14
|
+
'surface-3': '#363432',
|
|
15
|
+
border: '#3d3a37',
|
|
16
|
+
'border-focus': '#5c5752',
|
|
17
|
+
accent: '#d4952a',
|
|
18
|
+
'accent-muted': '#9a6d20',
|
|
19
|
+
'accent-wash': 'rgba(212,149,42,0.08)',
|
|
20
|
+
'text-primary': '#e8e2da',
|
|
21
|
+
'text-secondary': '#a8a29a',
|
|
22
|
+
'text-tertiary': '#78726a',
|
|
23
|
+
'text-ghost': '#524e48',
|
|
24
|
+
'status-idle': '#78726a',
|
|
25
|
+
'status-active': '#d4952a',
|
|
26
|
+
'status-calling': '#3b9a6e',
|
|
27
|
+
'status-error': '#c45c4a',
|
|
28
|
+
'status-delegated': '#5a8fc2',
|
|
29
|
+
success: '#3b9a6e',
|
|
30
|
+
warning: '#d4952a',
|
|
31
|
+
error: '#c45c4a',
|
|
20
32
|
},
|
|
21
33
|
},
|
|
34
|
+
fontFamily: {
|
|
35
|
+
ui: ['Geist', 'system-ui', 'sans-serif'],
|
|
36
|
+
mono: ['"Geist Mono"', '"IBM Plex Mono"', 'monospace'],
|
|
37
|
+
},
|
|
38
|
+
fontSize: {
|
|
39
|
+
'2xs': ['11px', { lineHeight: '15px' }],
|
|
40
|
+
},
|
|
22
41
|
},
|
|
23
42
|
},
|
|
24
43
|
plugins: [],
|
|
@@ -27,11 +27,33 @@ module PromptObjects
|
|
|
27
27
|
function: {
|
|
28
28
|
name: name,
|
|
29
29
|
description: description,
|
|
30
|
-
parameters: parameters
|
|
30
|
+
parameters: sanitize_schema(parameters)
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Ensure array-typed properties have an `items` field.
|
|
38
|
+
# LLM APIs (Gemini, OpenAI, Ollama) reject array schemas without items.
|
|
39
|
+
def sanitize_schema(schema)
|
|
40
|
+
return schema unless schema.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
schema = schema.dup
|
|
43
|
+
|
|
44
|
+
if schema[:type] == "array" && !schema.key?(:items) && !schema.key?("items")
|
|
45
|
+
schema[:items] = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if schema[:properties].is_a?(Hash)
|
|
49
|
+
schema[:properties] = schema[:properties].transform_values { |v| sanitize_schema(v) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
schema
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
public
|
|
56
|
+
|
|
35
57
|
# Define the parameters this capability accepts.
|
|
36
58
|
# Override in subclasses for specific parameter schemas.
|
|
37
59
|
# @return [Hash] JSON Schema for parameters
|
|
@@ -176,20 +176,14 @@ module PromptObjects
|
|
|
176
176
|
|
|
177
177
|
input_schema(
|
|
178
178
|
type: "object",
|
|
179
|
-
properties: {}
|
|
180
|
-
required: []
|
|
179
|
+
properties: {}
|
|
181
180
|
)
|
|
182
181
|
|
|
183
182
|
def self.call(server_context:)
|
|
184
183
|
env = server_context[:env]
|
|
185
184
|
|
|
186
185
|
pos = env.registry.prompt_objects.map do |po|
|
|
187
|
-
|
|
188
|
-
name: po.name,
|
|
189
|
-
description: po.description,
|
|
190
|
-
state: po.state.to_s,
|
|
191
|
-
capabilities: po.config["capabilities"] || []
|
|
192
|
-
}
|
|
186
|
+
po.to_summary_hash(registry: env.registry)
|
|
193
187
|
end
|
|
194
188
|
|
|
195
189
|
::MCP::Tool::Response.new([{
|
|
@@ -367,8 +361,7 @@ module PromptObjects
|
|
|
367
361
|
type: "string",
|
|
368
362
|
description: "Filter by source: tui, mcp, api, web (optional)"
|
|
369
363
|
}
|
|
370
|
-
}
|
|
371
|
-
required: []
|
|
364
|
+
}
|
|
372
365
|
)
|
|
373
366
|
|
|
374
367
|
def self.call(po_name: nil, source: nil, server_context:)
|
|
@@ -413,8 +406,7 @@ module PromptObjects
|
|
|
413
406
|
|
|
414
407
|
input_schema(
|
|
415
408
|
type: "object",
|
|
416
|
-
properties: {}
|
|
417
|
-
required: []
|
|
409
|
+
properties: {}
|
|
418
410
|
)
|
|
419
411
|
|
|
420
412
|
def self.call(server_context:)
|
|
@@ -502,16 +494,7 @@ module PromptObjects
|
|
|
502
494
|
}])
|
|
503
495
|
end
|
|
504
496
|
|
|
505
|
-
info =
|
|
506
|
-
name: po.name,
|
|
507
|
-
description: po.description,
|
|
508
|
-
state: po.state.to_s,
|
|
509
|
-
prompt: po.body,
|
|
510
|
-
capabilities: po.config["capabilities"] || [],
|
|
511
|
-
config: po.config,
|
|
512
|
-
session_id: po.instance_variable_get(:@session_id),
|
|
513
|
-
history_length: po.history.length
|
|
514
|
-
}
|
|
497
|
+
info = po.to_inspect_hash(registry: env.registry)
|
|
515
498
|
|
|
516
499
|
::MCP::Tool::Response.new([{
|
|
517
500
|
type: "text",
|
|
@@ -41,6 +41,7 @@ module PromptObjects
|
|
|
41
41
|
:current_provider, :current_model
|
|
42
42
|
attr_accessor :on_po_registered # Callback for when a PO is registered
|
|
43
43
|
attr_accessor :on_po_modified # Callback for when a PO is modified (capabilities changed, etc.)
|
|
44
|
+
attr_accessor :on_delegation_event # Callback for PO-to-PO delegation start/complete
|
|
44
45
|
|
|
45
46
|
# Initialize from an environment path (with manifest) or objects directory.
|
|
46
47
|
# @param env_path [String, nil] Path to environment directory (preferred)
|
|
@@ -215,6 +216,13 @@ module PromptObjects
|
|
|
215
216
|
@on_po_modified&.call(po)
|
|
216
217
|
end
|
|
217
218
|
|
|
219
|
+
# Notify that a PO-to-PO delegation has started or completed.
|
|
220
|
+
# @param event_type [Symbol] :started or :completed
|
|
221
|
+
# @param payload [Hash] { target:, caller:, thread_id:, tool_call_id: }
|
|
222
|
+
def notify_delegation(event_type, payload)
|
|
223
|
+
@on_delegation_event&.call(event_type, payload)
|
|
224
|
+
end
|
|
225
|
+
|
|
218
226
|
# Load a prompt object by name from the objects directory.
|
|
219
227
|
# @param name [String] Name of the prompt object (without .md extension)
|
|
220
228
|
# @return [PromptObject]
|
|
@@ -37,7 +37,29 @@ module PromptObjects
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
raw_response = @client.chat(parameters: params)
|
|
40
|
+
|
|
41
|
+
# Check for error responses (Ollama and some providers return errors inline)
|
|
42
|
+
if raw_response.is_a?(Hash) && raw_response["error"]
|
|
43
|
+
error_msg = raw_response["error"]
|
|
44
|
+
error_msg = error_msg["message"] if error_msg.is_a?(Hash)
|
|
45
|
+
raise Error, "#{@provider_name} API error: #{error_msg}"
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
parse_response(raw_response)
|
|
49
|
+
rescue Faraday::ClientError => e
|
|
50
|
+
# Extract error body from 4xx responses (ruby-openai wraps these)
|
|
51
|
+
body = e.response&.dig(:body) rescue nil
|
|
52
|
+
detail = if body.is_a?(String)
|
|
53
|
+
begin
|
|
54
|
+
parsed = JSON.parse(body)
|
|
55
|
+
parsed.dig("error", "message") || parsed["error"] || body
|
|
56
|
+
rescue JSON::ParserError
|
|
57
|
+
body
|
|
58
|
+
end
|
|
59
|
+
elsif body.is_a?(Hash)
|
|
60
|
+
body.dig("error", "message") || body["error"] || body.to_s
|
|
61
|
+
end
|
|
62
|
+
raise Error, "#{@provider_name} API error (#{e.response&.dig(:status)}): #{detail || e.message}"
|
|
41
63
|
end
|
|
42
64
|
|
|
43
65
|
private
|
|
@@ -30,37 +30,7 @@ module PromptObjects
|
|
|
30
30
|
}])
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
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
|
-
}
|
|
33
|
+
info = po.to_inspect_hash(registry: env.registry)
|
|
64
34
|
|
|
65
35
|
::MCP::Tool::Response.new([{
|
|
66
36
|
type: "text",
|
|
@@ -10,20 +10,14 @@ module PromptObjects
|
|
|
10
10
|
|
|
11
11
|
input_schema(
|
|
12
12
|
type: "object",
|
|
13
|
-
properties: {}
|
|
14
|
-
required: []
|
|
13
|
+
properties: {}
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
def self.call(server_context:, **_args)
|
|
18
17
|
env = server_context[:env]
|
|
19
18
|
|
|
20
19
|
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
|
-
}
|
|
20
|
+
po.to_summary_hash(registry: env.registry)
|
|
27
21
|
end
|
|
28
22
|
|
|
29
23
|
::MCP::Tool::Response.new([{
|
|
@@ -232,8 +232,134 @@ module PromptObjects
|
|
|
232
232
|
@session_id
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
+
# --- Serialization ---
|
|
236
|
+
# Canonical methods for converting PO state to hashes for broadcasting,
|
|
237
|
+
# REST API responses, and MCP tool output. All consumers should use these
|
|
238
|
+
# to ensure consistent capability format across WebSocket, REST, and MCP.
|
|
239
|
+
|
|
240
|
+
# Full state hash for WebSocket/broadcast consumers.
|
|
241
|
+
# Matches the frontend's PromptObject TypeScript type.
|
|
242
|
+
# @param registry [Registry, nil] Registry for resolving capability descriptions
|
|
243
|
+
# @return [Hash]
|
|
244
|
+
def to_state_hash(registry: nil)
|
|
245
|
+
{
|
|
246
|
+
status: (@state || :idle).to_s,
|
|
247
|
+
description: description,
|
|
248
|
+
capabilities: resolve_capabilities(registry: registry),
|
|
249
|
+
universal_capabilities: resolve_universal_capabilities(registry: registry),
|
|
250
|
+
current_session: serialize_current_session,
|
|
251
|
+
sessions: list_sessions.map { |s| self.class.serialize_session(s) },
|
|
252
|
+
prompt: body,
|
|
253
|
+
config: config
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Summary hash for list endpoints (REST API, MCP list tools).
|
|
258
|
+
# Lightweight: no prompt, no session messages, no universal capabilities.
|
|
259
|
+
# @param registry [Registry, nil] Registry for resolving capability descriptions
|
|
260
|
+
# @return [Hash]
|
|
261
|
+
def to_summary_hash(registry: nil)
|
|
262
|
+
{
|
|
263
|
+
name: name,
|
|
264
|
+
description: description,
|
|
265
|
+
status: (@state || :idle).to_s,
|
|
266
|
+
capabilities: resolve_capabilities(registry: registry),
|
|
267
|
+
session_count: list_sessions.size
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Detailed inspection hash for single-PO endpoints (REST GET, MCP inspect).
|
|
272
|
+
# Everything in summary plus prompt, config, sessions, and universals.
|
|
273
|
+
# @param registry [Registry, nil] Registry for resolving capability descriptions
|
|
274
|
+
# @return [Hash]
|
|
275
|
+
def to_inspect_hash(registry: nil)
|
|
276
|
+
{
|
|
277
|
+
name: name,
|
|
278
|
+
description: description,
|
|
279
|
+
status: (@state || :idle).to_s,
|
|
280
|
+
capabilities: resolve_capabilities(registry: registry),
|
|
281
|
+
universal_capabilities: resolve_universal_capabilities(registry: registry),
|
|
282
|
+
prompt: body,
|
|
283
|
+
config: config,
|
|
284
|
+
session_id: session_id,
|
|
285
|
+
sessions: list_sessions.map { |s| self.class.serialize_session(s) },
|
|
286
|
+
history_length: history.length
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Serialize a message (in-memory or from DB) to a JSON-ready hash.
|
|
291
|
+
# Handles both ToolCall objects and plain Hashes (from SQLite).
|
|
292
|
+
# @param msg [Hash] The message to serialize
|
|
293
|
+
# @return [Hash]
|
|
294
|
+
def self.serialize_message(msg)
|
|
295
|
+
case msg[:role]
|
|
296
|
+
when :user
|
|
297
|
+
from = msg[:from] || msg[:from_po]
|
|
298
|
+
{ role: "user", content: msg[:content], from: from }
|
|
299
|
+
when :assistant
|
|
300
|
+
hash = { role: "assistant", content: msg[:content] }
|
|
301
|
+
if msg[:tool_calls]
|
|
302
|
+
hash[:tool_calls] = msg[:tool_calls].map do |tc|
|
|
303
|
+
if tc.respond_to?(:id)
|
|
304
|
+
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
305
|
+
else
|
|
306
|
+
{ id: tc[:id] || tc["id"], name: tc[:name] || tc["name"], arguments: tc[:arguments] || tc["arguments"] || {} }
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
hash
|
|
311
|
+
when :tool
|
|
312
|
+
results = msg[:results] || msg[:tool_results]
|
|
313
|
+
{ role: "tool", results: results }
|
|
314
|
+
else
|
|
315
|
+
{ role: msg[:role].to_s, content: msg[:content] }
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Serialize a session record to a JSON-ready hash with thread fields.
|
|
320
|
+
# @param session [Hash] Session record from session store
|
|
321
|
+
# @return [Hash]
|
|
322
|
+
def self.serialize_session(session)
|
|
323
|
+
{
|
|
324
|
+
id: session[:id],
|
|
325
|
+
name: session[:name],
|
|
326
|
+
message_count: session[:message_count] || 0,
|
|
327
|
+
updated_at: session[:updated_at]&.iso8601,
|
|
328
|
+
parent_session_id: session[:parent_session_id],
|
|
329
|
+
parent_po: session[:parent_po],
|
|
330
|
+
thread_type: session[:thread_type] || "root"
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
235
334
|
private
|
|
236
335
|
|
|
336
|
+
# Build capability info objects for this PO's declared capabilities.
|
|
337
|
+
def resolve_capabilities(registry: nil)
|
|
338
|
+
declared = @config["capabilities"] || []
|
|
339
|
+
return declared.map { |n| { name: n, description: n } } unless registry
|
|
340
|
+
|
|
341
|
+
declared.map do |cap_name|
|
|
342
|
+
cap = registry.get(cap_name)
|
|
343
|
+
{ name: cap_name, description: cap&.description || "Capability not found", parameters: cap&.parameters }
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Build capability info objects for universal capabilities.
|
|
348
|
+
def resolve_universal_capabilities(registry: nil)
|
|
349
|
+
return [] unless registry
|
|
350
|
+
|
|
351
|
+
UNIVERSAL_CAPABILITIES.map do |cap_name|
|
|
352
|
+
cap = registry.get(cap_name)
|
|
353
|
+
{ name: cap_name, description: cap&.description || "Universal capability", parameters: cap&.parameters }
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Serialize current session with messages for real-time state.
|
|
358
|
+
def serialize_current_session
|
|
359
|
+
return nil unless session_id
|
|
360
|
+
{ id: session_id, messages: history.map { |m| self.class.serialize_message(m) } }
|
|
361
|
+
end
|
|
362
|
+
|
|
237
363
|
def normalize_message(message)
|
|
238
364
|
case message
|
|
239
365
|
when String
|
|
@@ -328,12 +454,30 @@ module PromptObjects
|
|
|
328
454
|
parent_message_id: get_last_message_id
|
|
329
455
|
)
|
|
330
456
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
target_po.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
457
|
+
# Notify delegation start so WebSocket clients see the target PO activate
|
|
458
|
+
@env.notify_delegation(:started, {
|
|
459
|
+
target: target_po.name,
|
|
460
|
+
caller: name,
|
|
461
|
+
thread_id: delegation_thread,
|
|
462
|
+
tool_call_id: tool_call.id
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
begin
|
|
466
|
+
if delegation_thread
|
|
467
|
+
# Execute in isolated thread
|
|
468
|
+
target_po.receive_in_thread(tool_call.arguments, context: context, thread_id: delegation_thread)
|
|
469
|
+
else
|
|
470
|
+
# Fallback: execute in target's current session (no session store)
|
|
471
|
+
target_po.receive(tool_call.arguments, context: context)
|
|
472
|
+
end
|
|
473
|
+
ensure
|
|
474
|
+
# Notify delegation complete — target PO is done
|
|
475
|
+
@env.notify_delegation(:completed, {
|
|
476
|
+
target: target_po.name,
|
|
477
|
+
caller: name,
|
|
478
|
+
thread_id: delegation_thread,
|
|
479
|
+
tool_call_id: tool_call.id
|
|
480
|
+
})
|
|
337
481
|
end
|
|
338
482
|
end
|
|
339
483
|
|
|
@@ -192,13 +192,7 @@ module PromptObjects
|
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
sessions = po.list_sessions.map do |s|
|
|
195
|
-
|
|
196
|
-
id: s[:id],
|
|
197
|
-
name: s[:name],
|
|
198
|
-
message_count: s[:message_count] || 0,
|
|
199
|
-
created_at: s[:created_at]&.iso8601,
|
|
200
|
-
updated_at: s[:updated_at]&.iso8601
|
|
201
|
-
}
|
|
195
|
+
PromptObject.serialize_session(s)
|
|
202
196
|
end
|
|
203
197
|
|
|
204
198
|
{ sessions: sessions }
|
|
@@ -372,31 +366,11 @@ module PromptObjects
|
|
|
372
366
|
# === Helpers ===
|
|
373
367
|
|
|
374
368
|
def po_summary(po)
|
|
375
|
-
|
|
376
|
-
name: po.name,
|
|
377
|
-
description: po.description,
|
|
378
|
-
capabilities: po.config["capabilities"] || [],
|
|
379
|
-
session_count: po.list_sessions.size
|
|
380
|
-
}
|
|
369
|
+
po.to_summary_hash(registry: @runtime.registry)
|
|
381
370
|
end
|
|
382
371
|
|
|
383
372
|
def po_full(po)
|
|
384
|
-
|
|
385
|
-
name: po.name,
|
|
386
|
-
description: po.description,
|
|
387
|
-
capabilities: po.config["capabilities"] || [],
|
|
388
|
-
body: po.body,
|
|
389
|
-
config: po.config,
|
|
390
|
-
sessions: po.list_sessions.map do |s|
|
|
391
|
-
{
|
|
392
|
-
id: s[:id],
|
|
393
|
-
name: s[:name],
|
|
394
|
-
message_count: s[:message_count] || 0
|
|
395
|
-
}
|
|
396
|
-
end,
|
|
397
|
-
current_session: po.session_id,
|
|
398
|
-
history: po.history.map { |m| format_history_message(m) }
|
|
399
|
-
}
|
|
373
|
+
po.to_inspect_hash(registry: @runtime.registry)
|
|
400
374
|
end
|
|
401
375
|
|
|
402
376
|
def format_message(msg)
|
|
@@ -410,25 +384,6 @@ module PromptObjects
|
|
|
410
384
|
}.compact
|
|
411
385
|
end
|
|
412
386
|
|
|
413
|
-
def format_history_message(msg)
|
|
414
|
-
case msg[:role]
|
|
415
|
-
when :user
|
|
416
|
-
{ role: "user", content: msg[:content], from: msg[:from] }
|
|
417
|
-
when :assistant
|
|
418
|
-
h = { role: "assistant", content: msg[:content] }
|
|
419
|
-
if msg[:tool_calls]
|
|
420
|
-
h[:tool_calls] = msg[:tool_calls].map do |tc|
|
|
421
|
-
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
h
|
|
425
|
-
when :tool
|
|
426
|
-
{ role: "tool", results: msg[:results] }
|
|
427
|
-
else
|
|
428
|
-
{ role: msg[:role].to_s, content: msg[:content] }
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
|
|
432
387
|
def path_param(path, index)
|
|
433
388
|
# Extract path parameter from regex match
|
|
434
389
|
# /prompt_objects/foo -> foo (index 1)
|
|
@@ -19,6 +19,15 @@ module PromptObjects
|
|
|
19
19
|
@public_path = File.expand_path("public", __dir__)
|
|
20
20
|
@connections = []
|
|
21
21
|
@connections_mutex = Mutex.new
|
|
22
|
+
|
|
23
|
+
# Broadcast PO-to-PO delegation events to all connected clients.
|
|
24
|
+
# This makes delegated POs visible in the UI (canvas + chat).
|
|
25
|
+
@runtime.on_delegation_event = ->(event_type, payload) {
|
|
26
|
+
broadcast(
|
|
27
|
+
type: "po_delegation_#{event_type}",
|
|
28
|
+
payload: payload
|
|
29
|
+
)
|
|
30
|
+
}
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
def call(env)
|