samagotchi 0.82.0 → 0.84.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 +28 -1
- data/README.md +2 -2
- data/bin/chi +27 -2
- data/docs/architecture.md +3 -0
- data/docs/cli.md +1 -0
- data/docs/configuration.md +52 -9
- data/docs/hooks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +85 -3
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +13 -1
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/bundle_session_settings.rb +212 -0
- data/lib/samagotchi/bundles/core/manifest.yml +4 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +13 -0
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +235 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +25 -8
- data/lib/samagotchi/engine.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
- data/lib/samagotchi/memory_bundle/installer.rb +2 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +24 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +40 -3
- data/lib/samagotchi/reply_wait.rb +13 -15
- data/lib/samagotchi/send_command.rb +24 -7
- data/lib/samagotchi/session.rb +16 -1
- data/lib/samagotchi/session_metrics/cost.rb +37 -0
- data/lib/samagotchi/session_metrics.rb +9 -0
- data/lib/samagotchi/session_setup.rb +14 -6
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +7 -2
- data/lib/samagotchi/testing/bundle.rb +31 -5
- data/lib/samagotchi/testing/host.rb +26 -1
- data/lib/samagotchi/testing/recorder.rb +4 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +45 -7
- data/lib/samagotchi/tools/delegate_wait.rb +40 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +13 -4
- metadata +5 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../cancellation_controller"
|
|
4
4
|
require_relative "../plugin/context"
|
|
5
|
+
require_relative "../session_metrics/cost"
|
|
5
6
|
require_relative "recorder"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
@@ -48,9 +49,33 @@ module Samagotchi
|
|
|
48
49
|
toolkit: -> { recorder[:toolkit] },
|
|
49
50
|
declared_tools: -> { recorder[:declared_tools] || tool_names.call },
|
|
50
51
|
use_toolkit: ->(name) { recorder.use_toolkit(name, toolkits.call) },
|
|
51
|
-
cancel_controller: ->(handler: false) { recorder[:cancelled] ? stopped : nil }
|
|
52
|
+
cancel_controller: ->(handler: false) { recorder[:cancelled] ? stopped : nil },
|
|
53
|
+
cost: -> { cost(recorder[:cost]) }
|
|
52
54
|
)
|
|
53
55
|
end
|
|
56
|
+
|
|
57
|
+
# The parts a test may give bundle.set(cost: {...}).
|
|
58
|
+
COST_PARTS = %i[reported estimated side known].freeze
|
|
59
|
+
|
|
60
|
+
# The SessionMetrics::Cost ctx.cost returns for the recorder's +cost+
|
|
61
|
+
# value (Recorder::DEFAULTS).
|
|
62
|
+
# @return [SessionMetrics::Cost]
|
|
63
|
+
def cost(value)
|
|
64
|
+
value = value.call while value.respond_to?(:call)
|
|
65
|
+
case value
|
|
66
|
+
when nil then SessionMetrics::Cost.zero
|
|
67
|
+
when SessionMetrics::Cost then value
|
|
68
|
+
when Numeric then SessionMetrics::Cost.zero.with(reported: value.to_f)
|
|
69
|
+
when Hash
|
|
70
|
+
unknown = value.keys.map(&:to_sym) - COST_PARTS
|
|
71
|
+
raise ArgumentError, "unknown cost parts: #{unknown.join(", ")}" unless unknown.empty?
|
|
72
|
+
|
|
73
|
+
parts = value.transform_keys(&:to_sym)
|
|
74
|
+
SessionMetrics::Cost.new(reported: parts.fetch(:reported, 0).to_f, estimated: parts.fetch(:estimated, 0).to_f,
|
|
75
|
+
side: parts.fetch(:side, 0).to_f, known: parts.fetch(:known, true))
|
|
76
|
+
else raise ArgumentError, "cost: a number, a Hash or a callable, not #{value.inspect}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
54
79
|
end
|
|
55
80
|
|
|
56
81
|
# The hook runtime (Hooks::Runtime: event[:notify] and the other
|
|
@@ -32,7 +32,10 @@ module Samagotchi
|
|
|
32
32
|
DEFAULTS = {
|
|
33
33
|
session_id: "test-session", cwd: nil, messages: [], messages_partial: false, model_name: "test:model",
|
|
34
34
|
model_key: "model", toolkit: "full", declared_tools: nil, frontend: :worker, scratch: false, setup: nil,
|
|
35
|
-
prompt_files: [], cancelled: false, state_dir: nil, running: true, toolkit_staged: true
|
|
35
|
+
prompt_files: [], cancelled: false, state_dir: nil, running: true, toolkit_staged: true,
|
|
36
|
+
# ctx.cost: nil (zero), a Float (reported USD), a Hash of the parts
|
|
37
|
+
# (reported:, estimated:, side:, known:) or a callable giving one.
|
|
38
|
+
cost: nil
|
|
36
39
|
}.freeze
|
|
37
40
|
|
|
38
41
|
attr_reader :notices, :cards, :steers, :stops, :questions, :model_requests, :toolkit_uses, :answers,
|
|
@@ -14,8 +14,9 @@ module Samagotchi
|
|
|
14
14
|
# of the child's turn the last follow-up went to (Delegate.follow_up),
|
|
15
15
|
# whose own reply a wait takes (ReplyWait's turn:); nil once that turn
|
|
16
16
|
# was reported, or when unknown (a new child, an older worker).
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# +report_claim+: a report reserved until the parent's turn is settled.
|
|
18
|
+
DelegateCursor = Data.define(:reply_file, :last_turn, :question_id, :messages, :turn, :report_claim) do
|
|
19
|
+
def initialize(reply_file: nil, last_turn: nil, question_id: nil, messages: nil, turn: nil, report_claim: nil) = super
|
|
19
20
|
|
|
20
21
|
def self.empty = new
|
|
21
22
|
|
|
@@ -33,6 +34,13 @@ module Samagotchi
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def to_json_hash = to_h.transform_keys(&:to_s).compact
|
|
37
|
+
|
|
38
|
+
def self.from_json_hash(data)
|
|
39
|
+
return empty unless data.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
new(reply_file: data["reply_file"], last_turn: data["last_turn"], question_id: data["question_id"],
|
|
42
|
+
messages: data["messages"], turn: data["turn"], report_claim: data["report_claim"])
|
|
43
|
+
end
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# The parent's cursors, one per child, in <parent dir>/delegates.json, so
|
|
@@ -68,6 +76,40 @@ module Samagotchi
|
|
|
68
76
|
end
|
|
69
77
|
end
|
|
70
78
|
|
|
79
|
+
# Reserve a report only if the cursor read to build it is still current.
|
|
80
|
+
# @return [DelegateCursor, nil] the reserved cursor, or nil if another path moved it
|
|
81
|
+
def claim_report(parent_id, child_id, expected:, claim:, state_dir:)
|
|
82
|
+
locked(parent_id, state_dir: state_dir) do
|
|
83
|
+
all = read(parent_id, state_dir: state_dir)
|
|
84
|
+
cursor = get_from(all, child_id)
|
|
85
|
+
next nil unless cursor == expected
|
|
86
|
+
|
|
87
|
+
claimed = cursor.with(report_claim: claim)
|
|
88
|
+
all[child_id] = claimed.to_json_hash
|
|
89
|
+
write(parent_id, all, state_dir: state_dir)
|
|
90
|
+
claimed
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A new owner can retry reports left reserved by a worker that exited
|
|
95
|
+
# before committing or releasing its turn. No cursor file means no
|
|
96
|
+
# claims: the lock would create the session dir and its lock file, so
|
|
97
|
+
# an empty session would no longer count as empty.
|
|
98
|
+
def clear_report_claims(parent_id, state_dir:)
|
|
99
|
+
return unless File.exist?(path(parent_id, state_dir: state_dir))
|
|
100
|
+
|
|
101
|
+
locked(parent_id, state_dir: state_dir) do
|
|
102
|
+
all = read(parent_id, state_dir: state_dir)
|
|
103
|
+
changed = false
|
|
104
|
+
all.each_value do |data|
|
|
105
|
+
next unless data.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
changed = true if data.delete("report_claim")
|
|
108
|
+
end
|
|
109
|
+
write(parent_id, all, state_dir: state_dir) if changed
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
71
113
|
# Add the cursors of +entries+ (child id → cursor fields, as #read
|
|
72
114
|
# gives them) that the parent has none for; one it has is kept.
|
|
73
115
|
# @return [Array<String>] the child ids added
|
|
@@ -94,11 +136,7 @@ module Samagotchi
|
|
|
94
136
|
end
|
|
95
137
|
|
|
96
138
|
def get_from(all, child_id)
|
|
97
|
-
|
|
98
|
-
return DelegateCursor.empty unless data.is_a?(Hash)
|
|
99
|
-
|
|
100
|
-
DelegateCursor.new(reply_file: data["reply_file"], last_turn: data["last_turn"],
|
|
101
|
-
question_id: data["question_id"], messages: data["messages"], turn: data["turn"])
|
|
139
|
+
DelegateCursor.from_json_hash(all[child_id])
|
|
102
140
|
end
|
|
103
141
|
|
|
104
142
|
def path(parent_id, state_dir:)
|
|
@@ -24,6 +24,7 @@ module Samagotchi
|
|
|
24
24
|
# the wait's first look (a fast failure) still ends it. The cursors are
|
|
25
25
|
# on disk (DelegateCursors), so a respawned worker doesn't repeat a reply.
|
|
26
26
|
module DelegateWait
|
|
27
|
+
Advance = Data.define(:cursor, :already_reported)
|
|
27
28
|
TIMEOUT_DEFAULT = 600
|
|
28
29
|
POLL_INTERVAL = 0.5
|
|
29
30
|
# Seconds with no worker before the child counts as gone: as long as
|
|
@@ -66,6 +67,11 @@ module Samagotchi
|
|
|
66
67
|
reports = reports_mode(peers)
|
|
67
68
|
others = relay && others_for(peers.session_id, child_id, relay, sd)
|
|
68
69
|
cursor = DelegateCursors.get(peers.session_id, child_id, state_dir: sd)
|
|
70
|
+
report_claim = cursor.report_claim
|
|
71
|
+
if report_claim
|
|
72
|
+
cursor = DelegateCursor.from_json_hash(report_claim["cursor"]).with(report_claim: report_claim)
|
|
73
|
+
end
|
|
74
|
+
|
|
69
75
|
baseline = cursor.baseline
|
|
70
76
|
left = timeout.to_i
|
|
71
77
|
outcomes = relayed_outcomes.delete(key) || []
|
|
@@ -78,7 +84,13 @@ module Samagotchi
|
|
|
78
84
|
left -= monotonic - started
|
|
79
85
|
unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
|
|
80
86
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
81
|
-
advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
87
|
+
movement = advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
88
|
+
if movement&.already_reported
|
|
89
|
+
file = movement.cursor.report_claim&.fetch("file", nil) || wait.file
|
|
90
|
+
return already_reported_result(child_id, file, state_dir: sd)
|
|
91
|
+
end
|
|
92
|
+
return already_reported_result(child_id, report_claim["file"], state_dir: sd) if report_claim && wait.status == :timeout
|
|
93
|
+
|
|
82
94
|
text = with_outcomes(finish(wait, child_id, timeout: timeout, reports: reports), outcomes)
|
|
83
95
|
return RefText.wrap(text, reply_path(child_id, wait, state_dir: sd))
|
|
84
96
|
end
|
|
@@ -159,15 +171,37 @@ module Samagotchi
|
|
|
159
171
|
# was reported; a question keeps it. Nothing moves on a timeout or a
|
|
160
172
|
# cancel.
|
|
161
173
|
# @param wait [ReplyWait::Result]
|
|
162
|
-
# @return [
|
|
174
|
+
# @return [Advance, nil] the cursor and whether another path reported it
|
|
163
175
|
def advance(parent_id, child_id, wait, state_dir:)
|
|
164
176
|
return nil unless REPORTED.include?(wait.status) && wait.session
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
already_reported = false
|
|
179
|
+
cursor = DelegateCursors.update(parent_id, child_id, state_dir: state_dir) do |current|
|
|
180
|
+
if (current.report_claim && current.report_claim["file"] == wait.file) ||
|
|
181
|
+
(wait.status == :done && current.reply_file == wait.file)
|
|
182
|
+
already_reported = true
|
|
183
|
+
next current
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
current = current.with(reply_file: wait.file) if wait.status == :done
|
|
187
|
+
current = current.with(turn: nil) unless wait.status == :waiting_for_answer
|
|
188
|
+
current.with_baseline(ReplyWait.baseline_of(wait.session))
|
|
170
189
|
end
|
|
190
|
+
Advance.new(cursor: cursor, already_reported: already_reported)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def already_reported_result(child_id, file, state_dir:)
|
|
194
|
+
path = if file.to_s.empty?
|
|
195
|
+
nil
|
|
196
|
+
else
|
|
197
|
+
File.join(ReplyWait.reply_dir(child_id, state_dir: state_dir), file)
|
|
198
|
+
end
|
|
199
|
+
text = if path
|
|
200
|
+
"session: #{child_id}\nstatus: reported\nThe reply was already reported to this session (file: #{File.basename(file)})."
|
|
201
|
+
else
|
|
202
|
+
"session: #{child_id}\nstatus: reported\nThe child's latest status was already reported to this session."
|
|
203
|
+
end
|
|
204
|
+
RefText.wrap(text, path)
|
|
171
205
|
end
|
|
172
206
|
|
|
173
207
|
# The tool result for how the wait ended.
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -649,6 +649,15 @@ module Samagotchi
|
|
|
649
649
|
thinking, error = request_thinking(body["thinking"])
|
|
650
650
|
return error if error
|
|
651
651
|
|
|
652
|
+
# bundle_settings: the session's own bundle settings (chi --set:
|
|
653
|
+
# {bundle => {key => scalar}}), checked against the installed
|
|
654
|
+
# bundles the same way.
|
|
655
|
+
begin
|
|
656
|
+
bundle_settings = BundleSessionSettings.from_request(body["bundle_settings"])
|
|
657
|
+
rescue BundleSessionSettings::Error => e
|
|
658
|
+
return error_response(400, "invalid_bundle_settings", e.message)
|
|
659
|
+
end
|
|
660
|
+
|
|
652
661
|
folder = dir ? { working_directory: dir } : {}
|
|
653
662
|
# model: the model the session starts on (the start page's picker,
|
|
654
663
|
# spelled as GET /api/models lists it); blank means the default.
|
|
@@ -659,7 +668,7 @@ module Samagotchi
|
|
|
659
668
|
# until that turn is saved.
|
|
660
669
|
preview = idle ? body["preview"].to_s.strip : ""
|
|
661
670
|
folder[:title] = preview unless preview.empty?
|
|
662
|
-
setup = SessionSetup.new(llm_context: llm_context, thinking: thinking)
|
|
671
|
+
setup = SessionSetup.new(llm_context: llm_context, thinking: thinking, bundle_settings: bundle_settings)
|
|
663
672
|
folder[:setup] = setup unless setup.empty?
|
|
664
673
|
begin
|
|
665
674
|
session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
|
|
@@ -679,7 +688,7 @@ module Samagotchi
|
|
|
679
688
|
# POST /api/sessions with continues: <id, prefix or last:id>: the next
|
|
680
689
|
# link of that session's chain (SessionManager.continue_session), in its
|
|
681
690
|
# folder and with its own setup (SessionSetup: llm_context, thinking
|
|
682
|
-
# and reasoning_history levels), so dir, llm_context and
|
|
691
|
+
# and reasoning_history levels, bundle settings), so dir, llm_context, thinking and bundle_settings are
|
|
683
692
|
# refused beside it (a create takes no reasoning_history). It runs on
|
|
684
693
|
# model (or model_name) when given, else on the previous link's: one
|
|
685
694
|
# whose host is gone falls back to the default, and the 201's
|
|
@@ -694,9 +703,9 @@ module Samagotchi
|
|
|
694
703
|
unless given.is_a?(String) && !given.strip.empty?
|
|
695
704
|
return error_response(400, "invalid_continues", "continues is a session id")
|
|
696
705
|
end
|
|
697
|
-
if (fields = %w[dir llm_context thinking].select { |key| body.key?(key) }).any?
|
|
706
|
+
if (fields = %w[dir llm_context thinking bundle_settings].select { |key| body.key?(key) }).any?
|
|
698
707
|
return error_response(400, "invalid_continues",
|
|
699
|
-
"continues takes the previous session's folder, llm_context and
|
|
708
|
+
"continues takes the previous session's folder, llm_context, thinking and bundle_settings; " \
|
|
700
709
|
"leave out #{fields.join(", ")}")
|
|
701
710
|
end
|
|
702
711
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.84.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- lib/samagotchi/broadcast_command.rb
|
|
181
181
|
- lib/samagotchi/bundle_command.rb
|
|
182
182
|
- lib/samagotchi/bundle_needs.rb
|
|
183
|
+
- lib/samagotchi/bundle_session_settings.rb
|
|
183
184
|
- lib/samagotchi/bundles/agents-md/manifest.yml
|
|
184
185
|
- lib/samagotchi/bundles/agents-md/plugin.rb
|
|
185
186
|
- lib/samagotchi/bundles/btw/manifest.yml
|
|
@@ -200,6 +201,8 @@ files:
|
|
|
200
201
|
- lib/samagotchi/bundles/coordinator/plugin.rb
|
|
201
202
|
- lib/samagotchi/bundles/coordinator/skill_coordinator.md
|
|
202
203
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
204
|
+
- lib/samagotchi/bundles/cost-cap/manifest.yml
|
|
205
|
+
- lib/samagotchi/bundles/cost-cap/plugin.rb
|
|
203
206
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
204
207
|
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
205
208
|
- lib/samagotchi/bundles/github-pr/plugin.rb
|
|
@@ -469,6 +472,7 @@ files:
|
|
|
469
472
|
- lib/samagotchi/session_inbox.rb
|
|
470
473
|
- lib/samagotchi/session_manager.rb
|
|
471
474
|
- lib/samagotchi/session_metrics.rb
|
|
475
|
+
- lib/samagotchi/session_metrics/cost.rb
|
|
472
476
|
- lib/samagotchi/session_metrics/model_segment.rb
|
|
473
477
|
- lib/samagotchi/session_metrics/side_totals.rb
|
|
474
478
|
- lib/samagotchi/session_metrics/totals.rb
|