samagotchi 0.98.0 → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +2 -2
  4. data/docs/architecture.md +6 -3
  5. data/docs/bundle-authoring.md +1 -1
  6. data/docs/cli.md +1 -1
  7. data/docs/configuration.md +8 -0
  8. data/docs/guardrails.md +10 -2
  9. data/docs/hooks.md +9 -1
  10. data/docs/internals/anthropic.md +79 -8
  11. data/docs/internals/engine.md +13 -3
  12. data/docs/internals/llm-context-forget.md +2 -1
  13. data/docs/internals/prompt-caching.md +9 -0
  14. data/docs/memory.md +1 -1
  15. data/docs/plugins.md +83 -3
  16. data/docs/releasing.md +1 -1
  17. data/docs/sessions.md +10 -0
  18. data/docs/sub-agent.md +7 -2
  19. data/docs/testing.md +27 -0
  20. data/lib/samagotchi/bundles/core/manifest.yml +4 -3
  21. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  22. data/lib/samagotchi/bundles/work-cap/manifest.yml +13 -0
  23. data/lib/samagotchi/bundles/work-cap/plugin.rb +391 -0
  24. data/lib/samagotchi/engine.rb +67 -9
  25. data/lib/samagotchi/hooks/loader.rb +17 -4
  26. data/lib/samagotchi/kernel_loop.rb +15 -2
  27. data/lib/samagotchi/llm/anthropic_messages.rb +85 -4
  28. data/lib/samagotchi/llm/anthropic_wire.rb +90 -11
  29. data/lib/samagotchi/llm/chat_loop.rb +76 -7
  30. data/lib/samagotchi/llm/dialect.rb +8 -5
  31. data/lib/samagotchi/llm/refusal_memo.rb +6 -0
  32. data/lib/samagotchi/llm/turn_settings.rb +13 -5
  33. data/lib/samagotchi/llm_context_stale.rb +2 -2
  34. data/lib/samagotchi/message_parts.rb +2 -1
  35. data/lib/samagotchi/plugin/api.rb +2 -1
  36. data/lib/samagotchi/plugin/context.rb +31 -4
  37. data/lib/samagotchi/prompt.rb +3 -0
  38. data/lib/samagotchi/session_metrics/usage.rb +31 -0
  39. data/lib/samagotchi/session_metrics.rb +48 -5
  40. data/lib/samagotchi/testing/bundle.rb +2 -0
  41. data/lib/samagotchi/testing/host.rb +31 -1
  42. data/lib/samagotchi/testing/recorder.rb +18 -3
  43. data/lib/samagotchi/tool_loads.rb +62 -0
  44. data/lib/samagotchi/tool_response.rb +5 -1
  45. data/lib/samagotchi/tool_runner.rb +29 -7
  46. data/lib/samagotchi/tools/builtin_calls.rb +16 -0
  47. data/lib/samagotchi/tools/deferred_tools.rb +80 -0
  48. data/lib/samagotchi/tools/registry.rb +7 -2
  49. data/lib/samagotchi/tools/tool_index.rb +88 -6
  50. data/lib/samagotchi/version.rb +1 -1
  51. metadata +6 -1
@@ -56,6 +56,8 @@ module Samagotchi
56
56
  group: { type: "string", description: "search only this group" }
57
57
  }.freeze
58
58
  CALL_DESCRIPTION = "Call a tool #{FIND_NAME} found.".freeze
