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
|
@@ -16,6 +16,19 @@ module PromptObjects
|
|
|
16
16
|
@tui_mode = false
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Resolve the workspace scope for shared environment data.
|
|
20
|
+
# @return [String, nil] Scope id, or nil if it can't be resolved
|
|
21
|
+
def env_data_scope
|
|
22
|
+
store = env.session_store
|
|
23
|
+
return nil unless store
|
|
24
|
+
|
|
25
|
+
if env.respond_to?(:env_data_scope)
|
|
26
|
+
env.env_data_scope
|
|
27
|
+
elsif env.respond_to?(:active_session_id) && env.active_session_id
|
|
28
|
+
env.active_session_id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
19
32
|
# Log a message to the bus.
|
|
20
33
|
# @param to [String] Destination capability
|
|
21
34
|
# @param message [String, Hash] The message
|
|
@@ -34,15 +47,31 @@ module PromptObjects
|
|
|
34
47
|
# The runtime environment that holds all capabilities and coordinates execution.
|
|
35
48
|
# Can be initialized either from:
|
|
36
49
|
# - A user environment directory (with manifest.yml, git integration)
|
|
37
|
-
# - A simple objects directory
|
|
50
|
+
# - A simple objects directory backed by an in-memory runtime store
|
|
38
51
|
class Runtime
|
|
52
|
+
DEFAULT_SCHEDULER_OPTIONS = {
|
|
53
|
+
worker_limit: 4,
|
|
54
|
+
provider_limit: 4,
|
|
55
|
+
tool_limit: 8,
|
|
56
|
+
cpu_limit: 2,
|
|
57
|
+
max_active_runs: 64,
|
|
58
|
+
max_active_per_po: 4,
|
|
59
|
+
max_queued_per_thread: 20,
|
|
60
|
+
max_delegation_depth: 8,
|
|
61
|
+
max_descendants: 64,
|
|
62
|
+
run_event_retention: Operations::Retention::DEFAULT_RUN_EVENT_LIMIT,
|
|
63
|
+
workspace_event_retention: Operations::Retention::DEFAULT_WORKSPACE_EVENT_LIMIT
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
39
66
|
attr_reader :llm, :registry, :objects_dir, :bus, :primitives_dir, :human_queue,
|
|
40
67
|
:manifest, :env_path, :auto_commit, :session_store,
|
|
41
|
-
:current_provider, :current_model
|
|
68
|
+
:current_provider, :current_model, :services, :connectors, :active_session_id,
|
|
69
|
+
:runtime_model
|
|
42
70
|
attr_accessor :on_po_registered # Callback for when a PO is registered
|
|
43
71
|
attr_accessor :on_po_modified # Callback for when a PO is modified (capabilities changed, etc.)
|
|
44
72
|
attr_accessor :on_delegation_event # Callback for PO-to-PO delegation start/complete
|
|
45
73
|
attr_accessor :on_env_data_changed # Callback for env data store/update/delete
|
|
74
|
+
attr_accessor :on_broadcast # Callback for services to push messages to all WebSocket clients
|
|
46
75
|
|
|
47
76
|
# Initialize from an environment path (with manifest) or objects directory.
|
|
48
77
|
# @param env_path [String, nil] Path to environment directory (preferred)
|
|
@@ -52,12 +81,15 @@ module PromptObjects
|
|
|
52
81
|
# @param provider [String, nil] LLM provider (openai, anthropic, gemini)
|
|
53
82
|
# @param model [String, nil] Model name
|
|
54
83
|
# @param auto_commit [Boolean] Auto-commit changes to git (default: true for env_path)
|
|
55
|
-
def initialize(env_path: nil, objects_dir: nil, primitives_dir: nil, llm: nil, provider: nil, model: nil,
|
|
84
|
+
def initialize(env_path: nil, objects_dir: nil, primitives_dir: nil, llm: nil, provider: nil, model: nil,
|
|
85
|
+
auto_commit: nil, runtime_model: nil, scheduler_options: nil)
|
|
56
86
|
if env_path
|
|
57
87
|
# Environment-based initialization
|
|
58
88
|
@env_path = env_path
|
|
59
89
|
@objects_dir = File.join(env_path, "objects")
|
|
60
90
|
@primitives_dir = primitives_dir || File.join(env_path, "primitives")
|
|
91
|
+
@services_dir = File.join(env_path, "services")
|
|
92
|
+
@connectors_dir = File.join(env_path, "connectors")
|
|
61
93
|
@manifest = Env::Manifest.load_from_dir(env_path)
|
|
62
94
|
@auto_commit = auto_commit.nil? ? true : auto_commit
|
|
63
95
|
@manifest.touch_opened!
|
|
@@ -71,10 +103,20 @@ module PromptObjects
|
|
|
71
103
|
@env_path = nil
|
|
72
104
|
@objects_dir = objects_dir || "objects"
|
|
73
105
|
@primitives_dir = primitives_dir || File.join(File.dirname(@objects_dir), "primitives")
|
|
106
|
+
@services_dir = nil
|
|
107
|
+
@connectors_dir = nil
|
|
74
108
|
@manifest = nil
|
|
75
109
|
@auto_commit = auto_commit || false
|
|
76
|
-
@session_store =
|
|
110
|
+
@session_store = Session::Store.new(":memory:")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@runtime_model = "runs"
|
|
114
|
+
if @manifest && @manifest.runtime_model != "runs"
|
|
115
|
+
@manifest.runtime_model = "runs"
|
|
116
|
+
@manifest.save_to_dir(@env_path)
|
|
77
117
|
end
|
|
118
|
+
@scheduler_options = DEFAULT_SCHEDULER_OPTIONS.merge(scheduler_options || {})
|
|
119
|
+
@child_admission_mutex = Mutex.new
|
|
78
120
|
|
|
79
121
|
# Initialize LLM - prefer explicit llm, then use factory with provider/model
|
|
80
122
|
if llm
|
|
@@ -82,17 +124,43 @@ module PromptObjects
|
|
|
82
124
|
@current_provider = nil
|
|
83
125
|
@current_model = nil
|
|
84
126
|
else
|
|
85
|
-
|
|
86
|
-
@
|
|
127
|
+
manifest_llm = @manifest&.llm || {}
|
|
128
|
+
@current_provider = provider || manifest_llm["provider"] || LLM::Factory::DEFAULT_PROVIDER
|
|
129
|
+
manifest_model = manifest_llm["model"] unless provider
|
|
130
|
+
@current_model = model || manifest_model || LLM::Factory.default_model(@current_provider)
|
|
87
131
|
@llm = LLM::Factory.create(provider: @current_provider, model: @current_model)
|
|
88
132
|
end
|
|
89
133
|
|
|
90
134
|
@registry = Registry.new
|
|
91
135
|
@bus = MessageBus.new(session_store: @session_store)
|
|
92
136
|
@human_queue = HumanQueue.new
|
|
137
|
+
# In-memory boot-cycle activity. Autonomous POs can opt to stay dormant
|
|
138
|
+
# until a human has initialized them during this server run; persisted
|
|
139
|
+
# sessions from a previous run intentionally do not arm the timer.
|
|
140
|
+
@human_message_received = {}
|
|
141
|
+
@autonomous_schedule_states = {}
|
|
142
|
+
@autonomous_schedule_mutex = Mutex.new
|
|
143
|
+
|
|
144
|
+
# Paths the app just wrote itself (via PO#save / #update_source). The
|
|
145
|
+
# FileWatcher consults this so an in-app save doesn't trigger a redundant,
|
|
146
|
+
# disruptive reload of a PO that's mid-conversation — the in-memory PO is
|
|
147
|
+
# already up to date. Manual/external edits aren't registered, so they
|
|
148
|
+
# still hot-reload normally.
|
|
149
|
+
@suppressed_reloads = {}
|
|
150
|
+
@suppressed_reloads_mutex = Mutex.new
|
|
151
|
+
|
|
152
|
+
validate_dependencies if @manifest&.dependencies&.any?
|
|
93
153
|
|
|
94
154
|
register_primitives
|
|
95
155
|
register_universal_capabilities
|
|
156
|
+
|
|
157
|
+
@services = []
|
|
158
|
+
load_custom_services
|
|
159
|
+
|
|
160
|
+
@connectors = []
|
|
161
|
+
load_custom_connectors
|
|
162
|
+
|
|
163
|
+
recover_interrupted_runs
|
|
96
164
|
end
|
|
97
165
|
|
|
98
166
|
# Create runtime from a user environment by name.
|
|
@@ -112,7 +180,7 @@ module PromptObjects
|
|
|
112
180
|
@manifest&.name || File.basename(@objects_dir)
|
|
113
181
|
end
|
|
114
182
|
|
|
115
|
-
# Check if this
|
|
183
|
+
# Check if this runtime has a persisted environment directory.
|
|
116
184
|
# @return [Boolean]
|
|
117
185
|
def environment?
|
|
118
186
|
!@env_path.nil?
|
|
@@ -140,15 +208,24 @@ module PromptObjects
|
|
|
140
208
|
# @param model [String, nil] Model name (defaults to provider's default)
|
|
141
209
|
# @return [Hash] New provider/model info
|
|
142
210
|
def switch_llm(provider:, model: nil)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
211
|
+
new_provider = provider
|
|
212
|
+
new_model = model || LLM::Factory.default_model(provider)
|
|
213
|
+
new_llm = LLM::Factory.create(provider: new_provider, model: new_model)
|
|
214
|
+
|
|
215
|
+
@current_provider = new_provider
|
|
216
|
+
@current_model = new_model
|
|
217
|
+
@llm = new_llm
|
|
146
218
|
|
|
147
219
|
# Update all loaded POs to use the new LLM
|
|
148
220
|
@registry.prompt_objects.each do |po|
|
|
149
221
|
po.instance_variable_set(:@llm, @llm)
|
|
150
222
|
end
|
|
151
223
|
|
|
224
|
+
if @manifest
|
|
225
|
+
@manifest.llm = { "provider" => @current_provider, "model" => @current_model }
|
|
226
|
+
@manifest.save_to_dir(@env_path)
|
|
227
|
+
end
|
|
228
|
+
|
|
152
229
|
{ provider: @current_provider, model: @current_model }
|
|
153
230
|
end
|
|
154
231
|
|
|
@@ -217,6 +294,27 @@ module PromptObjects
|
|
|
217
294
|
@on_po_modified&.call(po)
|
|
218
295
|
end
|
|
219
296
|
|
|
297
|
+
# Mark a file path as just-written-by-the-app, so the FileWatcher skips the
|
|
298
|
+
# next reload event for it. Idempotent; absolute paths recommended.
|
|
299
|
+
# @param path [String]
|
|
300
|
+
def suppress_next_reload(path)
|
|
301
|
+
return unless path
|
|
302
|
+
|
|
303
|
+
key = File.expand_path(path)
|
|
304
|
+
@suppressed_reloads_mutex.synchronize { @suppressed_reloads[key] = true }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Whether a pending in-app save should suppress the next reload of this path.
|
|
308
|
+
# Consumes the suppression (returns true at most once per registration).
|
|
309
|
+
# @param path [String]
|
|
310
|
+
# @return [Boolean]
|
|
311
|
+
def reload_suppressed?(path)
|
|
312
|
+
return false unless path
|
|
313
|
+
|
|
314
|
+
key = File.expand_path(path)
|
|
315
|
+
@suppressed_reloads_mutex.synchronize { !!@suppressed_reloads.delete(key) }
|
|
316
|
+
end
|
|
317
|
+
|
|
220
318
|
# Notify that a PO-to-PO delegation has started or completed.
|
|
221
319
|
# @param event_type [Symbol] :started or :completed
|
|
222
320
|
# @param payload [Hash] { target:, caller:, thread_id:, tool_call_id: }
|
|
@@ -277,22 +375,1162 @@ module PromptObjects
|
|
|
277
375
|
@registry.primitives.map(&:name)
|
|
278
376
|
end
|
|
279
377
|
|
|
378
|
+
# --- Environment Session API ---
|
|
379
|
+
|
|
380
|
+
# Get or create the active environment session.
|
|
381
|
+
# @param initiated_by [String] Who initiated the session (default: "human")
|
|
382
|
+
# @return [Hash] Session data
|
|
383
|
+
def active_session(initiated_by: "human")
|
|
384
|
+
@active_session_id ||= find_or_create_environment_session(initiated_by: initiated_by)
|
|
385
|
+
@session_store&.get_session(@active_session_id)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Resolve the env_data scope id for a given session, preferring the
|
|
389
|
+
# environment-level session (v8). Used by server-side readers (REST API,
|
|
390
|
+
# WebSocket, broadcasts) so they read from the same scope writers use.
|
|
391
|
+
# @param session_id [String, nil] A session to fall back to if no env session
|
|
392
|
+
# @return [String, nil] Scope id
|
|
393
|
+
def env_data_scope(session_id = nil)
|
|
394
|
+
return @active_session_id if @active_session_id
|
|
395
|
+
return nil unless @session_store && session_id
|
|
396
|
+
|
|
397
|
+
@session_store.resolve_root_thread(session_id)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Create a new environment session, making it the active one.
|
|
401
|
+
# @param name [String, nil] Optional session name
|
|
402
|
+
# @param initiated_by [String] Who initiated (human, timer, api, etc.)
|
|
403
|
+
# @return [Hash] Session data
|
|
404
|
+
def create_environment_session(name: nil, initiated_by: "human")
|
|
405
|
+
return nil unless @session_store
|
|
406
|
+
|
|
407
|
+
id = SecureRandom.uuid
|
|
408
|
+
workspace_repository.create(id: id, name: name)
|
|
409
|
+
id = @session_store.create_session(
|
|
410
|
+
id: id,
|
|
411
|
+
po_name: nil,
|
|
412
|
+
name: name,
|
|
413
|
+
source: "web",
|
|
414
|
+
thread_type: "root",
|
|
415
|
+
workspace_session_id: id
|
|
416
|
+
)
|
|
417
|
+
@active_session_id = id
|
|
418
|
+
@session_store.get_session(id)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# List the environment-level sessions (the shared-context "jams") with a
|
|
422
|
+
# flag marking which one is active. Side-effect-free: reads the already-
|
|
423
|
+
# resolved @active_session_id rather than calling active_session (which
|
|
424
|
+
# would create a session on read), and queries the store once.
|
|
425
|
+
# @return [Array<Hash>]
|
|
426
|
+
def environment_sessions
|
|
427
|
+
return [] unless @session_store
|
|
428
|
+
|
|
429
|
+
active = @active_session_id
|
|
430
|
+
@session_store.list_environment_sessions.map do |s|
|
|
431
|
+
s.merge(active: s[:id] == active)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Switch the active environment session (changes the shared env_data scope).
|
|
436
|
+
# @param session_id [String]
|
|
437
|
+
# @return [Hash, nil] The now-active session, or nil if not found
|
|
438
|
+
def switch_active_session(session_id)
|
|
439
|
+
return nil unless @session_store
|
|
440
|
+
|
|
441
|
+
session = @session_store.get_session(session_id)
|
|
442
|
+
return nil unless session
|
|
443
|
+
|
|
444
|
+
@active_session_id = session_id
|
|
445
|
+
session
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Rename an environment session.
|
|
449
|
+
# @return [Hash, nil] The updated session
|
|
450
|
+
def rename_session(session_id, name)
|
|
451
|
+
return nil unless @session_store && @session_store.get_session(session_id)
|
|
452
|
+
|
|
453
|
+
@session_store.update_session(session_id, name: name)
|
|
454
|
+
@session_store.get_session(session_id)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Delete an environment session (and its messages). If it was active, the
|
|
458
|
+
# next active_session call will find or create another.
|
|
459
|
+
# @return [Boolean]
|
|
460
|
+
def delete_environment_session(session_id)
|
|
461
|
+
return false unless @session_store && @session_store.get_session(session_id)
|
|
462
|
+
|
|
463
|
+
@session_store.delete_session(session_id)
|
|
464
|
+
@active_session_id = nil if @active_session_id == session_id
|
|
465
|
+
true
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Inject a message through the run scheduler. All sources share this entry
|
|
469
|
+
# point and differ only in their provenance and source-specific thread.
|
|
470
|
+
#
|
|
471
|
+
# @param target_po [String] Name of the PO to receive the message
|
|
472
|
+
# @param content [String] Message content
|
|
473
|
+
# @param from_source [String] Message source (human, timer:name, service:name, api)
|
|
474
|
+
# @return [String] The PO's response
|
|
475
|
+
def inject_message(target_po:, content:, from_source: "human",
|
|
476
|
+
workspace_session_id: nil, thread_id: nil, client_request_id: nil, deadline: nil)
|
|
477
|
+
po = @registry.get(target_po)
|
|
478
|
+
raise Error, "Unknown prompt object: #{target_po}" unless po.is_a?(PromptObject)
|
|
479
|
+
|
|
480
|
+
inject_run_message(
|
|
481
|
+
po: po,
|
|
482
|
+
content: content,
|
|
483
|
+
from_source: from_source,
|
|
484
|
+
workspace_session_id: workspace_session_id,
|
|
485
|
+
thread_id: thread_id,
|
|
486
|
+
client_request_id: client_request_id,
|
|
487
|
+
deadline: deadline
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def enqueue_run(target_po:, content:, from_source: "human", workspace_session_id: nil,
|
|
492
|
+
thread_id: nil, client_request_id: nil, deadline: nil)
|
|
493
|
+
po = @registry.get(target_po)
|
|
494
|
+
raise Error, "Unknown prompt object: #{target_po}" unless po.is_a?(PromptObject)
|
|
495
|
+
|
|
496
|
+
workspace_id = resolve_workspace_id(workspace_session_id, initiated_by: from_source)
|
|
497
|
+
thread = resolve_run_thread(
|
|
498
|
+
workspace_id: workspace_id,
|
|
499
|
+
po: po,
|
|
500
|
+
from_source: from_source,
|
|
501
|
+
thread_id: thread_id
|
|
502
|
+
)
|
|
503
|
+
# enqueue_run is the canonical entry point used by WebSocket, REST, CLI,
|
|
504
|
+
# connectors, and inject_message. Arm wait-for-human schedules here so a
|
|
505
|
+
# valid human run wakes autonomous POs regardless of which surface sent it.
|
|
506
|
+
@human_message_received[target_po] = true if from_source == "human"
|
|
507
|
+
definition = execution_definition_for(po)
|
|
508
|
+
source_type = source_type_for(from_source)
|
|
509
|
+
if source_type == "autonomous"
|
|
510
|
+
active = run_repository.active_for_source(
|
|
511
|
+
workspace_session_id: workspace_id,
|
|
512
|
+
po_name: po.name,
|
|
513
|
+
source_type: source_type,
|
|
514
|
+
source_name: from_source
|
|
515
|
+
)
|
|
516
|
+
return active if active
|
|
517
|
+
end
|
|
518
|
+
normalized_deadline = normalize_deadline(deadline)
|
|
519
|
+
run, created = run_repository.enqueue_with_result(
|
|
520
|
+
workspace_session_id: workspace_id,
|
|
521
|
+
thread_id: thread.id,
|
|
522
|
+
po_name: po.name,
|
|
523
|
+
input_content: content,
|
|
524
|
+
definition_version: definition.version,
|
|
525
|
+
provider: definition.provider,
|
|
526
|
+
model: definition.model,
|
|
527
|
+
capability_policy_version: definition.capability_policy_version,
|
|
528
|
+
source_type: source_type,
|
|
529
|
+
source_name: from_source,
|
|
530
|
+
client_request_id: client_request_id,
|
|
531
|
+
priority: priority_for(from_source),
|
|
532
|
+
metadata: { deadline: normalized_deadline&.iso8601(6) }.compact
|
|
533
|
+
)
|
|
534
|
+
definition_repository.attach(run_id: run.id, definition: definition) unless definition_repository.get(run.id)
|
|
535
|
+
if created
|
|
536
|
+
broadcast_workspace_snapshot(workspace_id)
|
|
537
|
+
publish_run_event(event_repository.append_run_event(
|
|
538
|
+
run_id: run.id,
|
|
539
|
+
kind: "run_queued",
|
|
540
|
+
data: { source_type: run.source_type, turn_ordinal: run.turn_ordinal }
|
|
541
|
+
))
|
|
542
|
+
broadcast_po_runtime_status(run)
|
|
543
|
+
end
|
|
544
|
+
run
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def execution_definition_for(po)
|
|
548
|
+
po.execution_definition(
|
|
549
|
+
provider: current_provider,
|
|
550
|
+
model: current_model,
|
|
551
|
+
capability_policy_version: capability_policy_version_for_config(po.config)
|
|
552
|
+
)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def capability_policy_version_for_config(config)
|
|
556
|
+
names = Array(config["capabilities"] || config[:capabilities]) + UNIVERSAL_CAPABILITIES
|
|
557
|
+
signatures = names.uniq.sort.map do |name|
|
|
558
|
+
capability = registry.get(name)
|
|
559
|
+
[name, capability&.execution_policy_signature || "missing"]
|
|
560
|
+
end
|
|
561
|
+
Digest::SHA256.hexdigest(JSON.generate(signatures))
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def execute_run(run_id, tui_mode: true)
|
|
565
|
+
run = run_repository.get(run_id)
|
|
566
|
+
raise Error, "Unknown run: #{run_id}" unless run
|
|
567
|
+
return run.final_response if run.status == "completed"
|
|
568
|
+
return :queued unless run.status == "queued"
|
|
569
|
+
|
|
570
|
+
run_scheduler.submit(run, deadline: run_deadline(run), tui_mode: tui_mode).wait
|
|
571
|
+
rescue Execution::Scheduler::RejectedError => e
|
|
572
|
+
reject_queued_run(run, e.message)
|
|
573
|
+
raise Error, e.message
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def execute_message_run(target_po:, content:, from_source:, workspace_session_id: nil,
|
|
577
|
+
thread_id: nil, client_request_id: nil, deadline: nil, tui_mode: false)
|
|
578
|
+
run = enqueue_run(
|
|
579
|
+
target_po: target_po,
|
|
580
|
+
content: content,
|
|
581
|
+
from_source: from_source,
|
|
582
|
+
workspace_session_id: workspace_session_id,
|
|
583
|
+
thread_id: thread_id,
|
|
584
|
+
client_request_id: client_request_id,
|
|
585
|
+
deadline: deadline
|
|
586
|
+
)
|
|
587
|
+
response = execute_run(run.id, tui_mode: tui_mode)
|
|
588
|
+
[run_repository.get(run.id), response]
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def cancel_run(run_id, reason: "cancelled")
|
|
592
|
+
run = run_repository.get(run_id)
|
|
593
|
+
return false unless run
|
|
594
|
+
|
|
595
|
+
cancelled = @run_scheduler&.cancel(run_id, reason: reason)
|
|
596
|
+
unless cancelled
|
|
597
|
+
return false unless run.status == "queued"
|
|
598
|
+
|
|
599
|
+
cancel_queued_run(run, reason)
|
|
600
|
+
cancelled = true
|
|
601
|
+
end
|
|
602
|
+
run_repository.active_children(parent_run_id: run_id).each do |child|
|
|
603
|
+
next if metadata_value(child, :child_lifetime) == "detached"
|
|
604
|
+
|
|
605
|
+
cancel_run(child.id, reason: "parent cancelled: #{reason}")
|
|
606
|
+
end
|
|
607
|
+
cancelled
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def schedule_run(run_id, tui_mode: true)
|
|
611
|
+
run = run_repository.get(run_id)
|
|
612
|
+
raise Error, "Unknown run: #{run_id}" unless run
|
|
613
|
+
return nil unless run.status == "queued"
|
|
614
|
+
|
|
615
|
+
run_scheduler.submit(run, deadline: run_deadline(run), tui_mode: tui_mode)
|
|
616
|
+
rescue Execution::Scheduler::RejectedError => e
|
|
617
|
+
reject_queued_run(run, e.message)
|
|
618
|
+
raise Error, e.message
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def with_child_run_admission(parent)
|
|
622
|
+
@child_admission_mutex.synchronize do
|
|
623
|
+
depth = run_depth(parent) + 1
|
|
624
|
+
raise Error, "Maximum delegation depth exceeded" if depth > @scheduler_options.fetch(:max_delegation_depth)
|
|
625
|
+
if run_repository.descendant_count(root_run_id: parent.root_run_id) >= @scheduler_options.fetch(:max_descendants)
|
|
626
|
+
raise Error, "Maximum run descendant count exceeded"
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
yield depth
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def dispatch_run(target_po:, content:, context:, thread_id: nil, lifetime: "attached", on_complete: nil)
|
|
634
|
+
raise Error, "Dispatch requires a run context" unless context.run_id
|
|
635
|
+
raise Error, "lifetime must be attached or detached" unless %w[attached detached].include?(lifetime)
|
|
636
|
+
|
|
637
|
+
parent = run_repository.get(context.run_id)
|
|
638
|
+
raise Error, "Unknown parent run: #{context.run_id}" unless parent
|
|
639
|
+
validate_dispatch_target!(parent.po_name, target_po)
|
|
640
|
+
target = registry.get(target_po)
|
|
641
|
+
definition = execution_definition_for(target)
|
|
642
|
+
thread = nil
|
|
643
|
+
child = nil
|
|
644
|
+
with_child_run_admission(parent) do |depth|
|
|
645
|
+
thread = if thread_id
|
|
646
|
+
required_dispatch_thread(thread_id, parent, target_po)
|
|
647
|
+
else
|
|
648
|
+
thread_repository.create(
|
|
649
|
+
workspace_session_id: parent.workspace_session_id,
|
|
650
|
+
po_name: target_po,
|
|
651
|
+
thread_type: "delegation",
|
|
652
|
+
parent_thread_id: parent.thread_id,
|
|
653
|
+
parent_run_id: parent.id,
|
|
654
|
+
retention_policy: "full"
|
|
655
|
+
)
|
|
656
|
+
end
|
|
657
|
+
child = run_repository.enqueue(
|
|
658
|
+
workspace_session_id: parent.workspace_session_id,
|
|
659
|
+
thread_id: thread.id,
|
|
660
|
+
po_name: target_po,
|
|
661
|
+
input_content: content,
|
|
662
|
+
definition_version: definition.version,
|
|
663
|
+
provider: definition.provider,
|
|
664
|
+
model: definition.model,
|
|
665
|
+
capability_policy_version: definition.capability_policy_version,
|
|
666
|
+
source_type: "delegation",
|
|
667
|
+
source_name: parent.po_name,
|
|
668
|
+
parent_run_id: parent.id,
|
|
669
|
+
priority: 80,
|
|
670
|
+
metadata: {
|
|
671
|
+
dispatch_mode: "dispatch",
|
|
672
|
+
child_lifetime: lifetime,
|
|
673
|
+
delegation_depth: depth,
|
|
674
|
+
deadline: context.deadline&.iso8601(6),
|
|
675
|
+
on_complete: on_complete
|
|
676
|
+
}
|
|
677
|
+
)
|
|
678
|
+
end
|
|
679
|
+
definition_repository.attach(run_id: child.id, definition: definition)
|
|
680
|
+
publish_workspace_snapshot(parent.workspace_session_id)
|
|
681
|
+
publish_run_event(event_repository.append_run_event(
|
|
682
|
+
run_id: child.id,
|
|
683
|
+
kind: "run_queued",
|
|
684
|
+
data: { source_type: child.source_type, turn_ordinal: child.turn_ordinal, mode: "dispatch" }
|
|
685
|
+
))
|
|
686
|
+
publish_run_event(event_repository.append_run_event(
|
|
687
|
+
run_id: parent.id,
|
|
688
|
+
kind: "delegation_started",
|
|
689
|
+
data: {
|
|
690
|
+
child_run_id: child.id,
|
|
691
|
+
child_thread_id: thread.id,
|
|
692
|
+
target_po: target_po,
|
|
693
|
+
mode: "dispatch",
|
|
694
|
+
lifetime: lifetime
|
|
695
|
+
}
|
|
696
|
+
))
|
|
697
|
+
schedule_run(child.id, tui_mode: context.tui_mode)
|
|
698
|
+
{ run_id: child.id, thread_id: thread.id, status: child.status, lifetime: lifetime }
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def join_runs(run_ids, context:, timeout: nil)
|
|
702
|
+
Array(run_ids).map { |run_id| join_run(run_id, context: context, timeout: timeout) }
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def run_status(run_id, context:)
|
|
706
|
+
run = authorized_related_run(run_id, context)
|
|
707
|
+
serialize_runtime_value(run.to_h)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def join_run(run_id, context:, timeout: nil)
|
|
711
|
+
run = authorized_related_run(run_id, context)
|
|
712
|
+
unless terminal_run_status?(run.status)
|
|
713
|
+
ticket = run_scheduler.ticket(run.id)
|
|
714
|
+
ticket ||= schedule_run(run.id, tui_mode: context.tui_mode)
|
|
715
|
+
ticket = run_scheduler.help(run.id) || ticket
|
|
716
|
+
ticket&.wait(timeout: timeout)
|
|
717
|
+
run = run_repository.get(run.id)
|
|
718
|
+
end
|
|
719
|
+
{
|
|
720
|
+
run_id: run.id,
|
|
721
|
+
thread_id: run.thread_id,
|
|
722
|
+
status: run.status,
|
|
723
|
+
response: run.final_response,
|
|
724
|
+
error: run.error_message
|
|
725
|
+
}
|
|
726
|
+
rescue Timeout::Error
|
|
727
|
+
{ run_id: run_id, status: "waiting", error: "join timed out" }
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def shutdown_execution(wait: true)
|
|
731
|
+
@run_scheduler&.shutdown(wait: wait)
|
|
732
|
+
@run_scheduler = nil
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def operational_diagnostics
|
|
736
|
+
Operations::Diagnostics.new(@session_store).snapshot(
|
|
737
|
+
scheduler: {
|
|
738
|
+
queued: @run_scheduler&.queued_count || 0,
|
|
739
|
+
running: @run_scheduler&.running_count || 0,
|
|
740
|
+
provider_permits_available: provider_permits.available,
|
|
741
|
+
tool_permits_available: tool_permits.available,
|
|
742
|
+
cpu_permits_available: cpu_permits.available,
|
|
743
|
+
locked_resource_count: resource_lock_manager.locked_keys.length
|
|
744
|
+
},
|
|
745
|
+
limits: @scheduler_options.slice(
|
|
746
|
+
:worker_limit, :provider_limit, :tool_limit, :cpu_limit, :max_active_runs,
|
|
747
|
+
:max_active_per_po, :max_queued_per_thread, :max_delegation_depth, :max_descendants,
|
|
748
|
+
:run_event_retention, :workspace_event_retention
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def enforce_retention
|
|
754
|
+
Operations::Retention.new(@session_store).prune_events!(
|
|
755
|
+
run_event_limit: @scheduler_options.fetch(:run_event_retention),
|
|
756
|
+
workspace_event_limit: @scheduler_options.fetch(:workspace_event_retention)
|
|
757
|
+
)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def create_runtime_thread(workspace_session_id:, po_name:, thread_type: "human", name: nil,
|
|
761
|
+
parent_thread_id: nil, parent_run_id: nil)
|
|
762
|
+
workspace_id = resolve_workspace_id(workspace_session_id, initiated_by: "human")
|
|
763
|
+
po = @registry.get(po_name)
|
|
764
|
+
raise Error, "Unknown prompt object: #{po_name}" unless po.is_a?(PromptObject)
|
|
765
|
+
|
|
766
|
+
thread = thread_repository.create(
|
|
767
|
+
workspace_session_id: workspace_id,
|
|
768
|
+
po_name: po_name,
|
|
769
|
+
thread_type: thread_type,
|
|
770
|
+
name: name,
|
|
771
|
+
parent_thread_id: parent_thread_id,
|
|
772
|
+
parent_run_id: parent_run_id,
|
|
773
|
+
retention_policy: thread_type == "autonomous" ? "summary" : "full"
|
|
774
|
+
)
|
|
775
|
+
broadcast_workspace_snapshot(workspace_id)
|
|
776
|
+
broadcast(**thread_snapshot_envelope(thread))
|
|
777
|
+
thread
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def rename_runtime_thread(thread_id, name:)
|
|
781
|
+
thread = required_runtime_thread(thread_id)
|
|
782
|
+
renamed = thread_repository.rename(thread.id, name)
|
|
783
|
+
broadcast_workspace_snapshot(thread.workspace_session_id)
|
|
784
|
+
renamed
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def archive_runtime_thread(thread_id)
|
|
788
|
+
thread = required_runtime_thread(thread_id)
|
|
789
|
+
archived = thread_repository.archive(thread.id)
|
|
790
|
+
broadcast_workspace_snapshot(thread.workspace_session_id)
|
|
791
|
+
archived
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def runtime_snapshot_envelopes(workspace_session_id: nil)
|
|
795
|
+
workspace_id = resolve_workspace_id(workspace_session_id, initiated_by: "reconnect")
|
|
796
|
+
threads = thread_repository.list(workspace_session_id: workspace_id, include_archived: true)
|
|
797
|
+
active_runs = run_repository.list(
|
|
798
|
+
workspace_session_id: workspace_id,
|
|
799
|
+
statuses: %w[queued running waiting],
|
|
800
|
+
limit: 500
|
|
801
|
+
)
|
|
802
|
+
events = active_runs.flat_map do |run|
|
|
803
|
+
event_repository.run_events(run_id: run.id).map do |event|
|
|
804
|
+
{
|
|
805
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
806
|
+
type: "run_event",
|
|
807
|
+
payload: serialize_runtime_value(event.to_h)
|
|
808
|
+
}
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
[workspace_snapshot_envelope(workspace_id)] +
|
|
812
|
+
threads.map { |thread| thread_snapshot_envelope(thread) } +
|
|
813
|
+
events +
|
|
814
|
+
[env_data_snapshot_envelope(workspace_id)]
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def publish_workspace_snapshot(workspace_session_id)
|
|
818
|
+
broadcast_workspace_snapshot(workspace_session_id)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def publish_thread_snapshot(thread_id, po_name: nil)
|
|
822
|
+
thread = thread_repository.get(thread_id)
|
|
823
|
+
raise Error, "Unknown thread: #{thread_id}" unless thread
|
|
824
|
+
|
|
825
|
+
broadcast_thread_snapshot(thread, po_name || thread.po_name)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Persist an artifact before exposing it to clients. The artifact repository
|
|
829
|
+
# is authoritative for identity, provenance, and revision history.
|
|
830
|
+
def persist_artifact(title:, html:, mode:, context:)
|
|
831
|
+
workspace_id = artifact_workspace_id(context)
|
|
832
|
+
owner_po = context.calling_po || (context.respond_to?(:target_po) ? context.target_po : nil) || "unknown"
|
|
833
|
+
owner_thread_id = artifact_owner_thread_id(context, owner_po)
|
|
834
|
+
producing_run_id = context.respond_to?(:run_id) ? context.run_id : nil
|
|
835
|
+
mutation = artifact_repository.upsert_with_cursor(
|
|
836
|
+
workspace_session_id: workspace_id,
|
|
837
|
+
owner_po: owner_po,
|
|
838
|
+
owner_thread_id: owner_thread_id,
|
|
839
|
+
producing_run_id: producing_run_id,
|
|
840
|
+
title: title,
|
|
841
|
+
html: html,
|
|
842
|
+
mode: mode,
|
|
843
|
+
metadata: {}
|
|
844
|
+
)
|
|
845
|
+
artifact = mutation.artifact
|
|
846
|
+
|
|
847
|
+
broadcast(
|
|
848
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
849
|
+
type: "artifact_upserted",
|
|
850
|
+
payload: {
|
|
851
|
+
cursor: mutation.cursor,
|
|
852
|
+
artifact: serialize_runtime_value(artifact.to_h)
|
|
853
|
+
}
|
|
854
|
+
)
|
|
855
|
+
if producing_run_id
|
|
856
|
+
publish_run_event(event_repository.append_run_event(
|
|
857
|
+
run_id: producing_run_id,
|
|
858
|
+
kind: "artifact_upserted",
|
|
859
|
+
data: {
|
|
860
|
+
artifact_id: artifact.id,
|
|
861
|
+
title: artifact.title,
|
|
862
|
+
revision: artifact.revision,
|
|
863
|
+
owner_thread_id: artifact.owner_thread_id,
|
|
864
|
+
mode: artifact.mode
|
|
865
|
+
}
|
|
866
|
+
))
|
|
867
|
+
end
|
|
868
|
+
artifact
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def set_environment_data(key:, short_description:, value:, context:)
|
|
872
|
+
workspace_id = environment_data_workspace_id(context)
|
|
873
|
+
stored_by = context.calling_po || (context.respond_to?(:target_po) ? context.target_po : nil) || "unknown"
|
|
874
|
+
unless workspace_repository.get(workspace_id)
|
|
875
|
+
@session_store.store_env_data(
|
|
876
|
+
root_thread_id: workspace_id,
|
|
877
|
+
key: key,
|
|
878
|
+
short_description: short_description,
|
|
879
|
+
value: value,
|
|
880
|
+
stored_by: stored_by
|
|
881
|
+
)
|
|
882
|
+
notify_env_data_changed(
|
|
883
|
+
action: "set", root_thread_id: workspace_id, key: key, stored_by: stored_by
|
|
884
|
+
)
|
|
885
|
+
return @session_store.get_env_data(root_thread_id: workspace_id, key: key)
|
|
886
|
+
end
|
|
887
|
+
mutation = env_data_repository.set_with_cursor(
|
|
888
|
+
workspace_session_id: workspace_id,
|
|
889
|
+
key: key,
|
|
890
|
+
short_description: short_description,
|
|
891
|
+
value: value,
|
|
892
|
+
stored_by: stored_by
|
|
893
|
+
)
|
|
894
|
+
publish_env_data_snapshot(workspace_id, cursor: mutation.cursor)
|
|
895
|
+
publish_environment_data_run_event(context, action: "set", key: key, stored_by: stored_by)
|
|
896
|
+
notify_env_data_changed(
|
|
897
|
+
action: "set",
|
|
898
|
+
root_thread_id: workspace_id,
|
|
899
|
+
key: key,
|
|
900
|
+
stored_by: stored_by
|
|
901
|
+
)
|
|
902
|
+
mutation.entry
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def get_environment_data(key:, context:)
|
|
906
|
+
workspace_id = environment_data_workspace_id(context)
|
|
907
|
+
if workspace_repository.get(workspace_id)
|
|
908
|
+
env_data_repository.get(workspace_session_id: workspace_id, key: key)
|
|
909
|
+
else
|
|
910
|
+
@session_store.get_env_data(root_thread_id: workspace_id, key: key)
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def list_environment_data(context:)
|
|
915
|
+
workspace_id = environment_data_workspace_id(context)
|
|
916
|
+
if workspace_repository.get(workspace_id)
|
|
917
|
+
env_data_repository.list(workspace_session_id: workspace_id)
|
|
918
|
+
else
|
|
919
|
+
@session_store.list_env_data_full(root_thread_id: workspace_id)
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def delete_environment_data(key:, context:)
|
|
924
|
+
workspace_id = environment_data_workspace_id(context)
|
|
925
|
+
stored_by = context.calling_po || (context.respond_to?(:target_po) ? context.target_po : nil) || "unknown"
|
|
926
|
+
unless workspace_repository.get(workspace_id)
|
|
927
|
+
deleted = @session_store.delete_env_data(root_thread_id: workspace_id, key: key)
|
|
928
|
+
if deleted
|
|
929
|
+
notify_env_data_changed(
|
|
930
|
+
action: "delete", root_thread_id: workspace_id, key: key, stored_by: stored_by
|
|
931
|
+
)
|
|
932
|
+
end
|
|
933
|
+
return deleted
|
|
934
|
+
end
|
|
935
|
+
removal = env_data_repository.delete_with_cursor(
|
|
936
|
+
workspace_session_id: workspace_id,
|
|
937
|
+
key: key,
|
|
938
|
+
stored_by: stored_by
|
|
939
|
+
)
|
|
940
|
+
return false unless removal.deleted
|
|
941
|
+
|
|
942
|
+
publish_env_data_snapshot(workspace_id, cursor: removal.cursor)
|
|
943
|
+
publish_environment_data_run_event(context, action: "delete", key: key, stored_by: stored_by)
|
|
944
|
+
notify_env_data_changed(
|
|
945
|
+
action: "delete",
|
|
946
|
+
root_thread_id: workspace_id,
|
|
947
|
+
key: key,
|
|
948
|
+
stored_by: stored_by
|
|
949
|
+
)
|
|
950
|
+
true
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def inject_run_message(po:, content:, from_source:, workspace_session_id: nil,
|
|
954
|
+
thread_id: nil, client_request_id: nil, deadline: nil)
|
|
955
|
+
run = enqueue_run(
|
|
956
|
+
target_po: po.name,
|
|
957
|
+
content: content,
|
|
958
|
+
from_source: from_source,
|
|
959
|
+
workspace_session_id: workspace_session_id,
|
|
960
|
+
thread_id: thread_id,
|
|
961
|
+
client_request_id: client_request_id,
|
|
962
|
+
deadline: deadline
|
|
963
|
+
)
|
|
964
|
+
execute_run(run.id)
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
# Whether a PO has received a new human message during this runtime boot.
|
|
968
|
+
# Used by opt-in autonomous schedules that must not preempt initialization.
|
|
969
|
+
def human_message_received?(po_name)
|
|
970
|
+
!!@human_message_received[po_name]
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Publish truthful autonomous scheduling state for reconnectable UI
|
|
974
|
+
# countdowns. The scheduler owns these transitions; clients only render the
|
|
975
|
+
# server-provided next_run_at instead of guessing from page-load time.
|
|
976
|
+
def update_autonomous_schedule(po_name, **state)
|
|
977
|
+
payload = @autonomous_schedule_mutex.synchronize do
|
|
978
|
+
current = @autonomous_schedule_states[po_name] || { po_name: po_name }
|
|
979
|
+
@autonomous_schedule_states[po_name] = current.merge(state).freeze
|
|
980
|
+
end
|
|
981
|
+
broadcast(type: "autonomous_schedule", payload: payload)
|
|
982
|
+
payload
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def autonomous_schedule_states
|
|
986
|
+
@autonomous_schedule_mutex.synchronize do
|
|
987
|
+
@autonomous_schedule_states.transform_values(&:dup)
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
# Broadcast a message to all connected WebSocket clients.
|
|
992
|
+
# No-op if on_broadcast callback isn't set (e.g., in tests or CLI mode).
|
|
993
|
+
# @param message [Hash] WebSocket message to broadcast
|
|
994
|
+
def broadcast(**message)
|
|
995
|
+
@on_broadcast&.call(message)
|
|
996
|
+
rescue => e
|
|
997
|
+
# Don't crash if broadcast fails
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def publish_run_event(event)
|
|
1001
|
+
broadcast(
|
|
1002
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
1003
|
+
type: "run_event",
|
|
1004
|
+
payload: serialize_runtime_value(event.to_h)
|
|
1005
|
+
)
|
|
1006
|
+
end
|
|
1007
|
+
|
|
280
1008
|
private
|
|
281
1009
|
|
|
1010
|
+
def normalize_deadline(deadline)
|
|
1011
|
+
return nil if deadline.nil? || deadline.to_s.empty?
|
|
1012
|
+
return deadline if deadline.is_a?(Time)
|
|
1013
|
+
|
|
1014
|
+
Time.iso8601(deadline.to_s)
|
|
1015
|
+
rescue ArgumentError
|
|
1016
|
+
raise Error, "deadline must be an ISO 8601 timestamp"
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def run_deadline(run)
|
|
1020
|
+
normalize_deadline(metadata_value(run, :deadline))
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
def workspace_repository
|
|
1024
|
+
@workspace_repository ||= Repositories::WorkspaceSessionRepository.new(@session_store)
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def thread_repository
|
|
1028
|
+
@thread_repository ||= Repositories::ThreadRepository.new(@session_store)
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
def run_repository
|
|
1032
|
+
@run_repository ||= Repositories::RunRepository.new(@session_store)
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def definition_repository
|
|
1036
|
+
@definition_repository ||= Repositories::DefinitionRepository.new(@session_store)
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
def runtime_message_repository
|
|
1040
|
+
@runtime_message_repository ||= Repositories::MessageRepository.new(@session_store)
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
def event_repository
|
|
1044
|
+
@event_repository ||= Repositories::EventRepository.new(@session_store)
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def artifact_repository
|
|
1048
|
+
@artifact_repository ||= Repositories::ArtifactRepository.new(@session_store)
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
def env_data_repository
|
|
1052
|
+
@env_data_repository ||= Repositories::EnvDataRepository.new(@session_store)
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def artifact_workspace_id(context)
|
|
1056
|
+
context_workspace = context.respond_to?(:workspace_session_id) ? context.workspace_session_id : nil
|
|
1057
|
+
workspace_id = context_workspace || active_session(initiated_by: "artifact")&.dig(:id)
|
|
1058
|
+
raise Error, "No active workspace for artifact" unless workspace_id
|
|
1059
|
+
|
|
1060
|
+
workspace_id
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def artifact_owner_thread_id(context, owner_po)
|
|
1064
|
+
context_thread = context.respond_to?(:thread_id) ? context.thread_id : nil
|
|
1065
|
+
return context_thread if context_thread
|
|
1066
|
+
|
|
1067
|
+
nil
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def environment_data_workspace_id(context)
|
|
1071
|
+
context_workspace = context.respond_to?(:workspace_session_id) ? context.workspace_session_id : nil
|
|
1072
|
+
workspace_id = context_workspace || context.env_data_scope
|
|
1073
|
+
raise Error, "No active workspace for environment data" unless workspace_id
|
|
1074
|
+
|
|
1075
|
+
workspace_id
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def publish_env_data_snapshot(workspace_id, cursor: nil)
|
|
1079
|
+
broadcast(**env_data_snapshot_envelope(workspace_id, cursor: cursor))
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def publish_environment_data_run_event(context, action:, key:, stored_by:)
|
|
1083
|
+
run_id = context.respond_to?(:run_id) ? context.run_id : nil
|
|
1084
|
+
return unless run_id
|
|
1085
|
+
|
|
1086
|
+
publish_run_event(event_repository.append_run_event(
|
|
1087
|
+
run_id: run_id,
|
|
1088
|
+
kind: "env_data_changed",
|
|
1089
|
+
data: { action: action, key: key, stored_by: stored_by }
|
|
1090
|
+
))
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def execution_runner
|
|
1094
|
+
@execution_runner ||= Execution::Runner.new(
|
|
1095
|
+
self,
|
|
1096
|
+
event_publisher: method(:publish_run_event),
|
|
1097
|
+
provider_permits: provider_permits,
|
|
1098
|
+
tool_permits: tool_permits,
|
|
1099
|
+
cpu_permits: cpu_permits,
|
|
1100
|
+
resource_lock_manager: resource_lock_manager
|
|
1101
|
+
)
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def provider_permits
|
|
1105
|
+
@provider_permits ||= Execution::PermitPool.new(@scheduler_options.fetch(:provider_limit))
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def tool_permits
|
|
1109
|
+
@tool_permits ||= Execution::PermitPool.new(@scheduler_options.fetch(:tool_limit))
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
def cpu_permits
|
|
1113
|
+
@cpu_permits ||= Execution::PermitPool.new(@scheduler_options.fetch(:cpu_limit))
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
def resource_lock_manager
|
|
1117
|
+
@resource_lock_manager ||= Execution::ResourceLockManager.new
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def run_scheduler
|
|
1121
|
+
@run_scheduler ||= begin
|
|
1122
|
+
Execution::Scheduler.new(
|
|
1123
|
+
worker_limit: @scheduler_options.fetch(:worker_limit),
|
|
1124
|
+
max_active_runs: @scheduler_options.fetch(:max_active_runs),
|
|
1125
|
+
max_active_per_po: @scheduler_options.fetch(:max_active_per_po),
|
|
1126
|
+
max_queued_per_thread: @scheduler_options.fetch(:max_queued_per_thread),
|
|
1127
|
+
execute: method(:execute_scheduled_run),
|
|
1128
|
+
ready: ->(run) { run_repository.thread_head?(run) },
|
|
1129
|
+
cancel_queued: method(:cancel_queued_run)
|
|
1130
|
+
)
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def execute_scheduled_run(run, cancellation_token, options)
|
|
1135
|
+
thread = thread_repository.get(run.thread_id)
|
|
1136
|
+
raise Error, "Unknown thread: #{run.thread_id}" unless thread
|
|
1137
|
+
|
|
1138
|
+
broadcast_po_runtime_status(run)
|
|
1139
|
+
execution_runner.execute(
|
|
1140
|
+
run.id,
|
|
1141
|
+
cancellation_token: cancellation_token,
|
|
1142
|
+
tui_mode: options.fetch(:tui_mode, true)
|
|
1143
|
+
)
|
|
1144
|
+
ensure
|
|
1145
|
+
publish_dispatch_completion(run)
|
|
1146
|
+
broadcast_thread_snapshot(thread, run.po_name) if thread && thread.thread_type != "autonomous"
|
|
1147
|
+
broadcast_workspace_snapshot(run.workspace_session_id)
|
|
1148
|
+
broadcast_po_runtime_status(run)
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
def cancel_queued_run(run, reason)
|
|
1152
|
+
cancelled = run_repository.transition(run.id, status: "cancelled", cancellation_reason: reason)
|
|
1153
|
+
publish_run_event(event_repository.append_run_event(
|
|
1154
|
+
run_id: run.id,
|
|
1155
|
+
kind: "run_cancelled",
|
|
1156
|
+
data: { reason: reason }
|
|
1157
|
+
))
|
|
1158
|
+
broadcast_workspace_snapshot(run.workspace_session_id)
|
|
1159
|
+
broadcast_po_runtime_status(cancelled)
|
|
1160
|
+
@run_scheduler&.wake
|
|
1161
|
+
publish_dispatch_completion(cancelled)
|
|
1162
|
+
cancelled
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
def reject_queued_run(run, reason)
|
|
1166
|
+
skipped = run_repository.transition(run.id, status: "skipped", cancellation_reason: reason)
|
|
1167
|
+
publish_run_event(event_repository.append_run_event(
|
|
1168
|
+
run_id: run.id,
|
|
1169
|
+
kind: "run_skipped",
|
|
1170
|
+
data: { reason: reason }
|
|
1171
|
+
))
|
|
1172
|
+
broadcast_workspace_snapshot(run.workspace_session_id)
|
|
1173
|
+
broadcast_po_runtime_status(skipped)
|
|
1174
|
+
@run_scheduler&.wake
|
|
1175
|
+
publish_dispatch_completion(skipped)
|
|
1176
|
+
skipped
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def recover_interrupted_runs
|
|
1180
|
+
return if @recovery_complete
|
|
1181
|
+
|
|
1182
|
+
workspace_id = active_session(initiated_by: "recovery")[:id]
|
|
1183
|
+
run_repository.list(
|
|
1184
|
+
workspace_session_id: workspace_id,
|
|
1185
|
+
statuses: %w[running waiting],
|
|
1186
|
+
limit: 10_000
|
|
1187
|
+
).each do |run|
|
|
1188
|
+
run_repository.transition(run.id, status: "interrupted")
|
|
1189
|
+
publish_run_event(event_repository.append_run_event(
|
|
1190
|
+
run_id: run.id,
|
|
1191
|
+
kind: "run_interrupted",
|
|
1192
|
+
data: { reason: "process restarted" }
|
|
1193
|
+
))
|
|
1194
|
+
end
|
|
1195
|
+
@recovery_complete = true
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
def broadcast_po_runtime_status(run)
|
|
1199
|
+
active = run_repository.active_count(
|
|
1200
|
+
workspace_session_id: run.workspace_session_id,
|
|
1201
|
+
po_name: run.po_name
|
|
1202
|
+
)
|
|
1203
|
+
broadcast(
|
|
1204
|
+
type: "po_state",
|
|
1205
|
+
payload: { name: run.po_name, state: { status: active.positive? ? "thinking" : "idle" } }
|
|
1206
|
+
)
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
def validate_dispatch_target!(caller_name, target_name)
|
|
1210
|
+
target = registry.get(target_name)
|
|
1211
|
+
raise Error, "Unknown dispatch target: #{target_name}" unless target.is_a?(PromptObject)
|
|
1212
|
+
|
|
1213
|
+
caller = registry.get(caller_name)
|
|
1214
|
+
allowed = Array(caller&.config&.dig("capabilities"))
|
|
1215
|
+
raise Error, "#{caller_name} cannot dispatch to #{target_name}" unless allowed.include?(target_name)
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
def required_dispatch_thread(thread_id, parent, target_po)
|
|
1219
|
+
thread = thread_repository.get(thread_id)
|
|
1220
|
+
unless thread && thread.workspace_session_id == parent.workspace_session_id && thread.po_name == target_po
|
|
1221
|
+
raise Error, "Thread not found for dispatch target #{target_po}: #{thread_id}"
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
thread
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
def run_depth(run)
|
|
1228
|
+
depth = metadata_value(run, :delegation_depth)
|
|
1229
|
+
return depth.to_i if depth
|
|
1230
|
+
|
|
1231
|
+
current = run
|
|
1232
|
+
count = 0
|
|
1233
|
+
while current.parent_run_id
|
|
1234
|
+
count += 1
|
|
1235
|
+
current = run_repository.get(current.parent_run_id)
|
|
1236
|
+
break unless current
|
|
1237
|
+
end
|
|
1238
|
+
count
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
def authorized_related_run(run_id, context)
|
|
1242
|
+
parent = run_repository.get(context.run_id)
|
|
1243
|
+
run = run_repository.get(run_id)
|
|
1244
|
+
raise Error, "Unknown run: #{run_id}" unless run
|
|
1245
|
+
unless parent && run.workspace_session_id == parent.workspace_session_id && run.root_run_id == parent.root_run_id
|
|
1246
|
+
raise Error, "Run is outside the caller's execution tree: #{run_id}"
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
run
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
def terminal_run_status?(status)
|
|
1253
|
+
%w[completed failed cancelled skipped interrupted].include?(status)
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def publish_dispatch_completion(run)
|
|
1257
|
+
return unless metadata_value(run, :dispatch_mode) == "dispatch" && run.parent_run_id
|
|
1258
|
+
return unless terminal_run_status?(run_repository.get(run.id)&.status)
|
|
1259
|
+
|
|
1260
|
+
existing = event_repository.run_events(run_id: run.parent_run_id).any? do |event|
|
|
1261
|
+
event.kind == "delegation_completed" && event.data[:child_run_id] == run.id
|
|
1262
|
+
end
|
|
1263
|
+
return if existing
|
|
1264
|
+
|
|
1265
|
+
current = run_repository.get(run.id)
|
|
1266
|
+
publish_run_event(event_repository.append_run_event(
|
|
1267
|
+
run_id: run.parent_run_id,
|
|
1268
|
+
kind: "delegation_completed",
|
|
1269
|
+
data: {
|
|
1270
|
+
child_run_id: run.id,
|
|
1271
|
+
child_thread_id: run.thread_id,
|
|
1272
|
+
target_po: run.po_name,
|
|
1273
|
+
mode: "dispatch",
|
|
1274
|
+
status: current.status
|
|
1275
|
+
}
|
|
1276
|
+
))
|
|
1277
|
+
enqueue_dispatch_followup(current)
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def metadata_value(run, key)
|
|
1281
|
+
return nil unless run
|
|
1282
|
+
|
|
1283
|
+
run.metadata.key?(key) ? run.metadata[key] : run.metadata[key.to_s]
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
def enqueue_dispatch_followup(child)
|
|
1287
|
+
instruction = metadata_value(child, :on_complete)
|
|
1288
|
+
return if instruction.to_s.empty?
|
|
1289
|
+
|
|
1290
|
+
parent = run_repository.get(child.parent_run_id)
|
|
1291
|
+
return unless parent
|
|
1292
|
+
return if %w[cancelled interrupted].include?(parent.status)
|
|
1293
|
+
|
|
1294
|
+
target = registry.get(parent.po_name)
|
|
1295
|
+
return unless target.is_a?(PromptObject)
|
|
1296
|
+
|
|
1297
|
+
definition = execution_definition_for(target)
|
|
1298
|
+
followup = run_repository.enqueue(
|
|
1299
|
+
workspace_session_id: parent.workspace_session_id,
|
|
1300
|
+
thread_id: parent.thread_id,
|
|
1301
|
+
po_name: parent.po_name,
|
|
1302
|
+
input_content: "#{instruction}\n\nDispatched run #{child.id} finished with status #{child.status}.",
|
|
1303
|
+
definition_version: definition.version,
|
|
1304
|
+
provider: definition.provider,
|
|
1305
|
+
model: definition.model,
|
|
1306
|
+
capability_policy_version: definition.capability_policy_version,
|
|
1307
|
+
source_type: "delegation",
|
|
1308
|
+
source_name: child.po_name,
|
|
1309
|
+
client_request_id: "dispatch_completion:#{child.id}",
|
|
1310
|
+
parent_run_id: child.id,
|
|
1311
|
+
priority: 80,
|
|
1312
|
+
metadata: { completion_trigger_for: child.id }
|
|
1313
|
+
)
|
|
1314
|
+
definition_repository.attach(run_id: followup.id, definition: definition) unless definition_repository.get(followup.id)
|
|
1315
|
+
return unless followup.status == "queued" && event_repository.run_events(run_id: followup.id).empty?
|
|
1316
|
+
|
|
1317
|
+
publish_workspace_snapshot(parent.workspace_session_id)
|
|
1318
|
+
publish_run_event(event_repository.append_run_event(
|
|
1319
|
+
run_id: followup.id,
|
|
1320
|
+
kind: "run_queued",
|
|
1321
|
+
data: { source_type: followup.source_type, turn_ordinal: followup.turn_ordinal, trigger_run_id: child.id }
|
|
1322
|
+
))
|
|
1323
|
+
schedule_run(followup.id)
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def resolve_workspace_id(requested_id, initiated_by:)
|
|
1327
|
+
active_id = active_session(initiated_by: initiated_by)[:id]
|
|
1328
|
+
if requested_id && requested_id != active_id
|
|
1329
|
+
raise Error, "Workspace session is not active: #{requested_id}"
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
active_id
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def required_runtime_thread(thread_id)
|
|
1336
|
+
thread = thread_repository.get(thread_id)
|
|
1337
|
+
raise Error, "Unknown thread: #{thread_id}" unless thread
|
|
1338
|
+
|
|
1339
|
+
resolve_workspace_id(thread.workspace_session_id, initiated_by: "human")
|
|
1340
|
+
thread
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
def resolve_run_thread(workspace_id:, po:, from_source:, thread_id:)
|
|
1344
|
+
if thread_id
|
|
1345
|
+
thread = thread_repository.get(thread_id)
|
|
1346
|
+
unless thread && thread.workspace_session_id == workspace_id && thread.po_name == po.name
|
|
1347
|
+
raise Error, "Thread not found for #{po.name}: #{thread_id}"
|
|
1348
|
+
end
|
|
1349
|
+
return thread
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
thread_type = thread_type_for(from_source)
|
|
1353
|
+
existing = thread_repository.list(workspace_session_id: workspace_id, po_name: po.name)
|
|
1354
|
+
.find { |thread| thread.thread_type == thread_type }
|
|
1355
|
+
existing || thread_repository.create(
|
|
1356
|
+
workspace_session_id: workspace_id,
|
|
1357
|
+
po_name: po.name,
|
|
1358
|
+
thread_type: thread_type,
|
|
1359
|
+
retention_policy: thread_type == "autonomous" ? "summary" : "full"
|
|
1360
|
+
)
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def thread_type_for(from_source)
|
|
1364
|
+
return "human" if from_source == "human"
|
|
1365
|
+
return "autonomous" if from_source.start_with?("timer:")
|
|
1366
|
+
return "artifact" if from_source.start_with?("artifact:")
|
|
1367
|
+
return "service" if from_source.start_with?("service:")
|
|
1368
|
+
|
|
1369
|
+
"api"
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def source_type_for(from_source)
|
|
1373
|
+
thread_type_for(from_source)
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
def priority_for(from_source)
|
|
1377
|
+
case thread_type_for(from_source)
|
|
1378
|
+
when "human" then 100
|
|
1379
|
+
when "artifact" then 90
|
|
1380
|
+
when "delegation" then 80
|
|
1381
|
+
when "api" then 60
|
|
1382
|
+
when "service" then 40
|
|
1383
|
+
else 10
|
|
1384
|
+
end
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
def broadcast_thread_snapshot(thread, _po_name)
|
|
1388
|
+
messages = runtime_message_repository.list(thread_id: thread.id, limit: 10_000)
|
|
1389
|
+
broadcast(**thread_snapshot_envelope(thread, messages: messages))
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def broadcast_workspace_snapshot(workspace_id)
|
|
1393
|
+
broadcast(**workspace_snapshot_envelope(workspace_id))
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
def workspace_snapshot_envelope(workspace_id)
|
|
1397
|
+
workspace = workspace_repository.get(workspace_id)
|
|
1398
|
+
raise Error, "Unknown workspace session: #{workspace_id}" unless workspace
|
|
1399
|
+
|
|
1400
|
+
{
|
|
1401
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
1402
|
+
type: "workspace_snapshot",
|
|
1403
|
+
payload: {
|
|
1404
|
+
cursor: event_repository.current_cursor(workspace_session_id: workspace_id),
|
|
1405
|
+
workspace: serialize_runtime_value(workspace.to_h),
|
|
1406
|
+
threads: thread_repository.list(
|
|
1407
|
+
workspace_session_id: workspace_id,
|
|
1408
|
+
include_archived: true
|
|
1409
|
+
).map { |thread| serialize_runtime_value(thread.to_h) },
|
|
1410
|
+
runs: run_repository.list(
|
|
1411
|
+
workspace_session_id: workspace_id,
|
|
1412
|
+
limit: 500
|
|
1413
|
+
).map { |run| serialize_runtime_value(run.to_h) },
|
|
1414
|
+
artifacts: artifact_repository.list(
|
|
1415
|
+
workspace_session_id: workspace_id
|
|
1416
|
+
).map { |artifact| serialize_runtime_value(artifact.to_h) }
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
def env_data_snapshot_envelope(workspace_id, cursor: nil)
|
|
1422
|
+
{
|
|
1423
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
1424
|
+
type: "env_data_snapshot",
|
|
1425
|
+
payload: {
|
|
1426
|
+
workspace_session_id: workspace_id,
|
|
1427
|
+
cursor: cursor || event_repository.current_cursor(workspace_session_id: workspace_id),
|
|
1428
|
+
entries: env_data_repository.list(
|
|
1429
|
+
workspace_session_id: workspace_id
|
|
1430
|
+
).map { |entry| serialize_runtime_value(entry) }
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
def thread_snapshot_envelope(thread, messages: nil)
|
|
1436
|
+
messages ||= runtime_message_repository.list(thread_id: thread.id, limit: 10_000)
|
|
1437
|
+
{
|
|
1438
|
+
version: Execution::Contracts::PROTOCOL_VERSION,
|
|
1439
|
+
type: "thread_snapshot",
|
|
1440
|
+
payload: {
|
|
1441
|
+
cursor: event_repository.current_cursor(workspace_session_id: thread.workspace_session_id),
|
|
1442
|
+
thread: serialize_runtime_value(thread.to_h),
|
|
1443
|
+
messages: messages.map { |message| serialize_runtime_message(message) }
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
end
|
|
1447
|
+
|
|
1448
|
+
def serialize_runtime_value(value)
|
|
1449
|
+
case value
|
|
1450
|
+
when Time then value.iso8601
|
|
1451
|
+
when Hash then value.to_h { |key, nested| [key, serialize_runtime_value(nested)] }
|
|
1452
|
+
when Array then value.map { |nested| serialize_runtime_value(nested) }
|
|
1453
|
+
else value
|
|
1454
|
+
end
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
def serialize_runtime_message(message)
|
|
1458
|
+
serialize_runtime_value(message).merge(
|
|
1459
|
+
id: message[:message_uid] || message[:id].to_s,
|
|
1460
|
+
cursor_id: message[:id]
|
|
1461
|
+
)
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
# Find the most recent environment session or create one. Looks only at
|
|
1465
|
+
# environment-level sessions (po_name IS NULL), not per-PO sessions, so the
|
|
1466
|
+
# active session is a real shared-context container.
|
|
1467
|
+
def find_or_create_environment_session(initiated_by: "human")
|
|
1468
|
+
return nil unless @session_store
|
|
1469
|
+
|
|
1470
|
+
existing = @session_store.list_environment_sessions(limit: 1)
|
|
1471
|
+
return existing.first[:id] if existing.any?
|
|
1472
|
+
|
|
1473
|
+
create_environment_session(initiated_by: initiated_by)[:id]
|
|
1474
|
+
end
|
|
1475
|
+
|
|
1476
|
+
# Load vendored gems and validate declared dependencies.
|
|
1477
|
+
# Templates can vendor gems in vendor/<gem-name>/lib/ to avoid
|
|
1478
|
+
# requiring users to install gems or fight Bundler isolation.
|
|
1479
|
+
# Falls back to system gem activation for non-vendored deps.
|
|
1480
|
+
def validate_dependencies
|
|
1481
|
+
load_vendored_gems
|
|
1482
|
+
|
|
1483
|
+
gems = @manifest.dependencies["gems"]
|
|
1484
|
+
return unless gems.is_a?(Array) && gems.any?
|
|
1485
|
+
|
|
1486
|
+
missing = gems.filter_map do |gem_info|
|
|
1487
|
+
name = gem_info["name"]
|
|
1488
|
+
# Check if the gem is already loadable (vendored or system)
|
|
1489
|
+
begin
|
|
1490
|
+
require name
|
|
1491
|
+
nil
|
|
1492
|
+
rescue LoadError
|
|
1493
|
+
gem_info
|
|
1494
|
+
end
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1497
|
+
return if missing.empty?
|
|
1498
|
+
|
|
1499
|
+
install_cmds = missing.map { |g| " gem install #{g['name']} # #{g['purpose']}" }
|
|
1500
|
+
warn "Warning: Missing dependencies for environment '#{@manifest.name}':"
|
|
1501
|
+
install_cmds.each { |cmd| warn cmd }
|
|
1502
|
+
warn "\nSome primitives may not load. Install the gems and restart."
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
# Add vendor/<gem>/lib directories to $LOAD_PATH so vendored
|
|
1506
|
+
# gems are requireable without system installation.
|
|
1507
|
+
def load_vendored_gems
|
|
1508
|
+
return unless @env_path
|
|
1509
|
+
|
|
1510
|
+
vendor_dir = File.join(@env_path, "vendor")
|
|
1511
|
+
return unless Dir.exist?(vendor_dir)
|
|
1512
|
+
|
|
1513
|
+
Dir.glob(File.join(vendor_dir, "*", "lib")).sort.each do |lib_path|
|
|
1514
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
1515
|
+
end
|
|
1516
|
+
end
|
|
1517
|
+
|
|
282
1518
|
# Register built-in primitive capabilities.
|
|
283
1519
|
def register_primitives
|
|
284
1520
|
@registry.register(Primitives::ReadFile.new)
|
|
285
1521
|
@registry.register(Primitives::ListFiles.new)
|
|
286
1522
|
@registry.register(Primitives::WriteFile.new)
|
|
287
1523
|
@registry.register(Primitives::HttpGet.new)
|
|
1524
|
+
@registry.register(Primitives::Think.new)
|
|
288
1525
|
|
|
289
1526
|
# Load custom primitives from environment's primitives directory
|
|
290
1527
|
load_custom_primitives
|
|
291
1528
|
end
|
|
292
1529
|
|
|
293
1530
|
# Load custom primitives from the environment's primitives/ directory.
|
|
294
|
-
#
|
|
295
|
-
#
|
|
1531
|
+
# Derives the class name from the filename (snake_case -> CamelCase), the
|
|
1532
|
+
# same convention create_capability uses when it generates a primitive.
|
|
1533
|
+
# Warns on errors rather than crashing so a broken primitive
|
|
296
1534
|
# file doesn't prevent the environment from starting (a PO can fix it).
|
|
297
1535
|
def load_custom_primitives
|
|
298
1536
|
return unless @primitives_dir && Dir.exist?(@primitives_dir)
|
|
@@ -304,7 +1542,7 @@ module PromptObjects
|
|
|
304
1542
|
load(path)
|
|
305
1543
|
klass = PromptObjects::Primitives.const_get(class_name)
|
|
306
1544
|
@registry.register(klass.new)
|
|
307
|
-
rescue SyntaxError, StandardError => e
|
|
1545
|
+
rescue SyntaxError, LoadError, StandardError => e
|
|
308
1546
|
warn "Warning: Failed to load custom primitive #{path}: #{e.message}"
|
|
309
1547
|
end
|
|
310
1548
|
end
|
|
@@ -312,25 +1550,104 @@ module PromptObjects
|
|
|
312
1550
|
|
|
313
1551
|
# Register universal capabilities (available to all prompt objects).
|
|
314
1552
|
def register_universal_capabilities
|
|
315
|
-
@registry.register(Universal::AskHuman.new)
|
|
316
|
-
@registry.register(Universal::Think.new)
|
|
317
1553
|
@registry.register(Universal::CreateCapability.new)
|
|
318
1554
|
@registry.register(Universal::AddCapability.new)
|
|
319
1555
|
@registry.register(Universal::RemoveCapability.new)
|
|
320
1556
|
@registry.register(Universal::ListCapabilities.new)
|
|
321
|
-
@registry.register(Universal::ListPrimitives.new)
|
|
322
|
-
@registry.register(Universal::AddPrimitive.new)
|
|
323
|
-
@registry.register(Universal::CreatePrimitive.new)
|
|
324
1557
|
@registry.register(Universal::DeletePrimitive.new)
|
|
325
1558
|
@registry.register(Universal::VerifyPrimitive.new)
|
|
326
1559
|
@registry.register(Universal::ModifyPrimitive.new)
|
|
327
1560
|
@registry.register(Universal::RequestPrimitive.new)
|
|
328
1561
|
@registry.register(Universal::ModifyPrompt.new)
|
|
329
|
-
@registry.register(Universal::
|
|
1562
|
+
@registry.register(Universal::SetEnvData.new)
|
|
330
1563
|
@registry.register(Universal::GetEnvData.new)
|
|
331
1564
|
@registry.register(Universal::ListEnvData.new)
|
|
332
|
-
@registry.register(Universal::UpdateEnvData.new)
|
|
333
1565
|
@registry.register(Universal::DeleteEnvData.new)
|
|
1566
|
+
@registry.register(Universal::RenderArtifact.new)
|
|
1567
|
+
@registry.register(Universal::DispatchPO.new)
|
|
1568
|
+
@registry.register(Universal::GetRunStatus.new)
|
|
1569
|
+
@registry.register(Universal::JoinRuns.new)
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
# Load custom services from the environment's services/ directory.
|
|
1573
|
+
# Services are background processes with a tick loop, following
|
|
1574
|
+
# the same loading pattern as custom primitives.
|
|
1575
|
+
def load_custom_services
|
|
1576
|
+
return unless @services_dir && Dir.exist?(@services_dir)
|
|
1577
|
+
|
|
1578
|
+
Dir.glob(File.join(@services_dir, "*.rb")).sort.each do |path|
|
|
1579
|
+
begin
|
|
1580
|
+
filename = File.basename(path, ".rb")
|
|
1581
|
+
class_name = filename.split("_").map(&:capitalize).join
|
|
1582
|
+
load(path)
|
|
1583
|
+
klass = PromptObjects::Services.const_get(class_name)
|
|
1584
|
+
service = klass.new(runtime: self)
|
|
1585
|
+
@services << service
|
|
1586
|
+
rescue SyntaxError, LoadError, StandardError => e
|
|
1587
|
+
warn "Warning: Failed to load service #{path}: #{e.message}"
|
|
1588
|
+
end
|
|
1589
|
+
end
|
|
1590
|
+
end
|
|
1591
|
+
|
|
1592
|
+
public
|
|
1593
|
+
|
|
1594
|
+
# Start all loaded services by calling their start method.
|
|
1595
|
+
# Does NOT schedule tick loops — that's the server's responsibility
|
|
1596
|
+
# using Async tasks (see Server.start).
|
|
1597
|
+
def start_services
|
|
1598
|
+
@services.each do |service|
|
|
1599
|
+
begin
|
|
1600
|
+
service.start
|
|
1601
|
+
rescue => e
|
|
1602
|
+
warn "Warning: Failed to start service #{service.name}: #{e.message}"
|
|
1603
|
+
end
|
|
1604
|
+
end
|
|
1605
|
+
end
|
|
1606
|
+
|
|
1607
|
+
# Stop all running services.
|
|
1608
|
+
def stop_services
|
|
1609
|
+
@services.each do |service|
|
|
1610
|
+
service.stop
|
|
1611
|
+
rescue => e
|
|
1612
|
+
warn "Warning: Failed to stop service #{service.name}: #{e.message}"
|
|
1613
|
+
end
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
# Load connectors from the environment's connectors/ directory. A connector
|
|
1617
|
+
# is a Connectors::Base subclass that bridges an external system (Discord,
|
|
1618
|
+
# Slack, a webhook listener, …) to POs by calling inject_message. Loaded
|
|
1619
|
+
# like services; started/stopped with the server.
|
|
1620
|
+
def load_custom_connectors
|
|
1621
|
+
return unless @connectors_dir && Dir.exist?(@connectors_dir)
|
|
1622
|
+
|
|
1623
|
+
Dir.glob(File.join(@connectors_dir, "*.rb")).sort.each do |path|
|
|
1624
|
+
begin
|
|
1625
|
+
class_name = File.basename(path, ".rb").split("_").map(&:capitalize).join
|
|
1626
|
+
load(path)
|
|
1627
|
+
klass = PromptObjects::Connectors.const_get(class_name)
|
|
1628
|
+
@connectors << klass.new(runtime: self)
|
|
1629
|
+
rescue SyntaxError, LoadError, StandardError => e
|
|
1630
|
+
warn "Warning: Failed to load connector #{path}: #{e.message}"
|
|
1631
|
+
end
|
|
1632
|
+
end
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
# Start all loaded connectors. Each connector manages its own loop/thread.
|
|
1636
|
+
def start_connectors
|
|
1637
|
+
@connectors.each do |connector|
|
|
1638
|
+
connector.start
|
|
1639
|
+
rescue => e
|
|
1640
|
+
warn "Warning: Failed to start connector #{connector.source_name}: #{e.message}"
|
|
1641
|
+
end
|
|
1642
|
+
end
|
|
1643
|
+
|
|
1644
|
+
# Stop all running connectors.
|
|
1645
|
+
def stop_connectors
|
|
1646
|
+
@connectors.each do |connector|
|
|
1647
|
+
connector.stop
|
|
1648
|
+
rescue => e
|
|
1649
|
+
warn "Warning: Failed to stop connector #{connector.source_name}: #{e.message}"
|
|
1650
|
+
end
|
|
334
1651
|
end
|
|
335
1652
|
end
|
|
336
1653
|
|