prompt_objects 0.4.0 → 0.6.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/CLAUDE.md +113 -44
  4. data/README.md +140 -14
  5. data/frontend/index.html +5 -1
  6. data/frontend/src/App.tsx +72 -79
  7. data/frontend/src/canvas/CanvasView.tsx +5 -5
  8. data/frontend/src/canvas/constants.ts +31 -31
  9. data/frontend/src/canvas/inspector/InspectorPanel.tsx +4 -4
  10. data/frontend/src/canvas/inspector/POInspector.tsx +35 -35
  11. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +13 -13
  12. data/frontend/src/canvas/nodes/PONode.ts +2 -2
  13. data/frontend/src/components/ContextMenu.tsx +5 -4
  14. data/frontend/src/components/EnvDataPane.tsx +69 -0
  15. data/frontend/src/components/Inspector.tsx +263 -0
  16. data/frontend/src/components/MarkdownMessage.tsx +22 -20
  17. data/frontend/src/components/MethodList.tsx +90 -0
  18. data/frontend/src/components/ModelSelector.tsx +13 -14
  19. data/frontend/src/components/NotificationPanel.tsx +29 -33
  20. data/frontend/src/components/ObjectList.tsx +78 -0
  21. data/frontend/src/components/PaneSlot.tsx +30 -0
  22. data/frontend/src/components/SourcePane.tsx +202 -0
  23. data/frontend/src/components/SystemBar.tsx +74 -0
  24. data/frontend/src/components/Transcript.tsx +76 -0
  25. data/frontend/src/components/UsagePanel.tsx +27 -27
  26. data/frontend/src/components/Workspace.tsx +260 -0
  27. data/frontend/src/components/index.ts +10 -9
  28. data/frontend/src/hooks/useResize.ts +55 -0
  29. data/frontend/src/hooks/useWebSocket.ts +70 -0
  30. data/frontend/src/index.css +27 -10
  31. data/frontend/src/store/index.ts +36 -0
  32. data/frontend/src/types/index.ts +13 -0
  33. data/frontend/tailwind.config.js +28 -9
  34. data/lib/prompt_objects/capability.rb +23 -1
  35. data/lib/prompt_objects/connectors/mcp.rb +2 -16
  36. data/lib/prompt_objects/environment.rb +15 -0
  37. data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
  38. data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
  39. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -6
  40. data/lib/prompt_objects/prompt_object.rb +239 -7
  41. data/lib/prompt_objects/server/api/routes.rb +16 -48
  42. data/lib/prompt_objects/server/app.rb +14 -0
  43. data/lib/prompt_objects/server/public/assets/{index-xvyeb-5Z.js → index-DEPawnfZ.js} +206 -206
  44. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +1 -0
  45. data/lib/prompt_objects/server/public/index.html +7 -3
  46. data/lib/prompt_objects/server/websocket_handler.rb +41 -98
  47. data/lib/prompt_objects/server.rb +6 -62
  48. data/lib/prompt_objects/session/store.rb +176 -4
  49. data/lib/prompt_objects/universal/delete_env_data.rb +70 -0
  50. data/lib/prompt_objects/universal/get_env_data.rb +64 -0
  51. data/lib/prompt_objects/universal/list_env_data.rb +61 -0
  52. data/lib/prompt_objects/universal/store_env_data.rb +87 -0
  53. data/lib/prompt_objects/universal/update_env_data.rb +88 -0
  54. data/lib/prompt_objects.rb +6 -1
  55. data/prompt_objects.gemspec +1 -1
  56. data/templates/arc-agi-1/objects/observer.md +4 -0
  57. data/templates/arc-agi-1/objects/solver.md +10 -1
  58. data/templates/arc-agi-1/objects/verifier.md +4 -0
  59. data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
  60. data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
  61. data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
  62. data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
  63. data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
  64. data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
  65. data/tools/thread-explorer.html +27 -0
  66. metadata +18 -16
  67. data/Gemfile.lock +0 -233
  68. data/IMPLEMENTATION_PLAN.md +0 -1073
  69. data/design-doc-v2.md +0 -1232
  70. data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
  71. data/frontend/src/components/ChatPanel.tsx +0 -296
  72. data/frontend/src/components/Dashboard.tsx +0 -83
  73. data/frontend/src/components/Header.tsx +0 -153
  74. data/frontend/src/components/MessageBus.tsx +0 -56
  75. data/frontend/src/components/POCard.tsx +0 -56
  76. data/frontend/src/components/PODetail.tsx +0 -124
  77. data/frontend/src/components/PromptPanel.tsx +0 -156
  78. data/frontend/src/components/SessionsPanel.tsx +0 -174
  79. data/frontend/src/components/ThreadsSidebar.tsx +0 -163
  80. data/lib/prompt_objects/server/public/assets/index-6y64NXFy.css +0 -1
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Geist Mono,IBM Plex Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-3{bottom:.75rem}.bottom-4{bottom:1rem}.left-3{left:.75rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-3{top:.75rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-40{height:10rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-56{width:14rem}.w-60{width:15rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[400px\]{width:400px}.w-full{width:100%}.min-w-\[140px\]{min-width:140px}.min-w-full{min-width:100%}.max-w-\[60px\]{max-width:60px}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.border-po-border{--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1))}.border-po-status-calling{--tw-border-opacity: 1;border-color:rgb(59 154 110 / var(--tw-border-opacity, 1))}.border-po-warning{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.border-po-warning\/30{border-color:#d4952a4d}.border-transparent{border-color:transparent}.bg-black\/50{background-color:#00000080}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-po-accent-wash{background-color:#d4952a14}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(26 25 24 / var(--tw-bg-opacity, 1))}.bg-po-error{--tw-bg-opacity: 1;background-color:rgb(196 92 74 / var(--tw-bg-opacity, 1))}.bg-po-status-active{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-po-status-calling{--tw-bg-opacity: 1;background-color:rgb(59 154 110 / var(--tw-bg-opacity, 1))}.bg-po-status-idle{--tw-bg-opacity: 1;background-color:rgb(120 114 106 / var(--tw-bg-opacity, 1))}.bg-po-success{--tw-bg-opacity: 1;background-color:rgb(59 154 110 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(34 33 32 / var(--tw-bg-opacity, 1))}.bg-po-surface-2{--tw-bg-opacity: 1;background-color:rgb(44 42 40 / var(--tw-bg-opacity, 1))}.bg-po-surface-2\/80{background-color:#2c2a28cc}.bg-po-text-ghost{--tw-bg-opacity: 1;background-color:rgb(82 78 72 / var(--tw-bg-opacity, 1))}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(212 149 42 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-\[4\.5rem\]{padding-left:4.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:Geist Mono,IBM Plex Mono,monospace}.font-ui{font-family:Geist,system-ui,sans-serif}.text-2xs{font-size:11px;line-height:15px}.text-\[0\.9em\]{font-size:.9em}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-po-accent{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.text-po-bg{--tw-text-opacity: 1;color:rgb(26 25 24 / var(--tw-text-opacity, 1))}.text-po-error{--tw-text-opacity: 1;color:rgb(196 92 74 / var(--tw-text-opacity, 1))}.text-po-status-active{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.text-po-status-calling{--tw-text-opacity: 1;color:rgb(59 154 110 / var(--tw-text-opacity, 1))}.text-po-status-delegated{--tw-text-opacity: 1;color:rgb(90 143 194 / var(--tw-text-opacity, 1))}.text-po-success{--tw-text-opacity: 1;color:rgb(59 154 110 / var(--tw-text-opacity, 1))}.text-po-text-ghost{--tw-text-opacity: 1;color:rgb(82 78 72 / var(--tw-text-opacity, 1))}.text-po-text-primary{--tw-text-opacity: 1;color:rgb(232 226 218 / var(--tw-text-opacity, 1))}.text-po-text-secondary{--tw-text-opacity: 1;color:rgb(168 162 154 / var(--tw-text-opacity, 1))}.text-po-text-tertiary{--tw-text-opacity: 1;color:rgb(120 114 106 / var(--tw-text-opacity, 1))}.text-po-warning{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.placeholder-po-text-ghost::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(82 78 72 / var(--tw-placeholder-opacity, 1))}.placeholder-po-text-ghost::placeholder{--tw-placeholder-opacity: 1;color:rgb(82 78 72 / var(--tw-placeholder-opacity, 1))}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(212\,149\,42\,0\.6\)\]{--tw-shadow: 0 0 5px rgba(212,149,42,.6);--tw-shadow-colored: 0 0 5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(59\,154\,110\,0\.6\)\]{--tw-shadow: 0 0 5px rgba(59,154,110,.6);--tw-shadow-colored: 0 0 5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(212\,149\,42\,0\.7\)\]{--tw-shadow: 0 0 6px rgba(212,149,42,.7);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(59\,154\,110\,0\.7\)\]{--tw-shadow: 0 0 6px rgba(59,154,110,.7);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(26 25 24 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(61 58 55 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(92 87 82 / var(--tw-bg-opacity, 1))}::-moz-selection{background:#d4952a4d}::selection{background:#d4952a4d}.resize-handle{width:.25rem;cursor:col-resize;border-left-width:1px;--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.resize-handle:hover{background-color:#d4952a4d}.resize-handle:active{background-color:#d4952a80}.resize-handle-h{height:.25rem;cursor:row-resize;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(61 58 55 / var(--tw-border-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.resize-handle-h:hover{background-color:#d4952a4d}.resize-handle-h:active{background-color:#d4952a80}.canvas-node-label{text-align:center;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.canvas-node-name{display:block;color:#e8e2da;font-size:12px;font-weight:500;font-family:Geist Mono,IBM Plex Mono,monospace;text-shadow:0 1px 4px rgba(0,0,0,.8)}.canvas-node-status{display:block;color:#78726a;font-size:10px;font-family:Geist Mono,IBM Plex Mono,monospace;text-shadow:0 1px 4px rgba(0,0,0,.8)}.canvas-node-badge{display:flex;align-items:center;justify-content:center;width:20px;height:20px;background:#d4952a;color:#1a1918;font-size:10px;font-weight:700;border-radius:50%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;box-shadow:0 0 8px #d4952a99}.canvas-toolcall-label{color:#3b9a6e;font-size:10px;font-family:Geist Mono,IBM Plex Mono,monospace;text-align:center;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;text-shadow:0 1px 4px rgba(0,0,0,.8)}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.hover\:border-po-border-focus:hover{--tw-border-opacity: 1;border-color:rgb(92 87 82 / var(--tw-border-opacity, 1))}.hover\:bg-po-accent-muted:hover{--tw-bg-opacity: 1;background-color:rgb(154 109 32 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface-2:hover{--tw-bg-opacity: 1;background-color:rgb(44 42 40 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface-3:hover{--tw-bg-opacity: 1;background-color:rgb(54 52 50 / var(--tw-bg-opacity, 1))}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(212 149 42 / var(--tw-text-opacity, 1))}.hover\:text-po-text-primary:hover{--tw-text-opacity: 1;color:rgb(232 226 218 / var(--tw-text-opacity, 1))}.hover\:text-po-text-secondary:hover{--tw-text-opacity: 1;color:rgb(168 162 154 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(212 149 42 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:inline{display:inline}}
@@ -4,11 +4,15 @@
4
4
  <meta charset="UTF-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
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
8
+ <link href="https://cdn.jsdelivr.net/fontsource/fonts/geist@latest/latin-400-normal.woff2" as="font" type="font/woff2" crossorigin />
9
+ <link href="https://cdn.jsdelivr.net/npm/geist@1/dist/fonts/geist-sans/style.css" rel="stylesheet" />
10
+ <link href="https://cdn.jsdelivr.net/npm/geist@1/dist/fonts/geist-mono/style.css" rel="stylesheet" />
7
11
  <title>PromptObjects</title>
8
- <script type="module" crossorigin src="/assets/index-xvyeb-5Z.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-6y64NXFy.css">
12
+ <script type="module" crossorigin src="/assets/index-DEPawnfZ.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-oMrRce1m.css">
10
14
  </head>
11
- <body class="bg-po-bg text-gray-100">
15
+ <body class="bg-po-bg text-po-text-primary font-ui">
12
16
  <div id="root"></div>
13
17
  </body>
14
18
  </html>
@@ -156,7 +156,7 @@ module PromptObjects
156
156
  type: "po_state",
157
157
  payload: {
158
158
  name: po.name,
159
- state: po_state_hash(po).merge(status: "idle")
159
+ state: po.to_state_hash(registry: @runtime.registry).merge(status: "idle")
160
160
  }
161
161
  )
162
162
  end
@@ -227,6 +227,8 @@ module PromptObjects
227
227
  handle_get_session_usage(message["payload"])
228
228
  when "export_thread"
229
229
  handle_export_thread(message["payload"])
230
+ when "get_env_data_list"
231
+ handle_get_env_data_list(message["payload"])
230
232
  when "ping"
231
233
  send_message(type: "pong", payload: {})
232
234
  else
@@ -269,7 +271,7 @@ module PromptObjects
269
271
  type: "po_state",
270
272
  payload: {
271
273
  name: po_name,
272
- state: { sessions: po.list_sessions.map { |s| session_summary(s) } }
274
+ state: { sessions: po.list_sessions.map { |s| PromptObject.serialize_session(s) } }
273
275
  }
274
276
  )
275
277
  end
@@ -311,7 +313,7 @@ module PromptObjects
311
313
  payload: {
312
314
  target: po_obj.name,
313
315
  session_id: session_id,
314
- messages: history.map { |m| message_to_hash(m) }
316
+ messages: history.map { |m| PromptObject.serialize_message(m) }
315
317
  }
316
318
  )
317
319
  }
