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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module Execution
|
|
7
|
+
# Thread-aware in-process scheduler. It owns admission, per-thread writer
|
|
8
|
+
# leases, worker slots, and cross-thread priority selection; Runner owns the
|
|
9
|
+
# execution lifecycle after a lease is granted.
|
|
10
|
+
class Scheduler
|
|
11
|
+
class RejectedError < StandardError; end
|
|
12
|
+
class ClosedError < StandardError; end
|
|
13
|
+
|
|
14
|
+
Entry = Data.define(:run, :ticket, :deadline, :enqueue_sequence, :options)
|
|
15
|
+
|
|
16
|
+
class Ticket
|
|
17
|
+
def initialize(run_id)
|
|
18
|
+
@run_id = run_id
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@condition = ConditionVariable.new
|
|
21
|
+
@complete = false
|
|
22
|
+
@value = nil
|
|
23
|
+
@error = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :run_id
|
|
27
|
+
|
|
28
|
+
def wait(timeout: nil)
|
|
29
|
+
deadline = timeout && monotonic_time + timeout
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
until @complete
|
|
32
|
+
remaining = deadline && deadline - monotonic_time
|
|
33
|
+
raise Timeout::Error, "run #{@run_id} did not finish" if remaining && !remaining.positive?
|
|
34
|
+
|
|
35
|
+
@condition.wait(@mutex, remaining)
|
|
36
|
+
end
|
|
37
|
+
raise @error if @error
|
|
38
|
+
|
|
39
|
+
@value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def complete?
|
|
44
|
+
@mutex.synchronize { @complete }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve(value)
|
|
48
|
+
finish(value: value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reject(error)
|
|
52
|
+
finish(error: error)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def finish(value: nil, error: nil)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
return if @complete
|
|
60
|
+
|
|
61
|
+
@value = value
|
|
62
|
+
@error = error
|
|
63
|
+
@complete = true
|
|
64
|
+
@condition.broadcast
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def monotonic_time
|
|
69
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def initialize(worker_limit:, max_active_runs:, max_active_per_po:, max_queued_per_thread:,
|
|
74
|
+
execute:, ready: nil, cancel_queued: nil)
|
|
75
|
+
raise ArgumentError, "max_active_runs must be positive" unless max_active_runs.positive?
|
|
76
|
+
raise ArgumentError, "max_active_per_po must be positive" unless max_active_per_po.positive?
|
|
77
|
+
raise ArgumentError, "max_queued_per_thread must be positive" unless max_queued_per_thread.positive?
|
|
78
|
+
|
|
79
|
+
@execute = execute
|
|
80
|
+
@ready = ready || ->(_run) { true }
|
|
81
|
+
@cancel_queued = cancel_queued
|
|
82
|
+
@max_active_runs = max_active_runs
|
|
83
|
+
@max_active_per_po = max_active_per_po
|
|
84
|
+
@max_queued_per_thread = max_queued_per_thread
|
|
85
|
+
@queues = Hash.new { |hash, key| hash[key] = [] }
|
|
86
|
+
@entries_by_run_id = {}
|
|
87
|
+
@running_tokens = {}
|
|
88
|
+
@active_po_counts = Hash.new(0)
|
|
89
|
+
@active_threads = {}
|
|
90
|
+
@mutex = Mutex.new
|
|
91
|
+
@condition = ConditionVariable.new
|
|
92
|
+
@closed = false
|
|
93
|
+
@enqueue_sequence = 0
|
|
94
|
+
@workers = Array.new(worker_limit) { start_worker }
|
|
95
|
+
@deadline_monitor = start_deadline_monitor
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def submit(run, deadline: nil, **options)
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
raise ClosedError, "scheduler is closed" if @closed
|
|
101
|
+
return @entries_by_run_id.fetch(run.id).ticket if @entries_by_run_id.key?(run.id)
|
|
102
|
+
raise RejectedError, "workspace active-run limit reached" if @entries_by_run_id.size >= @max_active_runs
|
|
103
|
+
|
|
104
|
+
queue = @queues[run.thread_id]
|
|
105
|
+
if queue.size >= @max_queued_per_thread
|
|
106
|
+
raise RejectedError, "thread queue limit reached for #{run.thread_id}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@enqueue_sequence += 1
|
|
110
|
+
ticket = Ticket.new(run.id)
|
|
111
|
+
entry = Entry.new(
|
|
112
|
+
run: run,
|
|
113
|
+
ticket: ticket,
|
|
114
|
+
deadline: deadline,
|
|
115
|
+
enqueue_sequence: @enqueue_sequence,
|
|
116
|
+
options: options.freeze
|
|
117
|
+
)
|
|
118
|
+
queue << entry
|
|
119
|
+
queue.sort_by! { |queued| [queued.run.turn_ordinal, queued.enqueue_sequence] }
|
|
120
|
+
@entries_by_run_id[run.id] = entry
|
|
121
|
+
@condition.broadcast
|
|
122
|
+
ticket
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cancel(run_id, reason: "cancelled")
|
|
127
|
+
queued = nil
|
|
128
|
+
token = nil
|
|
129
|
+
@mutex.synchronize do
|
|
130
|
+
entry = @entries_by_run_id[run_id]
|
|
131
|
+
return false unless entry
|
|
132
|
+
|
|
133
|
+
token = @running_tokens[run_id]
|
|
134
|
+
unless token
|
|
135
|
+
@queues[entry.run.thread_id].delete(entry)
|
|
136
|
+
cleanup_queue(entry.run.thread_id)
|
|
137
|
+
@entries_by_run_id.delete(run_id)
|
|
138
|
+
queued = entry
|
|
139
|
+
end
|
|
140
|
+
@condition.broadcast
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if token
|
|
144
|
+
token.cancel(reason)
|
|
145
|
+
else
|
|
146
|
+
@cancel_queued&.call(queued.run, reason)
|
|
147
|
+
queued.ticket.resolve(:cancelled)
|
|
148
|
+
end
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def queued_count
|
|
153
|
+
@mutex.synchronize { @queues.values.sum(&:size) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def running_count
|
|
157
|
+
@mutex.synchronize { @running_tokens.size }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ticket(run_id)
|
|
161
|
+
@mutex.synchronize { @entries_by_run_id[run_id]&.ticket }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Execute a queued run on the caller's current capacity. This is used by
|
|
165
|
+
# a parent joining its child so a one-worker scheduler cannot deadlock
|
|
166
|
+
# with the parent waiting and the child unable to start.
|
|
167
|
+
def help(run_id)
|
|
168
|
+
entry, token = claim_queued(run_id)
|
|
169
|
+
return ticket(run_id) unless entry
|
|
170
|
+
|
|
171
|
+
execute(entry, token)
|
|
172
|
+
entry.ticket
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def wake
|
|
176
|
+
@mutex.synchronize { @condition.broadcast }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def shutdown(wait: true)
|
|
180
|
+
queued = []
|
|
181
|
+
@mutex.synchronize do
|
|
182
|
+
return if @closed
|
|
183
|
+
|
|
184
|
+
@closed = true
|
|
185
|
+
queued = @queues.values.flatten
|
|
186
|
+
@queues.clear
|
|
187
|
+
queued.each { |entry| @entries_by_run_id.delete(entry.run.id) }
|
|
188
|
+
@running_tokens.each_value { |token| token.cancel("scheduler shutdown") }
|
|
189
|
+
@condition.broadcast
|
|
190
|
+
end
|
|
191
|
+
queued.each { |entry| entry.ticket.reject(ClosedError.new("scheduler shut down")) }
|
|
192
|
+
@workers.each(&:join) if wait
|
|
193
|
+
@deadline_monitor.join if wait
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def start_worker
|
|
199
|
+
Thread.new do
|
|
200
|
+
Thread.current.name = "prompt-objects-scheduler" if Thread.current.respond_to?(:name=)
|
|
201
|
+
worker_loop
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def start_deadline_monitor
|
|
206
|
+
Thread.new do
|
|
207
|
+
Thread.current.name = "prompt-objects-deadlines" if Thread.current.respond_to?(:name=)
|
|
208
|
+
@mutex.synchronize do
|
|
209
|
+
loop do
|
|
210
|
+
break if @closed
|
|
211
|
+
|
|
212
|
+
expire_queued_entries
|
|
213
|
+
@condition.wait(@mutex, next_deadline_wait)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def worker_loop
|
|
220
|
+
loop do
|
|
221
|
+
entry, token = next_entry
|
|
222
|
+
break unless entry
|
|
223
|
+
|
|
224
|
+
execute(entry, token)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def next_entry
|
|
229
|
+
@mutex.synchronize do
|
|
230
|
+
loop do
|
|
231
|
+
return [nil, nil] if @closed
|
|
232
|
+
|
|
233
|
+
expire_queued_entries
|
|
234
|
+
entry = runnable_heads.max_by do |candidate|
|
|
235
|
+
[candidate.run.priority, -candidate.enqueue_sequence]
|
|
236
|
+
end
|
|
237
|
+
if entry
|
|
238
|
+
queue = @queues[entry.run.thread_id]
|
|
239
|
+
queue.shift
|
|
240
|
+
cleanup_queue(entry.run.thread_id)
|
|
241
|
+
token = CancellationToken.new
|
|
242
|
+
@active_threads[entry.run.thread_id] = entry.run.id
|
|
243
|
+
@running_tokens[entry.run.id] = token
|
|
244
|
+
@active_po_counts[entry.run.po_name] += 1
|
|
245
|
+
return [entry, token]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
@condition.wait(@mutex, next_deadline_wait)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def claim_queued(run_id)
|
|
254
|
+
@mutex.synchronize do
|
|
255
|
+
entry = @entries_by_run_id[run_id]
|
|
256
|
+
return [nil, nil] unless entry
|
|
257
|
+
return [nil, nil] if @running_tokens.key?(run_id)
|
|
258
|
+
return [nil, nil] if @active_threads.key?(entry.run.thread_id)
|
|
259
|
+
return [nil, nil] if po_at_capacity?(entry.run)
|
|
260
|
+
return [nil, nil] unless @ready.call(entry.run)
|
|
261
|
+
|
|
262
|
+
@queues[entry.run.thread_id].delete(entry)
|
|
263
|
+
cleanup_queue(entry.run.thread_id)
|
|
264
|
+
token = CancellationToken.new
|
|
265
|
+
@active_threads[entry.run.thread_id] = run_id
|
|
266
|
+
@running_tokens[run_id] = token
|
|
267
|
+
@active_po_counts[entry.run.po_name] += 1
|
|
268
|
+
[entry, token]
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def runnable_heads
|
|
273
|
+
@queues.filter_map do |thread_id, queue|
|
|
274
|
+
next if queue.empty? || @active_threads.key?(thread_id)
|
|
275
|
+
|
|
276
|
+
head = queue.first
|
|
277
|
+
head if !po_at_capacity?(head.run) && @ready.call(head.run)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def expire_queued_entries
|
|
282
|
+
now = Time.now
|
|
283
|
+
expired = @queues.values.flatten.select { |entry| entry.deadline && entry.deadline <= now }
|
|
284
|
+
expired.each do |entry|
|
|
285
|
+
@queues[entry.run.thread_id].delete(entry)
|
|
286
|
+
cleanup_queue(entry.run.thread_id)
|
|
287
|
+
@entries_by_run_id.delete(entry.run.id)
|
|
288
|
+
@cancel_queued&.call(entry.run, "deadline exceeded")
|
|
289
|
+
entry.ticket.resolve(:cancelled)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def next_deadline_wait
|
|
294
|
+
deadline = @queues.values.flatten.filter_map(&:deadline).min
|
|
295
|
+
return nil unless deadline
|
|
296
|
+
|
|
297
|
+
[deadline - Time.now, 0].max
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def execute(entry, token)
|
|
301
|
+
value = @execute.call(entry.run, token, entry.options)
|
|
302
|
+
entry.ticket.resolve(value)
|
|
303
|
+
rescue => e
|
|
304
|
+
entry.ticket.reject(e)
|
|
305
|
+
ensure
|
|
306
|
+
@mutex.synchronize do
|
|
307
|
+
@running_tokens.delete(entry.run.id)
|
|
308
|
+
@active_threads.delete(entry.run.thread_id)
|
|
309
|
+
@active_po_counts[entry.run.po_name] -= 1
|
|
310
|
+
@active_po_counts.delete(entry.run.po_name) unless @active_po_counts[entry.run.po_name].positive?
|
|
311
|
+
@entries_by_run_id.delete(entry.run.id)
|
|
312
|
+
@condition.broadcast
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def cleanup_queue(thread_id)
|
|
317
|
+
@queues.delete(thread_id) if @queues[thread_id].empty?
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def po_at_capacity?(run)
|
|
321
|
+
@active_po_counts[run.po_name] >= @max_active_per_po
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -12,8 +12,8 @@ module PromptObjects
|
|
|
12
12
|
"openai" => {
|
|
13
13
|
adapter: "OpenAIAdapter",
|
|
14
14
|
env_key: "OPENAI_API_KEY",
|
|
15
|
-
default_model: "gpt-5.
|
|
16
|
-
models: %w[gpt-5.
|
|
15
|
+
default_model: "gpt-5.6-luna",
|
|
16
|
+
models: %w[gpt-5.6-luna gpt-5.6-terra gpt-5.6-sol gpt-5.4-mini gpt-5.4-nano gpt-5.2 gpt-4.1 gpt-4.1-mini]
|
|
17
17
|
},
|
|
18
18
|
"anthropic" => {
|
|
19
19
|
adapter: "AnthropicAdapter",
|
|
@@ -24,8 +24,8 @@ module PromptObjects
|
|
|
24
24
|
"gemini" => {
|
|
25
25
|
adapter: "GeminiAdapter",
|
|
26
26
|
env_key: "GEMINI_API_KEY",
|
|
27
|
-
default_model: "gemini-3-flash
|
|
28
|
-
models: %w[gemini-3-flash-preview gemini-2.5-pro gemini-2.5-flash]
|
|
27
|
+
default_model: "gemini-3.5-flash",
|
|
28
|
+
models: %w[gemini-3.5-flash gemini-3.1-flash-lite gemini-3-flash-preview gemini-2.5-pro gemini-2.5-flash]
|
|
29
29
|
},
|
|
30
30
|
"ollama" => {
|
|
31
31
|
adapter: "OpenAIAdapter",
|
|
@@ -10,7 +10,7 @@ module PromptObjects
|
|
|
10
10
|
# Google Gemini API adapter for LLM calls.
|
|
11
11
|
# Uses direct HTTP calls to the Gemini REST API.
|
|
12
12
|
class GeminiAdapter
|
|
13
|
-
DEFAULT_MODEL = "gemini-3-flash
|
|
13
|
+
DEFAULT_MODEL = "gemini-3.5-flash"
|
|
14
14
|
API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|
15
15
|
|
|
16
16
|
def initialize(api_key: nil, model: nil)
|
|
@@ -72,14 +72,22 @@ module PromptObjects
|
|
|
72
72
|
tc_id = tc.respond_to?(:id) ? tc.id : (tc[:id] || tc["id"])
|
|
73
73
|
tc_name = tc.respond_to?(:name) ? tc.name : (tc[:name] || tc["name"])
|
|
74
74
|
tc_args = tc.respond_to?(:arguments) ? tc.arguments : (tc[:arguments] || tc["arguments"] || {})
|
|
75
|
+
tc_metadata = if tc.respond_to?(:provider_metadata)
|
|
76
|
+
tc.provider_metadata
|
|
77
|
+
else
|
|
78
|
+
tc[:provider_metadata] || tc["provider_metadata"] || {}
|
|
79
|
+
end
|
|
75
80
|
|
|
76
81
|
last_tool_calls[tc_id] = tc_name
|
|
77
|
-
|
|
82
|
+
function_part = {
|
|
78
83
|
functionCall: {
|
|
79
84
|
name: tc_name,
|
|
80
85
|
args: tc_args
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
thought_signature = tc_metadata[:thought_signature] || tc_metadata["thought_signature"]
|
|
89
|
+
function_part[:thoughtSignature] = thought_signature if thought_signature
|
|
90
|
+
parts << function_part
|
|
83
91
|
end
|
|
84
92
|
end
|
|
85
93
|
result << { role: "model", parts: parts } if parts.any?
|
|
@@ -103,15 +111,19 @@ module PromptObjects
|
|
|
103
111
|
end
|
|
104
112
|
|
|
105
113
|
def parse_tool_response_content(content)
|
|
106
|
-
#
|
|
114
|
+
# Gemini's functionResponse.response must always be a Struct/object.
|
|
115
|
+
# Preserve JSON objects directly and wrap scalars, arrays, and text.
|
|
107
116
|
if content.is_a?(String)
|
|
108
117
|
begin
|
|
109
|
-
JSON.parse(content)
|
|
118
|
+
parsed = JSON.parse(content)
|
|
119
|
+
parsed.is_a?(Hash) ? parsed : { result: parsed }
|
|
110
120
|
rescue JSON::ParserError
|
|
111
121
|
{ result: content }
|
|
112
122
|
end
|
|
113
|
-
|
|
123
|
+
elsif content.is_a?(Hash)
|
|
114
124
|
content
|
|
125
|
+
else
|
|
126
|
+
{ result: content }
|
|
115
127
|
end
|
|
116
128
|
end
|
|
117
129
|
|
|
@@ -188,7 +200,7 @@ module PromptObjects
|
|
|
188
200
|
if part["text"]
|
|
189
201
|
text_content += part["text"]
|
|
190
202
|
elsif part["functionCall"]
|
|
191
|
-
tool_calls << parse_function_call(part
|
|
203
|
+
tool_calls << parse_function_call(part)
|
|
192
204
|
end
|
|
193
205
|
end
|
|
194
206
|
|
|
@@ -207,13 +219,18 @@ module PromptObjects
|
|
|
207
219
|
}
|
|
208
220
|
end
|
|
209
221
|
|
|
210
|
-
def parse_function_call(
|
|
222
|
+
def parse_function_call(part)
|
|
223
|
+
fc = part["functionCall"]
|
|
224
|
+
provider_metadata = {}
|
|
225
|
+
provider_metadata[:thought_signature] = part["thoughtSignature"] if part["thoughtSignature"]
|
|
226
|
+
|
|
211
227
|
# Gemini doesn't use tool_call_id like OpenAI, so we generate one
|
|
212
228
|
# based on the function name and a random suffix
|
|
213
229
|
ToolCall.new(
|
|
214
230
|
id: "call_#{fc['name']}_#{SecureRandom.hex(8)}",
|
|
215
231
|
name: fc["name"],
|
|
216
|
-
arguments: fc["args"] || {}
|
|
232
|
+
arguments: fc["args"] || {},
|
|
233
|
+
provider_metadata: provider_metadata
|
|
217
234
|
)
|
|
218
235
|
end
|
|
219
236
|
end
|
|
@@ -4,7 +4,7 @@ module PromptObjects
|
|
|
4
4
|
module LLM
|
|
5
5
|
# OpenAI API adapter for LLM calls.
|
|
6
6
|
class OpenAIAdapter
|
|
7
|
-
DEFAULT_MODEL = "gpt-5.
|
|
7
|
+
DEFAULT_MODEL = "gpt-5.6-luna"
|
|
8
8
|
|
|
9
9
|
def initialize(api_key: nil, model: nil, base_url: nil, extra_headers: nil, provider_name: nil)
|
|
10
10
|
@api_key = api_key || ENV.fetch("OPENAI_API_KEY") do
|
|
@@ -34,6 +34,10 @@ module PromptObjects
|
|
|
34
34
|
if tools.any?
|
|
35
35
|
params[:tools] = tools
|
|
36
36
|
params[:tool_choice] = "auto"
|
|
37
|
+
# GPT-5.6 supports function tools through Chat Completions only when
|
|
38
|
+
# reasoning is disabled. Other OpenAI-compatible providers may not
|
|
39
|
+
# recognize this parameter, so keep the compatibility fix narrow.
|
|
40
|
+
params[:reasoning_effort] = "none" if openai_gpt_5_6?
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
raw_response = @client.chat(parameters: params)
|
|
@@ -64,6 +68,10 @@ module PromptObjects
|
|
|
64
68
|
|
|
65
69
|
private
|
|
66
70
|
|
|
71
|
+
def openai_gpt_5_6?
|
|
72
|
+
@provider_name == "openai" && @model.start_with?("gpt-5.6")
|
|
73
|
+
end
|
|
74
|
+
|
|
67
75
|
def build_messages(system, messages)
|
|
68
76
|
result = [{ role: "system", content: system }]
|
|
69
77
|
|
|
@@ -8,6 +8,11 @@ module PromptObjects
|
|
|
8
8
|
class Pricing
|
|
9
9
|
RATES = {
|
|
10
10
|
# OpenAI
|
|
11
|
+
"gpt-5.6-sol" => { input: 5.00, output: 30.00 },
|
|
12
|
+
"gpt-5.6-terra" => { input: 2.50, output: 15.00 },
|
|
13
|
+
"gpt-5.6-luna" => { input: 1.00, output: 6.00 },
|
|
14
|
+
"gpt-5.4-mini" => { input: 0.75, output: 4.50 },
|
|
15
|
+
"gpt-5.4-nano" => { input: 0.20, output: 1.25 },
|
|
11
16
|
"gpt-5.2" => { input: 2.00, output: 8.00 },
|
|
12
17
|
"gpt-4.1" => { input: 2.00, output: 8.00 },
|
|
13
18
|
"gpt-4.1-mini" => { input: 0.40, output: 1.60 },
|
|
@@ -19,9 +24,11 @@ module PromptObjects
|
|
|
19
24
|
"claude-sonnet-4-5" => { input: 3.00, output: 15.00 },
|
|
20
25
|
"claude-haiku-4-5" => { input: 1.00, output: 5.00 },
|
|
21
26
|
# Gemini
|
|
22
|
-
"gemini-3-flash
|
|
27
|
+
"gemini-3.5-flash" => { input: 1.50, output: 9.00 },
|
|
28
|
+
"gemini-3.1-flash-lite" => { input: 0.25, output: 1.50 },
|
|
29
|
+
"gemini-3-flash-preview" => { input: 0.50, output: 3.00 },
|
|
23
30
|
"gemini-2.5-pro" => { input: 1.25, output: 10.00 },
|
|
24
|
-
"gemini-2.5-flash" => { input: 0.
|
|
31
|
+
"gemini-2.5-flash" => { input: 0.30, output: 2.50 },
|
|
25
32
|
}.freeze
|
|
26
33
|
|
|
27
34
|
# Calculate cost in USD for a given usage.
|
|
@@ -25,12 +25,13 @@ module PromptObjects
|
|
|
25
25
|
# Supports both method access (.id) and hash access ([:id]) for compatibility
|
|
26
26
|
# with code that may receive either ToolCall objects or Hashes from the DB.
|
|
27
27
|
class ToolCall
|
|
28
|
-
attr_reader :id, :name, :arguments
|
|
28
|
+
attr_reader :id, :name, :arguments, :provider_metadata
|
|
29
29
|
|
|
30
|
-
def initialize(id:, name:, arguments:)
|
|
30
|
+
def initialize(id:, name:, arguments:, provider_metadata: nil)
|
|
31
31
|
@id = id
|
|
32
32
|
@name = name
|
|
33
33
|
@arguments = arguments
|
|
34
|
+
@provider_metadata = provider_metadata || {}
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Allow hash-style access for compatibility with code expecting Hashes
|
|
@@ -39,12 +40,15 @@ module PromptObjects
|
|
|
39
40
|
when :id then @id
|
|
40
41
|
when :name then @name
|
|
41
42
|
when :arguments then @arguments
|
|
43
|
+
when :provider_metadata then @provider_metadata
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# Convert to a plain Hash (for serialization)
|
|
46
48
|
def to_h
|
|
47
|
-
{ id: @id, name: @name, arguments: @arguments }
|
|
49
|
+
result = { id: @id, name: @name, arguments: @arguments }
|
|
50
|
+
result[:provider_metadata] = @provider_metadata unless @provider_metadata.empty?
|
|
51
|
+
result
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
# Create a ToolCall from a Hash (for deserialization)
|
|
@@ -54,7 +58,8 @@ module PromptObjects
|
|
|
54
58
|
new(
|
|
55
59
|
id: hash[:id] || hash["id"],
|
|
56
60
|
name: hash[:name] || hash["name"],
|
|
57
|
-
arguments: hash[:arguments] || hash["arguments"] || {}
|
|
61
|
+
arguments: hash[:arguments] || hash["arguments"] || {},
|
|
62
|
+
provider_metadata: hash[:provider_metadata] || hash["provider_metadata"] || {}
|
|
58
63
|
)
|
|
59
64
|
end
|
|
60
65
|
end
|
|
@@ -11,15 +11,42 @@ module PromptObjects
|
|
|
11
11
|
raise Error, "File not found: #{path}" unless File.exist?(path)
|
|
12
12
|
|
|
13
13
|
content = File.read(path, encoding: "UTF-8")
|
|
14
|
+
parse(content, path: path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parse prompt object markdown from a raw string (frontmatter + body).
|
|
18
|
+
# Raises if the YAML frontmatter is malformed, so callers can validate
|
|
19
|
+
# before persisting (see PromptObject#update_source).
|
|
20
|
+
# @param content [String] Raw .md content
|
|
21
|
+
# @param path [String, nil] Source path to record in the result
|
|
22
|
+
# @return [Hash] Parsed data with :config, :body, and :path
|
|
23
|
+
def self.parse(content, path: nil)
|
|
14
24
|
parsed = FrontMatterParser::Parser.new(:md).call(content)
|
|
25
|
+
config = parsed.front_matter || {}
|
|
26
|
+
|
|
27
|
+
# Merge simplified frontmatter (delegates_to + tools) into capabilities.
|
|
28
|
+
# This lets PO authors distinguish PO references from primitive references
|
|
29
|
+
# while keeping the internal model uniform.
|
|
30
|
+
merge_capabilities(config)
|
|
15
31
|
|
|
16
32
|
{
|
|
17
|
-
config:
|
|
33
|
+
config: config,
|
|
18
34
|
body: parsed.content.strip,
|
|
19
35
|
path: path
|
|
20
36
|
}
|
|
21
37
|
end
|
|
22
38
|
|
|
39
|
+
# Merge delegates_to + tools into the capabilities array.
|
|
40
|
+
# If both new-style and old-style keys are present, they are combined.
|
|
41
|
+
def self.merge_capabilities(config)
|
|
42
|
+
delegates = config.delete("delegates_to") || []
|
|
43
|
+
tools = config.delete("tools") || []
|
|
44
|
+
return if delegates.empty? && tools.empty?
|
|
45
|
+
|
|
46
|
+
existing = config["capabilities"] || []
|
|
47
|
+
config["capabilities"] = (existing + delegates + tools).uniq
|
|
48
|
+
end
|
|
49
|
+
|
|
23
50
|
# Load all prompt objects from a directory.
|
|
24
51
|
# @param dir [String] Directory path
|
|
25
52
|
# @return [Array<Hash>] Array of parsed prompt objects
|
|
@@ -100,9 +100,12 @@ module PromptObjects
|
|
|
100
100
|
po = @env.registry.get(po_name)
|
|
101
101
|
return [{ uri: "po://#{po_name}/conversation", mimeType: "text/plain", text: "PO not found" }] unless po
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
workspace_id = @env.active_session(initiated_by: "service:mcp")[:id]
|
|
104
|
+
thread = Repositories::ThreadRepository.new(@env.session_store)
|
|
105
|
+
.list(workspace_session_id: workspace_id, po_name: po_name).last
|
|
106
|
+
history = thread ? Repositories::MessageRepository.new(@env.session_store)
|
|
107
|
+
.list(thread_id: thread.id, limit: 10_000)
|
|
108
|
+
.map { |message| { role: message[:role].to_s, content: message[:content] } } : []
|
|
106
109
|
|
|
107
110
|
[{
|
|
108
111
|
uri: "po://#{po_name}/conversation",
|
|
@@ -18,12 +18,16 @@ module PromptObjects
|
|
|
18
18
|
limit: {
|
|
19
19
|
type: "integer",
|
|
20
20
|
description: "Maximum number of messages to return (default: all)"
|
|
21
|
+
},
|
|
22
|
+
thread_id: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Optional runtime thread ID"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
required: %w[po_name]
|
|
24
28
|
)
|
|
25
29
|
|
|
26
|
-
def self.call(po_name:, limit: nil, server_context:)
|
|
30
|
+
def self.call(po_name:, limit: nil, thread_id: nil, server_context:)
|
|
27
31
|
env = server_context[:env]
|
|
28
32
|
|
|
29
33
|
po = env.registry.get(po_name)
|
|
@@ -34,21 +38,21 @@ module PromptObjects
|
|
|
34
38
|
}])
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
history = history.last(limit) if limit
|
|
41
|
+
workspace_id = env.active_session(initiated_by: "service:mcp")[:id]
|
|
42
|
+
threads = Repositories::ThreadRepository.new(env.session_store)
|
|
43
|
+
.list(workspace_session_id: workspace_id, po_name: po_name)
|
|
44
|
+
thread = thread_id ? threads.find { |candidate| candidate.id == thread_id } : threads.last
|
|
45
|
+
history = thread ? Repositories::MessageRepository.new(env.session_store)
|
|
46
|
+
.list(thread_id: thread.id, limit: limit || 10_000)
|
|
47
|
+
.map do |message|
|
|
48
|
+
{ role: message[:role].to_s, content: message[:content], from: message[:from_po] }.compact
|
|
49
|
+
end : []
|
|
47
50
|
|
|
48
51
|
::MCP::Tool::Response.new([{
|
|
49
52
|
type: "text",
|
|
50
53
|
text: JSON.pretty_generate({
|
|
51
54
|
po_name: po_name,
|
|
55
|
+
thread_id: thread&.id,
|
|
52
56
|
message_count: history.length,
|
|
53
57
|
history: history
|
|
54
58
|
})
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module PromptObjects
|
|
4
4
|
module MCP
|
|
5
5
|
module Tools
|
|
6
|
-
# Get
|
|
6
|
+
# Get pending capability-approval requests from the queue.
|
|
7
7
|
class GetPendingRequests < ::MCP::Tool
|
|
8
8
|
tool_name "get_pending_requests"
|
|
9
|
-
description "Get
|
|
9
|
+
description "Get pending capability-approval requests that await a response."
|
|
10
10
|
|
|
11
11
|
input_schema(
|
|
12
12
|
type: "object",
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module PromptObjects
|
|
4
4
|
module MCP
|
|
5
5
|
module Tools
|
|
6
|
-
# Respond to a pending
|
|
6
|
+
# Respond to a pending capability-approval request.
|
|
7
7
|
class RespondToRequest < ::MCP::Tool
|
|
8
8
|
tool_name "respond_to_request"
|
|
9
|
-
description "Respond to a pending
|
|
9
|
+
description "Respond to a pending capability-approval request."
|
|
10
10
|
|
|
11
11
|
input_schema(
|
|
12
12
|
type: "object",
|
|
@@ -48,7 +48,7 @@ module PromptObjects
|
|
|
48
48
|
env.bus.publish(
|
|
49
49
|
from: "mcp_client",
|
|
50
50
|
to: request.capability,
|
|
51
|
-
message: "[response to
|
|
51
|
+
message: "[response to pending request] #{response}"
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
::MCP::Tool::Response.new([{
|