openclacky 1.5.4 → 1.5.6
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 +63 -0
- data/lib/clacky/agent/goal_manager.rb +305 -0
- data/lib/clacky/agent/goal_state.rb +88 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +27 -3
- data/lib/clacky/agent.rb +202 -3
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/cli.rb +15 -15
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +130 -59
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +12 -4
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_searchers/tavily.rb +98 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +29 -1
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +114 -20
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/idle_compression_timer.rb +1 -3
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +8 -0
- data/lib/clacky/providers.rb +65 -8
- data/lib/clacky/search_config.rb +57 -0
- data/lib/clacky/server/http_server.rb +249 -31
- data/lib/clacky/server/session_registry.rb +10 -3
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/tools/web_search.rb +45 -6
- data/lib/clacky/ui2/components/command_suggestions.rb +17 -0
- data/lib/clacky/ui2/components/input_area.rb +26 -2
- data/lib/clacky/ui2/ui_controller.rb +17 -0
- data/lib/clacky/ui_interface.rb +9 -0
- data/lib/clacky/utils/gitignore_parser.rb +7 -1
- data/lib/clacky/utils/model_pricing.rb +0 -10
- data/lib/clacky/utils/searcher_manager.rb +87 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +530 -197
- data/lib/clacky/web/app.js +28 -3
- data/lib/clacky/web/components/chat-navigator.js +260 -0
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/brand/view.js +5 -1
- data/lib/clacky/web/features/extensions/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +158 -13
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +337 -140
- data/lib/clacky/web/features/skills/store.js +4 -2
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +72 -18
- data/lib/clacky/web/index.html +71 -64
- data/lib/clacky/web/projects.js +36 -19
- data/lib/clacky/web/sessions.js +173 -8
- data/lib/clacky/web/settings.js +85 -0
- data/lib/clacky/web/skills.js +24 -4
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +63 -3
- data/lib/clacky.rb +2 -0
- metadata +7 -1
data/lib/clacky/agent.rb
CHANGED
|
@@ -25,6 +25,8 @@ require_relative "agent/skill_evolution"
|
|
|
25
25
|
require_relative "agent/skill_reflector"
|
|
26
26
|
require_relative "agent/skill_auto_creator"
|
|
27
27
|
require_relative "agent/fake_tool_call_detector"
|
|
28
|
+
require_relative "agent/goal_state"
|
|
29
|
+
require_relative "agent/goal_manager"
|
|
28
30
|
|
|
29
31
|
module Clacky
|
|
30
32
|
class Agent
|
|
@@ -53,7 +55,7 @@ module Clacky
|
|
|
53
55
|
attr_accessor :channel_info
|
|
54
56
|
attr_accessor :project_id
|
|
55
57
|
|
|
56
|
-
REASONING_EFFORTS = %w[low medium high xhigh].freeze
|
|
58
|
+
REASONING_EFFORTS = %w[low medium high xhigh max].freeze
|
|
57
59
|
|
|
58
60
|
def permission_mode
|
|
59
61
|
@config&.permission_mode&.to_s || ""
|
|
@@ -169,6 +171,9 @@ module Clacky
|
|
|
169
171
|
|
|
170
172
|
# Ensure bundled shell scripts are in place (~/.clacky/scripts/)
|
|
171
173
|
Utils::ScriptsManager.setup!
|
|
174
|
+
|
|
175
|
+
# Ensure bundled search providers are in place (~/.clacky/searchers/)
|
|
176
|
+
Utils::SearcherManager.setup!
|
|
172
177
|
end
|
|
173
178
|
|
|
174
179
|
# Restore from a saved session
|
|
@@ -264,6 +269,170 @@ module Clacky
|
|
|
264
269
|
@config.effective_model_name
|
|
265
270
|
end
|
|
266
271
|
|
|
272
|
+
# ── /goal (Ralph-style standing goal loop) ────────────────────────────
|
|
273
|
+
|
|
274
|
+
# Lazily-built GoalManager bound to this session. The judge routes through
|
|
275
|
+
# this agent's Client on a lightweight model (the provider's lite model
|
|
276
|
+
# when available, else the primary model) — a cheap side call that never
|
|
277
|
+
# touches conversation history.
|
|
278
|
+
def goal_manager
|
|
279
|
+
@goal_manager ||= GoalManager.new(
|
|
280
|
+
judge_client: @client,
|
|
281
|
+
judge_model: judge_model_name
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# True if a standing goal loop is active for this session.
|
|
286
|
+
def goal_active?
|
|
287
|
+
@goal_manager&.active? || false
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Resolve the model name the goal judge should use. Prefer the provider's
|
|
291
|
+
# lite model (cheap/fast for a one-line verdict); fall back to the primary
|
|
292
|
+
# model when no lite is resolvable.
|
|
293
|
+
private def judge_model_name
|
|
294
|
+
lite = @config.lite_model_config_for_current
|
|
295
|
+
(lite && lite["model"]) || current_model
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Parse and handle a /goal command typed by the user. Returns a hash:
|
|
299
|
+
# { handled: true, result: <lightweight result> } — command consumed, no turn
|
|
300
|
+
# { handled: false, user_input: <text|nil> } — fall through to a normal turn
|
|
301
|
+
# (user_input set when `/goal <text>` seeds the first working prompt)
|
|
302
|
+
private def handle_goal_command(user_input)
|
|
303
|
+
text = user_input.to_s.strip
|
|
304
|
+
return { handled: false } unless text.start_with?("/goal", "/subgoal")
|
|
305
|
+
|
|
306
|
+
_cmd, rest = text.split(/\s+/, 2)
|
|
307
|
+
rest = rest.to_s.strip
|
|
308
|
+
|
|
309
|
+
# Sub-command dispatch: status/show/pause/resume/clear take no goal text.
|
|
310
|
+
case rest.downcase
|
|
311
|
+
when "status", "show"
|
|
312
|
+
return goal_command_reply(goal_manager.status_line)
|
|
313
|
+
when "pause"
|
|
314
|
+
goal_manager.pause(reason: "user-paused")
|
|
315
|
+
broadcast_goal_status
|
|
316
|
+
exit_goal_permission_mode!
|
|
317
|
+
return goal_command_reply(goal_manager.status_line)
|
|
318
|
+
when "resume"
|
|
319
|
+
if goal_manager.state.nil?
|
|
320
|
+
return goal_command_reply("No goal to resume. Set one with /goal <text>.")
|
|
321
|
+
end
|
|
322
|
+
goal_manager.resume
|
|
323
|
+
broadcast_goal_status
|
|
324
|
+
enter_goal_permission_mode!
|
|
325
|
+
# Resume runs a fresh working turn immediately.
|
|
326
|
+
return { handled: false, user_input: goal_manager.continuation_prompt }
|
|
327
|
+
when "clear", "stop"
|
|
328
|
+
goal_manager.clear
|
|
329
|
+
broadcast_goal_status
|
|
330
|
+
exit_goal_permission_mode!
|
|
331
|
+
return goal_command_reply("Goal cleared.")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# `/goal --turns N <text>` optional budget override.
|
|
335
|
+
max_turns = nil
|
|
336
|
+
if (m = rest.match(/\A--turns\s+(\d+)\s+(.*)\z/m))
|
|
337
|
+
max_turns = m[1].to_i
|
|
338
|
+
rest = m[2].strip
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if rest.empty?
|
|
342
|
+
return goal_command_reply(goal_manager.status_line)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
goal_manager.set(rest, max_turns: max_turns)
|
|
346
|
+
broadcast_goal_status
|
|
347
|
+
enter_goal_permission_mode!
|
|
348
|
+
@ui&.show_assistant_message("⊙ Goal set: #{rest}", files: [])
|
|
349
|
+
# Fall through: run the first working turn using the goal itself as the prompt.
|
|
350
|
+
{ handled: false, user_input: rest }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Show + persist a one-off goal control-plane reply (no LLM turn) and return
|
|
354
|
+
# a lightweight success result compatible with build_result's shape.
|
|
355
|
+
private def goal_command_reply(message)
|
|
356
|
+
@ui&.show_assistant_message(message, files: [])
|
|
357
|
+
{ handled: true, result: goal_control_result }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private def goal_control_result
|
|
361
|
+
{
|
|
362
|
+
status: :success,
|
|
363
|
+
session_id: @session_id,
|
|
364
|
+
model: current_model,
|
|
365
|
+
provider: current_provider,
|
|
366
|
+
iterations: 0,
|
|
367
|
+
duration_seconds: 0.0,
|
|
368
|
+
total_cost_usd: 0.0,
|
|
369
|
+
cost_source: :estimated,
|
|
370
|
+
cache_stats: @cache_stats,
|
|
371
|
+
history: @history,
|
|
372
|
+
error: nil,
|
|
373
|
+
goal_command: true
|
|
374
|
+
}
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# After a completed turn, consult the judge and, if the goal should keep
|
|
378
|
+
# going, run the next turn in this thread. Returns the final result of the
|
|
379
|
+
# continued run, or nil when no continuation happened.
|
|
380
|
+
private def maybe_continue_goal(result)
|
|
381
|
+
return nil unless @goal_manager&.active?
|
|
382
|
+
|
|
383
|
+
decision = @goal_manager.evaluate_after_turn(last_assistant_text)
|
|
384
|
+
broadcast_goal_status
|
|
385
|
+
|
|
386
|
+
unless decision[:message].to_s.empty?
|
|
387
|
+
@ui&.show_assistant_message(decision[:message], files: [])
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
unless decision[:should_continue]
|
|
391
|
+
exit_goal_permission_mode!
|
|
392
|
+
return nil
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
return nil unless decision[:continuation_prompt]
|
|
396
|
+
|
|
397
|
+
run(decision[:continuation_prompt])
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# The agent's final assistant text this turn — what the judge evaluates.
|
|
401
|
+
private def last_assistant_text
|
|
402
|
+
msg = @history.to_a.reverse.find { |m| m[:role].to_s == "assistant" }
|
|
403
|
+
return "" unless msg
|
|
404
|
+
content = msg[:content]
|
|
405
|
+
return content if content.is_a?(String)
|
|
406
|
+
Array(content).filter_map { |c| c.is_a?(Hash) ? (c[:text] || c["text"]) : c }.join("\n")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Emit a goal_status event over the UI bridge (Web UI live update). No-op
|
|
410
|
+
# when the UI adapter does not implement it (e.g. plain CLI).
|
|
411
|
+
private def broadcast_goal_status
|
|
412
|
+
return unless @ui.respond_to?(:show_goal_status)
|
|
413
|
+
@ui.show_goal_status(@goal_manager&.to_h)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
private def enter_goal_permission_mode!
|
|
417
|
+
return unless @config&.permission_mode
|
|
418
|
+
return if @config.permission_mode == :auto_approve
|
|
419
|
+
@pre_goal_permission_mode = @config.permission_mode
|
|
420
|
+
@config.permission_mode = :auto_approve
|
|
421
|
+
notify_permission_mode_change(:auto_approve)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
private def exit_goal_permission_mode!
|
|
425
|
+
return unless @pre_goal_permission_mode
|
|
426
|
+
mode = @pre_goal_permission_mode
|
|
427
|
+
@pre_goal_permission_mode = nil
|
|
428
|
+
@config.permission_mode = mode
|
|
429
|
+
notify_permission_mode_change(mode)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private def notify_permission_mode_change(mode)
|
|
433
|
+
@ui&.update_permission_mode(mode) if @ui&.respond_to?(:update_permission_mode)
|
|
434
|
+
end
|
|
435
|
+
|
|
267
436
|
private def current_provider
|
|
268
437
|
return nil unless @client.respond_to?(:provider_id)
|
|
269
438
|
@client.provider_id
|
|
@@ -275,6 +444,21 @@ module Clacky
|
|
|
275
444
|
end
|
|
276
445
|
|
|
277
446
|
def run(user_input, files: [], display_text: nil, created_at: nil)
|
|
447
|
+
# Intercept /goal ... commands before any task/LLM work. Control-plane
|
|
448
|
+
# commands (status/pause/resume/clear) return immediately without a turn;
|
|
449
|
+
# `/goal <text>` sets the goal, then falls through to run the first turn.
|
|
450
|
+
goal_intercept = handle_goal_command(user_input)
|
|
451
|
+
return goal_intercept[:result] if goal_intercept[:handled]
|
|
452
|
+
user_input = goal_intercept[:user_input] if goal_intercept[:user_input]
|
|
453
|
+
|
|
454
|
+
# Auto-clear a finished/paused goal when the user starts a new non-goal
|
|
455
|
+
# task. /goal <text> already replaced the goal above; control commands
|
|
456
|
+
# returned early. The "✓ Goal achieved" line stays in the thread.
|
|
457
|
+
if !goal_intercept[:user_input] && @goal_manager&.state && !@goal_manager.active?
|
|
458
|
+
@goal_manager.clear
|
|
459
|
+
broadcast_goal_status
|
|
460
|
+
end
|
|
461
|
+
|
|
278
462
|
# Show the "thinking" indicator as early as possible so the user gets
|
|
279
463
|
# immediate feedback after sending a message. Without this the UI stays
|
|
280
464
|
# silent during synchronous setup work (system prompt assembly, file
|
|
@@ -673,6 +857,18 @@ module Clacky
|
|
|
673
857
|
)
|
|
674
858
|
end
|
|
675
859
|
@hooks.trigger(:on_complete, result)
|
|
860
|
+
|
|
861
|
+
# Standing-goal loop: after a completed turn, ask the judge whether the
|
|
862
|
+
# goal is met. If not (and budget/health allow), auto-run the next turn
|
|
863
|
+
# in this same thread. Skipped for subagents and interrupts.
|
|
864
|
+
# awaiting_user_feedback (agent ended with '?') is intentionally not
|
|
865
|
+
# checked here - maybe_continue_goal is a no-op when no goal is active,
|
|
866
|
+
# and when one is active the judge decides done/continue, not punctuation.
|
|
867
|
+
unless @is_subagent || task_interrupted
|
|
868
|
+
continuation = maybe_continue_goal(result)
|
|
869
|
+
return continuation if continuation
|
|
870
|
+
end
|
|
871
|
+
|
|
676
872
|
result
|
|
677
873
|
rescue Clacky::AgentInterrupted
|
|
678
874
|
# Mark this run as interrupted so the next run() (e.g. user's
|
|
@@ -1063,9 +1259,12 @@ module Clacky
|
|
|
1063
1259
|
# Hook: after_tool_use
|
|
1064
1260
|
@hooks.trigger(:after_tool_use, call, result)
|
|
1065
1261
|
|
|
1066
|
-
# Update todos display after todo_manager execution
|
|
1262
|
+
# Update todos display after todo_manager execution.
|
|
1263
|
+
# Skip the broadcast for read-only "list" queries — they don't
|
|
1264
|
+
# mutate state, so pushing a WS event just adds noise.
|
|
1067
1265
|
if call[:name] == "todo_manager"
|
|
1068
|
-
|
|
1266
|
+
action = args[:action] || args["action"]
|
|
1267
|
+
@ui&.update_todos(@todos.dup) unless action == "list"
|
|
1069
1268
|
end
|
|
1070
1269
|
|
|
1071
1270
|
# Special handling for request_user_feedback: emit as interactive feedback card
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -159,7 +159,7 @@ module Clacky
|
|
|
159
159
|
DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
|
|
160
160
|
|
|
161
161
|
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
162
|
-
:enable_compression, :enable_prompt_caching,
|
|
162
|
+
:enable_compression, :enable_idle_compression, :enable_prompt_caching,
|
|
163
163
|
:compression_threshold, :message_count_threshold,
|
|
164
164
|
:models, :current_model_index, :current_model_id,
|
|
165
165
|
:memory_update_enabled, :skill_evolution,
|
|
@@ -173,6 +173,7 @@ module Clacky
|
|
|
173
173
|
@max_tokens = options[:max_tokens] || 16384
|
|
174
174
|
@verbose = options[:verbose] || false
|
|
175
175
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
176
|
+
@enable_idle_compression = options[:enable_idle_compression].nil? ? true : options[:enable_idle_compression]
|
|
176
177
|
# Enable prompt caching by default for cost savings
|
|
177
178
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
178
179
|
# Token threshold that triggers proactive history compression. Local models
|
|
@@ -411,7 +412,7 @@ module Clacky
|
|
|
411
412
|
# Settings keys that are persisted to config.yml.
|
|
412
413
|
# These map directly to AgentConfig accessors.
|
|
413
414
|
CONFIG_SETTINGS_KEYS = %w[
|
|
414
|
-
enable_compression enable_prompt_caching
|
|
415
|
+
enable_compression enable_idle_compression enable_prompt_caching
|
|
415
416
|
compression_threshold message_count_threshold
|
|
416
417
|
memory_update_enabled
|
|
417
418
|
skill_evolution max_running_agents max_idle_agents
|
|
@@ -428,6 +429,7 @@ module Clacky
|
|
|
428
429
|
end
|
|
429
430
|
settings = {
|
|
430
431
|
"enable_compression" => @enable_compression,
|
|
432
|
+
"enable_idle_compression" => @enable_idle_compression,
|
|
431
433
|
"enable_prompt_caching" => @enable_prompt_caching,
|
|
432
434
|
"compression_threshold" => @compression_threshold,
|
|
433
435
|
"message_count_threshold" => @message_count_threshold,
|
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -53,8 +53,11 @@ module Clacky
|
|
|
53
53
|
|
|
54
54
|
# List all available agent profiles across user + extension layers.
|
|
55
55
|
# Precedence on id collision: user override → extension unit.
|
|
56
|
+
# @param recency [Hash<String, Integer>] agent id => epoch of last use,
|
|
57
|
+
# used to order third-party extension agents by most-recently-used
|
|
58
|
+
# instead of their static `order` field.
|
|
56
59
|
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author:, avatar: }
|
|
57
|
-
def self.all
|
|
60
|
+
def self.all(recency: {})
|
|
58
61
|
out = {}
|
|
59
62
|
|
|
60
63
|
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar|
|
|
@@ -76,6 +79,7 @@ module Clacky
|
|
|
76
79
|
ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
|
|
77
80
|
ext_result&.agents&.each do |unit|
|
|
78
81
|
spec = unit.spec || {}
|
|
82
|
+
next if spec["hidden"]
|
|
79
83
|
title = spec["title"].to_s
|
|
80
84
|
title = unit.id if title.empty?
|
|
81
85
|
avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}"
|
|
@@ -102,8 +106,26 @@ module Clacky
|
|
|
102
106
|
)
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
# Builtin agents always come first, then user overrides, then third-party
|
|
110
|
+
# extension agents — so the "start new session" grid keeps the official
|
|
111
|
+
# agents grouped ahead of anything the user installed from the market.
|
|
112
|
+
# Within the third-party group, agents are ordered by most-recently-used
|
|
113
|
+
# first (via `recency`), falling back to their static `order` field for
|
|
114
|
+
# agents that have never been used yet.
|
|
115
|
+
layer_rank = lambda do |a|
|
|
116
|
+
next 0 if a[:layer] == "builtin"
|
|
117
|
+
next 1 if a[:source] == "user"
|
|
118
|
+
|
|
119
|
+
2
|
|
120
|
+
end
|
|
121
|
+
out.values.sort_by do |a|
|
|
122
|
+
rank = layer_rank.call(a)
|
|
123
|
+
if rank == 2
|
|
124
|
+
[rank, -(recency[a[:id]] || 0), a[:order] || 999, a[:id]]
|
|
125
|
+
else
|
|
126
|
+
[rank, a[:order] || 999, a[:id]]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
107
129
|
end
|
|
108
130
|
|
|
109
131
|
private_class_method def self.read_profile_yml(path)
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -330,7 +330,7 @@ module Clacky
|
|
|
330
330
|
|
|
331
331
|
# The overlay lives in the session file (not config.yml), so persist it
|
|
332
332
|
# now — otherwise it would be lost if the user quits before the next task.
|
|
333
|
-
session_manager&.save(agent.to_session_data)
|
|
333
|
+
session_manager&.save(agent.to_session_data(updated_at: Time.now))
|
|
334
334
|
|
|
335
335
|
ui_controller.config[:model] = config.model_name
|
|
336
336
|
ui_controller.update_sessionbar(
|
|
@@ -384,7 +384,7 @@ module Clacky
|
|
|
384
384
|
|
|
385
385
|
# Save session after switch
|
|
386
386
|
if session_manager
|
|
387
|
-
session_manager.save(agent.to_session_data(status: :success))
|
|
387
|
+
session_manager.save(agent.to_session_data(status: :success, updated_at: Time.now))
|
|
388
388
|
end
|
|
389
389
|
rescue StandardError => e
|
|
390
390
|
ui_controller.show_error("Time Machine failed: #{e.message}")
|
|
@@ -659,11 +659,11 @@ module Clacky
|
|
|
659
659
|
ui_controller.set_idle_status
|
|
660
660
|
|
|
661
661
|
if exception.is_a?(Clacky::AgentInterrupted)
|
|
662
|
-
session_manager&.save(agent.to_session_data(status: :interrupted))
|
|
662
|
+
session_manager&.save(agent.to_session_data(status: :interrupted, updated_at: Time.now))
|
|
663
663
|
ui_controller.show_warning("Task interrupted by user")
|
|
664
664
|
else
|
|
665
665
|
error_message = format_error(exception)
|
|
666
|
-
session_manager&.save(agent.to_session_data(status: :error, error_message: error_message))
|
|
666
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: error_message, updated_at: Time.now))
|
|
667
667
|
code = exception.is_a?(Clacky::InsufficientCreditError) ? exception.error_code : nil
|
|
668
668
|
ui_controller.show_error("Error: #{exception.message}", code: code)
|
|
669
669
|
end
|
|
@@ -682,7 +682,7 @@ module Clacky
|
|
|
682
682
|
begin
|
|
683
683
|
files = prepare_non_interactive_files(file_paths)
|
|
684
684
|
rescue => e
|
|
685
|
-
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e)))
|
|
685
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e), updated_at: Time.now))
|
|
686
686
|
|
|
687
687
|
if is_json
|
|
688
688
|
ui = Clacky::JsonUIController.new
|
|
@@ -719,14 +719,14 @@ module Clacky
|
|
|
719
719
|
begin
|
|
720
720
|
auto_name_session(agent, message)
|
|
721
721
|
agent.run(message, files: files)
|
|
722
|
-
session_manager&.save(agent.to_session_data(status: :success))
|
|
722
|
+
session_manager&.save(agent.to_session_data(status: :success, updated_at: Time.now))
|
|
723
723
|
exit(0)
|
|
724
724
|
rescue Clacky::AgentInterrupted
|
|
725
|
-
session_manager&.save(agent.to_session_data(status: :interrupted))
|
|
725
|
+
session_manager&.save(agent.to_session_data(status: :interrupted, updated_at: Time.now))
|
|
726
726
|
$stderr.puts "\nInterrupted."
|
|
727
727
|
exit(1)
|
|
728
728
|
rescue => e
|
|
729
|
-
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e)))
|
|
729
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e), updated_at: Time.now))
|
|
730
730
|
$stderr.puts "Error: #{e.message}"
|
|
731
731
|
exit(1)
|
|
732
732
|
end
|
|
@@ -801,7 +801,7 @@ module Clacky
|
|
|
801
801
|
|
|
802
802
|
# Final session save and shutdown
|
|
803
803
|
if session_manager && agent.total_tasks > 0
|
|
804
|
-
session_manager.save(agent.to_session_data(status: :exited))
|
|
804
|
+
session_manager.save(agent.to_session_data(status: :exited, updated_at: Time.now))
|
|
805
805
|
end
|
|
806
806
|
json_ui.emit("done", total_cost: agent.total_cost, total_tasks: agent.total_tasks)
|
|
807
807
|
end
|
|
@@ -810,15 +810,15 @@ module Clacky
|
|
|
810
810
|
def run_json_task(agent, json_ui, session_manager)
|
|
811
811
|
json_ui.set_working_status
|
|
812
812
|
yield
|
|
813
|
-
session_manager&.save(agent.to_session_data(status: :success))
|
|
813
|
+
session_manager&.save(agent.to_session_data(status: :success, updated_at: Time.now))
|
|
814
814
|
json_ui.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
815
815
|
:success
|
|
816
816
|
rescue Clacky::AgentInterrupted
|
|
817
|
-
session_manager&.save(agent.to_session_data(status: :interrupted))
|
|
817
|
+
session_manager&.save(agent.to_session_data(status: :interrupted, updated_at: Time.now))
|
|
818
818
|
json_ui.emit("interrupted")
|
|
819
819
|
:interrupted
|
|
820
820
|
rescue => e
|
|
821
|
-
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e)))
|
|
821
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: format_error(e), updated_at: Time.now))
|
|
822
822
|
json_ui.emit("error", message: e.message)
|
|
823
823
|
:error
|
|
824
824
|
ensure
|
|
@@ -950,7 +950,7 @@ module Clacky
|
|
|
950
950
|
|
|
951
951
|
# Save final session state before exit
|
|
952
952
|
if session_manager && agent.total_tasks > 0
|
|
953
|
-
session_data = agent.to_session_data(status: :exited)
|
|
953
|
+
session_data = agent.to_session_data(status: :exited, updated_at: Time.now)
|
|
954
954
|
saved_path = session_manager.save(session_data)
|
|
955
955
|
|
|
956
956
|
# Show session saved message in output area (before stopping UI)
|
|
@@ -1059,7 +1059,7 @@ module Clacky
|
|
|
1059
1059
|
|
|
1060
1060
|
# Save session after each task
|
|
1061
1061
|
if session_manager
|
|
1062
|
-
session_manager.save(agent.to_session_data(status: :success))
|
|
1062
|
+
session_manager.save(agent.to_session_data(status: :success, updated_at: Time.now))
|
|
1063
1063
|
end
|
|
1064
1064
|
|
|
1065
1065
|
# Update session bar with agent's cumulative stats
|
|
@@ -1100,7 +1100,7 @@ module Clacky
|
|
|
1100
1100
|
|
|
1101
1101
|
# Save final session state
|
|
1102
1102
|
if session_manager && agent.total_tasks > 0
|
|
1103
|
-
session_manager.save(agent.to_session_data)
|
|
1103
|
+
session_manager.save(agent.to_session_data(updated_at: Time.now))
|
|
1104
1104
|
end
|
|
1105
1105
|
end
|
|
1106
1106
|
|
|
@@ -75,6 +75,11 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
75
75
|
|
|
76
76
|
brand = Clacky::BrandConfig.load
|
|
77
77
|
|
|
78
|
+
origin = presence(json_body["origin"]) || "marketplace"
|
|
79
|
+
if origin == "self" && !brand.user_licensed?
|
|
80
|
+
error!("only brand administrators can publish to a brand", status: 403)
|
|
81
|
+
end
|
|
82
|
+
|
|
78
83
|
Dir.mktmpdir("clacky-ext-studio-publish") do |tmp|
|
|
79
84
|
res = Clacky::ExtensionPackager.pack(ext_id, out_dir: tmp)
|
|
80
85
|
zip_data = File.binread(res.path)
|
|
@@ -96,7 +101,8 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
96
101
|
force: json_body["force"] == true,
|
|
97
102
|
status: presence(json_body["status"]),
|
|
98
103
|
changelog: presence(json_body["changelog"]),
|
|
99
|
-
readme: local_readme
|
|
104
|
+
readme: local_readme,
|
|
105
|
+
origin: origin
|
|
100
106
|
)
|
|
101
107
|
|
|
102
108
|
if result[:success]
|
|
@@ -140,6 +146,28 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
140
146
|
json(extensions: exts)
|
|
141
147
|
end
|
|
142
148
|
|
|
149
|
+
# GET /api/ext/ext-studio/published_brand
|
|
150
|
+
# List extensions published to the current brand's library. Only available to
|
|
151
|
+
# brand administrators (user-licensed); used by the publish panel to detect
|
|
152
|
+
# existing brand versions for the "publish to brand" flow.
|
|
153
|
+
get "/published_brand" do
|
|
154
|
+
brand = Clacky::BrandConfig.load
|
|
155
|
+
error!("only brand administrators can list brand extensions", status: 403) unless brand.user_licensed?
|
|
156
|
+
|
|
157
|
+
result = brand.fetch_brand_extensions!
|
|
158
|
+
error!(result[:error] || "failed to fetch brand extensions", status: 502) unless result[:success]
|
|
159
|
+
|
|
160
|
+
exts = Array(result[:extensions]).map do |ext|
|
|
161
|
+
{
|
|
162
|
+
id: ext["name"] || ext["slug"] || ext["id"],
|
|
163
|
+
name: ext["display_name"] || ext["name"],
|
|
164
|
+
version: (ext["latest_version"] || {})["version"] || ext["version"],
|
|
165
|
+
status: ext["status"] || "published"
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
json(extensions: exts)
|
|
169
|
+
end
|
|
170
|
+
|
|
143
171
|
# DELETE /api/ext/ext-studio/local
|
|
144
172
|
# body: { ext_id }
|
|
145
173
|
# Permanently removes a local extension directory. Only allowed for
|
|
@@ -9,8 +9,8 @@ contributes:
|
|
|
9
9
|
api: api/handler.rb
|
|
10
10
|
panels:
|
|
11
11
|
- id: studio
|
|
12
|
-
title:
|
|
13
|
-
title_zh:
|
|
12
|
+
title: Creator Center
|
|
13
|
+
title_zh: 创作者中心
|
|
14
14
|
description: Build, debug and publish extensions and skills
|
|
15
15
|
description_zh: 开发、调试、发布扩展与 skill
|
|
16
16
|
view: panels/studio/view.js
|
|
@@ -19,8 +19,8 @@ contributes:
|
|
|
19
19
|
- id: ext-developer
|
|
20
20
|
title: Extension Developer
|
|
21
21
|
title_zh: 扩展开发
|
|
22
|
-
description: AI expert that helps you build
|
|
23
|
-
description_zh:
|
|
22
|
+
description: AI expert that helps you build custom AI agent workflows
|
|
23
|
+
description_zh: 帮你开发自定义 AI Agent 工作流的专家
|
|
24
24
|
order: 5
|
|
25
25
|
prompt: agents/ext-developer/system_prompt.md
|
|
26
26
|
avatar: agents/ext-developer/avatar.png
|