@@ -350,11 +352,24 @@ module PromptObjects
350
352
  type: "po_state",
351
353
  payload: {
352
354
  name: po_name,
353
- state: { sessions: po.list_sessions.map { |s| session_summary(s) } }
355
+ state: { sessions: po.list_sessions.map { |s| PromptObject.serialize_session(s) } }
354
356
  }
355
357
  )
356
358
  rescue => e
357
359
  send_error("Error from #{po_name}: #{e.message}")
360
+
361
+ # Broadcast rich error context for diagnostics
362
+ llm_config = @runtime.llm_config
363
+ send_message(
364
+ type: "llm_error",
365
+ payload: {
366
+ po_name: po_name,
367
+ provider: llm_config[:provider],
368
+ model: llm_config[:model],
369
+ error: e.message,
370
+ error_class: e.class.name
371
+ }
372
+ )
358
373
  ensure
359
374
  # Clean up the callback
360
375
  po.on_history_updated = nil
@@ -425,7 +440,7 @@ module PromptObjects
425
440
  # Also send updated PO state
426
441
  send_message(
427
442
  type: "po_state",
428
- payload: { name: po_name, state: po_state_hash(po) }
443
+ payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
429
444
  )
430
445
  end
431
446
 
@@ -445,7 +460,7 @@ module PromptObjects
445
460
  # Send updated PO state with new session's messages
