little_ghost 0.1.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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +122 -0
  4. data/docs/guides/Core Concepts.md +203 -0
  5. data/docs/guides/Getting Started.md +187 -0
  6. data/lib/little_ghost/ag_ui/adapter.rb +194 -0
  7. data/lib/little_ghost/ag_ui.rb +5 -0
  8. data/lib/little_ghost/agent/context_management.rb +285 -0
  9. data/lib/little_ghost/agent/delegation.rb +128 -0
  10. data/lib/little_ghost/agent/skills.rb +96 -0
  11. data/lib/little_ghost/agent/tool_loop.rb +239 -0
  12. data/lib/little_ghost/agent.rb +2111 -0
  13. data/lib/little_ghost/agent_builder.rb +191 -0
  14. data/lib/little_ghost/agent_interruptions.rb +197 -0
  15. data/lib/little_ghost/configuration.rb +337 -0
  16. data/lib/little_ghost/content.rb +324 -0
  17. data/lib/little_ghost/default_model_registry.rb +71 -0
  18. data/lib/little_ghost/errors.rb +48 -0
  19. data/lib/little_ghost/events.rb +264 -0
  20. data/lib/little_ghost/execution_state.rb +58 -0
  21. data/lib/little_ghost/instrumentation.rb +475 -0
  22. data/lib/little_ghost/invocation.rb +285 -0
  23. data/lib/little_ghost/lookup.rb +37 -0
  24. data/lib/little_ghost/mcp/client.rb +396 -0
  25. data/lib/little_ghost/mcp.rb +5 -0
  26. data/lib/little_ghost/message.rb +75 -0
  27. data/lib/little_ghost/model.rb +88 -0
  28. data/lib/little_ghost/model_capabilities.rb +126 -0
  29. data/lib/little_ghost/model_registry.rb +173 -0
  30. data/lib/little_ghost/model_request.rb +107 -0
  31. data/lib/little_ghost/model_response.rb +48 -0
  32. data/lib/little_ghost/path_set.rb +32 -0
  33. data/lib/little_ghost/prompt_resolver.rb +251 -0
  34. data/lib/little_ghost/providers/bedrock.rb +506 -0
  35. data/lib/little_ghost/providers/http_transport.rb +149 -0
  36. data/lib/little_ghost/providers/open_router.rb +171 -0
  37. data/lib/little_ghost/providers/openai.rb +27 -0
  38. data/lib/little_ghost/providers/openai_compatible.rb +745 -0
  39. data/lib/little_ghost/providers/sse_parser.rb +35 -0
  40. data/lib/little_ghost/run.rb +607 -0
  41. data/lib/little_ghost/run_context.rb +129 -0
  42. data/lib/little_ghost/run_result.rb +111 -0
  43. data/lib/little_ghost/runtime/hook.rb +31 -0
  44. data/lib/little_ghost/runtime.rb +392 -0
  45. data/lib/little_ghost/sandbox.rb +138 -0
  46. data/lib/little_ghost/session.rb +229 -0
  47. data/lib/little_ghost/session_store.rb +96 -0
  48. data/lib/little_ghost/session_stores/agent_core_memory.rb +1086 -0
  49. data/lib/little_ghost/session_stores/memory.rb +86 -0
  50. data/lib/little_ghost/skills/catalog.rb +283 -0
  51. data/lib/little_ghost/skills/skill.rb +60 -0
  52. data/lib/little_ghost/skills.rb +4 -0
  53. data/lib/little_ghost/stream_event.rb +49 -0
  54. data/lib/little_ghost/structured_output.rb +126 -0
  55. data/lib/little_ghost/subagents/agent_path.rb +63 -0
  56. data/lib/little_ghost/subagents/definition.rb +42 -0
  57. data/lib/little_ghost/subagents/manager.rb +1615 -0
  58. data/lib/little_ghost/support/callbacks.rb +151 -0
  59. data/lib/little_ghost/support/cancellation_token.rb +86 -0
  60. data/lib/little_ghost/support/class_attributes.rb +40 -0
  61. data/lib/little_ghost/support/content_capture.rb +150 -0
  62. data/lib/little_ghost/support/executor.rb +75 -0
  63. data/lib/little_ghost/support/interruptible_stream.rb +103 -0
  64. data/lib/little_ghost/support/loader.rb +263 -0
  65. data/lib/little_ghost/support/output_truncation.rb +71 -0
  66. data/lib/little_ghost/support/redactor.rb +66 -0
  67. data/lib/little_ghost/support.rb +34 -0
  68. data/lib/little_ghost/tool.rb +448 -0
  69. data/lib/little_ghost/tool_execution.rb +59 -0
  70. data/lib/little_ghost/tool_registry.rb +156 -0
  71. data/lib/little_ghost/tools/filesystem.rb +119 -0
  72. data/lib/little_ghost/tools/shell.rb +45 -0
  73. data/lib/little_ghost/tools/write_todos.rb +91 -0
  74. data/lib/little_ghost/tools.rb +6 -0
  75. data/lib/little_ghost/tracing/open_telemetry.rb +517 -0
  76. data/lib/little_ghost/unrestricted_sandbox.rb +306 -0
  77. data/lib/little_ghost/usage.rb +47 -0
  78. data/lib/little_ghost/version.rb +6 -0
  79. data/lib/little_ghost/workflow.rb +351 -0
  80. data/lib/little_ghost/workspace.rb +31 -0
  81. data/lib/little_ghost.rb +120 -0
  82. metadata +225 -0
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LittleGhost
4
+ # Ready-made model-facing tools for filesystem, process, and planning work.
5
+ # Each tool uses the same binding and sandbox rules as an application tool.
6
+ module Tools
7
+ # Filesystem gives an agent read, list, write, and replace tools backed by the
8
+ # application's Sandbox. A read-only sandbox automatically exposes only the
9
+ # non-mutating tools.
10
+ #
11
+ # binding = LittleGhost::Tool::Binding.new(sandbox: sandbox)
12
+ # registry = LittleGhost::ToolRegistry.new(
13
+ # [LittleGhost::Tools::Filesystem], binding: binding
14
+ # )
15
+ # registry.names # => ["read_file", "list_files"]
16
+ #
17
+ # === Security and trust
18
+ #
19
+ # These tools do not add isolation. The configured sandbox must enforce path
20
+ # containment, permissions, size limits, and cancellation.
21
+ class Filesystem
22
+ # Reads one UTF-8 text file through the configured sandbox.
23
+ class ReadFile < Tool
24
+ tool_name "read_file"
25
+ description "Read a UTF-8 text file within the configured workspace."
26
+ input_schema type: "object", properties: {path: {type: "string"}}, required: ["path"], additionalProperties: false
27
+
28
+ # Returns the UTF-8 text at <tt>input["path"]</tt> through the sandbox.
29
+ def call(input)
30
+ sandbox.read(input.fetch("path"), context:)
31
+ end
32
+ end
33
+
34
+ # Lists one directory through the configured sandbox.
35
+ class ListFiles < Tool
36
+ tool_name "list_files"
37
+ description "List files and directories within the configured workspace."
38
+ input_schema type: "object", properties: {path: {type: "string"}}, additionalProperties: false
39
+
40
+ # Returns the listing for <tt>input["path"]</tt>, or the workspace root
41
+ # when +path+ is omitted.
42
+ def call(input)
43
+ sandbox.list(input.fetch("path", "."), context:)
44
+ end
45
+ end
46
+
47
+ # Writes one UTF-8 text file through a writable sandbox.
48
+ class WriteFile < Tool
49
+ tool_name "write_file"
50
+ description "Write a UTF-8 text file within the configured writable workspace."
51
+ input_schema type: "object", properties: {
52
+ path: {type: "string"}, content: {type: "string"}
53
+ }, required: %w[path content], additionalProperties: false
54
+
55
+ # Writes <tt>input["content"]</tt> to <tt>input["path"]</tt> through the
56
+ # sandbox.
57
+ def call(input)
58
+ sandbox.write(input.fetch("path"), input.fetch("content"), context:)
59
+ end
60
+ end
61
+
62
+ # Replaces one unique text occurrence through a writable sandbox.
63
+ class ReplaceInFile < Tool
64
+ tool_name "replace_in_file"
65
+ description "Replace one unique occurrence of text in a UTF-8 file within a configured writable workspace."
66
+ input_schema type: "object", properties: {
67
+ path: {type: "string"}, old_text: {type: "string"}, new_text: {type: "string"}
68
+ }, required: %w[path old_text new_text], additionalProperties: false
69
+
70
+ # Replaces the one matching +old_text+ occurrence at +path+.
71
+ def call(input)
72
+ sandbox.replace(input.fetch("path"), input.fetch("old_text"), input.fetch("new_text"), context:)
73
+ end
74
+ end
75
+
76
+ class ExclusiveReadFile < ReadFile # :nodoc:
77
+ exclusive true
78
+ end
79
+
80
+ class ExclusiveListFiles < ListFiles # :nodoc:
81
+ exclusive true
82
+ end
83
+
84
+ class ExclusiveWriteFile < WriteFile # :nodoc:
85
+ exclusive true
86
+ end
87
+
88
+ class ExclusiveReplaceInFile < ReplaceInFile # :nodoc:
89
+ exclusive true
90
+ end
91
+
92
+ # Selects the tool classes allowed by +binding.sandbox+.
93
+ class << self
94
+ # Provides read tools for every sandbox and mutation tools only when the
95
+ # sandbox is writable.
96
+ def tools(binding)
97
+ [*read_tools, *(write_tools if binding.sandbox.writable?)]
98
+ end
99
+
100
+ private
101
+
102
+ def read_tools = [ReadFile, ListFiles]
103
+ def write_tools = [WriteFile, ReplaceInFile]
104
+ end
105
+
106
+ # Provides filesystem tools marked exclusive for shared workspace mutation.
107
+ # Read operations are also serialized so a batch cannot observe a concurrent
108
+ # write from another tool call in the same run.
109
+ class Exclusive < self
110
+ class << self
111
+ private
112
+
113
+ def read_tools = [ExclusiveReadFile, ExclusiveListFiles]
114
+ def write_tools = [ExclusiveWriteFile, ExclusiveReplaceInFile]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module LittleGhost
6
+ module Tools
7
+ # Shell lets an agent run one executable through the configured Sandbox. It
8
+ # accepts an argument vector, so model-supplied values are not interpreted as
9
+ # shell syntax.
10
+ #
11
+ # The child environment is cleared, runtime is limited to 30 seconds, and
12
+ # each output stream is limited to 1 MB. These defaults reduce accidental
13
+ # exposure but do not create an isolation boundary; the configured sandbox
14
+ # remains responsible for security.
15
+ class Shell < Tool
16
+ DEFAULT_TIMEOUT = 30 # :nodoc:
17
+ MAX_OUTPUT_BYTES = 1_000_000 # :nodoc:
18
+
19
+ tool_name "shell"
20
+ description "Run one executable with arguments in the configured workspace. Shell syntax is not interpreted."
21
+ input_schema type: "object", properties: {
22
+ command: {type: "array", items: {type: "string"}}
23
+ }, required: ["command"], additionalProperties: false
24
+
25
+ # Executes <tt>input["command"]</tt> and returns a JSON result containing
26
+ # standard output, standard error, exit status, and success.
27
+ def call(input)
28
+ result = sandbox.execute_program(
29
+ input.fetch("command"),
30
+ timeout: DEFAULT_TIMEOUT,
31
+ context:,
32
+ max_output_bytes: MAX_OUTPUT_BYTES,
33
+ environment: {},
34
+ inherit_environment: false
35
+ )
36
+ JSON.generate(
37
+ stdout: result.stdout,
38
+ stderr: result.stderr,
39
+ exit_status: result.exit_code,
40
+ success: result.success?
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LittleGhost
4
+ module Tools
5
+ # WriteTodos lets an agent share a live plan with the application and the
6
+ # person following its work. Add it like any other tool:
7
+ #
8
+ # class ResearchAgent < LittleGhost::Agent
9
+ # tools LittleGhost::Tools::WriteTodos
10
+ # end
11
+ #
12
+ # Each execution replaces the full plan in RunContext state. Todo IDs remain stable across
13
+ # updates, statuses are +pending+, +in_progress+, or +completed+, and no more
14
+ # than one todo may be in progress. When no context is available, the tool
15
+ # retains fallback state on its instance.
16
+ class WriteTodos < Tool
17
+ tool_name "write_todos"
18
+ description <<~DESCRIPTION.strip
19
+ Replace the plan for meaningful multi-step work. Skip this for simple requests. Give the plan and every todo a
20
+ short user-facing title. Put longer private working notes in details. Publish the full list before substantive
21
+ work, allow at most one in-progress step, and replace the full list as work progresses. Preserve IDs for todos
22
+ that remain, update titles and statuses in place, never reuse an ID for a different todo, and remove todos that
23
+ no longer apply. Complete every remaining todo before the final response; use an empty list when no plan remains.
24
+ DESCRIPTION
25
+ input_schema(
26
+ type: "object",
27
+ properties: {
28
+ plan_title: {type: "string", minLength: 1, maxLength: 80, pattern: "^[^\\x00-\\x1f\\x7f]+$"},
29
+ todos: {
30
+ type: "array",
31
+ maxItems: 20,
32
+ items: {
33
+ type: "object",
34
+ properties: {
35
+ id: {type: "string", minLength: 1, maxLength: 64, pattern: "^[a-z0-9][a-z0-9_-]*$"},
36
+ title: {type: "string", minLength: 1, maxLength: 48, pattern: "^[^\\x00-\\x1f\\x7f]+$"},
37
+ status: {type: "string", enum: %w[pending in_progress completed]},
38
+ details: {type: ["string", "null"], maxLength: 4_000}
39
+ },
40
+ required: %w[id title status],
41
+ additionalProperties: false
42
+ }
43
+ }
44
+ },
45
+ required: %w[plan_title todos],
46
+ additionalProperties: false
47
+ )
48
+
49
+ # Trims user-facing titles before normal tool validation and execution.
50
+ def execute(input, context: nil)
51
+ super(normalize_titles(input), context:)
52
+ end
53
+
54
+ # Replaces the stored plan after enforcing progress and ID invariants.
55
+ def call(input)
56
+ todos = input.fetch("todos")
57
+ raise ToolError, "only one todo may be in progress" if todos.count { |todo| todo["status"] == "in_progress" } > 1
58
+
59
+ ids = todos.map { |todo| todo.fetch("id") }
60
+ raise ToolError, "todo IDs must be unique" unless ids.uniq.length == ids.length
61
+
62
+ state = context ? (context.state["little_ghost.plan"] ||= empty_plan) : (@fallback_state ||= empty_plan)
63
+ state.replace("plan_title" => input.fetch("plan_title"), "todos" => todos.map(&:dup))
64
+ state.dup
65
+ end
66
+
67
+ private
68
+
69
+ def normalize_titles(input)
70
+ return input unless input.is_a?(Hash)
71
+
72
+ normalized = input.dup
73
+ normalized["plan_title"] = normalized["plan_title"].strip if normalized["plan_title"].is_a?(String)
74
+ if normalized["todos"].is_a?(Array)
75
+ normalized["todos"] = normalized["todos"].map do |todo|
76
+ next todo unless todo.is_a?(Hash)
77
+
78
+ todo = todo.dup
79
+ todo["title"] = todo["title"].strip if todo["title"].is_a?(String)
80
+ todo
81
+ end
82
+ end
83
+ normalized
84
+ end
85
+
86
+ def empty_plan
87
+ {"plan_title" => nil, "todos" => []}
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loads LittleGhost's dependency-free filesystem and shell tool adapters.
4
+ # Requiring +little_ghost+ alone does not load these model-facing tools.
5
+ require_relative "tools/filesystem"
6
+ require_relative "tools/shell"