openclacky 1.5.6 → 1.5.8
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/.clacky/skills/gem-release/SKILL.md +18 -1
- data/.clackyrules +25 -1
- data/CHANGELOG.md +59 -0
- data/CONTRIBUTING.md +9 -0
- data/README.md +2 -2
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/Rakefile +15 -0
- data/docs/design-emit-and-background-skills.md +253 -0
- data/docs/mcp-architecture.md +48 -9
- data/docs/mcp.example.json +9 -0
- data/docs/superpowers/plans/2026-08-09-chatcut-extension-bridge.md +191 -0
- data/docs/superpowers/plans/2026-08-09-native-mcp-oauth.md +243 -0
- data/docs/superpowers/specs/2026-08-09-mcp-oauth-extension-fallback-design.md +115 -0
- data/lib/clacky/agent/cost_tracker.rb +17 -2
- data/lib/clacky/agent/hook_manager.rb +14 -3
- data/lib/clacky/agent/memory_updater.rb +17 -18
- data/lib/clacky/agent/message_compressor_helper.rb +23 -0
- data/lib/clacky/agent/session_serializer.rb +136 -22
- data/lib/clacky/agent/skill_auto_creator.rb +1 -2
- data/lib/clacky/agent/skill_evolution.rb +11 -7
- data/lib/clacky/agent/skill_manager.rb +20 -18
- data/lib/clacky/agent/skill_reflector.rb +3 -6
- data/lib/clacky/agent.rb +113 -23
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/anthropic_stream_aggregator.rb +6 -1
- data/lib/clacky/cli.rb +4 -1
- data/lib/clacky/client.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +23 -10
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -7
- data/lib/clacky/default_extensions/general/ext.yml +1 -1
- data/lib/clacky/extension/api_extension.rb +11 -3
- data/lib/clacky/extension/api_loader.rb +4 -11
- data/lib/clacky/extension/packager.rb +1 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/fanout.rb +100 -0
- data/lib/clacky/locales/en.rb +2 -0
- data/lib/clacky/locales/zh.rb +2 -0
- data/lib/clacky/mcp/cli_commands.rb +61 -0
- data/lib/clacky/mcp/client.rb +18 -2
- data/lib/clacky/mcp/connection_manager.rb +85 -0
- data/lib/clacky/mcp/http_transport.rb +25 -9
- data/lib/clacky/mcp/oauth/authorization_manager.rb +257 -0
- data/lib/clacky/mcp/oauth/config.rb +51 -0
- data/lib/clacky/mcp/oauth/credential_store.rb +82 -0
- data/lib/clacky/mcp/oauth/session.rb +88 -0
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_format/anthropic.rb +27 -13
- data/lib/clacky/message_history.rb +33 -2
- data/lib/clacky/providers.rb +13 -6
- data/lib/clacky/server/http_server.rb +102 -98
- data/lib/clacky/server/session_registry.rb +11 -15
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/session_manager.rb +21 -12
- data/lib/clacky/shell_hook_loader.rb +14 -8
- data/lib/clacky/tools/invoke_skill.rb +3 -2
- data/lib/clacky/tools/terminal.rb +11 -4
- data/lib/clacky/ui2/components/inline_input.rb +1 -0
- data/lib/clacky/ui2/components/input_area.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +1 -0
- data/lib/clacky/ui2/line_editor.rb +1 -0
- data/lib/clacky/ui2/markdown_renderer.rb +5 -1
- data/lib/clacky/ui2/progress_handle.rb +27 -3
- data/lib/clacky/ui2/ui_controller.rb +207 -6
- data/lib/clacky/ui_interface.rb +13 -3
- data/lib/clacky/utils/arguments_parser.rb +73 -4
- data/lib/clacky/utils/model_pricing.rb +21 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +144 -213
- data/lib/clacky/web/components/chat-navigator.js +1 -1
- data/lib/clacky/web/features/billing/store.js +3 -3
- data/lib/clacky/web/features/billing/view.js +5 -61
- data/lib/clacky/web/features/extensions/store.js +1 -1
- data/lib/clacky/web/features/extensions/view.js +28 -5
- data/lib/clacky/web/features/new-session/view.js +43 -2
- data/lib/clacky/web/i18n.js +38 -22
- data/lib/clacky/web/index.html +19 -24
- data/lib/clacky/web/projects.js +96 -279
- data/lib/clacky/web/sessions.js +169 -119
- data/lib/clacky/web/settings.js +7 -1
- data/lib/clacky/web/ws-dispatcher.js +61 -34
- data/lib/clacky.rb +2 -0
- metadata +12 -1
|
@@ -68,20 +68,23 @@ module Clacky
|
|
|
68
68
|
def run_memory_update_subagent
|
|
69
69
|
return unless should_update_memory?
|
|
70
70
|
|
|
71
|
-
with_memory_update_phase
|
|
72
|
-
run_memory_update_subagent_inner
|
|
73
|
-
end
|
|
71
|
+
with_memory_update_phase { run_memory_update_subagent_inner }
|
|
74
72
|
end
|
|
75
73
|
|
|
76
74
|
private def with_memory_update_phase
|
|
77
75
|
return yield unless @ui.respond_to?(:with_phase)
|
|
78
76
|
|
|
79
|
-
@ui.
|
|
77
|
+
pid = @ui.phase_start(kind: "memory_update", label: "Updating long-term memory")
|
|
78
|
+
summary = nil
|
|
79
|
+
begin
|
|
80
|
+
summary = yield
|
|
81
|
+
ensure
|
|
82
|
+
@ui.phase_end(pid, summary: summary)
|
|
83
|
+
end
|
|
84
|
+
summary
|
|
80
85
|
end
|
|
81
86
|
|
|
82
87
|
private def run_memory_update_subagent_inner
|
|
83
|
-
handle = @ui&.start_progress(message: "Updating long-term memory…", style: :primary)
|
|
84
|
-
|
|
85
88
|
# Fork subagent inheriting main agent's model, tools, and history.
|
|
86
89
|
# Maximizes prompt-cache reuse: same model, same tool set, same
|
|
87
90
|
# cloned history — only the +system_prompt_suffix+ (the memory
|
|
@@ -114,8 +117,6 @@ module Clacky
|
|
|
114
117
|
}
|
|
115
118
|
Clacky::Logger.error("memory_update_error", error: e)
|
|
116
119
|
return
|
|
117
|
-
ensure
|
|
118
|
-
handle&.finish
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
return unless result
|
|
@@ -124,16 +125,14 @@ module Clacky
|
|
|
124
125
|
# sessionbar shows the real total. The parent's task-complete cost
|
|
125
126
|
# (result[:total_cost_usd] in Agent#run) stays unaffected — it
|
|
126
127
|
# still reflects ONLY the user's task, not the memory update.
|
|
127
|
-
subagent_cost = result
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@ui&.show_info("Memory updated: #{result[:iterations]} iterations, $#{subagent_cost.round(4)}")
|
|
136
|
-
end
|
|
128
|
+
subagent_cost = absorb_subagent_cost(result)
|
|
129
|
+
|
|
130
|
+
# A summary is only worth showing if the subagent actually wrote
|
|
131
|
+
# something. The common "No memory updates needed." path returns nil
|
|
132
|
+
# so the phase leaves no trace at all.
|
|
133
|
+
return unless subagent_wrote_memory?(subagent)
|
|
134
|
+
|
|
135
|
+
"#{result[:iterations]} steps · $#{subagent_cost.round(4)}"
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
private def memory_update_enabled?
|
|
@@ -628,6 +628,7 @@ module Clacky
|
|
|
628
628
|
lines << ""
|
|
629
629
|
lines << format_message_content(content)
|
|
630
630
|
lines << ""
|
|
631
|
+
append_ext_events_line(lines, msg)
|
|
631
632
|
when "assistant"
|
|
632
633
|
# If this message is itself a compressed summary, annotate the header
|
|
633
634
|
# so the reader knows the original conversation is in the referenced chunk
|
|
@@ -660,6 +661,7 @@ module Clacky
|
|
|
660
661
|
end
|
|
661
662
|
lines << format_message_content(content) if content
|
|
662
663
|
lines << ""
|
|
664
|
+
append_ext_events_line(lines, msg)
|
|
663
665
|
when "tool"
|
|
664
666
|
tool_name = msg[:name] || "tool"
|
|
665
667
|
lines << "### Tool Result: #{tool_name}"
|
|
@@ -668,11 +670,32 @@ module Clacky
|
|
|
668
670
|
lines << truncate_content(content.to_s, max_length: 500)
|
|
669
671
|
lines << "```"
|
|
670
672
|
lines << ""
|
|
673
|
+
append_ext_events_line(lines, msg)
|
|
671
674
|
end
|
|
672
675
|
end
|
|
673
676
|
lines
|
|
674
677
|
end
|
|
675
678
|
|
|
679
|
+
# Serialize persisted extension events into the chunk MD so they survive
|
|
680
|
+
# compression and can be replayed in place. Mirrors the "_Tool calls:"
|
|
681
|
+
# convention; parse_chunk_md_to_rounds reads it back.
|
|
682
|
+
def append_ext_events_line(lines, msg)
|
|
683
|
+
events = Array(msg[:ext_events])
|
|
684
|
+
return if events.empty?
|
|
685
|
+
|
|
686
|
+
parts = events.filter_map do |ev|
|
|
687
|
+
type = ev[:type] || ev["type"]
|
|
688
|
+
next nil if type.nil? || type.to_s.empty?
|
|
689
|
+
|
|
690
|
+
data = ev[:data] || ev["data"] || {}
|
|
691
|
+
"#{type} | #{data.to_json}"
|
|
692
|
+
end
|
|
693
|
+
return if parts.empty?
|
|
694
|
+
|
|
695
|
+
lines << "_Ext events: #{parts.join("; ")}_"
|
|
696
|
+
lines << ""
|
|
697
|
+
end
|
|
698
|
+
|
|
676
699
|
# Format message content (handles string or array of content blocks)
|
|
677
700
|
def format_message_content(content)
|
|
678
701
|
return "" if content.nil?
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
3
5
|
module Clacky
|
|
4
6
|
class Agent
|
|
5
7
|
# Session serialization for saving and restoring agent state
|
|
@@ -11,7 +13,6 @@ module Clacky
|
|
|
11
13
|
@session_id = session_data[:session_id]
|
|
12
14
|
@name = session_data[:name] || ""
|
|
13
15
|
@pinned = session_data[:pinned] || false
|
|
14
|
-
@hidden = session_data[:hidden] || false
|
|
15
16
|
@history = MessageHistory.new(session_data[:messages] || [])
|
|
16
17
|
@todos = session_data[:todos] || [] # Restore todos from session
|
|
17
18
|
@iterations = session_data.dig(:stats, :total_iterations) || 0
|
|
@@ -226,7 +227,6 @@ module Clacky
|
|
|
226
227
|
session_id: @session_id,
|
|
227
228
|
name: @name,
|
|
228
229
|
pinned: @pinned,
|
|
229
|
-
hidden: @hidden,
|
|
230
230
|
created_at: @created_at,
|
|
231
231
|
updated_at: (updated_at || @persisted_updated_at || Time.now.iso8601).then { |v| v.is_a?(String) ? v : v.iso8601 },
|
|
232
232
|
working_dir: @working_dir,
|
|
@@ -393,6 +393,7 @@ module Clacky
|
|
|
393
393
|
all_files = image_files + disk_files
|
|
394
394
|
ui.show_user_message(raw_text, created_at: msg[:created_at], files: all_files,
|
|
395
395
|
editable: round[:editable] != false)
|
|
396
|
+
replay_ext_events(msg, ui)
|
|
396
397
|
|
|
397
398
|
round[:events].each do |ev|
|
|
398
399
|
# Skip system-injected messages (e.g. synthetic skill content, memory prompts)
|
|
@@ -522,6 +523,7 @@ module Clacky
|
|
|
522
523
|
|
|
523
524
|
sections.each do |sec|
|
|
524
525
|
text = sec[:lines].join.strip
|
|
526
|
+
text, sec_ext_events = extract_ext_events_from_text(text)
|
|
525
527
|
|
|
526
528
|
# Nested chunk: expand it recursively, prepend before current rounds
|
|
527
529
|
if sec[:nested_chunk]
|
|
@@ -534,7 +536,7 @@ module Clacky
|
|
|
534
536
|
next
|
|
535
537
|
end
|
|
536
538
|
|
|
537
|
-
next if text.empty?
|
|
539
|
+
next if text.empty? && sec_ext_events.empty?
|
|
538
540
|
|
|
539
541
|
if sec[:role] == "user"
|
|
540
542
|
round_index += 1
|
|
@@ -545,6 +547,7 @@ module Clacky
|
|
|
545
547
|
role: "user",
|
|
546
548
|
content: text,
|
|
547
549
|
created_at: synthetic_ts,
|
|
550
|
+
ext_events: sec_ext_events,
|
|
548
551
|
_from_chunk: true
|
|
549
552
|
},
|
|
550
553
|
events: [],
|
|
@@ -569,19 +572,7 @@ module Clacky
|
|
|
569
572
|
stripped = line.strip
|
|
570
573
|
if (m = stripped.match(/\A_Tool calls?:\s*(.+?)_?\z/i))
|
|
571
574
|
raw = m[1]
|
|
572
|
-
|
|
573
|
-
# Old format uses "," with no JSON part.
|
|
574
|
-
entries = raw.include?(" | ") ? raw.split(/;\s*/) : raw.split(/,\s*/)
|
|
575
|
-
entries.each do |entry|
|
|
576
|
-
entry = entry.strip
|
|
577
|
-
if (parts = entry.match(/\A(.+?)\s*\|\s*(\{.+\})\z/))
|
|
578
|
-
tool_name = parts[1].strip
|
|
579
|
-
args = JSON.parse(parts[2]) rescue {}
|
|
580
|
-
pending_tool_entries << { name: tool_name, args: args }
|
|
581
|
-
else
|
|
582
|
-
pending_tool_entries << { name: entry, args: {} }
|
|
583
|
-
end
|
|
584
|
-
end
|
|
575
|
+
pending_tool_entries.concat(parse_tool_calls_line(raw))
|
|
585
576
|
else
|
|
586
577
|
remaining_lines << line
|
|
587
578
|
end
|
|
@@ -599,8 +590,11 @@ module Clacky
|
|
|
599
590
|
tool_calls: [{ name: entry[:name], arguments: entry[:args] }]
|
|
600
591
|
}
|
|
601
592
|
end
|
|
593
|
+
|
|
594
|
+
attach_ext_events_to_last(current_round, sec_ext_events)
|
|
602
595
|
else
|
|
603
596
|
current_round[:events] << { role: "tool", content: text }
|
|
597
|
+
attach_ext_events_to_last(current_round, sec_ext_events)
|
|
604
598
|
end
|
|
605
599
|
end
|
|
606
600
|
end
|
|
@@ -612,6 +606,108 @@ module Clacky
|
|
|
612
606
|
end
|
|
613
607
|
|
|
614
608
|
|
|
609
|
+
# Pull the "_Ext events: type | {json}; ..._" line out of a chunk section,
|
|
610
|
+
# returning the remaining text plus the parsed events. Counterpart to
|
|
611
|
+
# MessageCompressorHelper#append_ext_events_line.
|
|
612
|
+
def extract_ext_events_from_text(text)
|
|
613
|
+
return [text, []] unless text.include?("_Ext events:")
|
|
614
|
+
|
|
615
|
+
events = []
|
|
616
|
+
kept = text.each_line.reject do |line|
|
|
617
|
+
m = line.strip.match(/\A_Ext events?:\s*(.+?)_?\z/i)
|
|
618
|
+
next false unless m
|
|
619
|
+
|
|
620
|
+
parse_piped_entries(m[1]).each do |entry|
|
|
621
|
+
next if entry[:args].nil?
|
|
622
|
+
|
|
623
|
+
events << { type: entry[:name], data: entry[:args] }
|
|
624
|
+
end
|
|
625
|
+
true
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
[kept.join.strip, events]
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Split a "_Tool calls:_" payload into entries. The separator ";" also
|
|
632
|
+
# occurs inside args JSON (shell commands, JS snippets), so entry
|
|
633
|
+
# boundaries are found by brace balancing rather than String#split.
|
|
634
|
+
def parse_tool_calls_line(raw)
|
|
635
|
+
parse_piped_entries(raw).map { |e| { name: e[:name], args: e[:args] || {} } }
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Scan a "name | {json}; name | {json}" payload. Shared by the
|
|
639
|
+
# "_Tool calls:" and "_Ext events:" chunk MD lines, whose ";" separator
|
|
640
|
+
# is ambiguous with semicolons inside the JSON values. Entries with no
|
|
641
|
+
# JSON payload yield args: nil so callers can tell "absent" from "empty".
|
|
642
|
+
def parse_piped_entries(raw)
|
|
643
|
+
return [] if raw.nil?
|
|
644
|
+
|
|
645
|
+
scanner = StringScanner.new(raw)
|
|
646
|
+
entries = []
|
|
647
|
+
|
|
648
|
+
until scanner.eos?
|
|
649
|
+
scanner.skip(/[;,]?\s*/)
|
|
650
|
+
break if scanner.eos?
|
|
651
|
+
|
|
652
|
+
name = scanner.scan(/[^|;,]+?(?=\s*\|\s*\{)/)
|
|
653
|
+
unless name
|
|
654
|
+
plain = scanner.scan(/[^;,]+/)
|
|
655
|
+
break unless plain
|
|
656
|
+
|
|
657
|
+
plain = plain.strip
|
|
658
|
+
entries << { name: plain, args: nil } unless plain.empty?
|
|
659
|
+
next
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
scanner.skip(/\s*\|\s*/)
|
|
663
|
+
json = scan_balanced_json(scanner)
|
|
664
|
+
next unless json
|
|
665
|
+
|
|
666
|
+
parsed = JSON.parse(json) rescue nil
|
|
667
|
+
entries << { name: name.strip, args: parsed.is_a?(Hash) ? parsed : {} }
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
entries
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Consume one balanced {...} literal, ignoring braces inside JSON strings.
|
|
674
|
+
def scan_balanced_json(scanner)
|
|
675
|
+
start = scanner.pos
|
|
676
|
+
return nil unless scanner.getch == "{"
|
|
677
|
+
|
|
678
|
+
depth = 1
|
|
679
|
+
in_string = false
|
|
680
|
+
escaped = false
|
|
681
|
+
|
|
682
|
+
while (ch = scanner.getch)
|
|
683
|
+
if escaped
|
|
684
|
+
escaped = false
|
|
685
|
+
elsif ch == "\\"
|
|
686
|
+
escaped = true
|
|
687
|
+
elsif ch == '"'
|
|
688
|
+
in_string = !in_string
|
|
689
|
+
elsif !in_string && ch == "{"
|
|
690
|
+
depth += 1
|
|
691
|
+
elsif !in_string && ch == "}"
|
|
692
|
+
depth -= 1
|
|
693
|
+
return scanner.string[start...scanner.pos] if depth.zero?
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
nil
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Anchor chunk-restored events to the last message of the round so they
|
|
701
|
+
# replay after it, matching where they were emitted live.
|
|
702
|
+
def attach_ext_events_to_last(round, events)
|
|
703
|
+
return if events.nil? || events.empty?
|
|
704
|
+
|
|
705
|
+
target = round[:events].last || round[:user_msg]
|
|
706
|
+
return unless target
|
|
707
|
+
|
|
708
|
+
target[:ext_events] = Array(target[:ext_events]) + events
|
|
709
|
+
end
|
|
710
|
+
|
|
615
711
|
# Render a single non-user message into the UI.
|
|
616
712
|
# Used by both the normal round-based replay and the compressed-session fallback.
|
|
617
713
|
def _replay_single_message(msg, ui)
|
|
@@ -664,20 +760,38 @@ module Clacky
|
|
|
664
760
|
|
|
665
761
|
when "user"
|
|
666
762
|
# Anthropic-format tool results (role: user, content: array of tool_result blocks)
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
next unless blk.is_a?(Hash) && blk[:type] == "tool_result"
|
|
763
|
+
if msg[:content].is_a?(Array)
|
|
764
|
+
msg[:content].each do |blk|
|
|
765
|
+
next unless blk.is_a?(Hash) && blk[:type] == "tool_result"
|
|
671
766
|
|
|
672
|
-
|
|
767
|
+
ui.show_tool_result(blk[:content].to_s)
|
|
768
|
+
end
|
|
769
|
+
replay_subagent_transcript(msg, ui)
|
|
673
770
|
end
|
|
674
|
-
replay_subagent_transcript(msg, ui)
|
|
675
771
|
|
|
676
772
|
when "tool"
|
|
677
773
|
# OpenAI-format tool result
|
|
678
774
|
ui.show_tool_result(msg[:content].to_s)
|
|
679
775
|
replay_subagent_transcript(msg, ui)
|
|
680
776
|
end
|
|
777
|
+
|
|
778
|
+
replay_ext_events(msg, ui)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Re-emit custom extension events recorded on a message so extension
|
|
782
|
+
# panels can rebuild their state after a page reload. Mirrors what
|
|
783
|
+
# Agent#emit_event pushed live. No-op when the message carries none.
|
|
784
|
+
def replay_ext_events(msg, ui)
|
|
785
|
+
Array(msg[:ext_events]).each do |ev|
|
|
786
|
+
next unless ev.is_a?(Hash)
|
|
787
|
+
|
|
788
|
+
type = ev[:type] || ev["type"]
|
|
789
|
+
next if type.nil? || type.to_s.empty?
|
|
790
|
+
|
|
791
|
+
data = ev[:data] || ev["data"] || {}
|
|
792
|
+
data = data.is_a?(Hash) ? data.transform_keys(&:to_sym) : {}
|
|
793
|
+
ui.emit(type.to_s, **data)
|
|
794
|
+
end
|
|
681
795
|
end
|
|
682
796
|
|
|
683
797
|
# Replay a subagent transcript stored on a tool result message. Emits a
|
|
@@ -20,11 +20,10 @@ module Clacky
|
|
|
20
20
|
def maybe_create_skill_from_task
|
|
21
21
|
return unless should_auto_create_skill?
|
|
22
22
|
|
|
23
|
-
@ui&.show_info("Analyzing task for skill creation opportunity...")
|
|
24
|
-
|
|
25
23
|
# Fork an isolated subagent to evaluate + create — does NOT touch main history
|
|
26
24
|
subagent = fork_subagent
|
|
27
25
|
subagent.run(build_skill_creation_prompt)
|
|
26
|
+
nil
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
# Determine if this task is a candidate for skill auto-creation
|
|
@@ -26,10 +26,11 @@ module Clacky
|
|
|
26
26
|
def run_skill_evolution_hooks
|
|
27
27
|
return unless skill_evolution_enabled?
|
|
28
28
|
return if @is_subagent
|
|
29
|
-
return unless skill_evolution_visible? || skill_evolution_has_work?
|
|
30
29
|
|
|
31
30
|
with_skill_evolution_phase do
|
|
32
|
-
if
|
|
31
|
+
if !skill_evolution_has_work?
|
|
32
|
+
Clacky::I18n.t("phase.skipped")
|
|
33
|
+
elsif @skill_execution_context
|
|
33
34
|
maybe_reflect_on_skill
|
|
34
35
|
else
|
|
35
36
|
maybe_create_skill_from_task
|
|
@@ -37,10 +38,6 @@ module Clacky
|
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
private def skill_evolution_visible?
|
|
41
|
-
@config.respond_to?(:verbose) && @config.verbose
|
|
42
|
-
end
|
|
43
|
-
|
|
44
41
|
private def skill_evolution_has_work?
|
|
45
42
|
if @skill_execution_context
|
|
46
43
|
should_reflect_on_skill?
|
|
@@ -52,7 +49,14 @@ module Clacky
|
|
|
52
49
|
private def with_skill_evolution_phase
|
|
53
50
|
return yield unless @ui.respond_to?(:with_phase)
|
|
54
51
|
|
|
55
|
-
@ui.
|
|
52
|
+
pid = @ui.phase_start(kind: "skill_evolution", label: Clacky::I18n.t("phase.skill_evolution"))
|
|
53
|
+
summary = nil
|
|
54
|
+
begin
|
|
55
|
+
summary = yield
|
|
56
|
+
ensure
|
|
57
|
+
@ui.phase_end(pid, summary: summary)
|
|
58
|
+
end
|
|
59
|
+
summary
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# Check if skill evolution is enabled in config
|
|
@@ -576,8 +576,10 @@ module Clacky
|
|
|
576
576
|
# Execute a skill in a forked subagent
|
|
577
577
|
# @param skill [Skill] The skill to execute
|
|
578
578
|
# @param arguments [String] Arguments for the skill
|
|
579
|
+
# @param tool_call_id [String, nil] The invoke_skill call this run belongs to;
|
|
580
|
+
# nil for the slash-command path, which has no tool result to attach to.
|
|
579
581
|
# @return [String] Summary of subagent execution
|
|
580
|
-
def execute_skill_with_subagent(skill, arguments)
|
|
582
|
+
def execute_skill_with_subagent(skill, arguments, tool_call_id: nil)
|
|
581
583
|
touch_skill_for_lru(skill)
|
|
582
584
|
# For encrypted brand skills with supporting scripts: decrypt to a tmpdir.
|
|
583
585
|
# Subagent path has a clear boundary (subagent.run returns), so we shred inline
|
|
@@ -603,17 +605,22 @@ module Clacky
|
|
|
603
605
|
system_prompt_suffix: skill_instructions
|
|
604
606
|
)
|
|
605
607
|
|
|
606
|
-
# Log which model the subagent is actually using (may differ from requested
|
|
607
|
-
# when "lite" falls back to default due to no lite model configured)
|
|
608
|
-
@ui&.show_info("Subagent start: #{skill.identifier}#{skill.name_zh.to_s.empty? ? "" : " (#{skill.name_zh})"} [#{subagent.current_model_info[:model]}]")
|
|
609
|
-
|
|
610
608
|
# Run subagent with the actual task as the sole user turn.
|
|
611
609
|
# If the user typed the skill command with no arguments (e.g. "/jade-appraisal"),
|
|
612
610
|
# use a generic trigger phrase so the user message is never empty.
|
|
613
611
|
task_input = arguments.to_s.strip.empty? ? "Please proceed." : arguments
|
|
614
612
|
|
|
615
613
|
begin
|
|
616
|
-
|
|
614
|
+
# The model may differ from the requested one ("lite" falls back to
|
|
615
|
+
# default when no lite model is configured), so name it on the card.
|
|
616
|
+
phase_label = "#{skill.identifier} · #{subagent.current_model_info[:model]}"
|
|
617
|
+
|
|
618
|
+
result = within_phase(phase_label, kind: "subagent", concurrent: false) do
|
|
619
|
+
run_result = subagent.run(task_input)
|
|
620
|
+
cost = absorb_subagent_cost(run_result)
|
|
621
|
+
@ui&.show_info("Completed in #{run_result[:iterations]} iterations · $#{cost.round(4)}")
|
|
622
|
+
run_result
|
|
623
|
+
end
|
|
617
624
|
rescue Clacky::AgentInterrupted
|
|
618
625
|
# Subagent was interrupted by user (Ctrl+C).
|
|
619
626
|
# Write an interrupted summary into history so the parent agent's history
|
|
@@ -642,9 +649,13 @@ module Clacky
|
|
|
642
649
|
# Capture the subagent's own message trail (everything it appended after
|
|
643
650
|
# the fork) so the parent session can persist and later replay the full
|
|
644
651
|
# subagent process — not just the collapsed summary. Stored transiently
|
|
645
|
-
# on the parent agent
|
|
646
|
-
#
|
|
647
|
-
|
|
652
|
+
# on the parent agent, keyed by the invoke_skill call it belongs to;
|
|
653
|
+
# observe() attaches each trail to its own tool result message.
|
|
654
|
+
# Kept out of the LLM payload via INTERNAL_FIELDS.
|
|
655
|
+
if tool_call_id
|
|
656
|
+
@pending_subagent_transcripts[tool_call_id] =
|
|
657
|
+
extract_subagent_transcript(subagent, skill.identifier)
|
|
658
|
+
end
|
|
648
659
|
|
|
649
660
|
# Mutate the subagent_instructions message in-place to become the result summary
|
|
650
661
|
@history.mutate_last_matching(->(m) { m[:subagent_instructions] }) do |m|
|
|
@@ -654,15 +665,6 @@ module Clacky
|
|
|
654
665
|
m[:skill_name] = skill.identifier
|
|
655
666
|
end
|
|
656
667
|
|
|
657
|
-
# Merge subagent cost into parent agent's total so the sessionbar reflects
|
|
658
|
-
# the real cumulative spend across all subagents
|
|
659
|
-
subagent_cost = result[:total_cost_usd] || 0.0
|
|
660
|
-
@total_cost += subagent_cost
|
|
661
|
-
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
662
|
-
|
|
663
|
-
# Log completion
|
|
664
|
-
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{subagent_cost.round(4)} (total: $#{@total_cost.round(4)})")
|
|
665
|
-
|
|
666
668
|
# Return summary as the skill execution result
|
|
667
669
|
summary
|
|
668
670
|
end
|
|
@@ -23,17 +23,14 @@ module Clacky
|
|
|
23
23
|
|
|
24
24
|
skill_name = @skill_execution_context[:skill_name]
|
|
25
25
|
|
|
26
|
-
@ui&.show_info("Reflecting on skill execution: #{skill_name}")
|
|
27
26
|
subagent = fork_subagent
|
|
28
27
|
result = subagent.run(build_skill_reflection_prompt(skill_name))
|
|
29
28
|
|
|
30
|
-
if result
|
|
31
|
-
subagent_cost = result[:total_cost_usd] || 0.0
|
|
32
|
-
@total_cost += subagent_cost
|
|
33
|
-
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
34
|
-
end
|
|
29
|
+
cost = absorb_subagent_cost(result) if result
|
|
35
30
|
|
|
36
31
|
@skill_execution_context = nil
|
|
32
|
+
|
|
33
|
+
"#{skill_name} · $#{cost.round(4)}" if cost
|
|
37
34
|
end
|
|
38
35
|
|
|
39
36
|
private def should_reflect_on_skill?
|