446
461
  send_message(
447
462
  type: "po_state",
448
- payload: { name: po_name, state: po_state_hash(po) }
463
+ payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
449
464
  )
450
465
  else
451
466
  send_error("Could not switch to session: #{session_id}")
@@ -475,7 +490,7 @@ module PromptObjects
475
490
  # Also send updated PO state
476
491
  send_message(
477
492
  type: "po_state",
478
- payload: { name: po_name, state: po_state_hash(po) }
493
+ payload: { name: po_name, state: po.to_state_hash(registry: @runtime.registry) }
479
494
  )
480
495
  end
481
496
 
@@ -501,7 +516,7 @@ module PromptObjects
501
516
  info = LLM::Factory.provider_info(provider)
502
517
  {
503
518
  name: provider,
504
- models: info[:models],
519
+ models: LLM::Factory.models_for(provider),
505
520
  default_model: info[:default_model],
506
521
  available: LLM::Factory.available_providers[provider]
507
522
  }
@@ -626,56 +641,28 @@ module PromptObjects
626
641
  )
627
642
  end
628
643
 
629
- # === Helpers ===
630
-
631
- def send_error(message)
632
- send_message(type: "error", payload: { message: message })
633
- end
634
-
635
- def po_state_hash(po)
636
- {
637
- status: po.instance_variable_get(:@state) || "idle",
638
- description: po.description,
639
- capabilities: declared_capabilities_info(po),
640
- universal_capabilities: universal_capabilities_info,
641
- current_session: current_session_hash(po),
642
- sessions: po.list_sessions.map { |s| session_summary(s) },
643
- # Include full prompt for inspection
644
- prompt: po.body,
645
- config: po.config
646
- }
647
- end
644
+ def handle_get_env_data_list(payload)
645
+ session_id = payload["session_id"]
646
+ return send_error("Session ID required") unless session_id
647
+ return send_error("No session store available") unless @runtime.session_store
648
648
 