59
+ # A keyword answer's first line.
60
+ CALL_HINT = "Call one with #{CALL_NAME}(tool: \"<group>/<tool>\", args: {…}).".freeze
59
61
  CALL_PARAMS = {
60
62
  tool: { type: "string", required: true, description: "the tool as <group>/<tool>" },
61
63
  args: { type: "object", description: "the arguments, per the inputSchema #{FIND_NAME} gave" }
@@ -84,6 +86,19 @@ module Samagotchi
84
86
  # A Group's states.
85
87
  STATES = %i[ready starting failed].freeze
86
88
 
89
+ # A search's answer (#find): its text, and the tools it loads on a host
90
+ # that loads tools (+loaded+: the wire names of a keyword answer's
91
+ # callable matches, the ones a request can declare deferred; none for
92
+ # an empty query, a miss or the names listing).
93
+ class FoundText < String
94
+ attr_reader :loaded
95
+
96
+ def initialize(text, loaded = [])
97
+ super(text)
98
+ @loaded = loaded.freeze
99
+ end
100
+ end
101
+
87
102
  # One group of a source's tools (an MCP server): +summary+ its line in
88
103
  # the search tool's description (nil: its name and tool count only),
89
104
  # +state+ :ready, :starting (its tools may come: a lookup waits) or
@@ -236,7 +251,8 @@ module Samagotchi
236
251
  registry.register(spec.name, source: SOURCE, unwrap: unwrap, **spec.entry_fields(nil))
237
252
  spec
238
253
  end
239
- registry.index = Registry::Index.new(source: SOURCE, call_name: CALL_NAME, sourced: !@sources.empty?)
254
+ registry.index = Registry::Index.new(source: SOURCE, call_name: CALL_NAME, sourced: !@sources.empty?,
255
+ deferrable: -> { deferrable }, wrap: ->(call) { wrap(call) })
240
256
  declared!(declared)
241
257
  declared.map(&:name)
242
258
  end
@@ -286,11 +302,12 @@ module Samagotchi
286
302
  # empty query every group's tool names. +server+ is the parameter's
287
303
  # name before it was +group+: a model that still gives it is told
288
304
  # every group was searched.
305
+ # @return [FoundText]
289
306
  def find(query, only = nil, server: nil)
290
307
  answer = find_in(query, only)
291
308
  return answer unless (only.nil? || only.strip.empty?) && !server.to_s.strip.empty?
292
309
 
293
- "(searched every group)\n#{answer}"
310
+ FoundText.new("(searched every group)\n#{answer}", answer.loaded)
294
311
  end
295
312
 
296
313
  # ── The call ────────────────────────────────────────────────────────
@@ -348,6 +365,56 @@ module Samagotchi
348
365
  inner && BuiltinCalls.build(entry.name, inner).merge(via: CALL_NAME)
349
366
  end
350
367
 
368
+ # The sources' tools a host that loads tools may declare deferred
369
+ # (Tools::DeferredTools), by their acts_as: callable (no refusal),
370
+ # the only tool with that acts_as, a name Messages takes, and no
371
+ # registered tool's. Sorted by acts_as, so the same index gives the
372
+ # same list in any session and after a respawn.
373
+ # @return [Array<Tool>]
374
+ def deferrable
375
+ tools = groups.reject { |group| own?(group) }.flat_map(&:tools)
376
+ counts = tools.map(&:acts_as).tally
377
+ tools.select do |tool|
378
+ tool.refusal.nil? && counts[tool.acts_as] == 1 && tool.acts_as.match?(DEFERRABLE_NAME) &&
379
+ !@registry&.key?(tool.acts_as)
380
+ end.sort_by(&:acts_as)
381
+ end
382
+
383
+ # The name a request declares +name+'s tool by when it goes out
384
+ # deferred (Tools::DeferredTools): a reachable built-in's (by its
385
+ # name or chi/<name>), a deferrable source tool's acts_as (by it or
386
+ # <group>/<tool>). Nil for any other: declared, removed, unknown, or a
387
+ # source tool that can't be deferred. Never waits for a start.
388
+ # @return [String, nil]
389
+ def deferred_name(name)
390
+ name = name.to_s.strip
391
+ bare = name.delete_prefix("#{CHI_GROUP}/")
392
+ return bare if @registry&.indexed(bare)
393
+
394
+ tool = lookup_quietly(name) or return nil
395
+ return nil if tool.entry || tool.refusal
396
+
397
+ deferrable.any? { |t| t.acts_as == tool.acts_as } ? tool.acts_as : nil
398
+ end
399
+
400
+ # What a deferred source tool's wire name may be (Messages' tool
401
+ # name rule).
402
+ DEFERRABLE_NAME = /\A[a-zA-Z0-9_-]{1,128}\z/
403
+
404
+ # A direct call of a deferrable tool (#deferrable) by its acts_as, as
405
+ # the call_tool call it runs as (ToolRunner#run, under a host that
406
+ # loads tools): the call tool's row, rules and approvals. Nil for any
407
+ # other call.
408
+ # @return [Hash, nil]
409
+ def wrap(call)
410
+ name = call[:name].to_s
411
+ return nil if name.empty? || @registry&.key?(name)
412
+
413
+ tool = deferrable.find { |t| t.acts_as == name } or return nil
414
+ args = call[:args].is_a?(Hash) ? call[:args] : {}
415
+ BuiltinCalls.build(CALL_NAME, { "tool" => tool.full_name, "args" => args })
416
+ end
417
+
351
418
  # The call tool's activity row: <group>/<tool> and its arguments.
352
419
  def call_preview(args)
353
420
  tool = lookup_quietly(args["tool"].to_s)
@@ -403,17 +470,32 @@ module Samagotchi
403
470
  searched = all
404
471
  if only && !only.strip.empty?
405
472
  searched = all.select { |group| group.name == only.strip }
406
- return "Error: no group #{only.strip}. Groups: #{all.map(&:name).join(", ")}" if searched.empty?
473
+ return FoundText.new("Error: no group #{only.strip}. Groups: #{all.map(&:name).join(", ")}") if searched.empty?
407
474
  end
408
- return names_text(searched) if words(query).empty?
475
+ return FoundText.new(names_text(searched)) if words(query).empty?
409
476
 
410
477
  matches = ranked(query, searched.flat_map(&:tools)).take(FIND_RESULTS)
411
- return "No tool matches \"#{query.strip}\".\n\n#{names_text(searched)}" if matches.empty?
478
+ return FoundText.new("No tool matches \"#{query.strip}\".\n\n#{names_text(searched)}") if matches.empty?
412
479
 
413
480
  blocks = matches.map { |tool| match_text(tool, all) }
414
481
  unsearched = searched.filter_map { |group| "#{group.name} #{group.note}" if group.tools.empty? && group.note }
415
482
  blocks << "Not searched: #{unsearched.join("; ")}." unless unsearched.empty?
416
- "Call one with #{CALL_NAME}(tool: \"<group>/<tool>\", args: {…}).\n\n#{blocks.join("\n\n")}"
483
+ FoundText.new("#{CALL_HINT}\n\n#{blocks.join("\n\n")}",
484
+ loadable(matches))
485
+ end
486
+
487
+ # The wire names of +matches+ a request can declare deferred: a
488
+ # built-in of the index's own groups by its name, a source's tool by
489
+ # its acts_as when it is #deferrable.
490
+ def loadable(matches)
491
+ sourced = nil
492
+ matches.filter_map do |tool|
493
+ next tool.name if tool.entry
494
+ next if tool.refusal
495
+
496
+ sourced ||= deferrable.map(&:acts_as)
497
+ tool.acts_as if sourced.include?(tool.acts_as)
498
+ end
417
499
  end
418
500
 
419
501
  # What the sources hear of the declared tools (/mcp's last line).
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.98.0"
4
+ VERSION = "0.100.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
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.98.0
4
+ version: 0.100.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -266,6 +266,8 @@ files:
266
266
  - lib/samagotchi/bundles/system/manifest.yml
267
267
  - lib/samagotchi/bundles/system/memory_guide.md
268
268
  - lib/samagotchi/bundles/system/self_map.md
269
+ - lib/samagotchi/bundles/work-cap/manifest.yml
270
+ - lib/samagotchi/bundles/work-cap/plugin.rb
269
271
  - lib/samagotchi/cache_confirm.rb
270
272
  - lib/samagotchi/cancellation_controller.rb
271
273
  - lib/samagotchi/child_env.rb
@@ -523,6 +525,7 @@ files:
523
525
  - lib/samagotchi/session_metrics/model_segment.rb
524
526
  - lib/samagotchi/session_metrics/side_totals.rb
525
527
  - lib/samagotchi/session_metrics/totals.rb
528
+ - lib/samagotchi/session_metrics/usage.rb
526
529
  - lib/samagotchi/session_model_settings.rb
527
530
  - lib/samagotchi/session_observer.rb
528
531
  - lib/samagotchi/session_retention.rb
@@ -590,6 +593,7 @@ files:
590
593
  - lib/samagotchi/tool_call_parser.rb
591
594
  - lib/samagotchi/tool_declarations.rb
592
595
  - lib/samagotchi/tool_ids.rb
596
+ - lib/samagotchi/tool_loads.rb
593
597
  - lib/samagotchi/tool_output_cut.rb
594
598
  - lib/samagotchi/tool_output_files.rb
595
599
  - lib/samagotchi/tool_response.rb
@@ -605,6 +609,7 @@ files:
605
609
  - lib/samagotchi/tools/builtins.rb
606
610
  - lib/samagotchi/tools/cancel_reminder.rb
607
611
  - lib/samagotchi/tools/context_read.rb
612
+ - lib/samagotchi/tools/deferred_tools.rb
608
613
  - lib/samagotchi/tools/delegate.rb
609
614
  - lib/samagotchi/tools/delegate_cursor.rb
610
615
  - lib/samagotchi/tools/delegate_relay.rb