little_ghost 0.7.0 → 0.9.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 (270) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -6
  3. data/docs/guides/assemblies.md +21 -9
  4. data/docs/guides/code_mode.md +2 -2
  5. data/docs/guides/getting_started.md +27 -2
  6. data/docs/guides/integrations.md +20 -161
  7. data/docs/guides/mcp.md +288 -0
  8. data/docs/guides/models_and_providers.md +21 -54
  9. data/docs/guides/production.md +14 -5
  10. data/docs/guides/prompt_views.md +234 -27
  11. data/docs/guides/providers.md +275 -0
  12. data/docs/guides/sandboxing.md +42 -7
  13. data/docs/guides/skills.md +1 -3
  14. data/docs/guides/tools.md +2 -0
  15. data/exe/little_ghost +12 -0
  16. data/lib/little_ghost/agent/context_management.rb +2 -13
  17. data/lib/little_ghost/agent/delegation.rb +7 -4
  18. data/lib/little_ghost/agent/skills.rb +8 -6
  19. data/lib/little_ghost/agent/tool_loop.rb +4 -4
  20. data/lib/little_ghost/agent.rb +230 -70
  21. data/lib/little_ghost/agent_builder.rb +16 -4
  22. data/lib/little_ghost/artifacts/workspace_store.rb +16 -14
  23. data/lib/little_ghost/assembly.rb +48 -7
  24. data/lib/little_ghost/assembly_builder.rb +3 -3
  25. data/lib/little_ghost/cli.rb +429 -0
  26. data/lib/little_ghost/code_mode/broker.rb +11 -3
  27. data/lib/little_ghost/code_mode/javascript/client.rb +58 -11
  28. data/lib/little_ghost/code_mode/javascript/host.rb +37 -13
  29. data/lib/little_ghost/code_mode/javascript/session.rb +21 -9
  30. data/lib/little_ghost/code_mode/javascript_engine.rb +21 -42
  31. data/lib/little_ghost/code_mode/ruby/host.rb +8 -4
  32. data/lib/little_ghost/code_mode/ruby/session.rb +41 -14
  33. data/lib/little_ghost/code_mode/ruby_engine.rb +6 -36
  34. data/lib/little_ghost/code_mode/runtime.rb +57 -24
  35. data/lib/little_ghost/console.rb +17 -0
  36. data/lib/little_ghost/errors.rb +20 -2
  37. data/lib/little_ghost/framework_prompts.rb +376 -0
  38. data/lib/little_ghost/generators/application_generator.rb +179 -0
  39. data/lib/little_ghost/generators/templates/application/Gemfile.tt +3 -0
  40. data/lib/little_ghost/generators/templates/application/README.md.tt +29 -0
  41. data/lib/little_ghost/generators/templates/application/app/agents/application_agent.rb.tt +5 -0
  42. data/lib/little_ghost/generators/templates/application/app/prompts/application/system_prompt.erb.tt +1 -0
  43. data/lib/little_ghost/generators/templates/application/bin/application.tt +10 -0
  44. data/lib/little_ghost/generators/templates/application/config/little_ghost.rb.tt +5 -0
  45. data/lib/little_ghost/generators/templates/application/gitignore.tt +2 -0
  46. data/lib/little_ghost/generators/templates/application/keep.tt +0 -0
  47. data/lib/little_ghost/graph.rb +24 -5
  48. data/lib/little_ghost/mcp/client.rb +307 -610
  49. data/lib/little_ghost/mcp/toolset.rb +133 -95
  50. data/lib/little_ghost/mcp.rb +21 -3
  51. data/lib/little_ghost/model_operations.rb +103 -36
  52. data/lib/little_ghost/model_resolver.rb +2 -0
  53. data/lib/little_ghost/models/catalog/models_dev_source.rb +4 -1
  54. data/lib/little_ghost/prompt_resolver.rb +61 -27
  55. data/lib/little_ghost/prompts/little_ghost/agent/interjections/instructions.erb +12 -0
  56. data/lib/little_ghost/prompts/little_ghost/agent/system/default.erb +10 -0
  57. data/lib/little_ghost/prompts/little_ghost/artifacts/batch/feedback/batch_too_large.erb +10 -0
  58. data/lib/little_ghost/prompts/little_ghost/artifacts/batch/feedback/batch_too_many.erb +10 -0
  59. data/lib/little_ghost/prompts/little_ghost/artifacts/batch/feedback/item_too_large.erb +10 -0
  60. data/lib/little_ghost/prompts/little_ghost/artifacts/errors/preparation_failed.erb +10 -0
  61. data/lib/little_ghost/prompts/little_ghost/artifacts/errors/resolver_invalid.erb +10 -0
  62. data/lib/little_ghost/prompts/little_ghost/artifacts/errors/value_unstorable.erb +10 -0
  63. data/lib/little_ghost/prompts/little_ghost/artifacts/presentation/format/reference.erb +13 -0
  64. data/lib/little_ghost/prompts/little_ghost/artifacts/presentation/format/references.erb +12 -0
  65. data/lib/little_ghost/prompts/little_ghost/artifacts/presentation/format/workspace_references.erb +12 -0
  66. data/lib/little_ghost/prompts/little_ghost/artifacts/presentation/notices/full_result.erb +14 -0
  67. data/lib/little_ghost/prompts/little_ghost/artifacts/presentation/notices/storage_failed.erb +13 -0
  68. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/errors/destination_unsafe.erb +10 -0
  69. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/errors/secure_unavailable.erb +10 -0
  70. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/errors/workspace_changed.erb +10 -0
  71. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/errors/workspace_unavailable.erb +10 -0
  72. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/errors/workspace_unsafe.erb +10 -0
  73. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/feedback/count_limit.erb +10 -0
  74. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/feedback/destination_exists.erb +10 -0
  75. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/feedback/too_large.erb +10 -0
  76. data/lib/little_ghost/prompts/little_ghost/artifacts/storage/feedback/total_limit.erb +10 -0
  77. data/lib/little_ghost/prompts/little_ghost/assembly/feedback/transition_only.erb +10 -0
  78. data/lib/little_ghost/prompts/little_ghost/assembly/tools/fallback/description.erb +10 -0
  79. data/lib/little_ghost/prompts/little_ghost/code_mode/artifacts/format/references.erb +12 -0
  80. data/lib/little_ghost/prompts/little_ghost/code_mode/errors/brokered_close.erb +10 -0
  81. data/lib/little_ghost/prompts/little_ghost/code_mode/errors/cleanup_failed.erb +10 -0
  82. data/lib/little_ghost/prompts/little_ghost/code_mode/errors/execution_failed.erb +11 -0
  83. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/arguments_object.erb +10 -0
  84. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/closed.erb +10 -0
  85. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/control_active.erb +10 -0
  86. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/limit_exceeded.erb +10 -0
  87. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/not_active.erb +10 -0
  88. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/program_active.erb +10 -0
  89. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/program_timed_out.erb +10 -0
  90. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/unavailable_tool.erb +10 -0
  91. data/lib/little_ghost/prompts/little_ghost/code_mode/feedback/unknown_program.erb +10 -0
  92. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/active_programs_limit.erb +10 -0
  93. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/cleanup_failed.erb +10 -0
  94. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/execution_limit.erb +10 -0
  95. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/invalid_request.erb +10 -0
  96. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/memory_limit.erb +10 -0
  97. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/pending_calls_limit.erb +10 -0
  98. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/source_size_limit.erb +10 -0
  99. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/errors/tools_array.erb +10 -0
  100. data/lib/little_ghost/prompts/little_ghost/code_mode/javascript/instructions.erb +35 -0
  101. data/lib/little_ghost/prompts/little_ghost/code_mode/ruby/feedback/parallel_callables.erb +10 -0
  102. data/lib/little_ghost/prompts/little_ghost/code_mode/ruby/instructions.erb +40 -0
  103. data/lib/little_ghost/prompts/little_ghost/code_mode/tools/exec/description.erb +10 -0
  104. data/lib/little_ghost/prompts/little_ghost/code_mode/tools/stop/description.erb +10 -0
  105. data/lib/little_ghost/prompts/little_ghost/code_mode/tools/stop/inputs/max_output_tokens/description.erb +10 -0
  106. data/lib/little_ghost/prompts/little_ghost/code_mode/tools/wait/description.erb +10 -0
  107. data/lib/little_ghost/prompts/little_ghost/code_mode/tools/wait/inputs/max_output_tokens/description.erb +10 -0
  108. data/lib/little_ghost/prompts/little_ghost/context_management/summary/request.erb +10 -0
  109. data/lib/little_ghost/prompts/little_ghost/context_management/summary/system_instruction.erb +16 -0
  110. data/lib/little_ghost/prompts/little_ghost/graph/context/format/failed_input.erb +11 -0
  111. data/lib/little_ghost/prompts/little_ghost/graph/context/format/previous_input.erb +13 -0
  112. data/lib/little_ghost/prompts/little_ghost/graph/context/headings/original_task.erb +10 -0
  113. data/lib/little_ghost/prompts/little_ghost/graph/context/headings/previous_inputs.erb +10 -0
  114. data/lib/little_ghost/prompts/little_ghost/mcp/errors/request_failed.erb +10 -0
  115. data/lib/little_ghost/prompts/little_ghost/mcp/errors/transformation_unserializable.erb +10 -0
  116. data/lib/little_ghost/prompts/little_ghost/mcp/tools/fallback/description.erb +10 -0
  117. data/lib/little_ghost/prompts/little_ghost/output/truncation/marker.erb +11 -0
  118. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/errors/mount_changed.erb +10 -0
  119. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/errors/secure_traversal_unavailable.erb +10 -0
  120. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/errors/workspace_root_changed.erb +10 -0
  121. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/directory_listing_limit.erb +10 -0
  122. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/entry_required.erb +10 -0
  123. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/file_read_limit.erb +10 -0
  124. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/invalid_utf8.erb +10 -0
  125. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/mount_escape.erb +10 -0
  126. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/mount_missing.erb +10 -0
  127. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/multiply_linked.erb +10 -0
  128. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/not_directory.erb +10 -0
  129. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/not_file.erb +10 -0
  130. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/null_byte.erb +10 -0
  131. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/outside_scope.erb +10 -0
  132. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/parent_escape.erb +10 -0
  133. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/path_missing.erb +10 -0
  134. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/path_traverses_symlink.erb +10 -0
  135. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/read_only.erb +10 -0
  136. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/replace_empty.erb +10 -0
  137. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/symlink_path.erb +10 -0
  138. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/text_multiple.erb +10 -0
  139. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/text_not_found.erb +10 -0
  140. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/write_limit.erb +10 -0
  141. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/write_parent_missing.erb +10 -0
  142. data/lib/little_ghost/prompts/little_ghost/sandbox/filesystem/feedback/write_target_symlink.erb +10 -0
  143. data/lib/little_ghost/prompts/little_ghost/sandbox/policy/feedback/feature_denied.erb +10 -0
  144. data/lib/little_ghost/prompts/little_ghost/sandbox/process/errors/program_supervisor_failed.erb +10 -0
  145. data/lib/little_ghost/prompts/little_ghost/sandbox/process/feedback/command_timed_out.erb +10 -0
  146. data/lib/little_ghost/prompts/little_ghost/sandbox/process/feedback/executable_required.erb +10 -0
  147. data/lib/little_ghost/prompts/little_ghost/sandbox/process/feedback/program_memory_exceeded.erb +10 -0
  148. data/lib/little_ghost/prompts/little_ghost/sandbox/process/feedback/program_output_exceeded.erb +10 -0
  149. data/lib/little_ghost/prompts/little_ghost/sandbox/process/feedback/program_timed_out.erb +10 -0
  150. data/lib/little_ghost/prompts/little_ghost/skills/discovery/instructions.erb +18 -0
  151. data/lib/little_ghost/prompts/little_ghost/skills/feedback/unknown.erb +11 -0
  152. data/lib/little_ghost/prompts/little_ghost/skills/resources/notices/truncated.erb +10 -0
  153. data/lib/little_ghost/prompts/little_ghost/skills/tools/activate/content.erb +28 -0
  154. data/lib/little_ghost/prompts/little_ghost/skills/tools/activate/description.erb +10 -0
  155. data/lib/little_ghost/prompts/little_ghost/skills/tools/activate/inputs/name/description.erb +10 -0
  156. data/lib/little_ghost/prompts/little_ghost/structured_output/persistence/format/redaction.erb +12 -0
  157. data/lib/little_ghost/prompts/little_ghost/structured_output/repair/feedback/invalid_result.erb +11 -0
  158. data/lib/little_ghost/prompts/little_ghost/structured_output/repair/feedback/schema_errors.erb +11 -0
  159. data/lib/little_ghost/prompts/little_ghost/structured_output/repair/request.erb +14 -0
  160. data/lib/little_ghost/prompts/little_ghost/structured_output/tools/result/description.erb +11 -0
  161. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/invalid_json.erb +11 -0
  162. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/missing_tool.erb +11 -0
  163. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/multiple_tools.erb +11 -0
  164. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/too_complex.erb +11 -0
  165. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/too_deep.erb +11 -0
  166. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/too_large.erb +11 -0
  167. data/lib/little_ghost/prompts/little_ghost/structured_output/validation/feedback/tool_not_exclusive.erb +11 -0
  168. data/lib/little_ghost/prompts/little_ghost/subagents/errors/create_failed.erb +10 -0
  169. data/lib/little_ghost/prompts/little_ghost/subagents/errors/previous_turn_failed.erb +10 -0
  170. data/lib/little_ghost/prompts/little_ghost/subagents/errors/restore_failed.erb +10 -0
  171. data/lib/little_ghost/prompts/little_ghost/subagents/errors/turn_failed.erb +10 -0
  172. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/duplicate_ids.erb +10 -0
  173. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/duplicate_path.erb +10 -0
  174. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/identity_capacity.erb +10 -0
  175. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/inactive.erb +10 -0
  176. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/interjection_chars_limit.erb +10 -0
  177. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/interjection_limit.erb +10 -0
  178. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/interjection_unsupported.erb +10 -0
  179. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/invalid_cursor.erb +10 -0
  180. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/invalid_mode.erb +10 -0
  181. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/list_limit.erb +10 -0
  182. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/message_limit.erb +10 -0
  183. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/message_type.erb +10 -0
  184. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/not_running.erb +10 -0
  185. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/queue_capacity.erb +10 -0
  186. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/task_name_invalid.erb +10 -0
  187. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/task_name_limit.erb +10 -0
  188. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/terminal_identity.erb +11 -0
  189. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/turn_capacity.erb +10 -0
  190. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/unknown_id.erb +10 -0
  191. data/lib/little_ghost/prompts/little_ghost/subagents/feedback/unknown_kind.erb +10 -0
  192. data/lib/little_ghost/prompts/little_ghost/subagents/notices/turn_cancelled.erb +10 -0
  193. data/lib/little_ghost/prompts/little_ghost/subagents/tools/interject/description.erb +10 -0
  194. data/lib/little_ghost/prompts/little_ghost/subagents/tools/interject/inputs/active_id/description.erb +10 -0
  195. data/lib/little_ghost/prompts/little_ghost/subagents/tools/interject/inputs/message/description.erb +10 -0
  196. data/lib/little_ghost/prompts/little_ghost/subagents/tools/list/description.erb +10 -0
  197. data/lib/little_ghost/prompts/little_ghost/subagents/tools/send/description.erb +10 -0
  198. data/lib/little_ghost/prompts/little_ghost/subagents/tools/send/inputs/id/description.erb +10 -0
  199. data/lib/little_ghost/prompts/little_ghost/subagents/tools/send/inputs/message/description.erb +10 -0
  200. data/lib/little_ghost/prompts/little_ghost/subagents/tools/send/inputs/send_mode/description.erb +10 -0
  201. data/lib/little_ghost/prompts/little_ghost/subagents/tools/spawn/description.erb +10 -0
  202. data/lib/little_ghost/prompts/little_ghost/subagents/tools/spawn/inputs/kind/description.erb +11 -0
  203. data/lib/little_ghost/prompts/little_ghost/subagents/tools/spawn/inputs/spawn_mode/description.erb +10 -0
  204. data/lib/little_ghost/prompts/little_ghost/subagents/tools/spawn/inputs/task/description.erb +10 -0
  205. data/lib/little_ghost/prompts/little_ghost/subagents/tools/spawn/inputs/task_name/description.erb +10 -0
  206. data/lib/little_ghost/prompts/little_ghost/subagents/tools/wait/description.erb +10 -0
  207. data/lib/little_ghost/prompts/little_ghost/subagents/tools/wait/inputs/ids/description.erb +10 -0
  208. data/lib/little_ghost/prompts/little_ghost/swarm/handoff/notice.erb +10 -0
  209. data/lib/little_ghost/prompts/little_ghost/swarm/handoff/request.erb +16 -0
  210. data/lib/little_ghost/prompts/little_ghost/swarm/tools/handoff/description.erb +10 -0
  211. data/lib/little_ghost/prompts/little_ghost/tools/built_in/list_files/description.erb +10 -0
  212. data/lib/little_ghost/prompts/little_ghost/tools/built_in/read_file/description.erb +10 -0
  213. data/lib/little_ghost/prompts/little_ghost/tools/built_in/replace_in_file/description.erb +10 -0
  214. data/lib/little_ghost/prompts/little_ghost/tools/built_in/shell/description.erb +10 -0
  215. data/lib/little_ghost/prompts/little_ghost/tools/built_in/write_file/description.erb +10 -0
  216. data/lib/little_ghost/prompts/little_ghost/tools/built_in/write_todos/description.erb +10 -0
  217. data/lib/little_ghost/prompts/little_ghost/tools/built_in/write_todos/feedback/single_progress.erb +10 -0
  218. data/lib/little_ghost/prompts/little_ghost/tools/built_in/write_todos/feedback/unique_ids.erb +10 -0
  219. data/lib/little_ghost/prompts/little_ghost/tools/errors/unexpected_failure.erb +10 -0
  220. data/lib/little_ghost/prompts/little_ghost/tools/errors/unserializable_result.erb +10 -0
  221. data/lib/little_ghost/prompts/little_ghost/tools/feedback/invalid_input.erb +10 -0
  222. data/lib/little_ghost/prompts/little_ghost/tools/feedback/unknown.erb +10 -0
  223. data/lib/little_ghost/prompts/little_ghost/tools/loop/notices/final_warning.erb +10 -0
  224. data/lib/little_ghost/prompts/little_ghost/tools/loop/notices/termination.erb +10 -0
  225. data/lib/little_ghost/prompts/little_ghost/tools/loop/notices/warning.erb +10 -0
  226. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/additional_property.erb +10 -0
  227. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/enum.erb +11 -0
  228. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/invalid_format.erb +10 -0
  229. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/invalid_pattern.erb +10 -0
  230. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/max_items.erb +11 -0
  231. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/max_length.erb +11 -0
  232. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/maximum.erb +11 -0
  233. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/min_items.erb +11 -0
  234. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/min_length.erb +11 -0
  235. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/minimum.erb +11 -0
  236. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/required.erb +10 -0
  237. data/lib/little_ghost/prompts/little_ghost/tools/validation/schema/type.erb +11 -0
  238. data/lib/little_ghost/prompts/little_ghost/workspace/feedback/path_changed.erb +10 -0
  239. data/lib/little_ghost/provider_registry.rb +1 -0
  240. data/lib/little_ghost/providers/lm_studio/catalog_source.rb +201 -0
  241. data/lib/little_ghost/providers/lm_studio.rb +52 -0
  242. data/lib/little_ghost/providers/openai.rb +3 -75
  243. data/lib/little_ghost/providers/openai_compatible.rb +78 -1
  244. data/lib/little_ghost/runtime/hooks/artifacts.rb +58 -22
  245. data/lib/little_ghost/runtime.rb +8 -3
  246. data/lib/little_ghost/sandbox/environment_policy.rb +29 -4
  247. data/lib/little_ghost/sandbox/filesystem.rb +51 -33
  248. data/lib/little_ghost/sandbox/policy.rb +28 -6
  249. data/lib/little_ghost/sandbox/process_runner.rb +6 -2
  250. data/lib/little_ghost/sandbox/process_session.rb +8 -4
  251. data/lib/little_ghost/sandbox/scope.rb +4 -1
  252. data/lib/little_ghost/sandboxes/seatbelt.rb +3 -1
  253. data/lib/little_ghost/sandboxes/unrestricted.rb +17 -9
  254. data/lib/little_ghost/skills/catalog.rb +41 -36
  255. data/lib/little_ghost/structured_output.rb +6 -6
  256. data/lib/little_ghost/subagents/control_tool.rb +58 -0
  257. data/lib/little_ghost/subagents/manager.rb +99 -74
  258. data/lib/little_ghost/support/output_truncation.rb +28 -4
  259. data/lib/little_ghost/swarm.rb +20 -7
  260. data/lib/little_ghost/tool.rb +47 -20
  261. data/lib/little_ghost/tool_registry.rb +2 -2
  262. data/lib/little_ghost/tools/filesystem.rb +12 -4
  263. data/lib/little_ghost/tools/shell.rb +5 -1
  264. data/lib/little_ghost/tools/write_todos.rb +9 -9
  265. data/lib/little_ghost/version.rb +1 -1
  266. data/lib/little_ghost/workflow.rb +199 -27
  267. data/lib/little_ghost/workspace.rb +2 -2
  268. data/lib/little_ghost.rb +6 -2
  269. metadata +222 -7
  270. data/lib/little_ghost/mcp/types.rb +0 -216