649
- def declared_capabilities_info(po)
650
- declared = po.config["capabilities"] || []
651
- declared.map do |name|
652
- cap = @runtime.registry.get(name)
653
- {
654
- name: name,
655
- description: cap&.description || "Capability not found",
656
- parameters: cap&.parameters
657
- }
658
- end
659
- end
649
+ root_thread_id = @runtime.session_store.resolve_root_thread(session_id)
650
+ entries = @runtime.session_store.list_env_data_full(root_thread_id: root_thread_id)
660
651
 
661
- def universal_capabilities_info
662
- UNIVERSAL_CAPABILITIES.map do |name|
663
- cap = @runtime.registry.get(name)
664
- {
665
- name: name,
666
- description: cap&.description || "Universal capability",
667
- parameters: cap&.parameters
652
+ send_message(
653
+ type: "env_data_list",
654
+ payload: {
655
+ session_id: session_id,
656
+ root_thread_id: root_thread_id,
657
+ entries: entries
668
658
  }
669
- end
659
+ )
670
660
  end
671
661
 
672
- def current_session_hash(po)
673
- return nil unless po.session_id
662
+ # === Helpers ===
674
663
 
675
- {
676
- id: po.session_id,
677
- messages: po.history.map { |m| message_to_hash(m) }
678
- }
664
+ def send_error(message)
665
+ send_message(type: "error", payload: { message: message })
679
666
  end
680
667
 
681
668
  # Get messages for a specific session (may not be current session)
@@ -683,20 +670,7 @@ module PromptObjects
683
670
  return [] unless @runtime.session_store
684
671
 
685
672
  messages = @runtime.session_store.get_messages(session_id)
686
- messages.map { |m| message_to_hash(m) }
687
- end
688
-
689
- def session_summary(session)
690
- {
691
- id: session[:id],
692
- name: session[:name],
693
- message_count: session[:message_count] || 0,
694
- updated_at: session[:updated_at]&.iso8601,
695
- # Thread fields
696
- parent_session_id: session[:parent_session_id],
697
- parent_po: session[:parent_po],
698
- thread_type: session[:thread_type] || "root"
699
- }
673
+ messages.map { |m| PromptObject.serialize_message(m) }
700
674
  end
701
675
 
702
676
  # Recursively serialize a thread tree for JSON
@@ -704,42 +678,11 @@ module PromptObjects
704
678
  return nil unless tree
705
679
 
706
680
  {
707
- session: session_summary(tree[:session]),
681
+ session: PromptObject.serialize_session(tree[:session]),
708
682
  children: (tree[:children] || []).map { |child| serialize_thread_tree(child) }
709
683
  }
710
684
  end
711
685
 
712
- def message_to_hash(msg)
713
- case msg[:role]
714
- when :user
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 }
718
- when :assistant
719
- hash = { role: "assistant", content: msg[:content] }
720
- if msg[:tool_calls]
721
- hash[:tool_calls] = msg[:tool_calls].map do |tc|
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
731
- end
732
- end
733
- hash
734
- when :tool
735
- # In-memory messages use :results, SQLite-loaded messages use :tool_results
736
- results = msg[:results] || msg[:tool_results]
737
- { role: "tool", results: results }
738
- else
739
- { role: msg[:role].to_s, content: msg[:content] }
740
- end
741
- end
742
-
743
686
  def request_to_hash(request)
