prompt_objects 0.2.0 → 0.3.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +2 -2
  5. data/exe/prompt_objects +387 -1
  6. data/frontend/src/App.tsx +11 -3
  7. data/frontend/src/components/ContextMenu.tsx +67 -0
  8. data/frontend/src/components/MessageBus.tsx +4 -3
  9. data/frontend/src/components/ModelSelector.tsx +5 -1
  10. data/frontend/src/components/ThreadsSidebar.tsx +46 -2
  11. data/frontend/src/components/UsagePanel.tsx +105 -0
  12. data/frontend/src/hooks/useWebSocket.ts +53 -0
  13. data/frontend/src/store/index.ts +10 -0
  14. data/frontend/src/types/index.ts +4 -1
  15. data/lib/prompt_objects/cli.rb +1 -0
  16. data/lib/prompt_objects/connectors/mcp.rb +1 -0
  17. data/lib/prompt_objects/environment.rb +24 -1
  18. data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
  19. data/lib/prompt_objects/llm/factory.rb +93 -6
  20. data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
  21. data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
  22. data/lib/prompt_objects/llm/pricing.rb +49 -0
  23. data/lib/prompt_objects/llm/response.rb +3 -2
  24. data/lib/prompt_objects/mcp/server.rb +1 -0
  25. data/lib/prompt_objects/message_bus.rb +27 -8
  26. data/lib/prompt_objects/prompt_object.rb +5 -3
  27. data/lib/prompt_objects/server/api/routes.rb +186 -29
  28. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
  29. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
  30. data/lib/prompt_objects/server/public/index.html +2 -2
  31. data/lib/prompt_objects/server/websocket_handler.rb +93 -9
  32. data/lib/prompt_objects/server.rb +54 -0
  33. data/lib/prompt_objects/session/store.rb +399 -4
  34. data/lib/prompt_objects.rb +1 -0
  35. data/prompt_objects.gemspec +1 -1
  36. data/templates/arc-agi-1/manifest.yml +22 -0
  37. data/templates/arc-agi-1/objects/data_manager.md +42 -0
  38. data/templates/arc-agi-1/objects/observer.md +100 -0
  39. data/templates/arc-agi-1/objects/solver.md +118 -0
  40. data/templates/arc-agi-1/objects/verifier.md +79 -0
  41. data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
  42. data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
  43. data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
  44. data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
  45. data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
  46. data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
  47. data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
  48. data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
  49. metadata +20 -3
  50. data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
  51. data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +0 -1
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>PromptObjects</title>
8
- <script type="module" crossorigin src="/assets/index-CeNJvqLG.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Vx4-uMOU.css">
8
+ <script type="module" crossorigin src="/assets/index-CQ7lVDF_.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Bkme6COu.css">
10
10
  </head>
11
11
  <body class="bg-po-bg text-gray-100">
12
12
  <div id="root"></div>
@@ -121,7 +121,8 @@ module PromptObjects
121
121
  payload: {
122
122
  from: entry[:from],
123
123
  to: entry[:to],
124
- content: entry[:message],
124
+ summary: entry[:summary],
125
+ content: serialize_bus_content(entry[:message]),
125
126
  timestamp: entry[:timestamp].iso8601
126
127
  }
127
128
  )
@@ -175,7 +176,8 @@ module PromptObjects
175
176
  payload: {
176
177
  from: entry[:from],
177
178
  to: entry[:to],
178
- content: entry[:message],
179
+ summary: entry[:summary],
180
+ content: serialize_bus_content(entry[:message]),
179
181
  timestamp: entry[:timestamp].iso8601
180
182
  }
181
183
  )
@@ -221,6 +223,10 @@ module PromptObjects
221
223
  handle_switch_llm(message["payload"])
222
224
  when "update_prompt"
223
225
  handle_update_prompt(message["payload"])
226
+ when "get_session_usage"
227
+ handle_get_session_usage(message["payload"])
228
+ when "export_thread"
229
+ handle_export_thread(message["payload"])
224
230
  when "ping"
225
231
  send_message(type: "pong", payload: {})
226
232
  else
@@ -563,6 +569,63 @@ module PromptObjects
563
569
  end
564
570
  end
565
571
 
572
+ def handle_get_session_usage(payload)
573
+ session_id = payload["session_id"]
574
+ include_tree = payload["include_tree"] || false
575
+ return send_error("Session ID required") unless session_id
576
+ return send_error("No session store available") unless @runtime.session_store
577
+
578
+ usage = if include_tree
579
+ @runtime.session_store.thread_tree_usage(session_id)
580
+ else
581
+ @runtime.session_store.session_usage(session_id)
582
+ end
583
+
584
+ # Convert by_model symbol keys to strings for JSON
585
+ by_model = {}
586
+ usage[:by_model].each { |model, data| by_model[model.to_s] = data }
587
+
588
+ send_message(
589
+ type: "session_usage",
590
+ payload: {
591
+ session_id: session_id,
592
+ include_tree: include_tree,
593
+ input_tokens: usage[:input_tokens],
594
+ output_tokens: usage[:output_tokens],
595
+ total_tokens: usage[:total_tokens],
596
+ estimated_cost_usd: usage[:estimated_cost_usd].round(6),
597
+ calls: usage[:calls],
598
+ by_model: by_model
599
+ }
600
+ )
601
+ end
602
+
603
+ def handle_export_thread(payload)
604
+ session_id = payload["session_id"]
605
+ format = payload["format"] || "markdown"
606
+ return send_error("Session ID required") unless session_id
607
+ return send_error("No session store available") unless @runtime.session_store
608
+
609
+ content = case format
610
+ when "markdown"
611
+ @runtime.session_store.export_thread_tree_markdown(session_id)
612
+ when "json"
613
+ data = @runtime.session_store.export_thread_tree_json(session_id)
614
+ data ? JSON.pretty_generate(data) : nil
615
+ end
616
+
617
+ return send_error("Session not found") unless content
618
+
619
+ send_message(
620
+ type: "thread_export",
621
+ payload: {
622
+ session_id: session_id,
623
+ format: format,
624
+ content: content
625
+ }
626
+ )
627
+ end
628
+
566
629
  # === Helpers ===