@@ -16,6 +16,18 @@ module LittleGhost
16
16
  JAVASCRIPT_TIMEOUT_MS = 10_000
17
17
  MAX_PENDING_TOOL_CALLS = 1_024
18
18
  Terminated = Class.new(StandardError)
19
+ DEFAULT_MESSAGES = {
20
+ "execution_limit" => "JavaScript execution exceeded its limit.",
21
+ "memory_limit" => "JavaScript execution exceeded its memory limit.",
22
+ "cleanup_failed" => "JavaScript context cleanup failed.",
23
+ "pending_calls_limit" => "The code-mode program exceeded the pending Tool-call limit.",
24
+ "active_programs_limit" => "The code-mode host has too many active programs.",
25
+ "invalid_request" => "Invalid code-mode request: __DETAIL__",
26
+ "unavailable_tool" => "Unavailable code-mode Tool: __TOOL_NAME__",
27
+ "execution_failed" => "__ERROR_CLASS__: __ERROR_MESSAGE__",
28
+ "source_size_limit" => "Code-mode source exceeds the size limit.",
29
+ "tools_array" => "Code-mode tools must be an array."
30
+ }.freeze
19
31
 
20
32
  BOOTSTRAP = <<~'JAVASCRIPT'
21
33
  const __LITTLE_GHOST_CONTROL_IDENTIFIER__ = (() => {
@@ -26,6 +38,7 @@ module LittleGhost
26
38
  const outputs = [];
27
39
  const pending = new Map();
28
40
  const exitSignal = Object.freeze({exit: true});
41
+ const unavailableToolMessage = __LITTLE_GHOST_UNAVAILABLE_TOOL_MESSAGE__;
29
42
  let nextCallId = 0;
30
43
  let done = false;
31
44
  let failure = null;
@@ -40,7 +53,7 @@ module LittleGhost
40
53
 
41
54
  const enqueue = (name, args) => new Promise((resolve, reject) => {
42
55
  if (!Object.prototype.hasOwnProperty.call(definitionIndex, name)) {
43
- reject(new Error(`Unknown tool: ${name}`));
56
+ reject(new Error(unavailableToolMessage.split("__TOOL_NAME__").join(name)));
44
57
  return;
45
58
  }
46
59
  const id = String(++nextCallId);
@@ -129,10 +142,11 @@ module LittleGhost
129
142
  JAVASCRIPT
130
143
 
131
144
  class Program
132
- def initialize(id:, source:, tools:, writer:, finished:)
145
+ def initialize(id:, source:, tools:, writer:, finished:, messages: nil)
133
146
  @id = id
134
147
  @source = source
135
148
  @tools = tools
149
+ @messages = DEFAULT_MESSAGES.merge(messages || {}).freeze
136
150
  @writer = writer
137
151
  @finished = finished
138
152
  @incoming = Queue.new
@@ -175,7 +189,11 @@ module LittleGhost
175
189
  context.eval(
176
190
  BOOTSTRAP
177
191
  .gsub("__LITTLE_GHOST_CONTROL_IDENTIFIER__", @control_identifier)
178
- .sub("__LITTLE_GHOST_TOOL_DEFINITIONS__", definitions),
192
+ .sub("__LITTLE_GHOST_TOOL_DEFINITIONS__", definitions)
193
+ .sub(
194
+ "__LITTLE_GHOST_UNAVAILABLE_TOOL_MESSAGE__",
195
+ JSON.generate(@messages.fetch("unavailable_tool"))
196
+ ),
179
197
  filename: "little-ghost-code-mode-bootstrap.js"
180
198
  )
181
199
  call_control(context, :run, @source)
@@ -186,15 +204,18 @@ module LittleGhost
186
204
  terminal = if @context_mutex.synchronize { @terminating }
187
205
  {type: "terminated"}
188
206
  else
189
- {type: "failed", error: "JavaScript execution exceeded its limit", fatal: true}
207
+ {type: "failed", error: @messages.fetch("execution_limit"), fatal: true}
190
208
  end
191
209
  rescue MiniRacer::V8OutOfMemoryError
192
- terminal = {type: "failed", error: "JavaScript execution exceeded its memory limit", fatal: true}
210
+ terminal = {type: "failed", error: @messages.fetch("memory_limit"), fatal: true}
193
211
  rescue => error
194
- terminal = {type: "failed", error: "#{error.class}: #{error.message}", fatal: true}
212
+ failure = @messages.fetch("execution_failed")
213
+ .gsub("__ERROR_CLASS__") { error.class.to_s }
214
+ .gsub("__ERROR_MESSAGE__") { error.message }
215
+ terminal = {type: "failed", error: failure, fatal: true}
195
216
  ensure
196
217
  cleanup_error = dispose_context
197
- terminal = {type: "failed", error: "JavaScript context cleanup failed", fatal: true} if cleanup_error
218
+ terminal = {type: "failed", error: @messages.fetch("cleanup_failed"), fatal: true} if cleanup_error
198
219
  begin
199
220
  emit(**terminal) if terminal
200
221
  ensure
@@ -210,7 +231,7 @@ module LittleGhost
210
231
  if calls.length > MAX_PENDING_TOOL_CALLS
211
232
  return {
212
233
  type: "failed",
213
- error: "Code-mode program exceeded the pending tool-call limit",
234
+ error: @messages.fetch("pending_calls_limit"),
214
235
  fatal: true
215
236
  }
216
237
  end
@@ -308,23 +329,26 @@ module LittleGhost
308
329
  id = message.fetch("program_id").to_s
309
330
  source = message.fetch("source").to_s
310
331
  tools = message.fetch("tools")
311
- raise Protocol::Error, "Code-mode source exceeds the size limit" if source.bytesize > MAX_SOURCE_BYTES
312
- raise Protocol::Error, "Code-mode tools must be an array" unless tools.is_a?(Array)
332
+ messages = DEFAULT_MESSAGES.merge(message["messages"] || {})
333
+ raise Protocol::Error, messages.fetch("source_size_limit") if source.bytesize > MAX_SOURCE_BYTES
334
+ raise Protocol::Error, messages.fetch("tools_array") unless tools.is_a?(Array)
313
335
 
314
336
  created = @programs_mutex.synchronize do
315
337
  next false if @programs.key?(id) || @programs.length >= MAX_ACTIVE_PROGRAMS
316
338
 
317
339
  @programs[id] = Program.new(
318
- id:, source:, tools:, writer: method(:write),
340
+ id:, source:, tools:, messages:, writer: method(:write),
319
341
  finished: ->(program_id) { @programs_mutex.synchronize { @programs.delete(program_id) } }
320
342
  )
321
343
  true
322
344
  end
323
345
  unless created
324
- write(type: "failed", program_id: id, error: "Code-mode host has too many active programs", fatal: true)
346
+ write(type: "failed", program_id: id, error: messages.fetch("active_programs_limit"), fatal: true)
325
347
  end
326
348
  rescue KeyError, TypeError, Protocol::Error => error
327
- write(type: "failed", program_id: id.to_s, error: "Invalid code-mode request: #{error.message}", fatal: true)
349
+ template = messages.fetch("invalid_request")
350
+ detail = error.message
351
+ write(type: "failed", program_id: id.to_s, error: template.gsub("__DETAIL__") { detail }, fatal: true)
328
352
  end
329
353
 
330
354
  def program(id)
@@ -17,12 +17,14 @@ module LittleGhost
17
17
  Deadline = Struct.new(:task, :cancelled, :expiring, :finished, :error)
18
18
 
19
19
  def initialize(broker:, client:, sandbox: nil, workspace: nil, max_concurrency: 8,
20
- wall_seconds: 3_600, observation_seconds: OBSERVATION_SECONDS, cleanup_timeout: CLEANUP_TIMEOUT)
20
+ wall_seconds: 3_600, observation_seconds: OBSERVATION_SECONDS, cleanup_timeout: CLEANUP_TIMEOUT,
21
+ framework_prompt_scope: {})
21
22
  @broker = broker
22
23
  @task_runner = broker.task_runner
23
24
  @client = client
24
25
  @sandbox = sandbox
25
26
  @workspace = workspace
27
+ @framework_prompt_scope = framework_prompt_scope.freeze
26
28
  @max_concurrency = Integer(max_concurrency)
27
29
  raise ArgumentError, "max_concurrency must be positive" unless @max_concurrency.positive?
28
30
  @wall_seconds = Float(wall_seconds)
@@ -119,6 +121,16 @@ module LittleGhost
119
121
 
120
122
  private
121
123
 
124
+ def prompt(key, **locals)
125
+ framework_prompts = @framework_prompt_scope[:framework_prompts] || FrameworkPrompts.new
126
+ framework_prompts.render(
127
+ key,
128
+ locals:,
129
+ invocation_paths: @framework_prompt_scope.fetch(:invocation_paths, []),
130
+ agent_path: @framework_prompt_scope[:agent_path]
131
+ )
132
+ end
133
+
122
134
  def execute_program(source:, catalog:, frame:, max_output_tokens:, context:)
123
135
  ensure_open
124
136
  raise_pending_deadline_error!
@@ -126,7 +138,7 @@ module LittleGhost
126
138
  program_id = SecureRandom.uuid
127
139
  @frames_mutex.synchronize do
128
140
  if @current_program_id
129
- raise LittleGhost::ToolError, "Wait for or stop the active JavaScript program before starting another"
141
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/program_active")
130
142
  end
131
143
  @frames[program_id] = javascript_catalog
132
144
  @current_program_id = program_id
@@ -152,7 +164,7 @@ module LittleGhost
152
164
  ensure_open
153
165
  program_id = @frames_mutex.synchronize { @current_program_id }
154
166
  raise_pending_deadline_error! unless program_id
155
- raise LittleGhost::ToolError, "There is no active JavaScript program" unless program_id
167
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/not_active") unless program_id
156
168
  result = @client.observe(
157
169
  owner: self, program_id:, timeout: @observation_seconds,
158
170
  max_tokens: output_tokens(max_output_tokens), context:
@@ -175,7 +187,7 @@ module LittleGhost
175
187
  context&.check!
176
188
  program_id = @frames_mutex.synchronize { @current_program_id }
177
189
  raise_pending_deadline_error! unless program_id
178
- raise LittleGhost::ToolError, "There is no active JavaScript program" unless program_id
190
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/not_active") unless program_id
179
191
 
180
192
  finish(program_id, terminate_program(program_id, max_output_tokens:))
181
193
  rescue LittleGhost::CleanupError
@@ -196,7 +208,7 @@ module LittleGhost
196
208
  )
197
209
  end
198
210
  @mutex.synchronize do
199
- raise LittleGhost::ToolError, "Code-mode session is closed" if @closed
211
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/closed", resource: "session") if @closed
200
212
 
201
213
  ensure_worker
202
214
  register_dispatch(batch)
@@ -213,7 +225,7 @@ module LittleGhost
213
225
  def with_control
214
226
  acquired = @control_mutex.try_lock
215
227
  unless acquired
216
- raise LittleGhost::ToolError, "another code-mode control operation is already active"
228
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/control_active")
217
229
  end
218
230
 
219
231
  yield
@@ -441,8 +453,8 @@ module LittleGhost
441
453
 
442
454
  def ensure_open
443
455
  @mutex.synchronize do
444
- raise LittleGhost::ToolError, "Code-mode session is closed" if @closed
445
- raise LittleGhost::ToolError, "Code-mode session cannot be reused after cleanup failed" if @poisoned
456
+ raise LittleGhost::ToolError, prompt("code_mode/feedback/closed", resource: "session") if @closed
457
+ raise LittleGhost::ToolError, prompt("code_mode/errors/cleanup_failed") if @poisoned
446
458
  end
447
459
  end
448
460
 
@@ -482,7 +494,7 @@ module LittleGhost
482
494
  end
483
495
  return unless current
484
496
 
485
- error = LittleGhost::ToolError.new("code-mode program timed out")
497
+ error = LittleGhost::ToolError.new(FrameworkPrompts.reference("code_mode/feedback/program_timed_out"))
486
498
  begin
487
499
  terminate_program(program_id)
488
500
  rescue => cleanup_error
@@ -50,40 +50,15 @@ module LittleGhost
50
50
 
51
51
  # Builds JavaScript usage instructions and TypeScript declarations for
52
52
  # +catalog+.
53
- def instructions(catalog:)
53
+ def instructions(catalog:, prompts: nil)
54
54
  javascript_catalog = Javascript::Catalog.new(catalog)
55
- <<~INSTRUCTIONS.strip
56
- Use JavaScript to call the available tools and compose their results.
57
-
58
- Program lifecycle:
59
- - Every exec call starts a fresh program in a new V8 context.
60
- - Exec and wait observe it for up to one minute. They return sooner when it finishes.
61
- - If exec or wait returns `still_working`, call wait to observe the same running program again.
62
- - Wait does not pause, resume, or restart the program.
63
- - Use stop when the result is no longer needed.
64
- - Do not call wait or stop after `completed`, `error`, or `terminated`.
65
-
66
- Tool calls:
67
- - Tool functions return Promises. Use await or Promise.all.
68
- - JSON results become objects or arrays. Other results remain strings.
69
- - Unawaited tool calls finish before the program exits.
70
- - `ALL_TOOLS` contains the complete runtime catalog.
71
-
72
- Output and completion:
73
- - Use `text(value)` for user-visible output.
74
- - Use `exit()` to complete early.
75
-
76
- The V8 context has no Node.js APIs, filesystem, network, console, WebAssembly, or process API.
77
-
78
- Available tool methods:
79
-
80
- #{javascript_catalog.declarations}
81
- INSTRUCTIONS
55
+ renderer = prompts || ->(key, **locals) { FrameworkPrompts.new.render(key, locals:) }
56
+ renderer.call("code_mode/javascript/instructions", declarations: javascript_catalog.declarations)
82
57
  end
83
58
 
84
59
  # Opens a JavaScript Session with engine defaults merged with +limits+.
85
60
  # Unsupported limit keys raise ArgumentError.
86
- def open_session(broker:, sandbox_factory:, limits: {})
61
+ def open_session(broker:, sandbox_factory:, limits: {}, framework_prompt_scope: {})
87
62
  configured_limits = normalize_limits(limits, defaults: DEFAULT_LIMITS)
88
63
  root = Dir.mktmpdir("little-ghost-javascript-")
89
64
  runtime_paths = javascript_runtime_paths
@@ -99,22 +74,26 @@ module LittleGhost
99
74
  required_runtime_paths: runtime_paths.keys.to_h { |name| [name, :read_only] }
100
75
  )
101
76
  sandbox.open
102
- client = Javascript::Client.new(session_factory: lambda {
103
- sandbox.start_program(
104
- host_command,
105
- cwd: ".",
106
- environment: child_environment,
107
- output_bytes: configured_limits.fetch(:output_bytes),
108
- memory_bytes: configured_limits[:memory_bytes],
109
- cpu_seconds: configured_limits[:cpu_seconds],
110
- file_bytes: configured_limits[:file_bytes],
111
- allow_subprocesses: allow_subprocesses_for(sandbox)
112
- )
113
- })
77
+ client = Javascript::Client.new(
78
+ session_factory: lambda {
79
+ sandbox.start_program(
80
+ host_command,
81
+ cwd: ".",
82
+ environment: child_environment,
83
+ output_bytes: configured_limits.fetch(:output_bytes),
84
+ memory_bytes: configured_limits[:memory_bytes],
85
+ cpu_seconds: configured_limits[:cpu_seconds],
86
+ file_bytes: configured_limits[:file_bytes],
87
+ allow_subprocesses: allow_subprocesses_for(sandbox)
88
+ )
89
+ },
90
+ framework_prompt_scope:
91
+ )
114
92
  Javascript::Session.new(
115
93
  broker:, client:, sandbox:, workspace:,
116
94
  max_concurrency: configured_limits.fetch(:max_concurrency),
117
- wall_seconds: configured_limits.fetch(:wall_seconds)
95
+ wall_seconds: configured_limits.fetch(:wall_seconds),
96
+ framework_prompt_scope:
118
97
  )
119
98
  rescue
120
99
  sandbox&.close
@@ -35,6 +35,7 @@ module LittleGhost
35
35
  response_queues = {}
36
36
  calls = 0
37
37
  max_calls = request.fetch("tool_calls")
38
+ messages = request.fetch("messages")
38
39
  output_buffer = +""
39
40
  flush_output = lambda do
40
41
  unless output_buffer.empty?
@@ -81,7 +82,7 @@ module LittleGhost
81
82
  invoke = lambda do |name, arguments|
82
83
  id, queue = queues_lock.synchronize do
83
84
  calls += 1
84
- raise "tool call limit exceeded" if calls > max_calls
85
+ raise messages.fetch("tool_calls_limit") if calls > max_calls
85
86
  id = "call-#{calls}"
86
87
  queue = Queue.new
87
88
  response_queues[id] = queue
@@ -102,7 +103,7 @@ module LittleGhost
102
103
  end
103
104
  concurrency = request.fetch("concurrency")
104
105
  tools.define_singleton_method(:parallel) do |*operations|
105
- raise ArgumentError, "parallel accepts callables" unless operations.all? { |operation| operation.respond_to?(:call) }
106
+ raise ArgumentError, messages.fetch("parallel_callables") unless operations.all? { |operation| operation.respond_to?(:call) }
106
107
  operations.each_slice(concurrency).flat_map do |batch|
107
108
  batch.map { |operation| Thread.new { operation.call } }.map(&:value)
108
109
  end
@@ -130,8 +131,11 @@ module LittleGhost
130
131
  rescue SignalException
131
132
  exit 0
132
133
  rescue Exception => error
133
- STDERR.puts("#{error.class}: #{error.message}")
134
- error_frame = {type: "error", error: "#{error.class}: #{error.message}"}
134
+ failure = messages.fetch("execution_failed")
135
+ .gsub("__ERROR_CLASS__") { error.class.to_s }
136
+ .gsub("__ERROR_MESSAGE__") { error.message }
137
+ STDERR.puts(failure)
138
+ error_frame = {type: "error", error: failure}
135
139
  emit ? emit.call(error_frame) : write_frame&.call(error_frame)
136
140
  exit 1
137
141
  end
@@ -12,13 +12,17 @@ module LittleGhost
12
12
  class Session < CodeMode::Session # :nodoc:
13
13
  OBSERVATION_SECONDS = 60
14
14
 
15
- def initialize(broker:, sandbox_factory:, subprocess_policy:, limits:, observation_seconds: OBSERVATION_SECONDS)
15
+ def initialize(
16
+ broker:, sandbox_factory:, subprocess_policy:, limits:, observation_seconds: OBSERVATION_SECONDS,
17
+ framework_prompt_scope: {}
18
+ )
16
19
  @broker = broker
17
20
  @task_runner = broker.task_runner
18
21
  @sandbox_factory = sandbox_factory
19
22
  @subprocess_policy = subprocess_policy
20
23
  @limits = limits
21
24
  @observation_seconds = Float(observation_seconds)
25
+ @framework_prompt_scope = framework_prompt_scope.freeze
22
26
  raise ArgumentError, "observation_seconds must be positive" unless @observation_seconds.positive?
23
27
  @session = nil
24
28
  @workspace = nil
@@ -65,7 +69,7 @@ module LittleGhost
65
69
  @closed = true
66
70
  acquired = @control_mutex.try_lock
67
71
  unless acquired
68
- raise ToolError, "cannot close while another code-mode control operation is active"
72
+ raise ToolError, prompt("code_mode/feedback/control_active")
69
73
  end
70
74
  cleanup_error = begin
71
75
  close_process
@@ -87,13 +91,23 @@ module LittleGhost
87
91
 
88
92
  private
89
93
 
94
+ def prompt(key, **locals)
95
+ framework_prompts = @framework_prompt_scope[:framework_prompts] || FrameworkPrompts.new
96
+ framework_prompts.render(
97
+ key,
98
+ locals:,
99
+ invocation_paths: @framework_prompt_scope.fetch(:invocation_paths, []),
100
+ agent_path: @framework_prompt_scope[:agent_path]
101
+ )
102
+ end
103
+
90
104
  def execute_program(source:, catalog:, frame:, max_output_tokens:, context:)
91
- raise ToolError, "code-mode session is closed" if @closed
105
+ raise ToolError, prompt("code_mode/feedback/closed", resource: "session") if @closed
92
106
  ensure_program_can_start!
93
107
  @programs += 1
94
- raise ToolError, "code-mode program limit exceeded" if @programs > @limits.fetch(:programs)
108
+ raise ToolError, prompt("code_mode/feedback/limit_exceeded", limit: "program") if @programs > @limits.fetch(:programs)
95
109
  source = String(source)
96
- raise ToolError, "code-mode source exceeds the limit" if source.bytesize > @limits.fetch(:source_bytes)
110
+ raise ToolError, prompt("code_mode/feedback/limit_exceeded", limit: "source size") if source.bytesize > @limits.fetch(:source_bytes)
97
111
  normalized_catalog = Catalog.new(catalog).host_definitions
98
112
 
99
113
  generation, process = open_process
@@ -103,7 +117,16 @@ module LittleGhost
103
117
  catalog: normalized_catalog,
104
118
  frame:,
105
119
  tool_calls: @limits.fetch(:tool_calls),
106
- concurrency: @limits.fetch(:concurrency)
120
+ concurrency: @limits.fetch(:concurrency),
121
+ messages: {
122
+ parallel_callables: prompt("code_mode/ruby/feedback/parallel_callables"),
123
+ tool_calls_limit: prompt("code_mode/feedback/limit_exceeded", limit: "Tool call"),
124
+ execution_failed: prompt(
125
+ "code_mode/errors/execution_failed",
126
+ error_class: "__ERROR_CLASS__",
127
+ message: "__ERROR_MESSAGE__"
128
+ )
129
+ }
107
130
  ))