744
687
  {
745
688
  id: request.id,
@@ -40,7 +40,7 @@ module PromptObjects
40
40
  type: "po_added",
41
41
  payload: {
42
42
  name: po.name,
43
- state: po_state_hash(po)
43
+ state: po.to_state_hash(registry: runtime.registry)
44
44
  }
45
45
  )
46
46
  puts "Broadcast: PO registered - #{po.name}"
@@ -53,7 +53,7 @@ module PromptObjects
53
53
  type: "po_modified",
54
54
  payload: {
55
55
  name: po.name,
56
- state: po_state_hash(po)
56
+ state: po.to_state_hash(registry: runtime.registry)
57
57
  }
58
58
  )
59
59
  puts "Broadcast: PO modified (programmatic) - #{po.name}"
@@ -64,7 +64,7 @@ module PromptObjects
64
64
  if env_path
65
65
  file_watcher = FileWatcher.new(runtime: runtime, env_path: env_path)
66
66
  file_watcher.subscribe do |event, data|
67
- handle_file_event(app, event, data)
67
+ handle_file_event(app, event, data, runtime: runtime)
68
68
  end
69
69
  file_watcher.start
70
70
  end
@@ -140,7 +140,7 @@ module PromptObjects
140
140
  end
141
141
 
142
142
  # Handle file change events and broadcast to connected clients.
