little_ghost 0.3.0 → 0.4.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 +4 -4
- data/README.md +68 -84
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +126 -231
- data/docs/guides/getting_started.md +114 -87
- data/docs/guides/production.md +187 -0
- data/docs/guides/prompt_views.md +132 -0
- data/lib/little_ghost/ag_ui/adapter.rb +3 -3
- data/lib/little_ghost/agent/delegation.rb +1 -1
- data/lib/little_ghost/agent.rb +167 -172
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +55 -21
- data/lib/little_ghost/assembly_builder.rb +40 -2
- data/lib/little_ghost/assembly_execution.rb +87 -4
- data/lib/little_ghost/configuration.rb +263 -39
- data/lib/little_ghost/content.rb +5 -5
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +2 -2
- data/lib/little_ghost/execution.rb +32 -32
- data/lib/little_ghost/graph.rb +22 -3
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_resolver.rb +2 -2
- data/lib/little_ghost/prompt_resolver.rb +2 -0
- data/lib/little_ghost/run.rb +87 -49
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/runtime/hook.rb +3 -3
- data/lib/little_ghost/runtime.rb +71 -31
- data/lib/little_ghost/session.rb +12 -23
- data/lib/little_ghost/session_store.rb +9 -5
- data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
- data/lib/little_ghost/session_stores/filesystem.rb +261 -0
- data/lib/little_ghost/session_stores/memory.rb +7 -0
- data/lib/little_ghost/subagents/manager.rb +42 -42
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +56 -14
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +30 -21
- data/lib/little_ghost.rb +29 -25
- metadata +7 -2
|
@@ -21,11 +21,11 @@ module LittleGhost
|
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
23
|
# LittleGhost then gives +CustomerSupportAgent+ tools to spawn, message, wait for,
|
|
24
|
-
#
|
|
24
|
+
# interject, and list research agents. The manager keeps each child identity
|
|
25
25
|
# stable across follow-up turns.
|
|
26
26
|
#
|
|
27
|
-
# Follow-up messages are FIFO turns and never
|
|
28
|
-
# #
|
|
27
|
+
# Follow-up messages are FIFO turns and never interject active work.
|
|
28
|
+
# #interject is the separate synchronous path for delivery at the next model
|
|
29
29
|
# boundary; delivery does not stop the child, and tool calls from that model
|
|
30
30
|
# response continue in the child run.
|
|
31
31
|
#
|
|
@@ -58,16 +58,16 @@ module LittleGhost
|
|
|
58
58
|
CURSOR_MAX_BYTES = 512 # :nodoc:
|
|
59
59
|
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/ # :nodoc:
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
InterjectionExchange = Struct.new(:message, :response, :complete) # :nodoc:
|
|
62
62
|
Turn = Struct.new( # :nodoc:
|
|
63
63
|
:number,
|
|
64
64
|
:message,
|
|
65
65
|
:completion,
|
|
66
66
|
:operation_id,
|
|
67
67
|
:parent_operation_id,
|
|
68
|
-
:
|
|
69
|
-
:
|
|
70
|
-
:
|
|
68
|
+
:interjections,
|
|
69
|
+
:interjection_metadata,
|
|
70
|
+
:interjection_ids
|
|
71
71
|
)
|
|
72
72
|
Identity = Struct.new( # :nodoc:
|
|
73
73
|
:subagent_id,
|
|
@@ -368,7 +368,7 @@ module LittleGhost
|
|
|
368
368
|
# model response. The returned +response_disposition+ says whether that
|
|
369
369
|
# response also initiated tool calls; it does not imply the subagent has
|
|
370
370
|
# stopped.
|
|
371
|
-
def
|
|
371
|
+
def interject(subagent_id:, message:, cancellation_token: @cancellation_token, deadline: @deadline)
|
|
372
372
|
unless message.is_a?(String)
|
|
373
373
|
raise ToolError, "Subagent messages must be strings."
|
|
374
374
|
end
|
|
@@ -376,30 +376,30 @@ module LittleGhost
|
|
|
376
376
|
raise ToolError, "Subagent messages cannot exceed #{@max_message_chars} characters."
|
|
377
377
|
end
|
|
378
378
|
|
|
379
|
-
exchange =
|
|
379
|
+
exchange = InterjectionExchange.new(message:, complete: false)
|
|
380
380
|
identity, turn = @mutex.synchronize do
|
|
381
381
|
ensure_open!
|
|
382
382
|
value = fetch_identity!(subagent_id)
|
|
383
|
-
unless value.agent.respond_to?(:
|
|
384
|
-
raise ToolError, "Subagent #{subagent_id.inspect} does not support
|
|
383
|
+
unless value.agent.respond_to?(:interject)
|
|
384
|
+
raise ToolError, "Subagent #{subagent_id.inspect} does not support interjections."
|
|
385
385
|
end
|
|
386
386
|
unless value.status == "running"
|
|
387
387
|
raise ToolError, "Subagent #{subagent_id.inspect} is not currently running."
|
|
388
388
|
end
|
|
389
|
-
if value.current.
|
|
390
|
-
raise ToolError, "Subagent #{subagent_id.inspect} has reached its
|
|
389
|
+
if value.current.interjections.length >= @max_queued_turns_per_identity
|
|
390
|
+
raise ToolError, "Subagent #{subagent_id.inspect} has reached its interject limit."
|
|
391
391
|
end
|
|
392
|
-
|
|
393
|
-
if
|
|
394
|
-
raise ToolError, "Subagent
|
|
392
|
+
interjection_chars = value.current.interjections.sum { |pending| pending.message.length }
|
|
393
|
+
if interjection_chars + message.length > @max_message_chars
|
|
394
|
+
raise ToolError, "Subagent interjection messages cannot exceed #{@max_message_chars} total characters."
|
|
395
395
|
end
|
|
396
396
|
|
|
397
|
-
value.current.
|
|
397
|
+
value.current.interjections << exchange
|
|
398
398
|
[value, value.current]
|
|
399
399
|
end
|
|
400
400
|
|
|
401
|
-
|
|
402
|
-
identity.agent.
|
|
401
|
+
result = begin
|
|
402
|
+
identity.agent.interject(
|
|
403
403
|
message,
|
|
404
404
|
cancellation_token:,
|
|
405
405
|
deadline:,
|
|
@@ -407,16 +407,16 @@ module LittleGhost
|
|
|
407
407
|
)
|
|
408
408
|
rescue
|
|
409
409
|
@mutex.synchronize do
|
|
410
|
-
turn.
|
|
410
|
+
turn.interjections.delete(exchange)
|
|
411
411
|
@condition.broadcast
|
|
412
412
|
end
|
|
413
413
|
raise
|
|
414
414
|
end
|
|
415
|
-
response =
|
|
415
|
+
response = result.text
|
|
416
416
|
truncated = response.length > @max_response_chars
|
|
417
417
|
returned_response = truncated ? response[0, @max_response_chars] : response
|
|
418
418
|
@mutex.synchronize do
|
|
419
|
-
used_response_chars = turn.
|
|
419
|
+
used_response_chars = turn.interjections.sum do |pending|
|
|
420
420
|
pending.equal?(exchange) ? 0 : pending.response.to_s.length
|
|
421
421
|
end
|
|
422
422
|
remaining_response_chars = [@max_response_chars - used_response_chars, 0].max
|
|
@@ -428,17 +428,17 @@ module LittleGhost
|
|
|
428
428
|
snapshot(identity, include_response: true, include_progress: true)
|
|
429
429
|
end
|
|
430
430
|
value = {
|
|
431
|
-
status: "
|
|
431
|
+
status: "interjection_delivered",
|
|
432
432
|
subagent_id: identity.subagent_id,
|
|
433
433
|
kind: identity.definition.kind,
|
|
434
434
|
subagent:,
|
|
435
435
|
turn: turn.number,
|
|
436
436
|
response: returned_response,
|
|
437
|
-
response_disposition:
|
|
437
|
+
response_disposition: result.tool_calls? ? "text_with_tool_calls" : "text_only"
|
|
438
438
|
}
|
|
439
439
|
value[:response_truncated] = true if truncated
|
|
440
440
|
value
|
|
441
|
-
rescue
|
|
441
|
+
rescue AgentInterjectionError => error
|
|
442
442
|
raise ToolError, error.message
|
|
443
443
|
end
|
|
444
444
|
|
|
@@ -504,7 +504,7 @@ module LittleGhost
|
|
|
504
504
|
end
|
|
505
505
|
end
|
|
506
506
|
|
|
507
|
-
# Builds spawn, follow-up,
|
|
507
|
+
# Builds spawn, follow-up, interject, wait, and list tools bound to this
|
|
508
508
|
# manager. Closing the first tool closes the shared manager.
|
|
509
509
|
def tools
|
|
510
510
|
manager = self
|
|
@@ -559,8 +559,8 @@ module LittleGhost
|
|
|
559
559
|
description: <<~DESCRIPTION.strip,
|
|
560
560
|
Send a follow-up turn to an existing active or persisted subagent identity. Persisted conversations
|
|
561
561
|
are restored transparently before the follow-up. Messages are processed in order after the
|
|
562
|
-
current turn and never
|
|
563
|
-
finalization; use
|
|
562
|
+
current turn and never interject active work. Do not use this for status, steering, stopping, or
|
|
563
|
+
finalization; use interject_subagent for an active subagent. Mode controls delivery: sync waits for the
|
|
564
564
|
later turn's response, while async enqueues the turn and returns immediately.
|
|
565
565
|
DESCRIPTION
|
|
566
566
|
input_schema: {
|
|
@@ -586,7 +586,7 @@ module LittleGhost
|
|
|
586
586
|
)
|
|
587
587
|
end,
|
|
588
588
|
Tool.define(
|
|
589
|
-
name: "
|
|
589
|
+
name: "interject_subagent",
|
|
590
590
|
description: <<~DESCRIPTION.strip,
|
|
591
591
|
Interrupt an actively running subagent in its current turn. The message is added at the next model
|
|
592
592
|
boundary. This call waits for that model response and reports its ordinary text, whether the same
|
|
@@ -606,7 +606,7 @@ module LittleGhost
|
|
|
606
606
|
options = {}
|
|
607
607
|
options[:cancellation_token] = context.cancellation_token if context
|
|
608
608
|
options[:deadline] = context.deadline if context&.deadline
|
|
609
|
-
manager.
|
|
609
|
+
manager.interject(
|
|
610
610
|
subagent_id: input.fetch("subagent_id"),
|
|
611
611
|
message: input.fetch("message"),
|
|
612
612
|
**options
|
|
@@ -1089,9 +1089,9 @@ module LittleGhost
|
|
|
1089
1089
|
completion: Completion.new,
|
|
1090
1090
|
operation_id: SecureRandom.uuid,
|
|
1091
1091
|
parent_operation_id:,
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1092
|
+
interjections: [],
|
|
1093
|
+
interjection_metadata: context&.interjection_metadata,
|
|
1094
|
+
interjection_ids: context&.interjection_ids || []
|
|
1095
1095
|
)
|
|
1096
1096
|
identity.next_turn += 1
|
|
1097
1097
|
@turn_count += 1 if count_turn
|
|
@@ -1157,8 +1157,8 @@ module LittleGhost
|
|
|
1157
1157
|
options[:history] = identity.history
|
|
1158
1158
|
options[:context] = identity.state
|
|
1159
1159
|
options[:conversation_id] = identity.conversation_id
|
|
1160
|
-
options[:
|
|
1161
|
-
options[:
|
|
1160
|
+
options[:interjection_metadata] = turn.interjection_metadata
|
|
1161
|
+
options[:interjection_ids] = turn.interjection_ids
|
|
1162
1162
|
end
|
|
1163
1163
|
result = if identity.agent.is_a?(Agent)
|
|
1164
1164
|
run_agent_turn(identity, turn, options)
|
|
@@ -1284,8 +1284,8 @@ module LittleGhost
|
|
|
1284
1284
|
response = serialized_response[0, @max_response_chars] if truncated
|
|
1285
1285
|
persisted_response = response.is_a?(String) ? response : serialized_response
|
|
1286
1286
|
|
|
1287
|
-
|
|
1288
|
-
while identity.durable && turn.
|
|
1287
|
+
interject_exchanges = @mutex.synchronize do
|
|
1288
|
+
while identity.durable && turn.interjections.any? { |exchange| !exchange.complete } && !@closed
|
|
1289
1289
|
@condition.wait(@mutex, CANCELLATION_POLL_INTERVAL)
|
|
1290
1290
|
end
|
|
1291
1291
|
if @closed
|
|
@@ -1294,11 +1294,11 @@ module LittleGhost
|
|
|
1294
1294
|
end
|
|
1295
1295
|
|
|
1296
1296
|
identity.status = "persisting" if identity.durable
|
|
1297
|
-
turn.
|
|
1297
|
+
turn.interjections.select(&:complete).map { |exchange| [exchange.message, exchange.response] }
|
|
1298
1298
|
end
|
|
1299
|
-
return unless
|
|
1299
|
+
return unless interject_exchanges
|
|
1300
1300
|
|
|
1301
|
-
retain_agent_conversation(identity, turn, result, persisted_response,
|
|
1301
|
+
retain_agent_conversation(identity, turn, result, persisted_response, interject_exchanges)
|
|
1302
1302
|
|
|
1303
1303
|
@mutex.synchronize do
|
|
1304
1304
|
identity.latest_turn = turn.number
|
|
@@ -1323,11 +1323,11 @@ module LittleGhost
|
|
|
1323
1323
|
end
|
|
1324
1324
|
end
|
|
1325
1325
|
|
|
1326
|
-
def retain_agent_conversation(identity, turn, result, persisted_response,
|
|
1326
|
+
def retain_agent_conversation(identity, turn, result, persisted_response, interject_exchanges)
|
|
1327
1327
|
if identity.durable
|
|
1328
1328
|
state = result.is_a?(RunResult) ? result.state : identity.state
|
|
1329
1329
|
messages = [Message.new(role: :user, content: turn.message)]
|
|
1330
|
-
|
|
1330
|
+
interject_exchanges.each do |message, response|
|
|
1331
1331
|
messages << Message.new(role: :user, content: message)
|
|
1332
1332
|
messages << Message.new(role: :assistant, content: response)
|
|
1333
1333
|
end
|
data/lib/little_ghost/swarm.rb
CHANGED
|
@@ -7,8 +7,8 @@ module LittleGhost
|
|
|
7
7
|
# Lets configured Agent members hand one request directly to one another.
|
|
8
8
|
#
|
|
9
9
|
# A swarm is an Assembly for model-selected routing. One member is active at a
|
|
10
|
-
# time. It either produces the final answer or
|
|
11
|
-
# to
|
|
10
|
+
# time. It either produces the final answer or uses a model-visible handoff
|
|
11
|
+
# tool to choose one of the next members allowed by the application.
|
|
12
12
|
#
|
|
13
13
|
# class ProblemSolverSwarm < LittleGhost::Swarm
|
|
14
14
|
# member TriageAgent
|
|
@@ -23,10 +23,15 @@ module LittleGhost
|
|
|
23
23
|
# run = ProblemSolverSwarm.ask("Why was I charged twice?")
|
|
24
24
|
# run.response
|
|
25
25
|
#
|
|
26
|
+
# Call a named Swarm with
|
|
27
|
+
# ask[rdoc-ref:LittleGhost::Assembly.ask] for its final Run, or
|
|
28
|
+
# the streaming entrypoint[rdoc-ref:LittleGhost::Assembly.stream_ask] for
|
|
29
|
+
# coordination and final-response events.
|
|
30
|
+
#
|
|
26
31
|
# Swarm members are Agent definitions rather than arbitrary assemblies so a
|
|
27
32
|
# handoff remains a direct model-to-model transition. Original conversation
|
|
28
33
|
# history and application context stay isolated unless a member opts in with
|
|
29
|
-
#
|
|
34
|
+
# <tt>history: true</tt> or <tt>context: true</tt>. Streams expose coordination
|
|
30
35
|
# events and the final member response, but not intermediate model text.
|
|
31
36
|
class Swarm < Assembly
|
|
32
37
|
MAX_BUFFERED_EVENTS = 10_000 # :nodoc:
|
|
@@ -91,7 +96,10 @@ module LittleGhost
|
|
|
91
96
|
self.swarm_max_steps_value = value
|
|
92
97
|
end
|
|
93
98
|
|
|
94
|
-
# Reads or assigns the
|
|
99
|
+
# Reads or assigns how often the same directed handoff may repeat.
|
|
100
|
+
#
|
|
101
|
+
# For example, a value of +2+ allows the transition from triage to billing
|
|
102
|
+
# twice during one Swarm run. +max_steps+ still limits total member calls.
|
|
95
103
|
def max_handoff_repeats(value = nil)
|
|
96
104
|
return swarm_max_handoff_repeats_value if value.nil?
|
|
97
105
|
|
|
@@ -101,7 +109,7 @@ module LittleGhost
|
|
|
101
109
|
self.swarm_max_handoff_repeats_value = value
|
|
102
110
|
end
|
|
103
111
|
|
|
104
|
-
# Validates the
|
|
112
|
+
# Validates the members and allowed handoff routes, then returns this class.
|
|
105
113
|
def validate!
|
|
106
114
|
swarm_definition!
|
|
107
115
|
self
|
data/lib/little_ghost/tool.rb
CHANGED
|
@@ -19,20 +19,60 @@ module LittleGhost
|
|
|
19
19
|
# end
|
|
20
20
|
# end
|
|
21
21
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# class CustomerSupportAgent < LittleGhost::Agent
|
|
23
|
+
# tools TicketStatusTool
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# run = CustomerSupportAgent.ask("What is happening with ticket SUP-481?")
|
|
27
|
+
# run.response
|
|
28
|
+
#
|
|
29
|
+
# Use application context for authorization, never model-selected input:
|
|
30
|
+
#
|
|
31
|
+
# class OrderStatusTool < LittleGhost::Tool
|
|
32
|
+
# description "Look up an order for the current account."
|
|
33
|
+
# input_schema type: "object", properties: {
|
|
34
|
+
# order_number: {type: "string"}
|
|
35
|
+
# }, required: ["order_number"], additionalProperties: false
|
|
36
|
+
#
|
|
37
|
+
# def call(input)
|
|
38
|
+
# Orders.status_for(
|
|
39
|
+
# actor_id: run.invocation.actor_id,
|
|
40
|
+
# account_id: run.invocation.context.fetch("account_id"),
|
|
41
|
+
# order_number: input.fetch("order_number")
|
|
42
|
+
# )
|
|
43
|
+
# end
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# OrderSupportAgent.ask(
|
|
47
|
+
# "Where is order 481?",
|
|
48
|
+
# actor_id: authenticated_user.id,
|
|
49
|
+
# context: {account_id: authenticated_user.account_id}
|
|
50
|
+
# )
|
|
51
|
+
#
|
|
52
|
+
# These values cross different trust boundaries:
|
|
53
|
+
#
|
|
54
|
+
# [<tt>input</tt>]
|
|
55
|
+
# Arguments selected by the model. The schema checks their shape, not their
|
|
56
|
+
# permission to perform an operation.
|
|
57
|
+
# [<tt>run.invocation.context</tt>]
|
|
58
|
+
# Current request values supplied by the application. Use these for
|
|
59
|
+
# authorization after the application authenticates the caller.
|
|
60
|
+
# [<tt>context.state</tt>]
|
|
61
|
+
# Mutable working state for the run. It may include values restored from a
|
|
62
|
+
# Session, so check saved values again before trusting them.
|
|
63
|
+
# [Tool::Binding]
|
|
64
|
+
# Run-scoped objects such as the Agent, Run, Runtime, workspace, and sandbox.
|
|
65
|
+
# The Binding supplies #run; it does not contain model arguments.
|
|
25
66
|
#
|
|
26
|
-
# The class DSL produces the
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# Tool.define offers the same contract
|
|
67
|
+
# The class DSL produces the specification sent to models. During an Agent
|
|
68
|
+
# run, the tool registry creates and binds one Tool instance. Tests and custom
|
|
69
|
+
# integrations may call +execute+ directly; it validates the arguments, calls
|
|
70
|
+
# +call+, and returns an ExecutionResult. Tool.define offers the same contract
|
|
71
|
+
# for an embedded implementation.
|
|
30
72
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# implement +close+; <tt>exclusive true</tt> serializes calls against every
|
|
35
|
-
# other exclusive tool in the same run.
|
|
73
|
+
# Mutable Tool instance state belongs to one Agent run. Registries close tool
|
|
74
|
+
# instances that implement +close+; <tt>exclusive true</tt> prevents that tool
|
|
75
|
+
# from overlapping other exclusive tools in the same run.
|
|
36
76
|
#
|
|
37
77
|
# Validation and application ToolError failures become error results. A
|
|
38
78
|
# ToolError message is visible to the model and must be safe to disclose;
|
|
@@ -45,8 +85,10 @@ module LittleGhost
|
|
|
45
85
|
# A binding can be copied with selected collaborators replaced.
|
|
46
86
|
#
|
|
47
87
|
# Tool instances expose the bound agent, run, runtime, model, workspace, and
|
|
48
|
-
# sandbox through matching accessors.
|
|
49
|
-
#
|
|
88
|
+
# sandbox through matching accessors. A Binding does not carry model Tool
|
|
89
|
+
# arguments or application state; the current RunContext carries that state.
|
|
90
|
+
# ToolRegistry and Agent normally create bindings on behalf of application
|
|
91
|
+
# code.
|
|
50
92
|
class Binding
|
|
51
93
|
# Agent, run, runtime, model, workspace, and sandbox available to a tool.
|
|
52
94
|
attr_reader :agent, :run, :runtime, :model, :workspace, :sandbox
|
|
@@ -59,7 +59,12 @@ module LittleGhost
|
|
|
59
59
|
ids = todos.map { |todo| todo.fetch("id") }
|
|
60
60
|
raise ToolError, "todo IDs must be unique" unless ids.uniq.length == ids.length
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
if context
|
|
63
|
+
context.state["little_ghost.plan"] ||= empty_plan
|
|
64
|
+
state = context.state.fetch("little_ghost.plan")
|
|
65
|
+
else
|
|
66
|
+
state = @fallback_state ||= empty_plan
|
|
67
|
+
end
|
|
63
68
|
state.replace("plan_title" => input.fetch("plan_title"), "todos" => todos.map(&:dup))
|
|
64
69
|
state.dup
|
|
65
70
|
end
|
|
@@ -358,7 +358,7 @@ module LittleGhost
|
|
|
358
358
|
return unless %i[diagnostic_input diagnostic_output].include?(key.to_sym)
|
|
359
359
|
|
|
360
360
|
prefix = (key.to_sym == :diagnostic_input) ? "input" : "output"
|
|
361
|
-
if %i[agent
|
|
361
|
+
if %i[agent interjection tool].include?(kind) &&
|
|
362
362
|
!(kind == :tool && prefix == "output" && attributes[:error_type])
|
|
363
363
|
result["#{prefix}.value"] = value
|
|
364
364
|
result["#{prefix}.mime_type"] = diagnostic_mime_type(value)
|
data/lib/little_ghost/version.rb
CHANGED
|
@@ -10,16 +10,13 @@ module LittleGhost
|
|
|
10
10
|
# Agent or another coordinated Assembly. The workflow consumes intermediate
|
|
11
11
|
# answers and streams one final participant response.
|
|
12
12
|
#
|
|
13
|
-
# A support workflow can
|
|
14
|
-
#
|
|
13
|
+
# A support workflow can guarantee that research happens before the responder
|
|
14
|
+
# writes the caller-visible answer:
|
|
15
15
|
#
|
|
16
16
|
# class ResponseWorkflow < LittleGhost::Workflow
|
|
17
17
|
# private
|
|
18
18
|
#
|
|
19
19
|
# def perform
|
|
20
|
-
# route = invoke(RouterAgent).output
|
|
21
|
-
# return invoke(CustomerSupportAgent) unless route["research"]
|
|
22
|
-
#
|
|
23
20
|
# evidence = invoke(ResearchAgent).output
|
|
24
21
|
# invoke CustomerSupportAgent, input: <<~PROMPT
|
|
25
22
|
# #{input.text}
|
|
@@ -31,27 +28,31 @@ module LittleGhost
|
|
|
31
28
|
# end
|
|
32
29
|
#
|
|
33
30
|
# run = ResponseWorkflow.ask("Why is transfer 481 pending?")
|
|
34
|
-
# run.response
|
|
31
|
+
# run.response
|
|
32
|
+
# # One possible response: Transfer 481 is waiting for the receiving bank.
|
|
33
|
+
#
|
|
34
|
+
# Call a named Workflow with
|
|
35
|
+
# ask[rdoc-ref:LittleGhost::Assembly.ask] for its final Run, or
|
|
36
|
+
# the streaming entrypoint[rdoc-ref:LittleGhost::Assembly.stream_ask] for live
|
|
37
|
+
# events.
|
|
35
38
|
#
|
|
36
39
|
# +invoke+ returns a lazy Workflow::Invocation. Reading +output+ consumes an
|
|
37
40
|
# intermediate invocation and returns RunResult#output; +perform+ must return
|
|
38
41
|
# its final invocation without consuming it so those events reach the caller.
|
|
39
42
|
# Intermediate usage is added to the final result.
|
|
40
43
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# JSON-like context is copied for each
|
|
44
|
-
# from mutating a sibling's state.
|
|
45
|
-
# ArgumentError.
|
|
44
|
+
# A child receives the Workflow input unless +invoke+ supplies another one.
|
|
45
|
+
# It also inherits history, settings, cancellation, deadline, template paths,
|
|
46
|
+
# and the parent tracing relationship. JSON-like context is copied for each
|
|
47
|
+
# child, preventing one intermediate Agent from mutating a sibling's state.
|
|
48
|
+
# Non-JSON-like workflow context raises ArgumentError.
|
|
46
49
|
#
|
|
47
|
-
# A
|
|
48
|
-
# already consumed invocation, or consuming
|
|
49
|
-
#
|
|
50
|
-
# cleanup
|
|
51
|
-
# lightweight invocation wrappers in reverse declaration order. Composition
|
|
52
|
-
# errors emit an +invocation_error+ event and then re-raise.
|
|
50
|
+
# A Workflow instance streams once. Returning the wrong value, returning an
|
|
51
|
+
# already consumed invocation, or consuming one twice raises ProtocolError.
|
|
52
|
+
# A composition error fails the owning top-level Run. Each child Assembly
|
|
53
|
+
# closes after its attempt, and a cleanup failure raises from that attempt.
|
|
53
54
|
class Workflow < Assembly
|
|
54
|
-
# Hold one lazy
|
|
55
|
+
# Hold one lazy Assembly call inside a workflow composition.
|
|
55
56
|
# Workflow implementations normally use only its output method or return the
|
|
56
57
|
# object as the final invocation.
|
|
57
58
|
class Invocation
|
|
@@ -126,8 +127,10 @@ module LittleGhost
|
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
129
|
|
|
129
|
-
#
|
|
130
|
-
attr_reader :run
|
|
130
|
+
# Run that owns this run-scoped Workflow.
|
|
131
|
+
attr_reader :run
|
|
132
|
+
# Runtime used to resolve child Assemblies.
|
|
133
|
+
attr_reader :runtime
|
|
131
134
|
|
|
132
135
|
def initialize(run: nil, runtime: nil) # :nodoc:
|
|
133
136
|
super(run:, runtime:, standalone: run.nil?)
|
|
@@ -269,7 +272,9 @@ module LittleGhost
|
|
|
269
272
|
# Creates a lazy invocation for +assembly+.
|
|
270
273
|
#
|
|
271
274
|
# Intermediate calls may use +output+; the final call must be returned from
|
|
272
|
-
# +perform+ without being consumed.
|
|
275
|
+
# +perform+ without being consumed. +as+ supplies the participant name used
|
|
276
|
+
# in steps and telemetry. Retries default to zero; a positive +retries+
|
|
277
|
+
# value requires explicit exception classes in +retry_on+.
|
|
273
278
|
def invoke(
|
|
274
279
|
assembly,
|
|
275
280
|
as: nil,
|
|
@@ -299,8 +304,12 @@ module LittleGhost
|
|
|
299
304
|
invocation
|
|
300
305
|
end
|
|
301
306
|
|
|
307
|
+
# :doc:
|
|
302
308
|
# Consumes independent invocations concurrently and returns their outputs in
|
|
303
309
|
# declaration order.
|
|
310
|
+
#
|
|
311
|
+
# +max_concurrency+ bounds active child executions. A child failure cancels
|
|
312
|
+
# siblings cooperatively before the error is raised.
|
|
304
313
|
def parallel(*invocations, max_concurrency: 8)
|
|
305
314
|
raise ArgumentError, "parallel requires at least one invocation" if invocations.empty?
|
|
306
315
|
unless invocations.all? { |invocation| invocation.is_a?(Invocation) && !invocation.consumed? }
|
data/lib/little_ghost.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "little_ghost/version"
|
|
|
4
4
|
require_relative "little_ghost/errors"
|
|
5
5
|
require_relative "little_ghost/execution_state"
|
|
6
6
|
require_relative "little_ghost/support"
|
|
7
|
+
require_relative "little_ghost/data_map"
|
|
7
8
|
require_relative "little_ghost/events"
|
|
8
9
|
require_relative "little_ghost/instrumentation"
|
|
9
10
|
require_relative "little_ghost/tracing/open_telemetry"
|
|
@@ -50,6 +51,7 @@ require_relative "little_ghost/structured_output"
|
|
|
50
51
|
require_relative "little_ghost/prompt_resolver"
|
|
51
52
|
require_relative "little_ghost/session_store"
|
|
52
53
|
require_relative "little_ghost/session_stores/memory"
|
|
54
|
+
require_relative "little_ghost/session_stores/filesystem"
|
|
53
55
|
require_relative "little_ghost/session"
|
|
54
56
|
require_relative "little_ghost/skills"
|
|
55
57
|
require_relative "little_ghost/tools/write_todos"
|
|
@@ -58,7 +60,7 @@ require_relative "little_ghost/execution"
|
|
|
58
60
|
require_relative "little_ghost/subagents/definition"
|
|
59
61
|
require_relative "little_ghost/subagents/agent_path"
|
|
60
62
|
require_relative "little_ghost/subagents/manager"
|
|
61
|
-
require_relative "little_ghost/
|
|
63
|
+
require_relative "little_ghost/agent_interjections"
|
|
62
64
|
require_relative "little_ghost/assembly"
|
|
63
65
|
require_relative "little_ghost/assembly_execution"
|
|
64
66
|
require_relative "little_ghost/agent"
|
|
@@ -70,39 +72,32 @@ require_relative "little_ghost/agent_factory"
|
|
|
70
72
|
require_relative "little_ghost/runtime/hook"
|
|
71
73
|
require_relative "little_ghost/runtime"
|
|
72
74
|
|
|
73
|
-
#
|
|
74
|
-
# sit inside an existing Ruby system or
|
|
75
|
-
#
|
|
76
|
-
# cohesive set of Ruby conventions.
|
|
75
|
+
# LittleGhost is a Ruby library for building AI features with reusable agents
|
|
76
|
+
# and composable assemblies. It can sit inside an existing Ruby system or
|
|
77
|
+
# support a dedicated AI service.
|
|
77
78
|
#
|
|
78
|
-
#
|
|
79
|
-
# specialist for research while the caller observes one run:
|
|
80
|
-
#
|
|
81
|
-
# class ResearchAgent < LittleGhost::Agent
|
|
82
|
-
# description "Researches difficult support questions"
|
|
83
|
-
# model "openai:gpt-5.6-luna"
|
|
84
|
-
# end
|
|
79
|
+
# Start with one model-driven behavior:
|
|
85
80
|
#
|
|
86
81
|
# class CustomerSupportAgent < LittleGhost::Agent
|
|
87
82
|
# description "Handles support requests"
|
|
88
|
-
# model "openai
|
|
89
|
-
#
|
|
90
|
-
# subagent ResearchAgent, kind: "research"
|
|
83
|
+
# model "openrouter:openai/gpt-5.6-luna"
|
|
84
|
+
# system_prompt "Answer customer questions clearly."
|
|
91
85
|
# end
|
|
92
86
|
#
|
|
93
87
|
# run = CustomerSupportAgent.ask("Why is transfer 481 still pending?")
|
|
94
88
|
# run.completed? # => true
|
|
95
|
-
# run.response
|
|
89
|
+
# run.response
|
|
90
|
+
# # One possible response: Transfer 481 is waiting for the receiving bank.
|
|
96
91
|
#
|
|
97
|
-
# Agent subclasses hold reusable behavior
|
|
98
|
-
# and Run objects own execution and cleanup.
|
|
99
|
-
# Swarm, and Graph the same caller interface
|
|
100
|
-
# coordination policy.
|
|
92
|
+
# Agent subclasses hold reusable behavior, Invocation objects carry one request,
|
|
93
|
+
# and Run objects own execution and cleanup. When one model loop is not enough,
|
|
94
|
+
# Assembly gives Agent, Workflow, Swarm, and Graph the same caller interface
|
|
95
|
+
# while each type owns a different coordination policy.
|
|
101
96
|
#
|
|
102
97
|
# LittleGhost.configuration is process-wide unless LittleGhost.with_configuration
|
|
103
|
-
# supplies an execution-scoped replacement.
|
|
104
|
-
#
|
|
105
|
-
#
|
|
98
|
+
# supplies an execution-scoped replacement. The first standalone call lazily
|
|
99
|
+
# builds one shared Runtime from that configuration, so make application changes
|
|
100
|
+
# before invoking an Agent or Assembly.
|
|
106
101
|
module LittleGhost
|
|
107
102
|
class << self
|
|
108
103
|
# The configuration active in the current execution context, falling back to
|
|
@@ -114,15 +109,24 @@ module LittleGhost
|
|
|
114
109
|
# Opens the active Configuration for application setup and returns it.
|
|
115
110
|
#
|
|
116
111
|
# Configuration files are loaded lazily when a runtime is first built, so
|
|
117
|
-
# make application-level changes before invoking an agent.
|
|
112
|
+
# make application-level changes before invoking an agent. Once the shared
|
|
113
|
+
# Runtime is ready, later mutations raise ConfigurationError.
|
|
118
114
|
def configure(&block)
|
|
119
115
|
configuration.configure(&block)
|
|
120
116
|
end
|
|
121
117
|
|
|
118
|
+
# Returns the shared Runtime for the active Configuration.
|
|
119
|
+
#
|
|
120
|
+
# Most applications do not need to call this method. Standalone Agent and
|
|
121
|
+
# Assembly entrypoints use it automatically. Runtime construction is lazy,
|
|
122
|
+
# thread-safe, and locks the active Configuration after it succeeds.
|
|
123
|
+
def runtime = configuration.runtime
|
|
124
|
+
|
|
122
125
|
# Returns the model resolver owned by the active process configuration.
|
|
123
126
|
def model_resolver = configuration.model_resolver
|
|
124
127
|
|
|
125
|
-
# Makes +configuration+
|
|
128
|
+
# Makes +configuration+ and its independent shared Runtime current only
|
|
129
|
+
# while the block runs.
|
|
126
130
|
#
|
|
127
131
|
# Execution state restores the previous configuration even when the block
|
|
128
132
|
# raises. Other execution contexts continue to see their own configuration.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: little_ghost
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Robinson
|
|
@@ -117,8 +117,11 @@ extra_rdoc_files: []
|
|
|
117
117
|
files:
|
|
118
118
|
- LICENSE.txt
|
|
119
119
|
- README.md
|
|
120
|
+
- docs/guides/assemblies.md
|
|
120
121
|
- docs/guides/core_concepts.md
|
|
121
122
|
- docs/guides/getting_started.md
|
|
123
|
+
- docs/guides/production.md
|
|
124
|
+
- docs/guides/prompt_views.md
|
|
122
125
|
- lib/little_ghost.rb
|
|
123
126
|
- lib/little_ghost/ag_ui.rb
|
|
124
127
|
- lib/little_ghost/ag_ui/adapter.rb
|
|
@@ -129,13 +132,14 @@ files:
|
|
|
129
132
|
- lib/little_ghost/agent/tool_loop.rb
|
|
130
133
|
- lib/little_ghost/agent_builder.rb
|
|
131
134
|
- lib/little_ghost/agent_factory.rb
|
|
132
|
-
- lib/little_ghost/
|
|
135
|
+
- lib/little_ghost/agent_interjections.rb
|
|
133
136
|
- lib/little_ghost/assembly.rb
|
|
134
137
|
- lib/little_ghost/assembly_builder.rb
|
|
135
138
|
- lib/little_ghost/assembly_execution.rb
|
|
136
139
|
- lib/little_ghost/configuration.rb
|
|
137
140
|
- lib/little_ghost/content.rb
|
|
138
141
|
- lib/little_ghost/data/model_catalog.json
|
|
142
|
+
- lib/little_ghost/data_map.rb
|
|
139
143
|
- lib/little_ghost/errors.rb
|
|
140
144
|
- lib/little_ghost/events.rb
|
|
141
145
|
- lib/little_ghost/execution.rb
|
|
@@ -188,6 +192,7 @@ files:
|
|
|
188
192
|
- lib/little_ghost/session.rb
|
|
189
193
|
- lib/little_ghost/session_store.rb
|
|
190
194
|
- lib/little_ghost/session_stores/agent_core_memory.rb
|
|
195
|
+
- lib/little_ghost/session_stores/filesystem.rb
|
|
191
196
|
- lib/little_ghost/session_stores/memory.rb
|
|
192
197
|
- lib/little_ghost/skills.rb
|
|
193
198
|
- lib/little_ghost/skills/catalog.rb
|