567
630
 
568
631
  def send_error(message)
@@ -649,21 +712,29 @@ module PromptObjects
649
712
  def message_to_hash(msg)
650
713
  case msg[:role]
651
714
  when :user
652
- { role: "user", content: msg[:content], from: msg[:from] }
715
+ # In-memory messages use :from, SQLite-loaded messages use :from_po
716
+ from = msg[:from] || msg[:from_po]
717
+ { role: "user", content: msg[:content], from: from }
653
718
  when :assistant
654
719
  hash = { role: "assistant", content: msg[:content] }
655
720
  if msg[:tool_calls]
656
721
  hash[:tool_calls] = msg[:tool_calls].map do |tc|
657
- # Handle both ToolCall objects and Hashes
658
- tc_id = tc.respond_to?(:id) ? tc.id : (tc[:id] || tc["id"])
659
- tc_name = tc.respond_to?(:name) ? tc.name : (tc[:name] || tc["name"])
660
- tc_args = tc.respond_to?(:arguments) ? tc.arguments : (tc[:arguments] || tc["arguments"] || {})
661
- { id: tc_id, name: tc_name, arguments: tc_args }
722
+ # Handle both ToolCall objects and Hashes (from DB with symbol or string keys)
723
+ if tc.is_a?(LLM::ToolCall)
724
+ { id: tc.id, name: tc.name, arguments: tc.arguments }
725
+ else
726
+ tc_id = tc[:id] || tc["id"]
727
+ tc_name = tc[:name] || tc["name"]
728
+ tc_args = tc[:arguments] || tc["arguments"] || {}
729
+ { id: tc_id, name: tc_name, arguments: tc_args }
730
+ end
662
731
  end
663
732
  end
664
733
  hash
665
734
  when :tool
666
- { role: "tool", results: msg[:results] }
735
+ # In-memory messages use :results, SQLite-loaded messages use :tool_results
736
+ results = msg[:results] || msg[:tool_results]
737
+ { role: "tool", results: results }
667
738
  else
668
739
  { role: msg[:role].to_s, content: msg[:content] }
669
740
  end
@@ -678,6 +749,19 @@ module PromptObjects
678
749
  options: request.options || []
679
750
  }
680
751
  end
752
+
753
+ # Serialize bus message content for JSON transmission.
754
+ # Handles String, Hash, and other types gracefully.
755
+ def serialize_bus_content(message)
756
+ case message
757
+ when Hash
758
+ message
759
+ when String
760
+ message
761
+ else
762
+ message.to_s
763
+ end
764
+ end
681
765
  end
682
766
  end
683
767
  end
@@ -69,6 +69,9 @@ module PromptObjects
69
69
  file_watcher.start
70
70
  end
71
71
 
72
+ # Write .server file for CLI discovery
73
+ server_file = write_server_file(env_path, host: host, port: port) if env_path
74
+
72
75
  Async do |task|
73
76
  endpoint = Async::HTTP::Endpoint.parse(url)
74
77
 
@@ -82,7 +85,58 @@ module PromptObjects
82
85
  end
83
86
  ensure
84
87
  file_watcher&.stop
88
+ remove_server_file(server_file) if server_file
89
+ end
90
+ end
91
+
92
+ # Write a .server file so CLI clients can discover the running server.
93
+ # @param env_path [String] Path to the environment directory
94
+ # @param host [String] Server host
95
+ # @param port [Integer] Server port
96
+ # @return [String] Path to the .server file
97
+ def self.write_server_file(env_path, host:, port:)
98
+ return nil unless env_path
99
+
100
+ server_file = File.join(env_path, ".server")
101
+ data = {
102
+ pid: Process.pid,
103
+ host: host,
104
+ port: port,
105
+ started_at: Time.now.iso8601
106
+ }
107
+ File.write(server_file, JSON.generate(data))
108
+ server_file
109
+ end
110
+
111
+ # Remove the .server file on shutdown.
112
+ # @param path [String] Path to the .server file
113
+ def self.remove_server_file(path)
114
+ File.delete(path) if path && File.exist?(path)
115
+ rescue StandardError
116
+ # Ignore cleanup errors
117
+ end
118
+
119
+ # Read a .server file to discover a running server.
120
+ # Returns nil if no server is running or the file is stale.
121
+ # @param env_path [String] Path to the environment directory
122
+ # @return [Hash, nil] Server info with :host, :port, :pid
123
+ def self.read_server_file(env_path)
124
+ server_file = File.join(env_path, ".server")
125
+ return nil unless File.exist?(server_file)
126
+
127
+ data = JSON.parse(File.read(server_file), symbolize_names: true)
128
+
129
+ # Check if the process is still running
130
+ begin
131
+ Process.kill(0, data[:pid])
132
+ data
133
+ rescue Errno::ESRCH
134
+ # Process is dead, clean up stale file
135
+ File.delete(server_file)
136
+ nil
85
137
  end
138
+ rescue StandardError
139
+ nil
86
140
  end
87
141
 
88
142
  # Handle file change events and broadcast to connected clients.