143
- def self.handle_file_event(app, event, data)
143
+ def self.handle_file_event(app, event, data, runtime: nil)
144
144
  case event
145
145
  when :po_added
146
146
  po = data
@@ -148,7 +148,7 @@ module PromptObjects
148
148
  type: "po_added",
149
149
  payload: {
150
150
  name: po.name,
151
- state: po_state_hash(po)
151
+ state: po.to_state_hash(registry: runtime&.registry)
152
152
  }
153
153
  )
154
154
  puts "Broadcast: PO added - #{po.name}"
@@ -159,7 +159,7 @@ module PromptObjects
159
159
  type: "po_modified",
160
160
  payload: {
161
161
  name: po.name,
162
- state: po_state_hash(po)
162
+ state: po.to_state_hash(registry: runtime&.registry)
163
163
  }
164
164
  )
165
165
  puts "Broadcast: PO modified - #{po.name}"
@@ -173,61 +173,5 @@ module PromptObjects
173
173
  end
174
174
  end
175
175
 
176
- # Helper to convert PO to state hash for broadcasting.
177
- def self.po_state_hash(po)
178
- {
179
- status: po.instance_variable_get(:@state) || "idle",
180
- description: po.description,
181
- capabilities: po.config["capabilities"] || [],
182
- current_session: current_session_hash(po),
183
- sessions: po.list_sessions.map do |s|
184
- {
185
- id: s[:id],
186
- name: s[:name],
187
- message_count: s[:message_count] || 0,
188
- updated_at: s[:updated_at]&.iso8601,
189
- # Thread fields
190
- parent_session_id: s[:parent_session_id],
191
- parent_po: s[:parent_po],
192
- thread_type: s[:thread_type] || "root"
193
- }
194
- end,
195
- prompt: po.body,
196
- config: po.config
197
- }
198
- end
199
-
200
- # Helper to get current session data for a PO.
201
- def self.current_session_hash(po)
202
- return nil unless po.session_id
203
-
204
- {
205
- id: po.session_id,
206
- messages: po.history.map { |m| message_to_hash(m) }
207
- }
208
- end
209
-
210
- # Helper to convert a message to JSON-serializable hash.
211
- def self.message_to_hash(msg)
212
- case msg[:role]
213
- when :user
214
- { role: "user", content: msg[:content], from: msg[:from] }
215
- when :assistant
216
- hash = { role: "assistant", content: msg[:content] }
217
- if msg[:tool_calls]
218
- hash[:tool_calls] = msg[:tool_calls].map do |tc|
219
- tc_id = tc.respond_to?(:id) ? tc.id : (tc[:id] || tc["id"])
220
- tc_name = tc.respond_to?(:name) ? tc.name : (tc[:name] || tc["name"])
221
- tc_args = tc.respond_to?(:arguments) ? tc.arguments : (tc[:arguments] || tc["arguments"] || {})
222
- { id: tc_id, name: tc_name, arguments: tc_args }
223
- end
224
- end
225
- hash
226
- when :tool
227
- { role: "tool", results: msg[:results] }
228
- else
229
- { role: msg[:role].to_s, content: msg[:content] }
230
- end
231
- end
232
176
  end
233
177
  end
@@ -9,7 +9,7 @@ module PromptObjects
9
9
  # SQLite-based session storage for conversation history.
10
10
  # Each environment has its own sessions.db file (gitignored for privacy).
11
11
  class Store
12
- SCHEMA_VERSION = 6
12
+ SCHEMA_VERSION = 7
13
13
 
14
14
  # Thread types for conversation branching
15
15
  THREAD_TYPES = %w[root continuation delegation fork].freeze
@@ -595,6 +595,137 @@ module PromptObjects
595
595
  row["count"]
596
596
  end
597
597
 
