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
@@ -107,9 +107,7 @@ LittleGhost.configure do |config|
107
107
 
108
108
  config.sandbox = {
109
109
  provider: :native,
110
- files: {root: :read_write, skills: :read_only},
111
- root_filesystem: :isolated,
112
- network: :none
110
+ files: {root: :read_write, skills: :read_only}
113
111
  }
114
112
  end
115
113
  ```
data/docs/guides/tools.md CHANGED
@@ -79,6 +79,8 @@ end
79
79
  Prefer `available_if` on an individual Tool when only that operation is
80
80
  conditional. Use a provider when the collection itself owns discovery,
81
81
  construction, or shared setup for an application or remote service.
82
+ The [MCP guide](mcp.md) shows how a Model Context Protocol server provides a
83
+ Toolset through the official Ruby client.
82
84
 
83
85
  LittleGhost does not enforce class names. A useful application convention is
84
86
  to end one model-callable operation with `Tool` and a provider of multiple
data/exe/little_ghost ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "little_ghost/cli"
8
+
9
+ source_path = File.expand_path("..", __dir__)
10
+ source_path = nil unless File.file?(File.join(source_path, "little_ghost.gemspec"))
11
+
12
+ exit LittleGhost::CLI.new(ARGV, source_path:).run
@@ -35,17 +35,6 @@ module LittleGhost
35
35
  DEFAULT_PRESERVE_RECENT_MESSAGES = 10 # :nodoc:
36
36
  ESTIMATED_CHARS_PER_TOKEN = 4 # :nodoc:
37
37
  OUTPUT_LIMIT_STOP_REASONS = %i[max_tokens limit_output_tokens limit_total_tokens limit_turns].freeze # :nodoc:
38
- SUMMARIZATION_PROMPT = <<~PROMPT # :nodoc:
39
- You are a conversation summarizer. Provide a concise summary of the conversation history.
40
-
41
- Format requirements:
42
- - Create a structured, concise summary in bullet-point format.
43
- - Do not respond conversationally, address the user directly, or comment on tool availability.
44
- - Preserve key topics, questions, significant tool executions and results, code or technical information, and key insights.
45
- - Do not assume tool executions failed unless otherwise stated.
46
- - Write the summary in the third person.
47
- PROMPT
48
-
49
38
  def self.included(base) # :nodoc:
50
39
  base.extend(ClassMethods)
51
40
  base.class_attribute :context_management_configuration_value
@@ -193,9 +182,9 @@ module LittleGhost
193
182
  started_at = monotonic_time
194
183
  summary_request = ModelRequest.new(
195
184
  messages: [
196
- Message.new(role: :system, content: SUMMARIZATION_PROMPT),
185
+ Message.new(role: :system, content: render_framework_prompt("context_management/summary/system_instruction")),
197
186
  *messages,
198
- Message.new(role: :user, content: "Please summarize this conversation.")
187
+ Message.new(role: :user, content: render_framework_prompt("context_management/summary/request"))
199
188
  ],
200
189
  tools: [],
201
190
  settings: request.settings,
@@ -17,8 +17,10 @@ module LittleGhost
17
17
  # Static declarations may be combined with a resolver that returns dynamic
18
18
  # Subagents::Definition objects. Managed conversations persist when a session
19
19
  # store is configured unless <tt>persist: false</tt> keeps them local to one
20
- # invocation. An agent exposed as a tool starts with empty history unless
21
- # <tt>preserve_context: true</tt> serializes calls and retains its history.
20
+ # invocation. A class-declared agent exposed as a tool starts with an empty
21
+ # history and a fresh run-scoped Agent for every call. Set
22
+ # <tt>preserve_context: true</tt> to serialize calls through one Agent and
23
+ # retain its history.
22
24
  #
23
25
  # Tool overrides must be classes. A delegated agent otherwise receives only
24
26
  # its own declared tools; it does not inherit the parent's registry. The
@@ -91,8 +93,9 @@ module LittleGhost
91
93
 
92
94
  # Exposes +agent_class+ as one ordinary tool.
93
95
  #
94
- # Pass <tt>preserve_context: true</tt> to retain the delegated agent's
95
- # conversational history between calls to that tool instance.
96
+ # By default, every tool call builds and closes a fresh delegated Agent.
97
+ # Pass <tt>preserve_context: true</tt> to serialize calls through one
98
+ # Agent and retain its conversational history between calls.
96
99
  def agent_as_tool(agent_class, name: nil, description: nil, model: nil, tools: nil,
97
100
  preserve_context: false)
98
101
  assembly_as_tool(
@@ -40,7 +40,8 @@ module LittleGhost
40
40
  paths:,
41
41
  resource_root: binding.run&.runtime&.skill_resource_root,
42
42
  workspace: binding.workspace,
43
- sandbox: binding.sandbox
43
+ sandbox: binding.sandbox,
44
+ prompt_renderer: binding.agent.method(:render_framework_prompt)
44
45
  )
45
46
  )
46
47
  return [] if catalog.names.empty?
@@ -70,10 +71,6 @@ module LittleGhost
70
71
  end
71
72
  self.skills_configuration_value = options.merge(paths: configured_paths)
72
73
  tools CatalogTools
73
- prompt_local(:skills_prompt) do
74
- tool = tools.fetch("skills") if tools.names.include?("skills")
75
- tool&.catalog&.discovery_prompt.to_s
76
- end
77
74
  before_invocation :include_skills_prompt
78
75
  end
79
76
 
@@ -82,8 +79,13 @@ module LittleGhost
82
79
 
83
80
  private
84
81
 
82
+ def skills_prompt
83
+ tool = tools.fetch("skills") if tools.names.include?("skills")
84
+ tool&.catalog&.discovery_prompt.to_s
85
+ end
86
+
85
87
  def include_skills_prompt(payload)
86
- prompt = prompt_locals[:skills_prompt].to_s
88
+ prompt = skills_prompt
87
89
  return Support::Callbacks.continue if prompt.empty?
88
90
 
89
91
  messages = payload.fetch(:messages).dup
@@ -27,8 +27,6 @@ module LittleGhost
27
27
  # changed result resets that sequence, while a terminating repeat prevents
28
28
  # the duplicate tool body from running again.
29
29
  module ToolLoop
30
- WARNING = "Repeated tool call detected. Change the approach or arguments before calling this tool again." # :nodoc:
31
- FINAL_WARNING = "Final repeated tool call warning. Calling this tool again with identical arguments and result will stop the run." # :nodoc:
32
30
  TRACKED_INVOCATION_LIMIT = 1_000 # :nodoc:
33
31
 
34
32
  def self.included(base) # :nodoc:
@@ -120,7 +118,7 @@ module LittleGhost
120
118
  newly_terminated = false
121
119
  if repeat && repeat[:count] >= @tool_loop_terminate_at - 1
122
120
  unless state[:termination]
123
- state[:termination] = "Stopped after detecting a repeated tool-call loop in #{tool_use.name.inspect}."
121
+ state[:termination] = render_framework_prompt("tools/loop/notices/termination", tool_name: tool_use.name)
124
122
  newly_terminated = true
125
123
  end
126
124
  end
@@ -165,7 +163,9 @@ module LittleGhost
165
163
  return Support::Callbacks.continue unless count == @tool_loop_warning_at || count == @tool_loop_terminate_at - 1
166
164
 
167
165
  action = (count == @tool_loop_warning_at) ? :warn : :final_warning
168
- warning = (action == :warn) ? WARNING : FINAL_WARNING
166
+ warning = render_framework_prompt(
167
+ (action == :warn) ? "tools/loop/notices/warning" : "tools/loop/notices/final_warning"
168
+ )
169
169
  Instrumentation.publish(
170
170
  :tool_loop,
171
171
  operation_id: payload[:operation_id],
@@ -39,18 +39,17 @@ module LittleGhost
39
39
  # an explicitly isolated setup.
40
40
  #
41
41
  # Agent declarations are inherited. Define a short prompt inline, or place a
42
- # growing prompt in <tt>app/prompts/customer_support/system.erb</tt> for
42
+ # growing prompt in <tt>app/prompts/customer_support/system_prompt.erb</tt> for
43
43
  # +CustomerSupportAgent+. The {Prompts as Views guide}[rdoc-ref:docs/guides/prompt_views.md]
44
- # explains conventional lookup, locals, and partials. Optional features such
45
- # as skills, context management, loop detection, and delegation stay inactive
46
- # until their DSL is used.
44
+ # explains conventional lookup, values prepared by the Agent, locals, and
45
+ # partials. Optional features such as skills, context management, loop
46
+ # detection, and delegation stay inactive until their DSL is used.
47
47
  #
48
48
  # Models may return text or locally validated structured data. LittleGhost
49
49
  # hides unexpected Tool exception messages from the model. See
50
50
  # Run[rdoc-ref:LittleGhost::Run] for outcomes, cancellation, and cleanup, and
51
51
  # Assembly[rdoc-ref:LittleGhost::Assembly] for the advanced run-scoped form.
52
52
  class Agent < Assembly
53
- DEFAULT_SYSTEM_PROMPT = "You are a helpful agent." # :nodoc:
54
53
  DEFAULT_MAX_TOOL_RESULT_TOKENS = 10_000 # :nodoc:
55
54
  MAX_STRUCTURED_RESULT_BYTES = 1_000_000 # :nodoc:
56
55
  MAX_STRUCTURED_RESULT_DEPTH = 64 # :nodoc:
@@ -84,7 +83,6 @@ module LittleGhost
84
83
  class_attribute :system_prompt_builder_value
85
84
  class_attribute :tool_declarations_value, default: []
86
85
  class_attribute :code_mode_configuration_value
87
- class_attribute :prompt_local_values, default: {}
88
86
  class_attribute :callback_values, default: Support::Callbacks.new(*CALLBACKS)
89
87
 
90
88
  class << self
@@ -284,16 +282,6 @@ module LittleGhost
284
282
 
285
283
  def code_mode_configuration = code_mode_configuration_value # :nodoc:
286
284
 
287
- # Adds a named value or resolver to every prompt rendered for the agent.
288
- def prompt_local(name, *values, &resolver)
289
- raise ArgumentError, "Provide a prompt local value or block" if values.empty? && !resolver
290
- raise ArgumentError, "Provide a prompt local value or block, not both" unless values.empty? || !resolver
291
-
292
- self.prompt_local_values = prompt_local_values.merge(name.to_sym => resolver || values.fetch(0))
293
- end
294
-
295
- def prompt_local_resolvers = prompt_local_values # :nodoc:
296
-
297
285
  def callbacks = callback_values # :nodoc:
298
286
 
299
287
  CALLBACKS.each do |name|
@@ -539,7 +527,8 @@ module LittleGhost
539
527
  # The first form is the application-facing entrypoint. It may be reused for
540
528
  # independent concurrent calls and creates a fresh Run for each one. The
541
529
  # second form is run-scoped; Runtime builders supply its dependencies and it
542
- # must not outlive or be shared outside its owning Run.
530
+ # must not outlive its owning Run. Calls on a run-scoped instance must be
531
+ # sequential. An overlapping call raises AgentBusyError when its stream begins.
543
532
  def initialize(
544
533
  model: nil,
545
534
  runtime: nil,
@@ -560,6 +549,8 @@ module LittleGhost
560
549
  standalone = model.nil? && run.nil?
561
550
  super(run:, runtime:, workspace:, sandbox:, standalone:)
562
551
  if standalone
552
+ @framework_prompts = FrameworkPrompts.new(paths: template_paths)
553
+ @framework_prompt_invocation_paths = [].freeze
563
554
  @owns_resources = true
564
555
  @closed = false
565
556
  @close_mutex = Mutex.new
@@ -570,6 +561,12 @@ module LittleGhost
570
561
 
571
562
  @model = model
572
563
  @runtime = runtime || run&.runtime
564
+ @framework_prompts = if @runtime
565
+ FrameworkPrompts.for_runtime(@runtime)
566
+ else
567
+ FrameworkPrompts.new(paths: template_paths)
568
+ end
569
+ @framework_prompt_invocation_paths = [].freeze
573
570
  @run = run
574
571
  @workspace = workspace || run&.workspace
575
572
  @sandbox = sandbox || run&.sandbox
@@ -606,6 +603,8 @@ module LittleGhost
606
603
  @assembly_transitions = {}
607
604
  @assembly_tool_batch_sizes = {}
608
605
  @assembly_transition = nil
606
+ @_little_ghost_invocation_mutex = Mutex.new
607
+ @_little_ghost_invocation_active = false
609
608
  raise ArgumentError, "max_turns must be at least 1" if @max_turns < 1
610
609
  raise ArgumentError, "max_tool_calls must be at least 1" if @max_tool_calls < 1
611
610
  raise ArgumentError, "max_tool_result_tokens must be at least 1" if @max_tool_result_tokens < 1
@@ -613,6 +612,7 @@ module LittleGhost
613
612
  resolved_runtime.runtime_hooks.find { |hook| hook.is_a?(Runtime::Hooks::Artifacts) }
614
613
  end
615
614
  apply_cancellation_decision!(run_callbacks(:after_initialize, self))
615
+ @_little_ghost_prompt_assign_baseline = prompt_application_state.freeze
616
616
  rescue
617
617
  @tool_registry&.close
618
618
  @code_mode_runtime&.close
@@ -643,7 +643,7 @@ module LittleGhost
643
643
  def request_assembly_transition(value, context:) # :nodoc:
644
644
  @assembly_transitions_mutex.synchronize do
645
645
  unless @assembly_tool_batch_sizes[context] == 1
646
- raise ToolError, "An assembly transition must be the only tool call in a model response"
646
+ raise ToolError, render_framework_prompt("assembly/feedback/transition_only")
647
647
  end
648
648
  if @assembly_transitions.key?(context)
649
649
  raise ProtocolError, "Multiple assembly transitions were requested in one agent turn"
@@ -658,7 +658,7 @@ module LittleGhost
658
658
 
659
659
  # Adds an interjection and returns the model's immediate result details.
660
660
  #
661
- # Use +target_operation_id+ when an agent has multiple active invocations.
661
+ # +target_operation_id+ may identify the active model operation.
662
662
  # Messages may contain only text, image, or document content. The returned
663
663
  # result value exposes +text+, +tool_calls?+, +interjection_ids+, and
664
664
  # +batch_key+; tool calls may continue after this result. Depend on these
@@ -729,11 +729,12 @@ module LittleGhost
729
729
  # Streams one invocation as StreamEvent objects.
730
730
  #
731
731
  # Agents built inside a run accept history, JSON-like context, cancellation,
732
- # deadlines, settings, and trusted invocation template paths. An Agent
732
+ # deadlines, settings, and invocation-specific prompt roots. An Agent
733
733
  # instance may be streamed only by its owning Run. Every template path must
734
- # be an application-created TrustedPath;
735
- # the wrapper records a trust decision and must never contain unchecked
736
- # request or model input.
734
+ # be an application-created TrustedPath, never a value selected by a request
735
+ # or model. A run-scoped Agent accepts one active invocation; enumerating an
736
+ # overlapping stream raises AgentBusyError. The instance may be invoked
737
+ # again after the first stream completes or fails.
737
738
  def stream(
738
739
  input = nil,
739
740
  history: nil,
@@ -778,18 +779,21 @@ module LittleGhost
778
779
  end
779
780
  settings = @model_settings.merge(settings)
780
781
  Enumerator.new do |events|
782
+ invocation_acquired = false
781
783
  interjections = AgentInterjections.new
782
- run_context = RunContext.new(
783
- state: context,
784
- cancellation_token: cancellation_token,
785
- deadline: deadline,
786
- metadata: {agent_id: self.class.agent_id},
787
- checkpoint:,
788
- conversation_id:,
789
- interjection_metadata:,
790
- interjection_ids:
791
- )
792
784
  begin
785
+ begin_invocation!
786
+ invocation_acquired = true
787
+ run_context = RunContext.new(
788
+ state: context,
789
+ cancellation_token: cancellation_token,
790
+ deadline: deadline,
791
+ metadata: {agent_id: self.class.agent_id},
792
+ checkpoint:,
793
+ conversation_id:,
794
+ interjection_metadata:,
795
+ interjection_ids:
796
+ )
793
797
  with_invocation(run_context) do
794
798
  execute(
795
799
  input,
@@ -808,23 +812,55 @@ module LittleGhost
808
812
  interjections.close(error)
809
813
  raise
810
814
  ensure
811
- @code_mode_runtime&.close(context: run_context)
812
- interjections.close(AgentInterjectionError.new("Agent finished before the interjection was delivered"))
813
- unregister_interjections(interjections)
815
+ cleanup_error = nil
816
+ begin
817
+ @code_mode_runtime&.close(context: run_context) if run_context
818
+ rescue => error
819
+ cleanup_error = error
820
+ end
821
+ begin
822
+ interjections.close(AgentInterjectionError.new("Agent finished before the interjection was delivered"))
823
+ rescue => error
824
+ cleanup_error ||= error
825
+ ensure
826
+ begin
827
+ unregister_interjections(interjections)
828
+ ensure
829
+ end_invocation! if invocation_acquired
830
+ end
831
+ end
832
+ raise cleanup_error if cleanup_error
814
833
  end
815
834
  end
816
835
  end
817
836
 
818
- # Materializes and freezes the prompt locals declared on the agent class.
819
- def prompt_locals
820
- self.class.prompt_local_resolvers.to_h do |name, resolver|
821
- value = if resolver.respond_to?(:call)
822
- resolver.parameters.empty? ? instance_exec(&resolver) : resolver.call(self)
823
- else
824
- resolver
825
- end
826
- [name, value]
827
- end.freeze
837
+ # :call-seq:
838
+ # system_prompt() -> Object
839
+ #
840
+ # Prepares application values for a file-backed system prompt. Override this
841
+ # method, assign instance variables, and read them from the ERB view:
842
+ #
843
+ # def system_prompt
844
+ # @company_name = "Northstar"
845
+ # end
846
+ #
847
+ # In <tt>app/prompts/customer_support/system_prompt.erb</tt>:
848
+ #
849
+ # You help customers of <%= @company_name %>.
850
+ #
851
+ # LittleGhost calls the action once before rendering the top-level prompt
852
+ # view. Partials share its prepared values without calling it again. Its
853
+ # return value is ignored. If it raises, the invocation fails with that
854
+ # exception. An inline prompt declared on the Agent class takes precedence
855
+ # and does not call this method.
856
+ #
857
+ # Each render starts with the application instance variables captured after
858
+ # the Agent's +after_initialize+ callbacks finish. LittleGhost copies the
859
+ # prepared values into the view, then restores the Agent's live instance
860
+ # variables, even when this method raises. Objects are not duplicated, so
861
+ # treat mutable values as read-only or assign a copy. Subclasses may call
862
+ # +super+ before preparing additional values.
863
+ def system_prompt
828
864
  end
829
865
 
830
866
  # The Tool registry available during this Agent run.
@@ -887,6 +923,73 @@ module LittleGhost
887
923
 
888
924
  private
889
925
 
926
+ PROTECTED_PROMPT_INSTANCE_VARIABLES = %i[
927
+ @active_assemblies @active_interjections @agent_path @agent_stream_path
928
+ @artifact_lifecycle @assembly_closed @assembly_definition @assembly_mutex
929
+ @assembly_tool_batch_sizes @assembly_transition @assembly_transitions
930
+ @assembly_transitions_mutex @close_mutex @closed @code_mode_declaration
931
+ @code_mode_runtime @delegation_activity @exclusive_tools_mutex @executor
932
+ @interjections_mutex @max_tool_calls @max_tool_result_tokens @max_turns
933
+ @model @model_settings @owns_resources @run @runtime @sandbox @standalone
934
+ @framework_prompts @framework_prompt_invocation_paths
935
+ @structured_output_strategy @task_runner @template_resolver @tool_loop_except
936
+ @tool_loop_fallback @tool_loop_mutex @tool_loop_runs @tool_loop_terminate_at
937
+ @tool_loop_warning_at @tool_registry @workspace
938
+ @_little_ghost_invocation_active @_little_ghost_invocation_mutex
939
+ ].freeze # :nodoc:
940
+
941
+ def begin_invocation!
942
+ @_little_ghost_invocation_mutex.synchronize do
943
+ if @_little_ghost_invocation_active
944
+ raise AgentBusyError,
945
+ "Agent is already handling an invocation; use the Agent class entrypoint or a fresh Agent for independent calls"
946
+ end
947
+
948
+ @_little_ghost_invocation_active = true
949
+ end
950
+ end
951
+
952
+ def end_invocation!
953
+ @_little_ghost_invocation_mutex.synchronize { @_little_ghost_invocation_active = false }
954
+ end
955
+
956
+ def prompt_view_assigns
957
+ names = prompt_application_instance_variables
958
+ names.to_h do |name|
959
+ [name.to_s.delete_prefix("@").to_sym, instance_variable_get(name)]
960
+ end
961
+ end
962
+
963
+ def prepare_prompt_view_assigns
964
+ saved = prompt_application_state
965
+ replace_prompt_application_state(@_little_ghost_prompt_assign_baseline)
966
+ begin
967
+ system_prompt
968
+ prompt_view_assigns
969
+ ensure
970
+ replace_prompt_application_state(saved)
971
+ end
972
+ end
973
+
974
+ def prompt_application_instance_variables
975
+ instance_variables.reject do |name|
976
+ PROTECTED_PROMPT_INSTANCE_VARIABLES.include?(name) || name.to_s.start_with?("@_little_ghost_")
977
+ end
978
+ end
979
+
980
+ def prompt_application_state
981
+ prompt_application_instance_variables.to_h do |name|
982
+ [name, instance_variable_get(name)]
983
+ end
984
+ end
985
+
986
+ def replace_prompt_application_state(state)
987
+ prompt_application_instance_variables.each { |name| remove_instance_variable(name) }
988
+ state.each do |name, value|
989
+ instance_variable_set(name, value)
990
+ end
991
+ end
992
+
890
993
  def entrypoint_payload(input, options)
891
994
  return options if input.nil?
892
995
  return input.merge(options) if input.is_a?(Hash)
@@ -911,12 +1014,7 @@ module LittleGhost
911
1014
  Message.new(
912
1015
  role: :user,
913
1016
  content: [
914
- Content::Text.new(text: <<~MESSAGE.strip),
915
- Agent interjection:
916
-
917
- Respond briefly in ordinary text before any tool calls, then continue the current task unless this
918
- interjection asks you to finish.
919
- MESSAGE
1017
+ Content::Text.new(text: render_framework_prompt("agent/interjections/instructions")),
920
1018
  *source.content
921
1019
  ],
922
1020
  metadata: source.metadata.merge(interjection.metadata).merge(
@@ -974,6 +1072,8 @@ module LittleGhost
974
1072
  interjections:,
975
1073
  interject_ready:
976
1074
  )
1075
+ previous_framework_prompt_invocation_paths = @framework_prompt_invocation_paths
1076
+ @framework_prompt_invocation_paths = template_paths.freeze
977
1077
  started_at = monotonic_time
978
1078
  operation_id = SecureRandom.uuid
979
1079
  events = agent_stream_events(
@@ -1322,6 +1422,8 @@ module LittleGhost
1322
1422
  metadata = model.details.to_h.merge(model_role: model.role)
1323
1423
  emit(events, :invocation_error, error:, usage: context.usage, metadata:)
1324
1424
  raise
1425
+ ensure
1426
+ @framework_prompt_invocation_paths = previous_framework_prompt_invocation_paths
1325
1427
  end
1326
1428
 
1327
1429
  def invoke_model(
@@ -1343,7 +1445,14 @@ module LittleGhost
1343
1445
  StructuredOutput.validate_tool_collision!(strategy, ordinary_tools) if strategy
1344
1446
  request = ModelRequest.new(
1345
1447
  messages: messages,
1346
- tools: model_tools(strategy ? strategy.tools(ordinary_tools) : ordinary_tools, context:, turn:),
1448
+ tools: model_tools(
1449
+ strategy ? strategy.tools(
1450
+ ordinary_tools,
1451
+ description: render_framework_prompt("structured_output/tools/result/description")
1452
+ ) : ordinary_tools,
1453
+ context:,
1454
+ turn:
1455
+ ),
1347
1456
  settings: settings,
1348
1457
  output_schema: strategy&.output_schema,
1349
1458
  tool_choice: strategy&.tool_choice(repair: structured_result_repair_due),
@@ -1573,7 +1682,11 @@ module LittleGhost
1573
1682
  }
1574
1683
  )
1575
1684
  if tool.is_a?(ToolError)
1576
- result = build_tool_result(tool_use_id: tool_use.id, content: tool.message, status: :error)
1685
+ result = build_tool_result(
1686
+ tool_use_id: tool_use.id,
1687
+ content: render_framework_prompt("tools/feedback/unknown", name: tool_use.name),
1688
+ status: :error
1689
+ )
1577
1690
  finish_instrumentation(
1578
1691
  tool_handle,
1579
1692
  operation_id:,
@@ -1659,6 +1772,7 @@ module LittleGhost
1659
1772
  tool_result,
1660
1773
  tool_use:,
1661
1774
  run: @run,
1775
+ agent: self,
1662
1776
  workspace:,
1663
1777
  context:
1664
1778
  )
@@ -1686,7 +1800,12 @@ module LittleGhost
1686
1800
  )
1687
1801
  ExecutedTool.new(result:, execution_result: tool_result)
1688
1802
  rescue ToolError => error
1689
- result = build_tool_result(tool_use_id: tool_use.id, content: error.message, status: :error)
1803
+ content = if error.framework_prompt
1804
+ render_framework_prompt(error.framework_prompt.key, **error.framework_prompt.locals)
1805
+ else
1806
+ error.message
1807
+ end
1808
+ result = build_tool_result(tool_use_id: tool_use.id, content:, status: :error)
1690
1809
  finish_instrumentation(
1691
1810
  tool_handle,
1692
1811
  operation_id:,
@@ -1746,7 +1865,7 @@ module LittleGhost
1746
1865
  content = if references.empty?
1747
1866
  result.content
1748
1867
  else
1749
- "#{result.content}\n\nArtifacts:\n#{references.join("\n")}"
1868
+ [result.content, render_framework_prompt("artifacts/presentation/format/references", references:)].join("\n\n")
1750
1869
  end
1751
1870
 
1752
1871
  Tool::ExecutionResult.new(
@@ -1763,8 +1882,12 @@ module LittleGhost
1763
1882
  artifacts.filter_map do |artifact|
1764
1883
  next unless artifact.reference.is_a?(String) && artifact.bytes
1765
1884
 
1766
- details = [artifact.media_type, "#{artifact.bytes} bytes"].compact.join(", ")
1767
- "- #{artifact.reference} (#{details})"
1885
+ render_framework_prompt(
1886
+ "artifacts/presentation/format/reference",
1887
+ reference: artifact.reference,
1888
+ media_type: artifact.media_type,
1889
+ bytes: artifact.bytes
1890
+ )
1768
1891
  end
1769
1892
  end
1770
1893
 
@@ -1852,7 +1975,10 @@ module LittleGhost
1852
1975
  configuration = self.class.result_schema
1853
1976
  schema_name = configuration.fetch(:name)
1854
1977
  validate_structured_result_limits!(value)
1855
- errors = Tool::SchemaValidator.new(configuration.fetch(:schema)).validate(value)
1978
+ errors = Tool::SchemaValidator.new(
1979
+ configuration.fetch(:schema),
1980
+ prompt_renderer: method(:render_framework_prompt)
1981
+ ).validate(value)
1856
1982
  return "Structured result does not match its schema: #{errors.join("; ")}" unless errors.empty?
1857
1983
 
1858
1984
  context.submit_structured_result(
@@ -1873,7 +1999,7 @@ module LittleGhost
1873
1999
  tool_uses.map do |tool_use|
1874
2000
  build_tool_result(
1875
2001
  tool_use_id: tool_use.id,
1876
- content: "The structured result was invalid. Submit it again using the required schema.",
2002
+ content: render_framework_prompt("structured_output/repair/feedback/invalid_result"),
1877
2003
  status: :error
1878
2004
  )
1879
2005
  end
@@ -1910,7 +2036,10 @@ module LittleGhost
1910
2036
  def redact_structured_result_message(message)
1911
2037
  Message.new(
1912
2038
  role: message.role,
1913
- content: "[Structured result #{self.class.result_schema.fetch(:name)} redacted]",
2039
+ content: render_framework_prompt(
2040
+ "structured_output/persistence/format/redaction",
2041
+ schema_name: self.class.result_schema.fetch(:name)
2042
+ ),
1914
2043
  metadata: message.metadata
1915
2044
  )
1916
2045
  end
@@ -1995,14 +2124,15 @@ module LittleGhost
1995
2124
  end
1996
2125
 
1997
2126
  def structured_result_repair_message
1998
- requirement = if @structured_output_strategy&.tool?
1999
- "Call #{@structured_output_strategy.schema_name} exactly once as your only tool call."
2000
- else
2001
- "Your final response must be JSON matching the configured output schema."
2002
- end
2003
2127
  Message.new(
2004
2128
  role: :user,
2005
- content: "#{requirement} You have one repair attempt. The previous structured result was invalid."
2129
+ content: render_framework_prompt(
2130
+ "structured_output/repair/request",
2131
+ tool: @structured_output_strategy&.tool?,
2132
+ schema_name: @structured_output_strategy&.schema_name,
2133
+ repairs_remaining: 1,
2134
+ feedback: render_framework_prompt("structured_output/repair/feedback/invalid_result")
2135
+ )
2006
2136
  )
2007
2137
  end
2008
2138
 
@@ -2110,14 +2240,17 @@ module LittleGhost
2110
2240
  prompt = prompt.call(locals) if prompt.respond_to?(:call)
2111
2241
  return append_code_mode_instructions(prompt) if prompt
2112
2242
  template = self.class.system_template
2113
- return append_code_mode_instructions(DEFAULT_SYSTEM_PROMPT) if instance_of?(Agent) && run && !template
2243
+ if instance_of?(Agent) && run && !template
2244
+ return append_code_mode_instructions(render_framework_prompt("agent/system/default"))
2245
+ end
2114
2246
 
2115
- template ||= "#{self.class.logical_path}/system" if run
2247
+ template ||= "#{self.class.logical_path}/system_prompt" if run
2116
2248
  return nil unless template
2117
2249
 
2118
2250
  append_code_mode_instructions(@template_resolver.render(
2119
2251
  template,
2120
2252
  locals: locals,
2253
+ assigns: prepare_prompt_view_assigns,
2121
2254
  invocation_paths: invocation_paths
2122
2255
  ))
2123
2256
  end
@@ -2146,6 +2279,27 @@ module LittleGhost
2146
2279
  LittleGhost::PromptResolver.new(paths:)
2147
2280
  end
2148
2281
 
2282
+ public
2283
+
2284
+ def render_framework_prompt(key, **locals) # :nodoc:
2285
+ @framework_prompts.render(
2286
+ key,
2287
+ locals:,
2288
+ invocation_paths: @framework_prompt_invocation_paths,
2289
+ agent_path: self.class.logical_path
2290
+ )
2291
+ end
2292
+
2293
+ def framework_prompt_scope # :nodoc:
2294
+ {
2295
+ framework_prompts: @framework_prompts,
2296
+ invocation_paths: @framework_prompt_invocation_paths,
2297
+ agent_path: self.class.logical_path
2298
+ }.freeze
2299
+ end
2300
+
2301
+ private
2302
+
2149
2303
  def apply_cancellation_decision!(decision)
2150
2304
  raise CancelledError, decision.reason if decision.cancel?
2151
2305
  end
@@ -2305,7 +2459,13 @@ module LittleGhost
2305
2459
  end
2306
2460
 
2307
2461
  def truncated_tool_result(content)
2308
- Support::OutputTruncation.truncate_middle_with_token_budget(content, @max_tool_result_tokens).first
2462
+ Support::OutputTruncation.truncate_middle_with_token_budget(
2463
+ content,
2464
+ @max_tool_result_tokens,
2465
+ framework_prompts: @framework_prompts,
2466
+ invocation_paths: @framework_prompt_invocation_paths,
2467
+ agent_path: self.class.logical_path
2468
+ ).first
2309
2469
  end
2310
2470
 
2311
2471
  def monotonic_time