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.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/docs/guides/Core Concepts.md +203 -0
- data/docs/guides/Getting Started.md +187 -0
- data/lib/little_ghost/ag_ui/adapter.rb +194 -0
- data/lib/little_ghost/ag_ui.rb +5 -0
- data/lib/little_ghost/agent/context_management.rb +285 -0
- data/lib/little_ghost/agent/delegation.rb +128 -0
- data/lib/little_ghost/agent/skills.rb +96 -0
- data/lib/little_ghost/agent/tool_loop.rb +239 -0
- data/lib/little_ghost/agent.rb +2111 -0
- data/lib/little_ghost/agent_builder.rb +191 -0
- data/lib/little_ghost/agent_interruptions.rb +197 -0
- data/lib/little_ghost/configuration.rb +337 -0
- data/lib/little_ghost/content.rb +324 -0
- data/lib/little_ghost/default_model_registry.rb +71 -0
- data/lib/little_ghost/errors.rb +48 -0
- data/lib/little_ghost/events.rb +264 -0
- data/lib/little_ghost/execution_state.rb +58 -0
- data/lib/little_ghost/instrumentation.rb +475 -0
- data/lib/little_ghost/invocation.rb +285 -0
- data/lib/little_ghost/lookup.rb +37 -0
- data/lib/little_ghost/mcp/client.rb +396 -0
- data/lib/little_ghost/mcp.rb +5 -0
- data/lib/little_ghost/message.rb +75 -0
- data/lib/little_ghost/model.rb +88 -0
- data/lib/little_ghost/model_capabilities.rb +126 -0
- data/lib/little_ghost/model_registry.rb +173 -0
- data/lib/little_ghost/model_request.rb +107 -0
- data/lib/little_ghost/model_response.rb +48 -0
- data/lib/little_ghost/path_set.rb +32 -0
- data/lib/little_ghost/prompt_resolver.rb +251 -0
- data/lib/little_ghost/providers/bedrock.rb +506 -0
- data/lib/little_ghost/providers/http_transport.rb +149 -0
- data/lib/little_ghost/providers/open_router.rb +171 -0
- data/lib/little_ghost/providers/openai.rb +27 -0
- data/lib/little_ghost/providers/openai_compatible.rb +745 -0
- data/lib/little_ghost/providers/sse_parser.rb +35 -0
- data/lib/little_ghost/run.rb +607 -0
- data/lib/little_ghost/run_context.rb +129 -0
- data/lib/little_ghost/run_result.rb +111 -0
- data/lib/little_ghost/runtime/hook.rb +31 -0
- data/lib/little_ghost/runtime.rb +392 -0
- data/lib/little_ghost/sandbox.rb +138 -0
- data/lib/little_ghost/session.rb +229 -0
- data/lib/little_ghost/session_store.rb +96 -0
- data/lib/little_ghost/session_stores/agent_core_memory.rb +1086 -0
- data/lib/little_ghost/session_stores/memory.rb +86 -0
- data/lib/little_ghost/skills/catalog.rb +283 -0
- data/lib/little_ghost/skills/skill.rb +60 -0
- data/lib/little_ghost/skills.rb +4 -0
- data/lib/little_ghost/stream_event.rb +49 -0
- data/lib/little_ghost/structured_output.rb +126 -0
- data/lib/little_ghost/subagents/agent_path.rb +63 -0
- data/lib/little_ghost/subagents/definition.rb +42 -0
- data/lib/little_ghost/subagents/manager.rb +1615 -0
- data/lib/little_ghost/support/callbacks.rb +151 -0
- data/lib/little_ghost/support/cancellation_token.rb +86 -0
- data/lib/little_ghost/support/class_attributes.rb +40 -0
- data/lib/little_ghost/support/content_capture.rb +150 -0
- data/lib/little_ghost/support/executor.rb +75 -0
- data/lib/little_ghost/support/interruptible_stream.rb +103 -0
- data/lib/little_ghost/support/loader.rb +263 -0
- data/lib/little_ghost/support/output_truncation.rb +71 -0
- data/lib/little_ghost/support/redactor.rb +66 -0
- data/lib/little_ghost/support.rb +34 -0
- data/lib/little_ghost/tool.rb +448 -0
- data/lib/little_ghost/tool_execution.rb +59 -0
- data/lib/little_ghost/tool_registry.rb +156 -0
- data/lib/little_ghost/tools/filesystem.rb +119 -0
- data/lib/little_ghost/tools/shell.rb +45 -0
- data/lib/little_ghost/tools/write_todos.rb +91 -0
- data/lib/little_ghost/tools.rb +6 -0
- data/lib/little_ghost/tracing/open_telemetry.rb +517 -0
- data/lib/little_ghost/unrestricted_sandbox.rb +306 -0
- data/lib/little_ghost/usage.rb +47 -0
- data/lib/little_ghost/version.rb +6 -0
- data/lib/little_ghost/workflow.rb +351 -0
- data/lib/little_ghost/workspace.rb +31 -0
- data/lib/little_ghost.rb +120 -0
- 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
|