598
+ # --- Environment Data (Shared Key-Value Store) ---
599
+
600
+ # Resolve the root thread ID for a session by walking up the delegation chain.
601
+ # @param session_id [String] Any session ID in a delegation chain
602
+ # @return [String] The root thread's session ID
603
+ def resolve_root_thread(session_id)
604
+ lineage = get_thread_lineage(session_id)
605
+ return session_id if lineage.empty?
606
+
607
+ lineage.first[:id]
608
+ end
609
+
610
+ # Store a key-value pair scoped to a root thread.
611
+ # Uses INSERT OR REPLACE to create or overwrite.
612
+ # @param root_thread_id [String] Root thread scope
613
+ # @param key [String] Data key
614
+ # @param short_description [String] Brief description for discoverability
615
+ # @param value [Object] Data value (will be JSON-serialized)
616
+ # @param stored_by [String] PO name that stored this
617
+ def store_env_data(root_thread_id:, key:, short_description:, value:, stored_by:)
618
+ now = Time.now.utc.iso8601
619
+ json_value = JSON.generate(value)
620
+
621
+ @db.execute(<<~SQL, [root_thread_id, key, short_description, json_value, stored_by, now, now])
622
+ INSERT INTO env_data (root_thread_id, key, short_description, value, stored_by, created_at, updated_at)
623
+ VALUES (?, ?, ?, ?, ?, ?, ?)
624
+ ON CONFLICT(root_thread_id, key) DO UPDATE SET
625
+ short_description = excluded.short_description,
626
+ value = excluded.value,
627
+ stored_by = excluded.stored_by,
628
+ updated_at = excluded.updated_at
629
+ SQL
630
+ end
631
+
632
+ # Get a single env data entry by key.
633
+ # @param root_thread_id [String] Root thread scope
634
+ # @param key [String] Data key
635
+ # @return [Hash, nil] Entry with parsed value, or nil if not found
636
+ def get_env_data(root_thread_id:, key:)
637
+ row = @db.get_first_row(<<~SQL, [root_thread_id, key])
638
+ SELECT key, short_description, value, stored_by, created_at, updated_at
639
+ FROM env_data
640
+ WHERE root_thread_id = ? AND key = ?
641
+ SQL
642
+
643
+ return nil unless row
644
+
645
+ {
646
+ key: row["key"],
647
+ short_description: row["short_description"],
648
+ value: JSON.parse(row["value"], symbolize_names: true),
649
+ stored_by: row["stored_by"],
650
+ created_at: row["created_at"],
651
+ updated_at: row["updated_at"]
652
+ }
653
+ end
654
+
655
+ # List all env data keys and descriptions for a root thread (no values).
656
+ # @param root_thread_id [String] Root thread scope
657
+ # @return [Array<Hash>] Entries with key and short_description only
658
+ def list_env_data(root_thread_id:)
659
+ rows = @db.execute(<<~SQL, [root_thread_id])
660
+ SELECT key, short_description FROM env_data
661
+ WHERE root_thread_id = ?
662
+ ORDER BY key ASC
663
+ SQL
664
+
665
+ rows.map { |row| { key: row["key"], short_description: row["short_description"] } }
666
+ end
667
+
668
+ # List all env data entries with full values for a root thread.
669
+ # @param root_thread_id [String] Root thread scope
670
+ # @return [Array<Hash>] Full entries with parsed values
671
+ def list_env_data_full(root_thread_id:)
672
+ rows = @db.execute(<<~SQL, [root_thread_id])
673
+ SELECT key, short_description, value, stored_by, created_at, updated_at
674
+ FROM env_data WHERE root_thread_id = ? ORDER BY key ASC
675
+ SQL
676
+
677
+ rows.map do |row|
678
+ {
679
+ key: row["key"],
680
+ short_description: row["short_description"],
681
+ value: JSON.parse(row["value"]),
682
+ stored_by: row["stored_by"],
683
+ created_at: row["created_at"],
684
+ updated_at: row["updated_at"]
685
+ }
686
+ end
687
+ end
688
+
689
+ # Update an existing env data entry.
690
+ # @param root_thread_id [String] Root thread scope
691
+ # @param key [String] Data key
692
+ # @param short_description [String, nil] New description (keeps existing if nil)
693
+ # @param value [Object, nil] New value (keeps existing if nil)
694
+ # @param stored_by [String] PO name performing the update
695
+ # @return [Boolean] True if updated, false if key not found
696
+ def update_env_data(root_thread_id:, key:, short_description: nil, value: nil, stored_by:)
697
+ existing = get_env_data(root_thread_id: root_thread_id, key: key)
698
+ return false unless existing
699
+
700
+ updates = ["updated_at = ?", "stored_by = ?"]
701
+ params = [Time.now.utc.iso8601, stored_by]
702
+
703
+ if short_description
704
+ updates << "short_description = ?"
705
+ params << short_description
706
+ end
707
+
708
+ if value
709
+ updates << "value = ?"
710
+ params << JSON.generate(value)
711
+ end
712
+
713
+ params << root_thread_id
714
+ params << key
715
+
716
+ @db.execute("UPDATE env_data SET #{updates.join(', ')} WHERE root_thread_id = ? AND key = ?", params)
717
+ true
718
+ end
719
+
720
+ # Delete an env data entry.
721
+ # @param root_thread_id [String] Root thread scope
722
+ # @param key [String] Data key
723
+ # @return [Boolean] True if deleted, false if key not found
724
+ def delete_env_data(root_thread_id:, key:)
725
+ @db.execute("DELETE FROM env_data WHERE root_thread_id = ? AND key = ?", [root_thread_id, key])
726
+ @db.changes > 0
727
+ end
728
+
598
729
  # --- Usage Aggregation ---
