samagotchi 0.33.0 → 0.35.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/CHANGELOG.md +91 -1
- data/README.md +6 -1
- data/bin/chi +3 -0
- data/docs/broadcast.md +129 -0
- data/docs/cli.md +8 -0
- data/docs/configuration.md +2 -0
- data/docs/context.md +5 -0
- data/docs/guardrails.md +35 -3
- data/docs/hooks.md +25 -4
- data/docs/memory.md +17 -1
- data/docs/plugins.md +78 -3
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +15 -9
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/broadcast/recipients.rb +63 -0
- data/lib/samagotchi/broadcast/scope_card.rb +94 -0
- data/lib/samagotchi/broadcast/tags.rb +154 -0
- data/lib/samagotchi/broadcast_command.rb +228 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
- data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +11 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +32 -12
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/delegated.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/children_status.rb +143 -0
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/cut_policy.rb +58 -0
- data/lib/samagotchi/engine.rb +45 -108
- data/lib/samagotchi/engine_peers.rb +30 -0
- data/lib/samagotchi/git_head.rb +24 -0
- data/lib/samagotchi/guardrail_wiring.rb +27 -2
- data/lib/samagotchi/guardrails/approvals.rb +7 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
- data/lib/samagotchi/guardrails/context.rb +32 -0
- data/lib/samagotchi/guardrails/shared_git.rb +124 -0
- data/lib/samagotchi/guardrails/verdict.rb +4 -3
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +11 -5
- data/lib/samagotchi/kernel_loop.rb +9 -22
- data/lib/samagotchi/llm/chat_loop.rb +11 -22
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +14 -1
- data/lib/samagotchi/memory_bundle/installer.rb +6 -2
- data/lib/samagotchi/memory_bundle/manifest.rb +26 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +13 -13
- data/lib/samagotchi/note_delivery.rb +42 -0
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +4 -4
- data/lib/samagotchi/plugin/sessions.rb +45 -1
- data/lib/samagotchi/process_group.rb +2 -5
- data/lib/samagotchi/session_manager.rb +4 -3
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/steer_cut.rb +125 -0
- data/lib/samagotchi/system_prompt.rb +21 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -2
- data/lib/samagotchi/terminal_ui/status_row.rb +23 -2
- data/lib/samagotchi/terminal_ui.rb +14 -0
- data/lib/samagotchi/tool_declarations.rb +9 -5
- data/lib/samagotchi/tools/builtins.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +103 -14
- data/lib/samagotchi/tools/delegate_wait.rb +55 -13
- data/lib/samagotchi/tools/send_note.rb +5 -11
- data/lib/samagotchi/turn_note.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/capabilities.rb +19 -0
- data/lib/samagotchi/web/public/activity.js +26 -6
- data/lib/samagotchi/web/public/app.js +515 -288
- data/lib/samagotchi/web/public/card.js +12 -3
- data/lib/samagotchi/web/public/command_view.js +2 -3
- data/lib/samagotchi/web/public/families_view.js +171 -0
- data/lib/samagotchi/web/public/format.js +52 -0
- data/lib/samagotchi/web/public/index.html +94 -21
- data/lib/samagotchi/web/public/live_timing.js +154 -0
- data/lib/samagotchi/web/public/live_turn.js +44 -0
- data/lib/samagotchi/web/public/sessions_list.js +228 -32
- data/lib/samagotchi/web/public/strip.js +5 -2
- data/lib/samagotchi/web/public/turn_html.js +107 -0
- data/lib/samagotchi/web/public/turn_model.js +5 -4
- data/lib/samagotchi/web/public/turn_view.js +43 -129
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +173 -309
- data/lib/samagotchi/worker_inbound.rb +251 -0
- data/lib/samagotchi/worker_wakes.rb +120 -0
- metadata +24 -1
|
@@ -351,7 +351,7 @@ module Samagotchi
|
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
name: "delegate",
|
|
354
|
-
description: "Hand a task to a child chi session that runs in parallel in this folder and returns only its final reply (its trace stays out of this context). The child is a normal session: it shows in the lists as a child of this one and the user can attach to it. With session, send a follow-up to one of this session's children instead. A tool call the child needs approved goes to your user as one of your own approvals; you only see how it was answered, as a line in the result. Children keep running if this turn is canceled.
|
|
354
|
+
description: "Hand a task to a child chi session that runs in parallel in this folder, or in cwd, and returns only its final reply (its trace stays out of this context). The child is a normal session: it shows in the lists as a child of this one and the user can attach to it. With session, send a follow-up to one of this session's children instead. A tool call the child needs approved goes to your user as one of your own approvals; you only see how it was answered, as a line in the result. Children keep running if this turn is canceled. Use wait false unless this turn can't go on without the reply: the call returns at once, you end your turn or keep working with your user, and chi brings the child's reply to you by itself as a delegate report when the child ends its turn, starting a turn for it if you are idle. After wait false don't call delegate_result for that child, not to check on it and not before ending your turn. To retry or redirect a child, use delegate with its session (task_* tools are for background commands, not sessions). A child stays until stopped: once its work is done, stop it with execute `chi sessions stop ID` (a follow-up with session wakes it again).",
|
|
355
355
|
parameters: {
|
|
356
356
|
type: "object",
|
|
357
357
|
properties: {
|
|
@@ -367,13 +367,17 @@ module Samagotchi
|
|
|
367
367
|
type: "string",
|
|
368
368
|
description: "A child's id or prefix: send the task there as a follow-up instead of starting a new child"
|
|
369
369
|
},
|
|
370
|
+
cwd: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "The new child's working folder: a worktree or subfolder of this session's repository (create a worktree with execute `git worktree add ../<repo>-<name> -b <branch>` first). Default: this session's folder"
|
|
373
|
+
},
|
|
370
374
|
wait: {
|
|
371
375
|
type: "boolean",
|
|
372
|
-
description: "
|
|
376
|
+
description: "true (default): this call waits for the reply; pick it only when this turn needs the reply to go on. false: returns at once and the reply comes later as a delegate report (also when you are idle); pick it for work that takes minutes, several children side by side, or to keep talking with your user"
|
|
373
377
|
},
|
|
374
378
|
timeout: {
|
|
375
379
|
type: "integer",
|
|
376
|
-
description: "Maximum seconds to wait (default: 600; 0 checks once without waiting); a timed-out child keeps running"
|
|
380
|
+
description: "Maximum seconds to wait (default: 600; 0 checks once without waiting); a timed-out child keeps running and its reply still comes as a delegate report"
|
|
377
381
|
}
|
|
378
382
|
},
|
|
379
383
|
required: ["task"]
|
|
@@ -381,7 +385,7 @@ module Samagotchi
|
|
|
381
385
|
},
|
|
382
386
|
{
|
|
383
387
|
name: "delegate_result",
|
|
384
|
-
description: "Wait for a delegated child session's next reply: the child named, or this session's newest running child (name the child when a delegate report is about it). A delegate report already carries the reply: don't call this for it. Returns early if the child waits for a question the user must answer. A tool call the child needs approved goes to your user while this waits; the result says how it was answered.",
|
|
388
|
+
description: "Wait for a delegated child session's next reply, only when this turn can't go on without it: the child named, or this session's newest running child (name the child when a delegate report is about it). After delegate with wait false you don't need this: chi brings each child's reply by itself as a delegate report, also when you are idle, so end your turn or keep working instead of waiting here. A delegate report already carries the reply: don't call this for it. Returns early if the child waits for a question the user must answer. A tool call the child needs approved goes to your user while this waits; the result says how it was answered.",
|
|
385
389
|
parameters: {
|
|
386
390
|
type: "object",
|
|
387
391
|
properties: {
|
|
@@ -391,7 +395,7 @@ module Samagotchi
|
|
|
391
395
|
},
|
|
392
396
|
timeout: {
|
|
393
397
|
type: "integer",
|
|
394
|
-
description: "Maximum seconds to wait (default: 600; 0 checks once without waiting)"
|
|
398
|
+
description: "Maximum seconds to wait (default: 600; 0 checks once without waiting); on a timeout the child keeps running and its reply comes as a delegate report"
|
|
395
399
|
}
|
|
396
400
|
}
|
|
397
401
|
}
|
|
@@ -98,8 +98,8 @@ module Samagotchi
|
|
|
98
98
|
ContextRead.call(call[:content], offset: call[:offset], limit: call[:limit], peers: kctx.peers)
|
|
99
99
|
end,
|
|
100
100
|
Delegate::NAME => lambda do |call, kctx|
|
|
101
|
-
Delegate.call(call[:content], model: call[:model], session: call[:session],
|
|
102
|
-
timeout: call[:timeout], peers: kctx.peers)
|
|
101
|
+
Delegate.call(call[:content], model: call[:model], session: call[:session], cwd: call[:cwd],
|
|
102
|
+
wait: call[:wait], timeout: call[:timeout], peers: kctx.peers)
|
|
103
103
|
end,
|
|
104
104
|
DelegateResult::NAME => lambda do |call, kctx|
|
|
105
105
|
DelegateResult.call(call[:content], session: call[:session], timeout: call[:timeout], peers: kctx.peers)
|
|
@@ -4,6 +4,7 @@ require_relative "../client_id"
|
|
|
4
4
|
require_relative "../session"
|
|
5
5
|
require_relative "../config"
|
|
6
6
|
require_relative "../model_profile"
|
|
7
|
+
require_relative "../memory_paths"
|
|
7
8
|
require_relative "peers"
|
|
8
9
|
require_relative "delegate_wait"
|
|
9
10
|
require_relative "../child_reports"
|
|
@@ -15,7 +16,8 @@ module Samagotchi
|
|
|
15
16
|
|
|
16
17
|
module Tools
|
|
17
18
|
# Hand a task to a child session: an ordinary chi session in its own
|
|
18
|
-
# worker, started in this session's folder
|
|
19
|
+
# worker, started in this session's folder (or cwd:, a worktree or
|
|
20
|
+
# subfolder of the same repository) with the task as its first
|
|
19
21
|
# user message and the `delegated` system memory preloaded. It shows in
|
|
20
22
|
# every list as a child of this one and the user can attach to it. With
|
|
21
23
|
# session:, a follow-up to a child that exists. Only the child's final
|
|
@@ -38,10 +40,11 @@ module Samagotchi
|
|
|
38
40
|
# @param content [String] the task (the child's user message, verbatim)
|
|
39
41
|
# @param model [String, nil] the child's model (an alias works); the parent's by default
|
|
40
42
|
# @param session [String, nil] a child's id or prefix: send the task there instead
|
|
43
|
+
# @param cwd [String, nil] the new child's folder (child_cwd checks it); the parent's by default
|
|
41
44
|
# @param wait [Boolean, String, nil] wait for the reply (default true)
|
|
42
45
|
# @param timeout [Integer, String, nil] seconds to wait (default DelegateWait::TIMEOUT_DEFAULT)
|
|
43
46
|
# @param peers [Peers, nil]
|
|
44
|
-
def self.call(content, model: nil, session: nil, wait: nil, timeout: nil, peers: nil)
|
|
47
|
+
def self.call(content, model: nil, session: nil, cwd: nil, wait: nil, timeout: nil, peers: nil)
|
|
45
48
|
return "Error: this session's id is not known here" unless peers&.session_id
|
|
46
49
|
|
|
47
50
|
task = content.to_s.strip
|
|
@@ -52,8 +55,11 @@ module Samagotchi
|
|
|
52
55
|
wait = parse_wait(wait)
|
|
53
56
|
timeout = parse_timeout(timeout)
|
|
54
57
|
|
|
58
|
+
cwd = cwd.to_s.strip
|
|
55
59
|
child_id, warning = if session.to_s.strip.empty?
|
|
56
|
-
start_child(task, parent: parent, model: model, state_dir: sd)
|
|
60
|
+
start_child(task, parent: parent, model: model, cwd: cwd, state_dir: sd, peers: peers)
|
|
61
|
+
elsif !cwd.empty?
|
|
62
|
+
"Error: cwd starts a new child; a follow-up with session keeps the child's folder"
|
|
57
63
|
else
|
|
58
64
|
follow_up(task, session: session.to_s.strip, parent: parent, state_dir: sd)
|
|
59
65
|
end
|
|
@@ -63,35 +69,58 @@ module Samagotchi
|
|
|
63
69
|
return note + DelegateWait.call(child_id, peers: peers, timeout: timeout) if wait
|
|
64
70
|
|
|
65
71
|
started = session.to_s.strip.empty? ? "Started a delegate session" : "Sent the follow-up to delegate #{child_id[0, 8]}"
|
|
66
|
-
"#{note}session: #{child_id}\nstatus: running\n#{started}; #{running_hint} " \
|
|
72
|
+
"#{note}session: #{child_id}\nstatus: running\n#{started}; #{running_hint(peers)} " \
|
|
67
73
|
"It shows in chi sessions list and the web as a child of this session; the user can attach to it."
|
|
68
74
|
rescue ArgumentError => e
|
|
69
75
|
"Error: #{e.message}"
|
|
70
76
|
end
|
|
71
77
|
|
|
72
|
-
# How the reply of a child left running comes back
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
78
|
+
# How the reply of a child left running comes back: a report only
|
|
79
|
+
# reaches a parent that can get one (DelegateWait.reports_mode).
|
|
80
|
+
def self.running_hint(peers)
|
|
81
|
+
case DelegateWait.reports_mode(peers)
|
|
82
|
+
when "off" then "delegate_result waits for its reply."
|
|
83
|
+
when "queue"
|
|
84
|
+
"chi adds its reply to your next turn by itself (a delegate report; you aren't woken while idle); " \
|
|
85
|
+
"don't poll with delegate_result."
|
|
86
|
+
else
|
|
87
|
+
"chi brings its reply to you by itself when it ends its turn (a delegate report), starting a turn for it " \
|
|
88
|
+
"if you are idle. Don't wait for it with delegate_result, also not to collect several children's replies: " \
|
|
89
|
+
"tell your user what started and end your turn, or keep working."
|
|
90
|
+
end
|
|
77
91
|
end
|
|
78
92
|
private_class_method :running_hint
|
|
79
93
|
|
|
94
|
+
# What to do when session.max_children are running: a parent that
|
|
95
|
+
# gets reports ends its turn (a report frees a slot); one that
|
|
96
|
+
# doesn't (DelegateWait.reports_mode off) waits with delegate_result.
|
|
97
|
+
def self.full_hint(peers)
|
|
98
|
+
if DelegateWait.reports_mode(peers) == "off"
|
|
99
|
+
"delegate_result waits for one; `chi sessions stop ID` stops one."
|
|
100
|
+
else
|
|
101
|
+
"End your turn: a delegate report frees a slot when a child finishes; or stop one with `chi sessions stop ID`."
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
private_class_method :full_hint
|
|
105
|
+
|
|
80
106
|
# @return [String] the new child's id, or an Error: line
|
|
81
|
-
def self.start_child(task, parent:, model:, state_dir:)
|
|
107
|
+
def self.start_child(task, parent:, model:, cwd:, state_dir:, peers:)
|
|
82
108
|
if parent.parent_id
|
|
83
109
|
return "Error: this session is a delegate of #{parent.parent_id}; delegated sessions don't delegate further"
|
|
84
110
|
end
|
|
85
111
|
|
|
112
|
+
folder = child_cwd(cwd, parent)
|
|
113
|
+
return folder if folder.start_with?("Error:")
|
|
114
|
+
|
|
86
115
|
running = running_children(parent.id, state_dir: state_dir)
|
|
87
116
|
max = max_children
|
|
88
117
|
if running.size >= max
|
|
89
118
|
ids = running.map { |s| s[:short_id] }.join(", ")
|
|
90
119
|
return "Error: #{running.size} delegate#{"s" if running.size != 1} of this session #{running.size == 1 ? "is" : "are"} running " \
|
|
91
|
-
"(the most is #{max}, #{MAX_CHILDREN_KEY}): #{ids}.
|
|
120
|
+
"(the most is #{max}, #{MAX_CHILDREN_KEY}): #{ids}. #{full_hint(peers)}"
|
|
92
121
|
end
|
|
93
122
|
|
|
94
|
-
child = SessionManager.spawn_session(prompt: task, working_directory:
|
|
123
|
+
child = SessionManager.spawn_session(prompt: task, working_directory: folder,
|
|
95
124
|
model_name: child_model(model, parent), memories: CHILD_MEMORIES,
|
|
96
125
|
parent_id: parent.id, delegate: true, state_dir: state_dir)
|
|
97
126
|
DelegateWait.mark_started(parent.id, child, state_dir: state_dir)
|
|
@@ -99,6 +128,64 @@ module Samagotchi
|
|
|
99
128
|
end
|
|
100
129
|
private_class_method :start_child
|
|
101
130
|
|
|
131
|
+
# The new child's folder: the parent's, or +cwd+ (relative to the
|
|
132
|
+
# parent's) when it is a folder of the parent's repository, in any of
|
|
133
|
+
# its worktrees (one project_root, from the common git dir). Outside
|
|
134
|
+
# git only a subfolder of the parent's folder. Nothing is created
|
|
135
|
+
# here: the model makes a worktree with execute, where guardrails see it.
|
|
136
|
+
# @return [String] an absolute folder, or an Error: line
|
|
137
|
+
def self.child_cwd(cwd, parent)
|
|
138
|
+
base = parent.working_directory
|
|
139
|
+
return base if cwd.empty?
|
|
140
|
+
|
|
141
|
+
folder = File.expand_path(cwd, base)
|
|
142
|
+
return "Error: cwd #{folder} is not a folder (create the worktree first)" unless File.directory?(folder)
|
|
143
|
+
|
|
144
|
+
folder = File.realpath(folder)
|
|
145
|
+
unless same_repository?(folder, base)
|
|
146
|
+
return "Error: cwd must be a folder of this session's repository (a worktree or subfolder of it), not #{folder}"
|
|
147
|
+
end
|
|
148
|
+
if MemoryPaths.in_repo?(base) && !work_tree?(folder)
|
|
149
|
+
return "Error: cwd #{folder} is not in a work tree of this session's repository (a git dir, or a bare " \
|
|
150
|
+
"repository's folder): give a worktree's folder or a subfolder of one"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
folder
|
|
154
|
+
end
|
|
155
|
+
private_class_method :child_cwd
|
|
156
|
+
|
|
157
|
+
# A folder git works in: under a checkout's top (work_tree_root), not
|
|
158
|
+
# inside the git dir (.git, .git/objects, a bare layout's .bare), and
|
|
159
|
+
# one `git rev-parse` says is in a work tree (it says false in a bare
|
|
160
|
+
# layout's container too). Read-only; IO.popen, so a spec's
|
|
161
|
+
# Process.spawn stub doesn't catch it.
|
|
162
|
+
def self.work_tree?(folder)
|
|
163
|
+
return false unless MemoryPaths.work_tree_root(folder)
|
|
164
|
+
|
|
165
|
+
git_dir = MemoryPaths.git_dir(folder)
|
|
166
|
+
return false if git_dir && within?(folder, File.realpath(git_dir))
|
|
167
|
+
|
|
168
|
+
out = IO.popen(["git", "-C", folder, "rev-parse", "--is-inside-work-tree"], err: File::NULL, &:read)
|
|
169
|
+
out.strip == "true"
|
|
170
|
+
rescue SystemCallError
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
private_class_method :work_tree?
|
|
174
|
+
|
|
175
|
+
def self.within?(path, dir) = path == dir || path.start_with?("#{dir}/")
|
|
176
|
+
private_class_method :within?
|
|
177
|
+
|
|
178
|
+
def self.same_repository?(folder, base)
|
|
179
|
+
if MemoryPaths.in_repo?(base)
|
|
180
|
+
MemoryPaths.project_root(folder) == MemoryPaths.project_root(base)
|
|
181
|
+
else
|
|
182
|
+
within?(folder, File.realpath(base))
|
|
183
|
+
end
|
|
184
|
+
rescue SystemCallError
|
|
185
|
+
false
|
|
186
|
+
end
|
|
187
|
+
private_class_method :same_repository?
|
|
188
|
+
|
|
102
189
|
# @return [String] the child's id, or an Error: line
|
|
103
190
|
def self.follow_up(task, session:, parent:, state_dir:)
|
|
104
191
|
id = Session.resolve_id(session, state_dir: state_dir)
|
|
@@ -147,7 +234,10 @@ module Samagotchi
|
|
|
147
234
|
SessionManager.children_of(parent_id, state_dir: state_dir).select { |s| running?(s) }
|
|
148
235
|
end
|
|
149
236
|
|
|
150
|
-
# busy (a worker runs its turn), or running with a worker on its way
|
|
237
|
+
# busy (a worker runs its turn), or running with a worker on its way
|
|
238
|
+
# (ChildrenStatus asks it too).
|
|
239
|
+
# @param summary [Hash] a children_of row, or the same keys: busy,
|
|
240
|
+
# status, live, updated_at
|
|
151
241
|
def self.running?(summary)
|
|
152
242
|
return true if summary[:busy]
|
|
153
243
|
return false unless summary[:status] == Session::STATUS_RUNNING && !summary[:live]
|
|
@@ -156,7 +246,6 @@ module Samagotchi
|
|
|
156
246
|
rescue ArgumentError
|
|
157
247
|
false
|
|
158
248
|
end
|
|
159
|
-
private_class_method :running?
|
|
160
249
|
|
|
161
250
|
def self.max_children
|
|
162
251
|
value = Integer(Config.get(MAX_CHILDREN_KEY), exception: false)
|
|
@@ -9,6 +9,10 @@ require_relative "delegate_relay"
|
|
|
9
9
|
require_relative "delegate_cursor"
|
|
10
10
|
|
|
11
11
|
module Samagotchi
|
|
12
|
+
# Loaded on first use: child_reports requires this file (a circular
|
|
13
|
+
# require otherwise).
|
|
14
|
+
autoload :ChildRing, File.expand_path("../child_reports", __dir__)
|
|
15
|
+
|
|
12
16
|
module Tools
|
|
13
17
|
# Waiting for a delegated session's next reply, shared by delegate and
|
|
14
18
|
# delegate_result: ReplyWait in the tools' words, with the status words
|
|
@@ -58,6 +62,7 @@ module Samagotchi
|
|
|
58
62
|
key = [peers.session_id, child_id]
|
|
59
63
|
cancelled = -> { peers.cancelled? }
|
|
60
64
|
relay = peers.respond_to?(:relay) ? peers.relay : nil
|
|
65
|
+
reports = reports_mode(peers)
|
|
61
66
|
others = relay && others_for(peers.session_id, child_id, relay, sd)
|
|
62
67
|
cursor = DelegateCursors.get(peers.session_id, child_id, state_dir: sd)
|
|
63
68
|
baseline = cursor.baseline
|
|
@@ -72,12 +77,12 @@ module Samagotchi
|
|
|
72
77
|
unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
|
|
73
78
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
74
79
|
advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
75
|
-
return with_outcomes(finish(wait, child_id, timeout: timeout), outcomes)
|
|
80
|
+
return with_outcomes(finish(wait, child_id, timeout: timeout, reports: reports), outcomes)
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
outcome = DelegateRelay.call(child_id, wait.question, relay: relay, state_dir: sd, more: others&.count.to_i)
|
|
79
84
|
outcomes << outcome.line
|
|
80
|
-
return with_outcomes(canceled_result(child_id), outcomes) if outcome.stopped
|
|
85
|
+
return with_outcomes(canceled_result(child_id, reports: reports), outcomes) if outcome.stopped
|
|
81
86
|
|
|
82
87
|
# Wait on with the child as it is now: a turn that ends with no
|
|
83
88
|
# reply after the relay still ends the wait, and the relayed
|
|
@@ -104,8 +109,26 @@ module Samagotchi
|
|
|
104
109
|
text.sub(/\A(session: [^\n]*\nstatus: [^\n]*\n)/) { "#{::Regexp.last_match(1)}#{outcomes.join("\n")}\n" }
|
|
105
110
|
end
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
# How a child's reply reaches this parent when no wait takes it:
|
|
113
|
+
# "wake" (a delegate report, a turn of its own when idle), "queue" (a
|
|
114
|
+
# report at its next step or turn, no wake) or "off" (only
|
|
115
|
+
# delegate_result). Only a worker's session reads its children's rings
|
|
116
|
+
# (its Engine gives a relay); a REPL, a -p run or a scratch session
|
|
117
|
+
# never gets a report.
|
|
118
|
+
# @param peers [Peers]
|
|
119
|
+
# @return [String] "wake", "queue" or "off"
|
|
120
|
+
def reports_mode(peers)
|
|
121
|
+
return "off" unless peers.respond_to?(:relay) && peers.relay
|
|
122
|
+
|
|
123
|
+
ChildRing.mode
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def canceled_result(child_id, reports: "off")
|
|
127
|
+
if reports == "off"
|
|
128
|
+
return result(child_id, "running", "wait canceled; the child keeps running; delegate_result #{child_id} waits again")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
result(child_id, "running", "wait canceled; the child keeps running; chi brings its reply as a delegate report")
|
|
109
132
|
end
|
|
110
133
|
|
|
111
134
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -135,8 +158,10 @@ module Samagotchi
|
|
|
135
158
|
# @param wait [ReplyWait::Result]
|
|
136
159
|
# @param report [Boolean] a delegate report's text (ChildReports), not
|
|
137
160
|
# the tool's: a question's last line says chi brings the next reply
|
|
161
|
+
# @param reports [String] #reports_mode: what a timeout, a cancel or
|
|
162
|
+
# a question tells the model about the reply to come
|
|
138
163
|
# @return [String]
|
|
139
|
-
def finish(wait, child_id, timeout:, report: false)
|
|
164
|
+
def finish(wait, child_id, timeout:, report: false, reports: "off")
|
|
140
165
|
status = ParentReport.status(wait)
|
|
141
166
|
case wait.status
|
|
142
167
|
when :done
|
|
@@ -146,16 +171,16 @@ module Samagotchi
|
|
|
146
171
|
when :stopped
|
|
147
172
|
result(child_id, status, "the child was stopped (chi sessions stop); delegate with session: #{child_id} starts it again with a message")
|
|
148
173
|
when :waiting_for_answer
|
|
149
|
-
result(child_id, status, waiting_text(child_id, wait.question, report: report))
|
|
174
|
+
result(child_id, status, waiting_text(child_id, wait.question, report: report, reports: reports))
|
|
150
175
|
when :canceled
|
|
151
|
-
canceled_result(child_id)
|
|
176
|
+
canceled_result(child_id, reports: reports)
|
|
152
177
|
when :no_reply
|
|
153
178
|
result(child_id, status, "#{no_reply_text(wait, child_id)}; its session shows what happened")
|
|
154
179
|
when :worker_gone
|
|
155
180
|
result(child_id, status,
|
|
156
181
|
"the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
|
|
157
182
|
else
|
|
158
|
-
timeout_result(child_id, timeout)
|
|
183
|
+
timeout_result(child_id, timeout, reports: reports)
|
|
159
184
|
end
|
|
160
185
|
end
|
|
161
186
|
|
|
@@ -191,10 +216,24 @@ module Samagotchi
|
|
|
191
216
|
"session: #{child_id}\nstatus: #{status}\n#{text}"
|
|
192
217
|
end
|
|
193
218
|
|
|
194
|
-
|
|
219
|
+
# The cursor didn't move (#advance), so the reply still comes as a
|
|
220
|
+
# delegate report when this parent can get one.
|
|
221
|
+
def timeout_result(child_id, timeout, reports: "off")
|
|
222
|
+
head = "no reply yet after #{timeout.to_i} s; the child keeps running."
|
|
223
|
+
if reports == "off"
|
|
224
|
+
return result(child_id, "running",
|
|
225
|
+
"#{head} delegate_result #{child_id} waits again; chi --attach #{child_id} shows it.")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
comes = if reports == "queue"
|
|
229
|
+
"chi adds its reply to your turn at its next step, or to your next turn, as a delegate report " \
|
|
230
|
+
"(you are not woken while idle)"
|
|
231
|
+
else
|
|
232
|
+
"chi brings its reply here by itself as a delegate report when it ends its turn (also after you end yours)"
|
|
233
|
+
end
|
|
195
234
|
result(child_id, "running",
|
|
196
|
-
"
|
|
197
|
-
"chi --attach #{child_id} shows it.")
|
|
235
|
+
"#{head} #{comes}, so carry on or end your turn. Only if this turn can't go on without it: " \
|
|
236
|
+
"delegate_result #{child_id} waits again. chi --attach #{child_id} shows it.")
|
|
198
237
|
end
|
|
199
238
|
|
|
200
239
|
# The whole question, as `chi send --wait` prints it (ParentReport):
|
|
@@ -205,7 +244,8 @@ module Samagotchi
|
|
|
205
244
|
# report back.
|
|
206
245
|
# A report's last line differs (+report+): "waits again" made models
|
|
207
246
|
# call delegate_result right after answering Continue.
|
|
208
|
-
|
|
247
|
+
# The tool's last line follows #reports_mode.
|
|
248
|
+
def waiting_text(child_id, pending, report: false, reports: "off")
|
|
209
249
|
text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}"
|
|
210
250
|
if ParentReport.continue?(pending)
|
|
211
251
|
text += "The child ran out of steps before it answered. Decide: continue it (run the chi answer command " \
|
|
@@ -213,8 +253,10 @@ module Samagotchi
|
|
|
213
253
|
"session: #{child_id} (that drops the question); or stop it and report back to your user.\n"
|
|
214
254
|
end
|
|
215
255
|
return "#{text}#{REPORT_NEXT_REPLY}" if report
|
|
256
|
+
return "#{text}delegate_result #{child_id} waits again once it is answered." if reports == "off"
|
|
216
257
|
|
|
217
|
-
"#{text}
|
|
258
|
+
"#{text}Once it is answered, chi brings the child's next reply here as a delegate report; " \
|
|
259
|
+
"delegate_result #{child_id} waits for it only if this turn needs it."
|
|
218
260
|
end
|
|
219
261
|
|
|
220
262
|
def cut(text)
|
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
require_relative "../session"
|
|
4
4
|
require_relative "../context_note"
|
|
5
5
|
require_relative "../session_inbox"
|
|
6
|
+
require_relative "../note_delivery"
|
|
6
7
|
require_relative "peers"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
|
-
# Loaded on first use: session_manager requires terminal_ui, which
|
|
10
|
-
# requires KernelLoop and so these tools (a require cycle otherwise).
|
|
11
|
-
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
12
|
-
|
|
13
10
|
module Tools
|
|
14
11
|
# Tell another chi session something: a context note it sees on its next
|
|
15
12
|
# turn as background from this session. It never starts a turn there.
|
|
@@ -29,14 +26,11 @@ module Samagotchi
|
|
|
29
26
|
Session.load(id, state_dir: peers.state_dir)
|
|
30
27
|
return "Error: #{id[0, 8]} is this session; send_note is for other sessions" if id == peers.session_id
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
29
|
+
result = NoteDelivery.deliver(id, text: content, source: "session", from_session: peers.session_id,
|
|
30
|
+
from_cwd: peers.cwd, state_dir: peers.state_dir)
|
|
31
|
+
return "Error: session #{id[0, 8]} is open in a chi REPL, which can't take notes" unless result.delivered?
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
from_cwd: peers.cwd, state_dir: peers.state_dir)
|
|
39
|
-
where = owner ? "its worker adds it before its next turn" : "it has no worker now, so it waits for its next start"
|
|
33
|
+
where = result.status == :queued ? "its worker adds it before its next turn" : "it has no worker now, so it waits for its next start"
|
|
40
34
|
"Queued a note for session #{id[0, 8]}: #{where}. It does not start a turn there."
|
|
41
35
|
rescue Session::AmbiguousId => e
|
|
42
36
|
"Error: #{e.message}"
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -36,8 +36,16 @@ module Samagotchi
|
|
|
36
36
|
# @param restored [Boolean] the prompt went back to its sender (the
|
|
37
37
|
# failed user message is not in the conversation any more)
|
|
38
38
|
# @param continued [Boolean] it was a continue turn (no user message)
|
|
39
|
-
|
|
39
|
+
# @param wake [String, nil] what woke it, when chi started the turn and
|
|
40
|
+
# not the user ("the change in attached context pr-7"): the user
|
|
41
|
+
# wrote nothing, and wakes pause until they do
|
|
42
|
+
# @param kept [String, nil] what becomes of the wake's news, after
|
|
43
|
+
# that (a delegate report's rings stay for the user's next turn)
|
|
44
|
+
def failed(summary, restored: false, continued: false, wake: nil, kept: nil)
|
|
40
45
|
tail = if restored then RESTORED
|
|
46
|
+
elsif wake
|
|
47
|
+
"The wake turn for #{one_line(wake)} was not answered; chi starts no other wake turn until the user writes." +
|
|
48
|
+
(kept ? " #{one_line(kept)}" : "")
|
|
41
49
|
elsif continued then "The continued turn stopped there."
|
|
42
50
|
else "The user's last message was not answered."
|
|
43
51
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -34,6 +34,7 @@ require_relative "../context_sources"
|
|
|
34
34
|
require_relative "../context_providers"
|
|
35
35
|
require_relative "../recap_store"
|
|
36
36
|
require_relative "markdown_renderer"
|
|
37
|
+
require_relative "capabilities"
|
|
37
38
|
require_relative "message_parts"
|
|
38
39
|
require_relative "session_summary"
|
|
39
40
|
require_relative "../log"
|
|
@@ -634,6 +635,7 @@ module Samagotchi
|
|
|
634
635
|
# names its plugins' too.
|
|
635
636
|
commands: turn_snapshot&.fetch("commands", nil) || SessionCommands.builtin_registry.listing,
|
|
636
637
|
markdown_warning: @markdown_renderer.warning,
|
|
638
|
+
capabilities: Capabilities.for(@markdown_renderer).to_h,
|
|
637
639
|
pending_question: pending,
|
|
638
640
|
last_event_seq: last_event_seq,
|
|
639
641
|
# The stream cursor `<seq>-<epoch>`: a later worker resets it
|
|
@@ -690,6 +692,7 @@ module Samagotchi
|
|
|
690
692
|
session: session_json,
|
|
691
693
|
messages: messages,
|
|
692
694
|
markdown_warning: @markdown_renderer.warning,
|
|
695
|
+
capabilities: Capabilities.for(@markdown_renderer).to_h,
|
|
693
696
|
timing: timing
|
|
694
697
|
})
|
|
695
698
|
rescue ArgumentError => e
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module Web
|
|
5
|
+
# What this web page renders that a notice may stand in for
|
|
6
|
+
# (Engine#hook_notify's fallback_for:). The session and tail JSON carry
|
|
7
|
+
# it as +capabilities+; the page leaves out a notice whose fallback_for
|
|
8
|
+
# names a capability that is true here. +display+: the answer's display
|
|
9
|
+
# is rendered as markdown, so its links are links (web.markdown on and
|
|
10
|
+
# commonmarker installed). A later capability is a new member. (The
|
|
11
|
+
# member hides Object#display, which nothing calls on it.)
|
|
12
|
+
Capabilities = Data.define(:display) do # rubocop:disable Lint/DataDefineOverride
|
|
13
|
+
# @param markdown_renderer [MarkdownRenderer]
|
|
14
|
+
def self.for(markdown_renderer)
|
|
15
|
+
new(display: markdown_renderer.available? == true)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -39,8 +39,10 @@ export function rowKey(iteration, callIndex) {
|
|
|
39
39
|
|
|
40
40
|
// Upsert a row from a :tool_call_started event. Returns { row, created }.
|
|
41
41
|
// If a row with the same key already exists (duplicate start), it is reset to
|
|
42
|
-
// running and updated rather than duplicated.
|
|
43
|
-
|
|
42
|
+
// running and updated rather than duplicated. +now+ (ms): when the page saw
|
|
43
|
+
// the call start, for its duration; none for an event a re-render replays
|
|
44
|
+
// (`replayed`: the call started before the page looked).
|
|
45
|
+
export function addStarted(model, event = {}, now = null) {
|
|
44
46
|
const iteration = event.iteration ?? 0;
|
|
45
47
|
const callIndex = event.call_index ?? 0;
|
|
46
48
|
const key = rowKey(iteration, callIndex);
|
|
@@ -52,6 +54,7 @@ export function addStarted(model, event = {}) {
|
|
|
52
54
|
row.params = event.params || row.params;
|
|
53
55
|
row.title = event.title || row.title;
|
|
54
56
|
row.view = event.view || row.view;
|
|
57
|
+
markStart(row, event, now);
|
|
55
58
|
return { row, created: false };
|
|
56
59
|
}
|
|
57
60
|
row = {
|
|
@@ -72,14 +75,23 @@ export function addStarted(model, event = {}) {
|
|
|
72
75
|
output: "",
|
|
73
76
|
output_truncated: false,
|
|
74
77
|
};
|
|
78
|
+
markStart(row, event, now);
|
|
75
79
|
model.rows.push(row);
|
|
76
80
|
return { row, created: true };
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
function markStart(row, event, now) {
|
|
84
|
+
if (now !== null && !event.replayed) row.startedAt = now;
|
|
85
|
+
else delete row.startedAt;
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
// Update (or synthesize) a row from a :tool_call_completed event. Returns the row.
|
|
80
89
|
// If no matching :tool_call_started was seen (e.g. a replay gap), a row is
|
|
81
|
-
// created on the fly so the completion is never lost.
|
|
82
|
-
|
|
90
|
+
// created on the fly so the completion is never lost. Its duration
|
|
91
|
+
// (duration_ms, as a reloaded row has it from the tool record): a
|
|
92
|
+
// snapshot's own, else from the start the page saw to +now+, less the time
|
|
93
|
+
// the call waited for an approval (waited_ms), as the worker times it.
|
|
94
|
+
export function addCompleted(model, event = {}, now = null) {
|
|
83
95
|
const iteration = event.iteration ?? 0;
|
|
84
96
|
const callIndex = event.call_index ?? 0;
|
|
85
97
|
const key = rowKey(iteration, callIndex);
|
|
@@ -102,14 +114,22 @@ export function addCompleted(model, event = {}) {
|
|
|
102
114
|
if (event.activity?.params) row.params = event.activity.params;
|
|
103
115
|
if (event.activity?.title) row.title = event.activity.title;
|
|
104
116
|
// The images the tool returned (a reload in the turn view draws them from
|
|
105
|
-
// the saved parts,
|
|
117
|
+
// the saved parts, turn_html.js toolRowHtml).
|
|
106
118
|
if (event.images?.length) row.images = event.images;
|
|
107
119
|
// What an edit/write changed in its file (a reload draws it from the
|
|
108
|
-
// saved parts,
|
|
120
|
+
// saved parts, turn_html.js toolRowHtml).
|
|
109
121
|
if (event.diff) row.diff = event.diff;
|
|
122
|
+
const duration = callDuration(row, event, now);
|
|
123
|
+
if (duration !== null) row.duration_ms = duration;
|
|
110
124
|
return row;
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
function callDuration(row, event, now) {
|
|
128
|
+
if (event.duration_ms != null) return event.duration_ms;
|
|
129
|
+
if (row.startedAt == null || now === null || event.replayed) return null;
|
|
130
|
+
return Math.max(0, now - row.startedAt - (Number(event.waited_ms) || 0));
|
|
131
|
+
}
|
|
132
|
+
|
|
113
133
|
// A completed call's row status from the server's: error, stopped,
|
|
114
134
|
// blocked, else ok.
|
|
115
135
|
export function completedStatus(status) {
|