prompt_objects 0.6.0 → 0.7.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 +126 -0
- data/README.md +93 -22
- data/exe/poop +5 -0
- data/exe/prompt_objects +1 -1108
- data/exe/prompt_objects_mcp +5 -3
- data/lib/prompt_objects/capability.rb +33 -0
- data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
- data/lib/prompt_objects/cli/env_command.rb +435 -0
- data/lib/prompt_objects/cli/events_command.rb +145 -0
- data/lib/prompt_objects/cli/explore_command.rb +155 -0
- data/lib/prompt_objects/cli/helpers.rb +91 -0
- data/lib/prompt_objects/cli/message_command.rb +216 -0
- data/lib/prompt_objects/cli/repl_command.rb +366 -0
- data/lib/prompt_objects/cli/serve_command.rb +162 -0
- data/lib/prompt_objects/cli.rb +64 -422
- data/lib/prompt_objects/connectors/base.rb +16 -29
- data/lib/prompt_objects/connectors/mcp.rb +82 -98
- data/lib/prompt_objects/domain/records.rb +45 -0
- data/lib/prompt_objects/environment/exporter.rb +5 -4
- data/lib/prompt_objects/environment/git.rb +21 -1
- data/lib/prompt_objects/environment/importer.rb +5 -4
- data/lib/prompt_objects/environment/manager.rb +44 -6
- data/lib/prompt_objects/environment/manifest.rb +26 -4
- data/lib/prompt_objects/environment.rb +1337 -20
- data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
- data/lib/prompt_objects/execution/context.rb +137 -0
- data/lib/prompt_objects/execution/contracts.rb +59 -0
- data/lib/prompt_objects/execution/permit_pool.rb +49 -0
- data/lib/prompt_objects/execution/policy.rb +47 -0
- data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
- data/lib/prompt_objects/execution/runner.rb +498 -0
- data/lib/prompt_objects/execution/scheduler.rb +325 -0
- data/lib/prompt_objects/llm/factory.rb +4 -4
- data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
- data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
- data/lib/prompt_objects/llm/pricing.rb +9 -2
- data/lib/prompt_objects/llm/response.rb +9 -4
- data/lib/prompt_objects/loader.rb +28 -1
- data/lib/prompt_objects/mcp/server.rb +6 -3
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
- data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
- data/lib/prompt_objects/operations/diagnostics.rb +115 -0
- data/lib/prompt_objects/operations/retention.rb +75 -0
- data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
- data/lib/prompt_objects/primitives/http_get.rb +2 -0
- data/lib/prompt_objects/primitives/list_files.rb +2 -0
- data/lib/prompt_objects/primitives/read_file.rb +2 -0
- data/lib/prompt_objects/primitives/think.rb +36 -0
- data/lib/prompt_objects/primitives/write_file.rb +9 -0
- data/lib/prompt_objects/prompt_object.rb +171 -519
- data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
- data/lib/prompt_objects/repositories/base_repository.rb +37 -0
- data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
- data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
- data/lib/prompt_objects/repositories/event_repository.rb +105 -0
- data/lib/prompt_objects/repositories/message_repository.rb +73 -0
- data/lib/prompt_objects/repositories/run_repository.rb +254 -0
- data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
- data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
- data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
- data/lib/prompt_objects/server/api/routes.rb +549 -53
- data/lib/prompt_objects/server/app.rb +133 -14
- data/lib/prompt_objects/server/file_watcher.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
- data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +202 -227
- data/lib/prompt_objects/server.rb +170 -1
- data/lib/prompt_objects/service.rb +129 -0
- data/lib/prompt_objects/session/store.rb +600 -13
- data/lib/prompt_objects/universal/add_capability.rb +1 -1
- data/lib/prompt_objects/universal/create_capability.rb +4 -2
- data/lib/prompt_objects/universal/delete_env_data.rb +23 -15
- data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
- data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
- data/lib/prompt_objects/universal/get_env_data.rb +8 -14
- data/lib/prompt_objects/universal/get_run_status.rb +30 -0
- data/lib/prompt_objects/universal/join_runs.rb +35 -0
- data/lib/prompt_objects/universal/list_env_data.rb +10 -14
- data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
- data/lib/prompt_objects/universal/render_artifact.rb +91 -0
- data/lib/prompt_objects/universal/request_primitive.rb +1 -1
- data/lib/prompt_objects/universal/{store_env_data.rb → set_env_data.rb} +34 -20
- data/lib/prompt_objects/version.rb +5 -0
- data/lib/prompt_objects.rb +33 -8
- data/objects/coordinator.md +8 -4
- data/templates/arc-agi-1/objects/data_manager.md +2 -2
- data/templates/arc-agi-1/objects/observer.md +2 -2
- data/templates/arc-agi-1/objects/solver.md +6 -5
- data/templates/arc-agi-1/objects/verifier.md +2 -2
- data/templates/arc-agi-3/manifest.yml +19 -0
- data/templates/arc-agi-3/objects/analyst.md +79 -0
- data/templates/arc-agi-3/objects/player.md +76 -0
- data/templates/arc-agi-3/objects/strategist.md +97 -0
- data/templates/arc-agi-3/primitives/action_log.rb +133 -0
- data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
- data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
- data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
- data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
- data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
- data/templates/basic/objects/basic.md +11 -3
- data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
- data/templates/chiptune/manifest.yml +20 -0
- data/templates/chiptune/objects/conductor.md +184 -0
- data/templates/chiptune/objects/sonic_pi.md +65 -0
- data/templates/chiptune/objects/voice_bass.md +44 -0
- data/templates/chiptune/objects/voice_lead.md +44 -0
- data/templates/chiptune/objects/voice_pad.md +43 -0
- data/templates/chiptune/objects/voice_perc.md +45 -0
- data/templates/chiptune/primitives/play_note.rb +59 -0
- data/templates/chiptune/primitives/play_sequence.rb +55 -0
- data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
- data/templates/chiptune/primitives/set_synth.rb +57 -0
- data/templates/chiptune/primitives/set_tempo.rb +45 -0
- data/templates/chiptune/primitives/stop_voice.rb +46 -0
- data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/developer/objects/code_reviewer.md +1 -1
- data/templates/developer/objects/coordinator.md +16 -3
- data/templates/developer/objects/debugger.md +3 -3
- data/templates/empty/objects/assistant.md +10 -5
- data/templates/minimal/objects/assistant.md +10 -5
- data/templates/music/extras/sonic_pi_boot.rb +220 -0
- data/templates/music/manifest.yml +16 -0
- data/templates/music/objects/conductor.md +154 -0
- data/templates/music/objects/sonic_pi.md +65 -0
- data/templates/music/primitives/play_note.rb +55 -0
- data/templates/music/primitives/play_sequence.rb +51 -0
- data/templates/music/primitives/query_sonic_pi.rb +139 -0
- data/templates/music/primitives/set_synth.rb +53 -0
- data/templates/music/primitives/set_tempo.rb +43 -0
- data/templates/music/primitives/stop_voice.rb +44 -0
- data/templates/music/primitives/support/osc_bridge.rb +33 -0
- data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/music-beginner/README.md +100 -0
- data/templates/music-beginner/extras/README.md +27 -0
- data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
- data/templates/music-beginner/manifest.yml +25 -0
- data/templates/music-beginner/objects/conductor.md +222 -0
- data/templates/music-beginner/objects/engine_setup.md +54 -0
- data/templates/music-beginner/objects/voice_bass.md +43 -0
- data/templates/music-beginner/objects/voice_keys.md +48 -0
- data/templates/music-beginner/objects/voice_pad.md +46 -0
- data/templates/music-beginner/objects/voice_percussion.md +45 -0
- data/templates/music-beginner/objects/voice_texture.md +37 -0
- data/templates/music-beginner/primitives/play_pattern.rb +118 -0
- data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
- data/templates/music-beginner/primitives/set_instrument.rb +47 -0
- data/templates/music-beginner/primitives/set_tempo.rb +43 -0
- data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
- data/templates/music-beginner/primitives/stop_voice.rb +37 -0
- data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/music-pro/extras/AUDITION.md +100 -0
- data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
- data/templates/music-pro/manifest.yml +19 -0
- data/templates/music-pro/objects/conductor.md +126 -0
- data/templates/music-pro/objects/sonic_pi.md +35 -0
- data/templates/music-pro/objects/voice_harmony.md +48 -0
- data/templates/music-pro/objects/voice_rhythm.md +43 -0
- data/templates/music-pro/objects/voice_texture.md +42 -0
- data/templates/music-pro/primitives/define_patch.rb +46 -0
- data/templates/music-pro/primitives/query_music_engine.rb +69 -0
- data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
- data/templates/music-pro/primitives/set_tempo.rb +31 -0
- data/templates/music-pro/primitives/stop_voice.rb +31 -0
- data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
- data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
- data/templates/music-pro/primitives/trigger_event.rb +44 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/writer/objects/coordinator.md +14 -3
- data/templates/writer/objects/editor.md +1 -1
- data/templates/writer/objects/researcher.md +2 -2
- data/tools/thread-explorer.html +1 -1
- metadata +172 -65
- data/CLAUDE.md +0 -177
- data/Gemfile +0 -10
- data/Rakefile +0 -27
- data/frontend/.gitignore +0 -3
- data/frontend/index.html +0 -17
- data/frontend/package-lock.json +0 -4540
- data/frontend/package.json +0 -36
- data/frontend/postcss.config.js +0 -6
- data/frontend/src/App.tsx +0 -104
- data/frontend/src/canvas/CanvasView.tsx +0 -113
- data/frontend/src/canvas/ForceLayout.ts +0 -115
- data/frontend/src/canvas/SceneManager.ts +0 -587
- data/frontend/src/canvas/canvasStore.ts +0 -47
- data/frontend/src/canvas/constants.ts +0 -95
- data/frontend/src/canvas/controls/CameraControls.ts +0 -98
- data/frontend/src/canvas/edges/MessageArc.ts +0 -149
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
- data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
- data/frontend/src/canvas/nodes/PONode.ts +0 -249
- data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
- data/frontend/src/canvas/types.ts +0 -24
- data/frontend/src/components/ContextMenu.tsx +0 -68
- data/frontend/src/components/EnvDataPane.tsx +0 -69
- data/frontend/src/components/Inspector.tsx +0 -263
- data/frontend/src/components/MarkdownMessage.tsx +0 -155
- data/frontend/src/components/MethodList.tsx +0 -90
- data/frontend/src/components/ModelSelector.tsx +0 -115
- data/frontend/src/components/NotificationPanel.tsx +0 -130
- data/frontend/src/components/ObjectList.tsx +0 -78
- data/frontend/src/components/PaneSlot.tsx +0 -30
- data/frontend/src/components/SourcePane.tsx +0 -202
- data/frontend/src/components/SystemBar.tsx +0 -74
- data/frontend/src/components/Transcript.tsx +0 -76
- data/frontend/src/components/UsagePanel.tsx +0 -105
- data/frontend/src/components/Workspace.tsx +0 -260
- data/frontend/src/components/index.ts +0 -12
- data/frontend/src/hooks/useResize.ts +0 -55
- data/frontend/src/hooks/useWebSocket.ts +0 -559
- data/frontend/src/index.css +0 -102
- data/frontend/src/main.tsx +0 -10
- data/frontend/src/store/index.ts +0 -311
- data/frontend/src/types/index.ts +0 -176
- data/frontend/tailwind.config.js +0 -44
- data/frontend/tsconfig.json +0 -30
- data/frontend/vite.config.ts +0 -29
- data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
- data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
- data/lib/prompt_objects/universal/add_primitive.rb +0 -118
- data/lib/prompt_objects/universal/ask_human.rb +0 -109
- data/lib/prompt_objects/universal/create_primitive.rb +0 -174
- data/lib/prompt_objects/universal/list_primitives.rb +0 -145
- data/lib/prompt_objects/universal/think.rb +0 -41
- data/lib/prompt_objects/universal/update_env_data.rb +0 -88
- data/prompt_objects.gemspec +0 -50
- data/templates/basic/.gitignore +0 -2
- data/templates/developer/.gitignore +0 -5
- data/templates/empty/.gitignore +0 -5
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/minimal/.gitignore +0 -5
- data/templates/writer/.gitignore +0 -5
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PromptObjects
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# An immutable-at-execution-time LLM capability definition. Conversation
|
|
5
|
+
# state belongs to runtime threads, runs, messages, and events; this object
|
|
6
|
+
# only owns the editable definition loaded from markdown.
|
|
6
7
|
class PromptObject < Capability
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# @param body [String] Markdown body (the "soul" - becomes system prompt)
|
|
12
|
-
# @param env [Environment] Reference to the environment
|
|
13
|
-
# @param llm [LLM::OpenAIAdapter] LLM adapter for making calls
|
|
14
|
-
# @param path [String, nil] Path to the source .md file (for persistence)
|
|
8
|
+
execution_policy parallel_safe: true, side_effect: :write, resource_keys: ->(_arguments, _context, _capability) { [] }
|
|
9
|
+
|
|
10
|
+
attr_reader :config, :body, :path
|
|
11
|
+
|
|
15
12
|
def initialize(config:, body:, env:, llm:, path: nil)
|
|
16
13
|
super()
|
|
17
14
|
@config = config
|
|
@@ -19,17 +16,6 @@ module PromptObjects
|
|
|
19
16
|
@env = env
|
|
20
17
|
@llm = llm
|
|
21
18
|
@path = path
|
|
22
|
-
@history = []
|
|
23
|
-
@session_id = nil
|
|
24
|
-
|
|
25
|
-
# Load existing session if session store is available
|
|
26
|
-
load_or_create_session if session_store
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Get the session store from the environment.
|
|
30
|
-
# @return [Session::Store, nil]
|
|
31
|
-
def session_store
|
|
32
|
-
@env.session_store
|
|
33
19
|
end
|
|
34
20
|
|
|
35
21
|
def name
|
|
@@ -40,7 +26,24 @@ module PromptObjects
|
|
|
40
26
|
@config["description"] || "A prompt object"
|
|
41
27
|
end
|
|
42
28
|
|
|
43
|
-
|
|
29
|
+
def autonomous?
|
|
30
|
+
!!@config["autonomous"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def autonomous_config
|
|
34
|
+
return nil unless autonomous?
|
|
35
|
+
|
|
36
|
+
auto = @config["autonomous"]
|
|
37
|
+
auto = {} unless auto.is_a?(Hash)
|
|
38
|
+
{
|
|
39
|
+
"interval" => auto["interval"] || 30,
|
|
40
|
+
"initial_delay" => auto["initial_delay"] || auto["interval"] || 30,
|
|
41
|
+
"message" => auto["message"] || "Check your environment and act.",
|
|
42
|
+
"start_on_boot" => auto.fetch("start_on_boot", true),
|
|
43
|
+
"wait_for_human" => auto.fetch("wait_for_human", false)
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
44
47
|
def parameters
|
|
45
48
|
{
|
|
46
49
|
type: "object",
|
|
@@ -54,271 +57,138 @@ module PromptObjects
|
|
|
54
57
|
}
|
|
55
58
|
end
|
|
56
59
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
content = normalize_message(message)
|
|
64
|
-
|
|
65
|
-
# Track who sent this message - another PO or a human?
|
|
66
|
-
sender = context.calling_po
|
|
67
|
-
from = (sender && sender != name) ? sender : "human"
|
|
68
|
-
|
|
69
|
-
user_msg = { role: :user, content: content, from: from }
|
|
70
|
-
@history << user_msg
|
|
71
|
-
persist_message(user_msg)
|
|
72
|
-
@state = :working
|
|
73
|
-
|
|
74
|
-
# Conversation loop - keep going until LLM responds without tool calls
|
|
75
|
-
loop do
|
|
76
|
-
response = @llm.chat(
|
|
77
|
-
system: build_system_prompt,
|
|
78
|
-
messages: @history,
|
|
79
|
-
tools: available_tool_descriptors
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
if response.tool_calls?
|
|
83
|
-
# Execute tools and continue the loop
|
|
84
|
-
results = execute_tool_calls(response.tool_calls, context)
|
|
85
|
-
assistant_msg = {
|
|
86
|
-
role: :assistant,
|
|
87
|
-
# Don't include content when there are tool calls - force LLM to
|
|
88
|
-
# wait for tool results before generating a response. This prevents
|
|
89
|
-
# the model from "hedging" by generating both a response AND a tool call.
|
|
90
|
-
content: nil,
|
|
91
|
-
tool_calls: response.tool_calls,
|
|
92
|
-
usage: response.usage
|
|
93
|
-
}
|
|
94
|
-
@history << assistant_msg
|
|
95
|
-
persist_message(assistant_msg)
|
|
96
|
-
|
|
97
|
-
tool_msg = { role: :tool, results: results }
|
|
98
|
-
@history << tool_msg
|
|
99
|
-
persist_message(tool_msg)
|
|
100
|
-
|
|
101
|
-
# Notify callback for real-time UI updates (tool calls as they happen)
|
|
102
|
-
notify_history_updated
|
|
103
|
-
else
|
|
104
|
-
# No tool calls - we have our final response
|
|
105
|
-
assistant_msg = { role: :assistant, content: response.content, usage: response.usage }
|
|
106
|
-
@history << assistant_msg
|
|
107
|
-
persist_message(assistant_msg)
|
|
108
|
-
@state = :idle
|
|
109
|
-
return response.content
|
|
110
|
-
end
|
|
60
|
+
# The raw markdown source for definition editing. File watcher reloads
|
|
61
|
+
# replace instances, while direct edits update this memo in place.
|
|
62
|
+
def source
|
|
63
|
+
unless @source_loaded
|
|
64
|
+
@source_loaded = true
|
|
65
|
+
@source = (@path && File.exist?(@path)) ? File.read(@path, encoding: "UTF-8") : nil
|
|
111
66
|
end
|
|
67
|
+
@source
|
|
112
68
|
end
|
|
113
69
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# List all sessions for this PO.
|
|
117
|
-
# @return [Array<Hash>] Session data
|
|
118
|
-
def list_sessions
|
|
119
|
-
return [] unless session_store
|
|
120
|
-
|
|
121
|
-
session_store.list_sessions(po_name: name)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Switch to a different session.
|
|
125
|
-
# @param session_id [String] Session ID to switch to
|
|
126
|
-
# @return [Boolean] True if switch was successful
|
|
127
|
-
def switch_session(session_id)
|
|
128
|
-
return false unless session_store
|
|
129
|
-
|
|
130
|
-
session = session_store.get_session(session_id)
|
|
131
|
-
return false unless session && session[:po_name] == name
|
|
70
|
+
def update_source(raw)
|
|
71
|
+
return false unless @path
|
|
132
72
|
|
|
133
|
-
|
|
134
|
-
|
|
73
|
+
data = Loader.parse(raw, path: @path)
|
|
74
|
+
@env.suppress_next_reload(@path) if @env.respond_to?(:suppress_next_reload)
|
|
75
|
+
File.write(@path, raw, encoding: "UTF-8")
|
|
76
|
+
@config = data[:config]
|
|
77
|
+
@body = data[:body]
|
|
78
|
+
@source = raw
|
|
79
|
+
@source_loaded = true
|
|
135
80
|
true
|
|
81
|
+
rescue => e
|
|
82
|
+
puts "Error updating source for PO #{name}: #{e.message}" if ENV["DEBUG"]
|
|
83
|
+
false
|
|
136
84
|
end
|
|
137
85
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return nil unless session_store
|
|
143
|
-
|
|
144
|
-
@session_id = session_store.create_session(po_name: self.name, name: name)
|
|
145
|
-
@history = []
|
|
146
|
-
@session_id
|
|
147
|
-
end
|
|
86
|
+
def relocate(new_path)
|
|
87
|
+
return false unless @path && File.exist?(@path)
|
|
88
|
+
return false if new_path == @path
|
|
89
|
+
return false if File.exist?(new_path)
|
|
148
90
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
91
|
+
File.rename(@path, new_path)
|
|
92
|
+
@path = new_path
|
|
93
|
+
true
|
|
94
|
+
rescue => e
|
|
95
|
+
puts "Error relocating PO #{name}: #{e.message}" if ENV["DEBUG"]
|
|
96
|
+
false
|
|
153
97
|
end
|
|
154
98
|
|
|
155
|
-
# --- File Persistence ---
|
|
156
|
-
|
|
157
|
-
# Save the current config and body back to the source file.
|
|
158
|
-
# This persists any runtime changes (like added capabilities) to disk.
|
|
159
|
-
# @return [Boolean] True if saved successfully, false if no path or error
|
|
160
99
|
def save
|
|
161
100
|
return false unless @path
|
|
162
101
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# Combine frontmatter and body
|
|
167
|
-
content = "#{yaml_content}---\n\n#{@body}\n"
|
|
168
|
-
|
|
102
|
+
content = "#{save_frontmatter_yaml}---\n\n#{@body}\n"
|
|
103
|
+
@env.suppress_next_reload(@path) if @env.respond_to?(:suppress_next_reload)
|
|
169
104
|
File.write(@path, content, encoding: "UTF-8")
|
|
105
|
+
@source = content
|
|
106
|
+
@source_loaded = true
|
|
170
107
|
true
|
|
171
108
|
rescue => e
|
|
172
109
|
puts "Error saving PO #{name} to #{@path}: #{e.message}" if ENV["DEBUG"]
|
|
173
110
|
false
|
|
174
111
|
end
|
|
175
112
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# @param parent_po [String] Name of the PO that initiated the call
|
|
180
|
-
# @param parent_session_id [String] Session ID in the parent PO
|
|
181
|
-
# @param parent_message_id [Integer, nil] Message ID that triggered the delegation
|
|
182
|
-
# @return [String, nil] New thread ID or nil if no session store
|
|
183
|
-
def create_delegation_thread(parent_po:, parent_session_id:, parent_message_id: nil)
|
|
184
|
-
return nil unless session_store
|
|
113
|
+
def save_frontmatter_yaml
|
|
114
|
+
frontmatter = @config.dup
|
|
115
|
+
capabilities = frontmatter.delete("capabilities") || []
|
|
185
116
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
thread_type: "delegation",
|
|
192
|
-
source: "delegation"
|
|
193
|
-
)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Execute a message in a specific thread, then restore the original session.
|
|
197
|
-
# @param message [String, Hash] The message to process
|
|
198
|
-
# @param context [Context] Execution context
|
|
199
|
-
# @param thread_id [String] Thread ID to execute in
|
|
200
|
-
# @return [String] The response
|
|
201
|
-
def receive_in_thread(message, context:, thread_id:)
|
|
202
|
-
original_session = @session_id
|
|
203
|
-
original_history = @history.dup
|
|
204
|
-
|
|
205
|
-
# Switch to delegation thread
|
|
206
|
-
@session_id = thread_id
|
|
207
|
-
@history = []
|
|
208
|
-
reload_history_from_session
|
|
209
|
-
|
|
210
|
-
begin
|
|
211
|
-
result = receive(message, context: context)
|
|
212
|
-
result
|
|
213
|
-
ensure
|
|
214
|
-
# Restore original session
|
|
215
|
-
@session_id = original_session
|
|
216
|
-
@history = original_history
|
|
117
|
+
if capabilities.any?
|
|
118
|
+
registry = @env.respond_to?(:registry) ? @env.registry : nil
|
|
119
|
+
delegates, tools = capabilities.partition { |capability_name| registry&.get(capability_name).is_a?(PromptObject) }
|
|
120
|
+
frontmatter["delegates_to"] = delegates if delegates.any?
|
|
121
|
+
frontmatter["tools"] = tools if tools.any?
|
|
217
122
|
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Create a new root thread and switch to it.
|
|
221
|
-
# @param name [String, nil] Optional thread name
|
|
222
|
-
# @return [String] New thread ID
|
|
223
|
-
def new_thread(name: nil)
|
|
224
|
-
return nil unless session_store
|
|
225
123
|
|
|
226
|
-
|
|
227
|
-
po_name: self.name,
|
|
228
|
-
name: name,
|
|
229
|
-
thread_type: "root"
|
|
230
|
-
)
|
|
231
|
-
@history = []
|
|
232
|
-
@session_id
|
|
124
|
+
frontmatter.to_yaml
|
|
233
125
|
end
|
|
234
126
|
|
|
235
|
-
#
|
|
236
|
-
#
|
|
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]
|
|
127
|
+
# Definition state for WebSocket and REST consumers. Runtime activity is
|
|
128
|
+
# delivered separately from authoritative repository snapshots/events.
|
|
244
129
|
def to_state_hash(registry: nil)
|
|
245
130
|
{
|
|
246
|
-
status:
|
|
131
|
+
status: "idle",
|
|
247
132
|
description: description,
|
|
248
133
|
capabilities: resolve_capabilities(registry: registry),
|
|
249
134
|
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
135
|
prompt: body,
|
|
253
|
-
config: config
|
|
136
|
+
config: config,
|
|
137
|
+
source: source
|
|
254
138
|
}
|
|
255
139
|
end
|
|
256
140
|
|
|
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
141
|
def to_summary_hash(registry: nil)
|
|
262
142
|
{
|
|
263
143
|
name: name,
|
|
264
144
|
description: description,
|
|
265
|
-
status:
|
|
266
|
-
capabilities: resolve_capabilities(registry: registry)
|
|
267
|
-
session_count: list_sessions.size
|
|
145
|
+
status: "idle",
|
|
146
|
+
capabilities: resolve_capabilities(registry: registry)
|
|
268
147
|
}
|
|
269
148
|
end
|
|
270
149
|
|
|
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
150
|
def to_inspect_hash(registry: nil)
|
|
276
151
|
{
|
|
277
152
|
name: name,
|
|
278
153
|
description: description,
|
|
279
|
-
status:
|
|
154
|
+
status: "idle",
|
|
280
155
|
capabilities: resolve_capabilities(registry: registry),
|
|
281
156
|
universal_capabilities: resolve_universal_capabilities(registry: registry),
|
|
282
157
|
prompt: body,
|
|
283
158
|
config: config,
|
|
284
|
-
|
|
285
|
-
sessions: list_sessions.map { |s| self.class.serialize_session(s) },
|
|
286
|
-
history_length: history.length
|
|
159
|
+
source: source
|
|
287
160
|
}
|
|
288
161
|
end
|
|
289
162
|
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def self.serialize_message(msg)
|
|
295
|
-
case msg[:role]
|
|
163
|
+
# Transitional wire serializers for repository records. They do not read or
|
|
164
|
+
# mutate PromptObject state and can be removed with the older endpoint shape.
|
|
165
|
+
def self.serialize_message(message)
|
|
166
|
+
case message[:role]
|
|
296
167
|
when :user
|
|
297
|
-
|
|
298
|
-
{ role: "user", content: msg[:content], from: from }
|
|
168
|
+
{ role: "user", content: message[:content], from: message[:from] || message[:from_po] }
|
|
299
169
|
when :assistant
|
|
300
|
-
hash = { role: "assistant", content:
|
|
301
|
-
if
|
|
302
|
-
hash[:tool_calls] =
|
|
303
|
-
if
|
|
304
|
-
{ id:
|
|
170
|
+
hash = { role: "assistant", content: message[:content] }
|
|
171
|
+
if message[:tool_calls]
|
|
172
|
+
hash[:tool_calls] = message[:tool_calls].map do |tool_call|
|
|
173
|
+
if tool_call.respond_to?(:id)
|
|
174
|
+
{ id: tool_call.id, name: tool_call.name, arguments: tool_call.arguments }
|
|
305
175
|
else
|
|
306
|
-
{
|
|
176
|
+
{
|
|
177
|
+
id: tool_call[:id] || tool_call["id"],
|
|
178
|
+
name: tool_call[:name] || tool_call["name"],
|
|
179
|
+
arguments: tool_call[:arguments] || tool_call["arguments"] || {}
|
|
180
|
+
}
|
|
307
181
|
end
|
|
308
182
|
end
|
|
309
183
|
end
|
|
310
184
|
hash
|
|
311
185
|
when :tool
|
|
312
|
-
results
|
|
313
|
-
{ role: "tool", results: results }
|
|
186
|
+
{ role: "tool", results: message[:results] || message[:tool_results] }
|
|
314
187
|
else
|
|
315
|
-
{ role:
|
|
188
|
+
{ role: message[:role].to_s, content: message[:content] }
|
|
316
189
|
end
|
|
317
190
|
end
|
|
318
191
|
|
|
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
192
|
def self.serialize_session(session)
|
|
323
193
|
{
|
|
324
194
|
id: session[:id],
|
|
@@ -331,66 +201,78 @@ module PromptObjects
|
|
|
331
201
|
}
|
|
332
202
|
end
|
|
333
203
|
|
|
204
|
+
# Capture the exact definition used by a run. Hot reload or definition
|
|
205
|
+
# editing cannot alter this deeply frozen snapshot after enqueue.
|
|
206
|
+
def execution_definition(provider: nil, model: nil, capability_policy_version: "1")
|
|
207
|
+
snapshot_config = deep_freeze(Marshal.load(Marshal.dump(@config)))
|
|
208
|
+
version_payload = JSON.generate(canonicalize({ "prompt" => @body, "config" => snapshot_config }))
|
|
209
|
+
Domain::PromptObjectDefinition.new(
|
|
210
|
+
po_name: name,
|
|
211
|
+
version: Digest::SHA256.hexdigest(version_payload),
|
|
212
|
+
prompt: @body.dup.freeze,
|
|
213
|
+
config: snapshot_config,
|
|
214
|
+
provider: provider,
|
|
215
|
+
model: model,
|
|
216
|
+
capability_policy_version: capability_policy_version
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def system_prompt_for(definition)
|
|
221
|
+
build_system_prompt_from(
|
|
222
|
+
prompt: definition.prompt,
|
|
223
|
+
config: definition.config,
|
|
224
|
+
object_name: definition.po_name
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def tool_descriptors_for(definition)
|
|
229
|
+
descriptors_for_config(definition.config)
|
|
230
|
+
end
|
|
231
|
+
|
|
334
232
|
private
|
|
335
233
|
|
|
336
|
-
# Build capability info objects for this PO's declared capabilities.
|
|
337
234
|
def resolve_capabilities(registry: nil)
|
|
338
235
|
declared = @config["capabilities"] || []
|
|
339
|
-
return declared.map { |
|
|
340
|
-
|
|
341
|
-
declared.map do |
|
|
342
|
-
|
|
343
|
-
|
|
236
|
+
return declared.map { |capability_name| { name: capability_name, description: capability_name, kind: "tool" } } unless registry
|
|
237
|
+
|
|
238
|
+
declared.map do |capability_name|
|
|
239
|
+
capability = registry.get(capability_name)
|
|
240
|
+
kind = capability.is_a?(PromptObject) ? "delegate" : "tool"
|
|
241
|
+
{
|
|
242
|
+
name: capability_name,
|
|
243
|
+
description: capability&.description || "Capability not found",
|
|
244
|
+
parameters: capability&.parameters,
|
|
245
|
+
kind: kind
|
|
246
|
+
}
|
|
344
247
|
end
|
|
345
248
|
end
|
|
346
249
|
|
|
347
|
-
# Build capability info objects for universal capabilities.
|
|
348
250
|
def resolve_universal_capabilities(registry: nil)
|
|
349
251
|
return [] unless registry
|
|
350
252
|
|
|
351
|
-
UNIVERSAL_CAPABILITIES.map do |
|
|
352
|
-
|
|
353
|
-
{
|
|
253
|
+
UNIVERSAL_CAPABILITIES.map do |capability_name|
|
|
254
|
+
capability = registry.get(capability_name)
|
|
255
|
+
{
|
|
256
|
+
name: capability_name,
|
|
257
|
+
description: capability&.description || "Universal capability",
|
|
258
|
+
parameters: capability&.parameters
|
|
259
|
+
}
|
|
354
260
|
end
|
|
355
261
|
end
|
|
356
262
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
def normalize_message(message)
|
|
364
|
-
case message
|
|
365
|
-
when String
|
|
366
|
-
message
|
|
367
|
-
when Hash
|
|
368
|
-
message[:message] || message["message"] || message.to_s
|
|
369
|
-
else
|
|
370
|
-
message.to_s
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def available_tool_descriptors
|
|
375
|
-
# Get declared capabilities from config
|
|
376
|
-
declared = @config["capabilities"] || []
|
|
377
|
-
|
|
378
|
-
# Add universal capabilities (available to all POs)
|
|
379
|
-
all_caps = declared + UNIVERSAL_CAPABILITIES
|
|
380
|
-
|
|
381
|
-
all_caps.filter_map do |cap_name|
|
|
382
|
-
cap = @env.registry&.get(cap_name)
|
|
383
|
-
cap&.descriptor
|
|
263
|
+
def descriptors_for_config(config)
|
|
264
|
+
declared = config["capabilities"] || config[:capabilities] || []
|
|
265
|
+
(declared + UNIVERSAL_CAPABILITIES).filter_map do |capability_name|
|
|
266
|
+
@env.registry&.get(capability_name)&.descriptor
|
|
384
267
|
end
|
|
385
268
|
end
|
|
386
269
|
|
|
387
|
-
def
|
|
388
|
-
declared_caps =
|
|
389
|
-
|
|
270
|
+
def build_system_prompt_from(prompt:, config:, object_name:)
|
|
271
|
+
declared_caps = config["capabilities"] || config[:capabilities] || []
|
|
390
272
|
context_block = <<~CONTEXT
|
|
391
273
|
## System Context
|
|
392
274
|
|
|
393
|
-
You are a prompt object named "#{
|
|
275
|
+
You are a prompt object named "#{object_name}" running in a PromptObjects environment.
|
|
394
276
|
|
|
395
277
|
### What is a Prompt Object?
|
|
396
278
|
You are an autonomous entity defined by a markdown file. You have an identity (your prompt),
|
|
@@ -401,277 +283,47 @@ module PromptObjects
|
|
|
401
283
|
You may receive messages from:
|
|
402
284
|
- **A human** interacting with you directly through the UI
|
|
403
285
|
- **Another prompt object** that has delegated a task to you as part of a larger workflow
|
|
286
|
+
- **Your own interface** — a person clicking a control in an artifact you rendered
|
|
287
|
+
(arrives from `artifact:<title>`)
|
|
288
|
+
- **A timer**, if you are configured as autonomous (arrives from `timer:#{object_name}`)
|
|
404
289
|
|
|
405
290
|
When called by another PO, you'll see a delegation context block in the message with details
|
|
406
291
|
about who called you and why. You can also check shared environment data (via `list_env_data`)
|
|
407
292
|
for context that other POs in the same workflow have stored.
|
|
408
293
|
|
|
409
294
|
### Your capabilities
|
|
410
|
-
When using tools that target a PO (like add_capability), you can use "self" or "#{
|
|
295
|
+
When using tools that target a PO (like add_capability), you can use "self" or "#{object_name}" to target yourself.
|
|
411
296
|
- Declared capabilities: #{declared_caps.empty? ? '(none)' : declared_caps.join(', ')}
|
|
412
297
|
- Universal capabilities (always available): #{UNIVERSAL_CAPABILITIES.join(', ')}
|
|
413
298
|
|
|
414
|
-
You can create new
|
|
299
|
+
You can create new capabilities at runtime with `create_capability` — use `type: "primitive"` for a deterministic Ruby tool or `type: "prompt_object"` for a new specialist PO. Use `list_capabilities` to see everything available.
|
|
300
|
+
|
|
301
|
+
### Your interface (interactive artifacts)
|
|
302
|
+
Besides replying in text, you can build a live UI with `render_artifact` — it renders on the
|
|
303
|
+
human's screen as your station. Artifacts are BIDIRECTIONAL: inside the artifact,
|
|
304
|
+
`PromptObjects.send("...")` sends a natural-language message back to you. Wire it to buttons,
|
|
305
|
+
forms, and controls so people can click instead of type — e.g.
|
|
306
|
+
`<button onClick={() => PromptObjects.send('approve invoice 3')}>Approve</button>`.
|
|
307
|
+
When an `artifact:<title>` message arrives, act on it, then update the UI by re-rendering with
|
|
308
|
+
the same title (replace mode). A re-render resets the artifact's local JS state, so render the
|
|
309
|
+
current state into the HTML each time. For anything with ongoing state — dashboards, controls,
|
|
310
|
+
forms, games — prefer offering an interface over walls of text.
|
|
415
311
|
CONTEXT
|
|
416
312
|
|
|
417
|
-
"#{
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
# Build a delegation preamble for a PO-to-PO call.
|
|
421
|
-
# @param target_po [PromptObject] The PO being called
|
|
422
|
-
# @param delegation_thread [String, nil] The delegation thread ID
|
|
423
|
-
# @param context [Context] Execution context
|
|
424
|
-
# @return [String, nil] Preamble text or nil if caller isn't a PO
|
|
425
|
-
def build_delegation_preamble(target_po, delegation_thread, context)
|
|
426
|
-
return nil unless context.calling_po
|
|
427
|
-
|
|
428
|
-
caller_po = context.env.registry.get(context.calling_po)
|
|
429
|
-
return nil unless caller_po.is_a?(PromptObject)
|
|
430
|
-
|
|
431
|
-
parts = []
|
|
432
|
-
parts << "---"
|
|
433
|
-
parts << "[Delegation Context]"
|
|
434
|
-
parts << "Called by: #{caller_po.name}"
|
|
435
|
-
parts << "#{caller_po.name} is: \"#{caller_po.description}\""
|
|
436
|
-
|
|
437
|
-
chain = build_delegation_chain(delegation_thread)
|
|
438
|
-
parts << "Delegation chain: #{chain}" if chain
|
|
439
|
-
|
|
440
|
-
if delegation_thread && env_data_available?(delegation_thread)
|
|
441
|
-
parts << "Shared environment data is available — call list_env_data() to see what context has been stored."
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
parts << "---"
|
|
445
|
-
parts.join("\n")
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
# Build a human-readable delegation chain from thread lineage.
|
|
449
|
-
# @param delegation_thread [String, nil] The delegation thread ID
|
|
450
|
-
# @return [String, nil] Chain like "human → coordinator → solver → you (observer)"
|
|
451
|
-
def build_delegation_chain(delegation_thread)
|
|
452
|
-
return nil unless delegation_thread && session_store
|
|
453
|
-
|
|
454
|
-
lineage = session_store.get_thread_lineage(delegation_thread)
|
|
455
|
-
return nil if lineage.empty?
|
|
456
|
-
|
|
457
|
-
chain = ["human"]
|
|
458
|
-
lineage[0..-2].each do |session|
|
|
459
|
-
chain << session[:po_name] if session[:po_name]
|
|
460
|
-
end
|
|
461
|
-
chain << "you (#{lineage.last[:po_name]})"
|
|
462
|
-
|
|
463
|
-
chain.join(" → ")
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Check if any shared environment data exists for a delegation thread.
|
|
467
|
-
# @param delegation_thread [String] The delegation thread ID
|
|
468
|
-
# @return [Boolean]
|
|
469
|
-
def env_data_available?(delegation_thread)
|
|
470
|
-
return false unless session_store
|
|
471
|
-
|
|
472
|
-
root_thread = session_store.resolve_root_thread(delegation_thread)
|
|
473
|
-
entries = session_store.list_env_data(root_thread_id: root_thread)
|
|
474
|
-
!entries.empty?
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
# Build enriched arguments with delegation preamble prepended to the message.
|
|
478
|
-
# Returns a NEW hash — does not mutate the original arguments.
|
|
479
|
-
# @param target_po [PromptObject] The PO being called
|
|
480
|
-
# @param arguments [Hash, String] Original tool call arguments
|
|
481
|
-
# @param delegation_thread [String, nil] The delegation thread ID
|
|
482
|
-
# @param context [Context] Execution context
|
|
483
|
-
# @return [Hash, String] Enriched arguments with preamble, or original if no preamble
|
|
484
|
-
def enrich_delegation_message(target_po, arguments, delegation_thread, context)
|
|
485
|
-
preamble = build_delegation_preamble(target_po, delegation_thread, context)
|
|
486
|
-
return arguments unless preamble
|
|
487
|
-
|
|
488
|
-
original_message = normalize_message(arguments)
|
|
489
|
-
enriched_message = "#{preamble}\n\n#{original_message}"
|
|
490
|
-
|
|
491
|
-
if arguments.is_a?(Hash)
|
|
492
|
-
# Create a new hash with the enriched message
|
|
493
|
-
key = arguments.key?(:message) ? :message : "message"
|
|
494
|
-
arguments.merge(key => enriched_message)
|
|
495
|
-
else
|
|
496
|
-
enriched_message
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
def execute_tool_calls(tool_calls, context)
|
|
501
|
-
# Track the caller for nested calls
|
|
502
|
-
previous_capability = context.current_capability
|
|
503
|
-
previous_calling_po = context.calling_po
|
|
504
|
-
|
|
505
|
-
tool_calls.map do |tc|
|
|
506
|
-
capability = @env.registry&.get(tc.name)
|
|
507
|
-
|
|
508
|
-
# Guard: only execute tools the PO is allowed to use (declared + universal).
|
|
509
|
-
# Without this, the LLM can hallucinate calls to tools it shouldn't have access to.
|
|
510
|
-
allowed = (@config["capabilities"] || []) + UNIVERSAL_CAPABILITIES
|
|
511
|
-
unless allowed.include?(tc.name)
|
|
512
|
-
next { tool_call_id: tc.id, name: tc.name, content: "Capability '#{tc.name}' is not available. Your declared capabilities are: #{(@config["capabilities"] || []).join(', ')}. Use add_capability to add it first, or use list_primitives / list_capabilities to discover what's available." }
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
if capability
|
|
516
|
-
# Log the outgoing message
|
|
517
|
-
@env.bus.publish(from: name, to: tc.name, message: tc.arguments)
|
|
518
|
-
|
|
519
|
-
# Set context for the tool call
|
|
520
|
-
# calling_po tracks which PO is making the call (for "self" resolution)
|
|
521
|
-
context.calling_po = name
|
|
522
|
-
context.current_capability = tc.name
|
|
523
|
-
|
|
524
|
-
result = if capability.is_a?(PromptObject)
|
|
525
|
-
# PO-to-PO call: create isolated delegation thread
|
|
526
|
-
execute_po_delegation(capability, tc, context)
|
|
527
|
-
else
|
|
528
|
-
# Primitive call: execute directly
|
|
529
|
-
capability.receive(tc.arguments, context: context)
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
# Restore context
|
|
533
|
-
context.current_capability = previous_capability
|
|
534
|
-
context.calling_po = previous_calling_po
|
|
535
|
-
|
|
536
|
-
# Log the response
|
|
537
|
-
@env.bus.publish(from: tc.name, to: name, message: result)
|
|
538
|
-
|
|
539
|
-
{ tool_call_id: tc.id, name: tc.name, content: result }
|
|
540
|
-
else
|
|
541
|
-
{ tool_call_id: tc.id, name: tc.name, content: "Unknown capability: #{tc.name}" }
|
|
542
|
-
end
|
|
543
|
-
end
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
# Execute a delegation to another PO in an isolated thread.
|
|
547
|
-
# @param target_po [PromptObject] The PO to delegate to
|
|
548
|
-
# @param tool_call [ToolCall] The tool call details
|
|
549
|
-
# @param context [Context] Execution context
|
|
550
|
-
# @return [String] The response
|
|
551
|
-
def execute_po_delegation(target_po, tool_call, context)
|
|
552
|
-
# Create a delegation thread in the target PO
|
|
553
|
-
delegation_thread = target_po.create_delegation_thread(
|
|
554
|
-
parent_po: name,
|
|
555
|
-
parent_session_id: @session_id,
|
|
556
|
-
parent_message_id: get_last_message_id
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
# Enrich the message with delegation context (must happen after thread creation
|
|
560
|
-
# so build_delegation_chain can walk the lineage)
|
|
561
|
-
enriched_args = enrich_delegation_message(target_po, tool_call.arguments, delegation_thread, context)
|
|
562
|
-
|
|
563
|
-
# Notify delegation start so WebSocket clients see the target PO activate
|
|
564
|
-
@env.notify_delegation(:started, {
|
|
565
|
-
target: target_po.name,
|
|
566
|
-
caller: name,
|
|
567
|
-
thread_id: delegation_thread,
|
|
568
|
-
tool_call_id: tool_call.id
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
begin
|
|
572
|
-
if delegation_thread
|
|
573
|
-
# Execute in isolated thread
|
|
574
|
-
target_po.receive_in_thread(enriched_args, context: context, thread_id: delegation_thread)
|
|
575
|
-
else
|
|
576
|
-
# Fallback: execute in target's current session (no session store)
|
|
577
|
-
target_po.receive(enriched_args, context: context)
|
|
578
|
-
end
|
|
579
|
-
ensure
|
|
580
|
-
# Notify delegation complete — target PO is done
|
|
581
|
-
@env.notify_delegation(:completed, {
|
|
582
|
-
target: target_po.name,
|
|
583
|
-
caller: name,
|
|
584
|
-
thread_id: delegation_thread,
|
|
585
|
-
tool_call_id: tool_call.id
|
|
586
|
-
})
|
|
587
|
-
end
|
|
313
|
+
"#{prompt}\n\n#{context_block}"
|
|
588
314
|
end
|
|
589
315
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
messages = session_store.get_messages(@session_id)
|
|
596
|
-
messages.last&.dig(:id)
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
# Notify the history updated callback if registered.
|
|
600
|
-
# Used for real-time UI updates during the receive loop.
|
|
601
|
-
def notify_history_updated
|
|
602
|
-
@on_history_updated&.call(self, @session_id, @history)
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
# --- Session Persistence Helpers ---
|
|
606
|
-
|
|
607
|
-
# Load existing session or create a new one.
|
|
608
|
-
def load_or_create_session
|
|
609
|
-
session = session_store.get_or_create_session(po_name: name)
|
|
610
|
-
@session_id = session[:id]
|
|
611
|
-
reload_history_from_session
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
# Reload history from the current session.
|
|
615
|
-
def reload_history_from_session
|
|
616
|
-
return unless session_store && @session_id
|
|
617
|
-
|
|
618
|
-
messages = session_store.get_messages(@session_id)
|
|
619
|
-
@history = messages.map { |msg| convert_db_message_to_history(msg) }
|
|
316
|
+
def deep_freeze(value)
|
|
317
|
+
value.each { |key, nested| deep_freeze(key); deep_freeze(nested) } if value.is_a?(Hash)
|
|
318
|
+
value.each { |nested| deep_freeze(nested) } if value.is_a?(Array)
|
|
319
|
+
value.freeze
|
|
620
320
|
end
|
|
621
321
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
return
|
|
322
|
+
def canonicalize(value)
|
|
323
|
+
return value.keys.sort_by(&:to_s).to_h { |key| [key.to_s, canonicalize(value[key])] } if value.is_a?(Hash)
|
|
324
|
+
return value.map { |nested| canonicalize(nested) } if value.is_a?(Array)
|
|
625
325
|
|
|
626
|
-
|
|
627
|
-
when :user
|
|
628
|
-
session_store.add_message(
|
|
629
|
-
session_id: @session_id,
|
|
630
|
-
role: :user,
|
|
631
|
-
content: msg[:content],
|
|
632
|
-
from_po: msg[:from]
|
|
633
|
-
)
|
|
634
|
-
when :assistant
|
|
635
|
-
# Serialize tool_calls if present
|
|
636
|
-
tool_calls_data = msg[:tool_calls]&.map do |tc|
|
|
637
|
-
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
session_store.add_message(
|
|
641
|
-
session_id: @session_id,
|
|
642
|
-
role: :assistant,
|
|
643
|
-
content: msg[:content],
|
|
644
|
-
tool_calls: tool_calls_data,
|
|
645
|
-
usage: msg[:usage]
|
|
646
|
-
)
|
|
647
|
-
when :tool
|
|
648
|
-
session_store.add_message(
|
|
649
|
-
session_id: @session_id,
|
|
650
|
-
role: :tool,
|
|
651
|
-
tool_results: msg[:results]
|
|
652
|
-
)
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
# Convert a database message row to history format.
|
|
657
|
-
def convert_db_message_to_history(db_msg)
|
|
658
|
-
case db_msg[:role]
|
|
659
|
-
when :user
|
|
660
|
-
{ role: :user, content: db_msg[:content], from: db_msg[:from_po] || "human" }
|
|
661
|
-
when :assistant
|
|
662
|
-
msg = { role: :assistant, content: db_msg[:content] }
|
|
663
|
-
if db_msg[:tool_calls]
|
|
664
|
-
# Reconstruct tool call objects from Hashes
|
|
665
|
-
msg[:tool_calls] = db_msg[:tool_calls].map do |tc|
|
|
666
|
-
LLM::ToolCall.from_hash(tc)
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
msg
|
|
670
|
-
when :tool
|
|
671
|
-
{ role: :tool, results: db_msg[:tool_results] || [] }
|
|
672
|
-
else
|
|
673
|
-
{ role: db_msg[:role], content: db_msg[:content] }
|
|
674
|
-
end
|
|
326
|
+
value
|
|
675
327
|
end
|
|
676
328
|
end
|
|
677
329
|
end
|