599
730
 
600
731
  # Get total token usage for a session.
@@ -763,7 +894,7 @@ module PromptObjects
763
894
  tree = get_thread_tree(session_id)
764
895
  return nil unless tree
765
896
 
766
- serialize_tree_for_export(tree)
897
+ serialize_tree_for_export(tree, is_root: true)
767
898
  end
768
899
 
769
900
  # --- Import ---
@@ -908,11 +1039,11 @@ module PromptObjects
908
1039
  end
909
1040
  end
910
1041
 
911
- def serialize_tree_for_export(node)
1042
+ def serialize_tree_for_export(node, is_root: false)
912
1043
  session = node[:session]
913
1044
  messages = get_messages(session[:id])
914
1045
 
915
- {
1046
+ result = {
916
1047
  session: {
917
1048
  id: session[:id],
918
1049
  po_name: session[:po_name],
@@ -934,6 +1065,13 @@ module PromptObjects
934
1065
  },
935
1066
  children: (node[:children] || []).map { |c| serialize_tree_for_export(c) }
936
1067
  }
1068
+
1069
+ if is_root
1070
+ env_data = list_env_data_full(root_thread_id: session[:id])
1071
+ result[:env_data] = env_data unless env_data.empty?
1072
+ end
1073
+
1074
+ result
937
1075
  end
938
1076
 
939
1077
  def setup_schema
@@ -1030,6 +1168,21 @@ module PromptObjects
1030
1168
 
1031
1169
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
1032
1170
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
1171
+
1172
+ -- Shared environment data for delegation chains (v7)
1173
+ CREATE TABLE IF NOT EXISTS env_data (
1174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1175
+ root_thread_id TEXT NOT NULL,
1176
+ key TEXT NOT NULL,
1177
+ short_description TEXT NOT NULL,
1178
+ value TEXT NOT NULL,
1179
+ stored_by TEXT NOT NULL,
1180
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1181
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
1182
+ UNIQUE(root_thread_id, key)
1183
+ );
1184
+
1185
+ CREATE INDEX IF NOT EXISTS idx_env_data_root ON env_data(root_thread_id);
1033
1186
  SQL
1034
1187
  end
1035
1188
 
@@ -1107,6 +1260,25 @@ module PromptObjects
1107
1260
  # Add usage column for token tracking
1108
1261
  @db.execute("ALTER TABLE messages ADD COLUMN usage TEXT")
1109
1262
  end
1263
+
1264
+ if from_version < 7
1265
+ # Add shared environment data table for delegation chains
1266
+ @db.execute_batch(<<~SQL)
1267
+ CREATE TABLE IF NOT EXISTS env_data (
1268
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1269
+ root_thread_id TEXT NOT NULL,
1270
+ key TEXT NOT NULL,
1271
+ short_description TEXT NOT NULL,
1272
+ value TEXT NOT NULL,
1273
+ stored_by TEXT NOT NULL,
1274
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1275
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
1276
+ UNIQUE(root_thread_id, key)
1277
+ );
1278
+
1279
+ CREATE INDEX IF NOT EXISTS idx_env_data_root ON env_data(root_thread_id);
1280
+ SQL
1281
+ end
1110
1282
  end
1111
1283
 
1112
1284
  def empty_usage