prompt_objects 0.5.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 +142 -0
- data/README.md +211 -19
- 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 +1350 -18
- 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 +190 -432
- 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 +561 -52
- data/lib/prompt_objects/server/app.rb +134 -1
- 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 -207
- data/lib/prompt_objects/server.rb +170 -1
- data/lib/prompt_objects/service.rb +129 -0
- data/lib/prompt_objects/session/store.rb +774 -15
- 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 +78 -0
- 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 +58 -0
- 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 +57 -0
- 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/set_env_data.rb +101 -0
- data/lib/prompt_objects/version.rb +5 -0
- data/lib/prompt_objects.rb +36 -6
- 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 +6 -2
- data/templates/arc-agi-1/objects/solver.md +14 -4
- data/templates/arc-agi-1/objects/verifier.md +6 -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 +28 -1
- metadata +175 -65
- data/CLAUDE.md +0 -176
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -233
- data/IMPLEMENTATION_PLAN.md +0 -1073
- data/Rakefile +0 -27
- data/design-doc-v2.md +0 -1232
- 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 -103
- 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/Inspector.tsx +0 -232
- 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 -517
- data/frontend/src/index.css +0 -103
- data/frontend/src/main.tsx +0 -10
- data/frontend/src/store/index.ts +0 -279
- data/frontend/src/types/index.ts +0 -165
- 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-D1myxE0l.js +0 -4345
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.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/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
|
|
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)
|
|
143
90
|
|
|
144
|
-
|
|
145
|
-
@
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def clear_history
|
|
151
|
-
@history = []
|
|
152
|
-
session_store&.clear_messages(@session_id) if @session_id
|
|
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,241 +201,129 @@ 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
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
371
260
|
end
|
|
372
261
|
end
|
|
373
262
|
|
|
374
|
-
def
|
|
375
|
-
|
|
376
|
-
declared
|
|
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
|
-
|
|
389
|
-
declared_caps = @config["capabilities"] || []
|
|
390
|
-
all_caps = declared_caps + UNIVERSAL_CAPABILITIES
|
|
391
|
-
|
|
270
|
+
def build_system_prompt_from(prompt:, config:, object_name:)
|
|
271
|
+
declared_caps = config["capabilities"] || config[:capabilities] || []
|
|
392
272
|
context_block = <<~CONTEXT
|
|
393
273
|
## System Context
|
|
394
274
|
|
|
395
|
-
You are a prompt object named "#{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
275
|
+
You are a prompt object named "#{object_name}" running in a PromptObjects environment.
|
|
276
|
+
|
|
277
|
+
### What is a Prompt Object?
|
|
278
|
+
You are an autonomous entity defined by a markdown file. You have an identity (your prompt),
|
|
279
|
+
capabilities (tools you can use), and you communicate by receiving messages and responding.
|
|
280
|
+
You exist alongside other prompt objects and primitive tools in a shared environment.
|
|
281
|
+
|
|
282
|
+
### How you get called
|
|
283
|
+
You may receive messages from:
|
|
284
|
+
- **A human** interacting with you directly through the UI
|
|
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}`)
|
|
289
|
+
|
|
290
|
+
When called by another PO, you'll see a delegation context block in the message with details
|
|
291
|
+
about who called you and why. You can also check shared environment data (via `list_env_data`)
|
|
292
|
+
for context that other POs in the same workflow have stored.
|
|
293
|
+
|
|
294
|
+
### Your capabilities
|
|
295
|
+
When using tools that target a PO (like add_capability), you can use "self" or "#{object_name}" to target yourself.
|
|
296
|
+
- Declared capabilities: #{declared_caps.empty? ? '(none)' : declared_caps.join(', ')}
|
|
297
|
+
- Universal capabilities (always available): #{UNIVERSAL_CAPABILITIES.join(', ')}
|
|
298
|
+
|
|
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.
|
|
400
311
|
CONTEXT
|
|
401
312
|
|
|
402
|
-
"#{
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def execute_tool_calls(tool_calls, context)
|
|
406
|
-
# Track the caller for nested calls
|
|
407
|
-
previous_capability = context.current_capability
|
|
408
|
-
previous_calling_po = context.calling_po
|
|
409
|
-
|
|
410
|
-
tool_calls.map do |tc|
|
|
411
|
-
capability = @env.registry&.get(tc.name)
|
|
412
|
-
|
|
413
|
-
if capability
|
|
414
|
-
# Log the outgoing message
|
|
415
|
-
@env.bus.publish(from: name, to: tc.name, message: tc.arguments)
|
|
416
|
-
|
|
417
|
-
# Set context for the tool call
|
|
418
|
-
# calling_po tracks which PO is making the call (for "self" resolution)
|
|
419
|
-
context.calling_po = name
|
|
420
|
-
context.current_capability = tc.name
|
|
421
|
-
|
|
422
|
-
result = if capability.is_a?(PromptObject)
|
|
423
|
-
# PO-to-PO call: create isolated delegation thread
|
|
424
|
-
execute_po_delegation(capability, tc, context)
|
|
425
|
-
else
|
|
426
|
-
# Primitive call: execute directly
|
|
427
|
-
capability.receive(tc.arguments, context: context)
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
# Restore context
|
|
431
|
-
context.current_capability = previous_capability
|
|
432
|
-
context.calling_po = previous_calling_po
|
|
433
|
-
|
|
434
|
-
# Log the response
|
|
435
|
-
@env.bus.publish(from: tc.name, to: name, message: result)
|
|
436
|
-
|
|
437
|
-
{ tool_call_id: tc.id, name: tc.name, content: result }
|
|
438
|
-
else
|
|
439
|
-
{ tool_call_id: tc.id, name: tc.name, content: "Unknown capability: #{tc.name}" }
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
# Execute a delegation to another PO in an isolated thread.
|
|
445
|
-
# @param target_po [PromptObject] The PO to delegate to
|
|
446
|
-
# @param tool_call [ToolCall] The tool call details
|
|
447
|
-
# @param context [Context] Execution context
|
|
448
|
-
# @return [String] The response
|
|
449
|
-
def execute_po_delegation(target_po, tool_call, context)
|
|
450
|
-
# Create a delegation thread in the target PO
|
|
451
|
-
delegation_thread = target_po.create_delegation_thread(
|
|
452
|
-
parent_po: name,
|
|
453
|
-
parent_session_id: @session_id,
|
|
454
|
-
parent_message_id: get_last_message_id
|
|
455
|
-
)
|
|
456
|
-
|
|
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
|
-
})
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
# Get the ID of the last message in the current session.
|
|
485
|
-
# @return [Integer, nil]
|
|
486
|
-
def get_last_message_id
|
|
487
|
-
return nil unless session_store && @session_id
|
|
488
|
-
|
|
489
|
-
messages = session_store.get_messages(@session_id)
|
|
490
|
-
messages.last&.dig(:id)
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
# Notify the history updated callback if registered.
|
|
494
|
-
# Used for real-time UI updates during the receive loop.
|
|
495
|
-
def notify_history_updated
|
|
496
|
-
@on_history_updated&.call(self, @session_id, @history)
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
# --- Session Persistence Helpers ---
|
|
500
|
-
|
|
501
|
-
# Load existing session or create a new one.
|
|
502
|
-
def load_or_create_session
|
|
503
|
-
session = session_store.get_or_create_session(po_name: name)
|
|
504
|
-
@session_id = session[:id]
|
|
505
|
-
reload_history_from_session
|
|
313
|
+
"#{prompt}\n\n#{context_block}"
|
|
506
314
|
end
|
|
507
315
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
messages = session_store.get_messages(@session_id)
|
|
513
|
-
@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
|
|
514
320
|
end
|
|
515
321
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return
|
|
519
|
-
|
|
520
|
-
case msg[:role]
|
|
521
|
-
when :user
|
|
522
|
-
session_store.add_message(
|
|
523
|
-
session_id: @session_id,
|
|
524
|
-
role: :user,
|
|
525
|
-
content: msg[:content],
|
|
526
|
-
from_po: msg[:from]
|
|
527
|
-
)
|
|
528
|
-
when :assistant
|
|
529
|
-
# Serialize tool_calls if present
|
|
530
|
-
tool_calls_data = msg[:tool_calls]&.map do |tc|
|
|
531
|
-
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
session_store.add_message(
|
|
535
|
-
session_id: @session_id,
|
|
536
|
-
role: :assistant,
|
|
537
|
-
content: msg[:content],
|
|
538
|
-
tool_calls: tool_calls_data,
|
|
539
|
-
usage: msg[:usage]
|
|
540
|
-
)
|
|
541
|
-
when :tool
|
|
542
|
-
session_store.add_message(
|
|
543
|
-
session_id: @session_id,
|
|
544
|
-
role: :tool,
|
|
545
|
-
tool_results: msg[:results]
|
|
546
|
-
)
|
|
547
|
-
end
|
|
548
|
-
end
|
|
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)
|
|
549
325
|
|
|
550
|
-
|
|
551
|
-
def convert_db_message_to_history(db_msg)
|
|
552
|
-
case db_msg[:role]
|
|
553
|
-
when :user
|
|
554
|
-
{ role: :user, content: db_msg[:content], from: db_msg[:from_po] || "human" }
|
|
555
|
-
when :assistant
|
|
556
|
-
msg = { role: :assistant, content: db_msg[:content] }
|
|
557
|
-
if db_msg[:tool_calls]
|
|
558
|
-
# Reconstruct tool call objects from Hashes
|
|
559
|
-
msg[:tool_calls] = db_msg[:tool_calls].map do |tc|
|
|
560
|
-
LLM::ToolCall.from_hash(tc)
|
|
561
|
-
end
|
|
562
|
-
end
|
|
563
|
-
msg
|
|
564
|
-
when :tool
|
|
565
|
-
{ role: :tool, results: db_msg[:tool_results] || [] }
|
|
566
|
-
else
|
|
567
|
-
{ role: db_msg[:role], content: db_msg[:content] }
|
|
568
|
-
end
|
|
326
|
+
value
|
|
569
327
|
end
|
|
570
328
|
end
|
|
571
329
|
end
|