108
131
  drive(generation:, process:, max_output_tokens:, context:)
109
132
  rescue
@@ -144,7 +167,7 @@ module LittleGhost
144
167
  def with_control
145
168
  acquired = @control_mutex.try_lock
146
169
  unless acquired
147
- raise ToolError, "another code-mode control operation is already active"
170
+ raise ToolError, prompt("code_mode/feedback/control_active")
148
171
  end
149
172
 
150
173
  yield
@@ -156,7 +179,7 @@ module LittleGhost
156
179
  error = @lifecycle_mutex.synchronize do
157
180
  @lifecycle_condition.wait(@lifecycle_mutex) while @expiring_generation
158
181
  pending = take_pending_program_error
159
- raise ToolError, "a code-mode program is already active" if !pending && (@generation || @session)
182
+ raise ToolError, prompt("code_mode/feedback/program_active") if !pending && (@generation || @session)
160
183
 
161
184
  pending
162
185
  end
@@ -178,7 +201,7 @@ module LittleGhost
178
201
  factory.new(workspace: @workspace)
179
202
  end
180
203
  @sandbox.open
181
- raise ToolError, "code-mode session is closed" if @closed
204
+ raise ToolError, prompt("code_mode/feedback/closed", resource: "session") if @closed
182
205
  @session = @sandbox.start_program(
183
206
  [RbConfig.ruby, "-e", Host::SOURCE],
184
207
  output_bytes: @limits.fetch(:output_bytes),
@@ -275,7 +298,7 @@ module LittleGhost
275
298
  active = @call_tasks.count(&:alive?)
276
299
  raise ProtocolError, "code-mode concurrent tool call limit exceeded" if active >= @limits.fetch(:concurrency)
277
300
  @tool_calls += 1
278
- raise ToolError, "tool call limit exceeded" if @tool_calls > @limits.fetch(:tool_calls)
301
+ raise ToolError, prompt("code_mode/feedback/limit_exceeded", limit: "Tool call") if @tool_calls > @limits.fetch(:tool_calls)
279
302
 
280
303
  call_errors = @call_errors
281
304
  closing_marker = @closing_marker
@@ -290,7 +313,7 @@ module LittleGhost
290
313
  def append_output(value)
291
314
  value = String(value).dup.force_encoding(Encoding::UTF_8).scrub
292
315
  @output_bytes += value.bytesize
293
- raise ToolError, "code-mode output exceeded the limit" if @output_bytes > @limits.fetch(:output_bytes)
316
+ raise ToolError, prompt("code_mode/feedback/limit_exceeded", limit: "output") if @output_bytes > @limits.fetch(:output_bytes)
294
317
 
295
318
  @output << value
296
319
  end
@@ -314,7 +337,11 @@ module LittleGhost
314
337
  @output = +""
315
338
  return output unless max_output_tokens
316
339
 
317
- Support::OutputTruncation.truncate_middle_with_token_budget(output, max_output_tokens).first
340
+ Support::OutputTruncation.truncate_middle_with_token_budget(
341
+ output,
342
+ max_output_tokens,
343
+ **@framework_prompt_scope
344
+ ).first
318
345
  end
319
346
 
320
347
  def active_process
@@ -323,7 +350,7 @@ module LittleGhost
323
350
  @lifecycle_condition.wait(@lifecycle_mutex) while @expiring_generation
324
351
  error = take_pending_program_error
325
352
  raise error if error
326
- raise ToolError, "there is no active code-mode program" unless @generation && @session
353
+ raise ToolError, prompt("code_mode/feedback/not_active") unless @generation && @session
327
354
 
328
355
  [@generation, @session]
329
356
  end
@@ -442,7 +469,7 @@ module LittleGhost
442
469
  end
443
470
  return unless process
444
471
 
445
- error = ToolError.new("code-mode program timed out")
472
+ error = ToolError.new(FrameworkPrompts.reference("code_mode/feedback/program_timed_out"))
446
473
  begin
447
474
  mark_process_closing
448
475
  process.terminate
@@ -34,50 +34,20 @@ module LittleGhost
34
34
  def language = :ruby
35
35
 
36
36
  # Builds Ruby usage instructions and method declarations for +catalog+.
37
- def instructions(catalog:)
38
- <<~INSTRUCTIONS.strip
39
- Use Ruby to call the available tools and compose their results.
40
-
41
- Program lifecycle:
42
- - Every exec call starts a fresh program in a new Ruby process.
43
- - Exec and wait observe it for up to one minute. They return sooner when it finishes.
44
- - If exec or wait returns `still_working`, call wait to observe the same running program again.
45
- - Wait does not pause, resume, or restart the program.
46
- - Use stop when the result is no longer needed.
47
- - Do not call wait or stop after `completed`, `error`, or `terminated`.
48
- - Local variables, constants, and other process state do not persist between exec calls.
49
-
50
- Tool calls:
51
- - Call a tool with `tools.<method>(keyword: value)` and only its documented keywords.
52
- - Use `tools.call(name, arguments)` when the name is dynamic.
53
- - Tool calls are synchronous. Use `tools.parallel` for independent calls; results keep callable order.
54
- - JSON results become ordinary Ruby values. Other results remain strings.
55
- - A failed call raises. Rescue it only when the program can recover.
56
- - `ALL_TOOLS` contains the complete runtime catalog.
57
-
58
- Output and completion:
59
- - The final Ruby expression becomes the completed program value.
60
- - Use `text(value)` for user-visible output.
61
- - Ordinary `puts`, `print`, `printf`, and `p` output is captured and combined into bounded chunks.
62
- - Use `finish(value)` to complete early with a value.
63
-
64
- The Sandbox controls filesystem, network, subprocess, and optional-library access. Do not assume host
65
- capabilities are available.
66
-
67
- Available tool methods:
68
-
69
- #{Ruby::Catalog.new(catalog).declarations}
70
- INSTRUCTIONS
37
+ def instructions(catalog:, prompts: nil)
38
+ renderer = prompts || ->(key, **locals) { FrameworkPrompts.new.render(key, locals:) }
39
+ renderer.call("code_mode/ruby/instructions", declarations: Ruby::Catalog.new(catalog).declarations)
71
40
  end
72
41
 
73
42
  # Opens a Ruby Session with engine defaults merged with +limits+.
74
43
  # Unsupported limit keys raise ArgumentError.
75
- def open_session(broker:, sandbox_factory:, limits: {})
44
+ def open_session(broker:, sandbox_factory:, limits: {}, framework_prompt_scope: {})
76
45
  Ruby::Session.new(
77
46
  broker:,
78
47
  sandbox_factory:,
79
48
  subprocess_policy: method(:allow_subprocesses_for),
80
- limits: normalize_limits(limits, defaults: DEFAULT_LIMITS)
49
+ limits: normalize_limits(limits, defaults: DEFAULT_LIMITS),
50
+ framework_prompt_scope:
81
51
  )
82
52
  end
83
53
  end