kward 0.76.0 → 0.77.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/.github/workflows/ci.yml +8 -1
- data/CHANGELOG.md +36 -0
- data/Gemfile.lock +2 -2
- data/README.md +12 -4
- data/doc/agent-tools.md +0 -3
- data/doc/api.md +4 -0
- data/doc/composer.md +219 -0
- data/doc/configuration.md +75 -27
- data/doc/editor.md +18 -2
- data/doc/files.md +1 -1
- data/doc/getting-started.md +4 -0
- data/doc/git.md +2 -2
- data/doc/lifecycle-hooks.md +2 -14
- data/doc/pan.md +167 -0
- data/doc/rpc.md +0 -22
- data/doc/security.md +180 -0
- data/doc/usage.md +15 -32
- data/lib/kward/ansi.rb +2 -2
- data/lib/kward/cancellation.rb +2 -2
- data/lib/kward/cli/commands.rb +1 -3
- data/lib/kward/cli/interactive_turn.rb +0 -17
- data/lib/kward/cli/plugins.rb +2 -7
- data/lib/kward/cli/prompt_interface.rb +1 -0
- data/lib/kward/cli/runtime_helpers.rb +0 -38
- data/lib/kward/cli/sessions.rb +14 -5
- data/lib/kward/cli/settings.rb +22 -0
- data/lib/kward/cli/slash_commands.rb +1 -561
- data/lib/kward/cli/tabs.rb +14 -2
- data/lib/kward/cli.rb +1 -17
- data/lib/kward/compactor.rb +3 -3
- data/lib/kward/config_files.rb +18 -0
- data/lib/kward/ekwsh.rb +0 -4
- data/lib/kward/hooks/catalog.rb +0 -5
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/pan/index.html.erb +459 -64
- data/lib/kward/pan/kward_logo.png +0 -0
- data/lib/kward/pan/server.rb +248 -23
- data/lib/kward/plugin_registry.rb +18 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
- data/lib/kward/prompt_interface/layout.rb +0 -4
- data/lib/kward/prompt_interface/project_browser.rb +40 -1
- data/lib/kward/prompt_interface/runtime_state.rb +4 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
- data/lib/kward/prompt_interface/transcript_renderer.rb +0 -12
- data/lib/kward/prompt_interface.rb +18 -1
- data/lib/kward/prompts/commands.rb +1 -3
- data/lib/kward/prompts.rb +20 -0
- data/lib/kward/rpc/server.rb +13 -41
- data/lib/kward/rpc/session_manager.rb +75 -25
- data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
- data/lib/kward/rpc/tool_metadata.rb +23 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -0
- data/lib/kward/session_store.rb +55 -16
- data/lib/kward/skills/registry.rb +21 -1
- data/lib/kward/tools/base.rb +14 -0
- data/lib/kward/tools/registry.rb +11 -16
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/search/web.rb +1 -1
- data/lib/kward/tools/tool_call.rb +0 -4
- data/lib/kward/transcript_export.rb +1 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/kward_navigation.rb +5 -1
- data/templates/default/layout/html/layout.erb +0 -2
- metadata +5 -12
- data/lib/kward/workers/git_guard.rb +0 -93
- data/lib/kward/workers/job.rb +0 -99
- data/lib/kward/workers/live_view.rb +0 -49
- data/lib/kward/workers/manager.rb +0 -328
- data/lib/kward/workers/queue_runner.rb +0 -166
- data/lib/kward/workers/queue_store.rb +0 -112
- data/lib/kward/workers/store.rb +0 -72
- data/lib/kward/workers/tool_policy.rb +0 -23
- data/lib/kward/workers/worker.rb +0 -82
- data/lib/kward/workers/write_lock.rb +0 -38
- data/lib/kward/workers.rb +0 -10
|
@@ -46,20 +46,6 @@ module Kward
|
|
|
46
46
|
when "pty"
|
|
47
47
|
run_interactive_pty_command(argument, agent)
|
|
48
48
|
[true, nil]
|
|
49
|
-
when "workers"
|
|
50
|
-
unless experimental_workers_enabled?
|
|
51
|
-
runtime_output("Workers are experimental. Start Kward with --experimental-workers to enable /workers.")
|
|
52
|
-
return [true, nil]
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
[true, handle_workers_command(argument, agent, session_store)]
|
|
56
|
-
when "queue"
|
|
57
|
-
unless experimental_workers_enabled?
|
|
58
|
-
runtime_output("Worker queues are experimental. Start Kward with --experimental-workers to enable /queue.")
|
|
59
|
-
return [true, nil]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
[true, handle_worker_queue_command(argument, agent, session_store)]
|
|
63
49
|
when "tab"
|
|
64
50
|
[true, handle_tab_command(argument, session_store)]
|
|
65
51
|
when "settings"
|
|
@@ -136,7 +122,7 @@ module Kward
|
|
|
136
122
|
|
|
137
123
|
path = argument.to_s.strip
|
|
138
124
|
if path.empty?
|
|
139
|
-
sessions = run_busy_local_command_and_requeue { session_store
|
|
125
|
+
sessions = run_busy_local_command_and_requeue { recent_sessions(session_store, tree: true) }
|
|
140
126
|
path = select_session_path_from_sessions(sessions, session_store: session_store)
|
|
141
127
|
end
|
|
142
128
|
|
|
@@ -255,552 +241,6 @@ module Kward
|
|
|
255
241
|
nil
|
|
256
242
|
end
|
|
257
243
|
|
|
258
|
-
def handle_worker_queue_command(argument, agent, session_store)
|
|
259
|
-
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
260
|
-
case action
|
|
261
|
-
when nil, "", "status", "list"
|
|
262
|
-
show_worker_queue
|
|
263
|
-
when "add", "enqueue"
|
|
264
|
-
enqueue_active_tab(value, agent)
|
|
265
|
-
when "open", "show"
|
|
266
|
-
open_worker_queue_job(session_store, value)
|
|
267
|
-
when "run", "start"
|
|
268
|
-
run_worker_queue(session_store)
|
|
269
|
-
when "resume"
|
|
270
|
-
value.to_s.strip.empty? ? run_worker_queue(session_store) : resume_worker_queue_job(session_store, value)
|
|
271
|
-
when "suspend", "pause"
|
|
272
|
-
suspend_worker_queue_job(session_store, value)
|
|
273
|
-
when "next"
|
|
274
|
-
run_next_worker_queue_job(session_store)
|
|
275
|
-
else
|
|
276
|
-
runtime_output("Usage: /queue [add|list|status|run|suspend <id>|resume <id>]")
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def worker_queue_store
|
|
281
|
-
@worker_queue_store ||= Workers::QueueStore.new
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def enqueue_active_tab(title, agent)
|
|
285
|
-
session = @active_session
|
|
286
|
-
unless session&.path
|
|
287
|
-
runtime_output("No active persisted session to queue.")
|
|
288
|
-
return
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
job = worker_queue_store.enqueue(
|
|
292
|
-
title: worker_queue_title(title, session, agent),
|
|
293
|
-
session_path: session.path,
|
|
294
|
-
workspace_root: session.cwd || current_workspace_root
|
|
295
|
-
)
|
|
296
|
-
runtime_output("Queued worker #{job.id}: #{job.title}")
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def worker_queue_title(title, session, agent)
|
|
300
|
-
explicit = title.to_s.strip
|
|
301
|
-
return explicit unless explicit.empty?
|
|
302
|
-
|
|
303
|
-
session_name = session&.name.to_s.strip
|
|
304
|
-
return session_name unless session_name.empty?
|
|
305
|
-
|
|
306
|
-
last_user = if agent&.respond_to?(:conversation)
|
|
307
|
-
agent.conversation.messages.reverse.find { |message| MessageAccess.role(message) == "user" }
|
|
308
|
-
end
|
|
309
|
-
content = MessageAccess.content(last_user).to_s.strip.gsub(/\s+/, " ")
|
|
310
|
-
content.empty? ? "Queued worker" : content[0, 80]
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def open_worker_queue_job(session_store, id)
|
|
314
|
-
unless session_store
|
|
315
|
-
runtime_output("Worker queue requires persisted sessions.")
|
|
316
|
-
return nil
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
id = id.to_s.strip
|
|
320
|
-
return runtime_output("Usage: /queue open <id>") if id.empty?
|
|
321
|
-
|
|
322
|
-
record = worker_queue_store.find(id)
|
|
323
|
-
return runtime_output("Unknown worker job: #{id}") unless record
|
|
324
|
-
|
|
325
|
-
load_session(session_store, record.fetch("session_path"), message: "Showing queued worker #{record.fetch('id')}")
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def run_worker_queue(session_store)
|
|
329
|
-
unless session_store
|
|
330
|
-
runtime_output("Worker queue requires persisted sessions.")
|
|
331
|
-
return
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
results = worker_queue_runner(session_store).run_all
|
|
335
|
-
if results.empty?
|
|
336
|
-
runtime_output("Worker queue has no queued jobs.")
|
|
337
|
-
return
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
summary = results.map { |record| "#{record.fetch('id')} #{record.fetch('status')}" }.join(", ")
|
|
341
|
-
runtime_output("Worker queue run finished: #{summary}")
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def run_next_worker_queue_job(session_store)
|
|
345
|
-
unless session_store
|
|
346
|
-
runtime_output("Worker queue requires persisted sessions.")
|
|
347
|
-
return
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
record = worker_queue_runner(session_store).run_next
|
|
351
|
-
if record
|
|
352
|
-
runtime_output("Worker #{record.fetch('id')} finished with status #{record.fetch('status')}.")
|
|
353
|
-
else
|
|
354
|
-
runtime_output("Worker queue has no queued jobs.")
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def suspend_worker_queue_job(session_store, id)
|
|
359
|
-
unless session_store
|
|
360
|
-
runtime_output("Worker queue requires persisted sessions.")
|
|
361
|
-
return
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
id = id.to_s.strip
|
|
365
|
-
return runtime_output("Usage: /queue suspend <id>") if id.empty?
|
|
366
|
-
|
|
367
|
-
record = worker_queue_runner(session_store).suspend(id)
|
|
368
|
-
runtime_output("Worker #{record.fetch('id')} suspended.")
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
def resume_worker_queue_job(session_store, id)
|
|
372
|
-
unless session_store
|
|
373
|
-
runtime_output("Worker queue requires persisted sessions.")
|
|
374
|
-
return
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
id = id.to_s.strip
|
|
378
|
-
return runtime_output("Usage: /queue resume <id>") if id.empty?
|
|
379
|
-
|
|
380
|
-
record = worker_queue_runner(session_store).resume(id)
|
|
381
|
-
runtime_output("Worker #{record.fetch('id')} finished with status #{record.fetch('status')}.")
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
def worker_queue_runner(session_store)
|
|
385
|
-
Workers::QueueRunner.new(
|
|
386
|
-
queue_store: worker_queue_store,
|
|
387
|
-
session_store: session_store,
|
|
388
|
-
client_factory: -> { Client.new },
|
|
389
|
-
prompt: @prompt,
|
|
390
|
-
workspace_root: current_workspace_root,
|
|
391
|
-
provider: current_model_provider,
|
|
392
|
-
model: current_model_id,
|
|
393
|
-
reasoning_effort: current_reasoning_effort,
|
|
394
|
-
write_lock: (@worker_write_lock ||= Workers::WriteLock.new)
|
|
395
|
-
)
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def show_worker_queue
|
|
399
|
-
jobs = worker_queue_store.list
|
|
400
|
-
if jobs.empty?
|
|
401
|
-
runtime_output("Worker queue is empty.")
|
|
402
|
-
return
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
lines = ["Worker queue:"]
|
|
406
|
-
jobs.each do |job|
|
|
407
|
-
details = [job.fetch("id"), "[#{job.fetch('status')}]"]
|
|
408
|
-
details << "##{job['position']}" if job["position"]
|
|
409
|
-
details << job.fetch("title")
|
|
410
|
-
details << "commit #{job['commit_sha']}" unless job["commit_sha"].to_s.empty?
|
|
411
|
-
details << "error: #{job['error']}" unless job["error"].to_s.empty?
|
|
412
|
-
lines << "- #{details.join(' ')}"
|
|
413
|
-
end
|
|
414
|
-
runtime_output(lines.join("\n"))
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
def handle_workers_command(argument, agent, session_store)
|
|
418
|
-
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
419
|
-
replacement_agent = case action
|
|
420
|
-
when nil, ""
|
|
421
|
-
open_worker_menu(agent, session_store)
|
|
422
|
-
when "list"
|
|
423
|
-
open_worker_list(agent, session_store)
|
|
424
|
-
when "new", "do"
|
|
425
|
-
prompt_for_worker_request(agent, value)
|
|
426
|
-
nil
|
|
427
|
-
else
|
|
428
|
-
runtime_output("Usage: /workers | /workers new | /workers list")
|
|
429
|
-
nil
|
|
430
|
-
end
|
|
431
|
-
replacement_agent?(replacement_agent) ? replacement_agent : nil
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def worker_store
|
|
435
|
-
@worker_store ||= Workers::Store.new
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def worker_manager(agent)
|
|
439
|
-
workspace_root = interactive_workspace_root(agent)
|
|
440
|
-
return @worker_manager if @worker_manager && @worker_manager_workspace_root == workspace_root
|
|
441
|
-
|
|
442
|
-
@worker_manager_workspace_root = workspace_root
|
|
443
|
-
@worker_manager = Workers::Manager.new(
|
|
444
|
-
client_factory: -> { Client.new },
|
|
445
|
-
prompt: @prompt,
|
|
446
|
-
workspace_root: workspace_root,
|
|
447
|
-
session_store: interactive_session_store(agent),
|
|
448
|
-
provider: current_model_provider,
|
|
449
|
-
model: current_model_id,
|
|
450
|
-
reasoning_effort: current_reasoning_effort,
|
|
451
|
-
write_lock: (@worker_write_lock ||= Workers::WriteLock.new),
|
|
452
|
-
worker_store: worker_store,
|
|
453
|
-
write_lane_available: -> { !@foreground_turn_active },
|
|
454
|
-
hook_manager: lifecycle_hook_manager(agent.conversation),
|
|
455
|
-
hook_context: lifecycle_hook_context(agent.conversation)
|
|
456
|
-
)
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def open_worker_menu(agent, session_store)
|
|
460
|
-
return runtime_output("Usage: /workers | /workers new | /workers list") unless @prompt.respond_to?(:select)
|
|
461
|
-
|
|
462
|
-
choice = @prompt.select(
|
|
463
|
-
"Workers",
|
|
464
|
-
["New worker", "List workers"],
|
|
465
|
-
title: "Workers",
|
|
466
|
-
custom: false
|
|
467
|
-
)
|
|
468
|
-
case choice
|
|
469
|
-
when "New worker"
|
|
470
|
-
prompt_for_worker_request(agent)
|
|
471
|
-
when "List workers"
|
|
472
|
-
open_worker_list(agent, session_store)
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def prompt_for_worker_request(agent, topic = nil)
|
|
477
|
-
topic = @prompt.ask("Worker task>") if topic.to_s.strip.empty? && @prompt.respond_to?(:ask)
|
|
478
|
-
send_worker_request(topic, agent) unless topic.to_s.strip.empty?
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def send_worker_request(topic, agent)
|
|
482
|
-
if topic.to_s.strip.empty?
|
|
483
|
-
runtime_output("Usage: /workers new <task>")
|
|
484
|
-
return
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
worker = worker_manager(agent).start(role: "request", prompt: topic)
|
|
488
|
-
runtime_output("Worker #{worker.id} started: #{worker.title}")
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def open_worker_list(agent, session_store, title: "Workers", empty_message: "No workers in the pipeline.")
|
|
492
|
-
return runtime_output(empty_message) unless @prompt.respond_to?(:select)
|
|
493
|
-
|
|
494
|
-
jobs = worker_jobs
|
|
495
|
-
if jobs.empty?
|
|
496
|
-
runtime_output(empty_message)
|
|
497
|
-
return
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
labels = jobs.map { |job| worker_choice_label(job) }
|
|
501
|
-
choice = @prompt.select("Select worker", labels, title: title, custom: false)
|
|
502
|
-
return unless choice
|
|
503
|
-
|
|
504
|
-
selected = jobs[labels.index(choice)]
|
|
505
|
-
open_worker_actions(selected, agent, session_store) if selected
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
def worker_jobs
|
|
509
|
-
runtime_worker_ids = @worker_manager ? @worker_manager.list.map(&:id) : []
|
|
510
|
-
persisted_workers = worker_store.list.reject { |job| runtime_worker_ids.include?(job["id"]) }
|
|
511
|
-
live_workers = @worker_manager ? @worker_manager.list.map(&:to_h) : []
|
|
512
|
-
[implementation_worker_job].compact + persisted_workers + live_workers
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def implementation_worker_job
|
|
516
|
-
remember_implementation_worker if implementation_agent?
|
|
517
|
-
path = @implementation_worker_session_path || @active_session&.path
|
|
518
|
-
return nil if path.to_s.empty?
|
|
519
|
-
|
|
520
|
-
{
|
|
521
|
-
"id" => "implementation",
|
|
522
|
-
"title" => @implementation_worker_title || @active_session&.name || "Implementation",
|
|
523
|
-
"role" => "implementation",
|
|
524
|
-
"status" => implementation_agent? ? "active" : "idle",
|
|
525
|
-
"session_path" => path
|
|
526
|
-
}
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def implementation_agent?
|
|
530
|
-
@active_worker_role.to_s.empty? || @active_worker_role == "implementation"
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def remember_implementation_worker
|
|
534
|
-
return unless @active_session&.path
|
|
535
|
-
return unless implementation_agent?
|
|
536
|
-
|
|
537
|
-
@implementation_worker_session_path = @active_session.path
|
|
538
|
-
@implementation_worker_title = @active_session.name || "Implementation"
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def open_worker_actions(job, _agent, session_store)
|
|
542
|
-
return open_implementation_actions(job, session_store) if job["id"] == "implementation"
|
|
543
|
-
|
|
544
|
-
open_background_worker_actions(job, session_store)
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
def open_implementation_actions(job, session_store)
|
|
548
|
-
actions = ["Show", "Back to list"]
|
|
549
|
-
choice = @prompt.select("#{job.fetch('id')} — #{job.fetch('title')}", actions, title: "Worker", custom: false)
|
|
550
|
-
case choice
|
|
551
|
-
when "Show"
|
|
552
|
-
load_implementation_session(session_store, job)
|
|
553
|
-
when "Back to list"
|
|
554
|
-
open_worker_list(nil, session_store)
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def handle_request_worker_input(input, agent, session_store)
|
|
559
|
-
return [false, nil] unless @active_worker_role == "request"
|
|
560
|
-
|
|
561
|
-
worker = visible_request_worker(agent)
|
|
562
|
-
return [false, nil] unless worker
|
|
563
|
-
|
|
564
|
-
text = input.to_s.strip
|
|
565
|
-
return [true, nil] if text.empty?
|
|
566
|
-
return [true, proceed_request_worker(worker.to_h, agent, session_store)] if proceed_request_input?(text)
|
|
567
|
-
|
|
568
|
-
runtime_output("Worker #{worker.id} is a read-only request review. Reply yes/proceed to queue implementation, or use /workers to switch workers.")
|
|
569
|
-
[true, nil]
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
def visible_request_worker(agent)
|
|
573
|
-
worker = @visible_worker
|
|
574
|
-
return worker if worker&.role == "request" && worker.status == "ready"
|
|
575
|
-
|
|
576
|
-
id = @visible_worker_id.to_s
|
|
577
|
-
return nil if id.empty? || id == "implementation"
|
|
578
|
-
|
|
579
|
-
worker = @worker_manager&.find(id)
|
|
580
|
-
return worker if worker&.role == "request" && worker.status == "ready"
|
|
581
|
-
|
|
582
|
-
job = worker_store.find(id)
|
|
583
|
-
return nil unless job && job["role"] == "request" && job["status"] == "ready"
|
|
584
|
-
return nil if job["session_path"].to_s.empty? || !session_matches_agent?(job["session_path"], agent)
|
|
585
|
-
|
|
586
|
-
Workers::Worker.new(
|
|
587
|
-
id: job.fetch("id"),
|
|
588
|
-
title: job.fetch("title"),
|
|
589
|
-
role: job.fetch("role"),
|
|
590
|
-
workspace_root: job["workspace_root"] || current_workspace_root,
|
|
591
|
-
status: job.fetch("status"),
|
|
592
|
-
prompt: job["prompt"]
|
|
593
|
-
).tap { |restored| restored.update_status("ready", report: job["report"], error: job["error"]) }
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
def session_matches_agent?(path, agent)
|
|
597
|
-
return true unless agent.respond_to?(:conversation)
|
|
598
|
-
return false unless @active_session&.path
|
|
599
|
-
|
|
600
|
-
File.expand_path(path) == File.expand_path(@active_session.path)
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
def proceed_request_input?(input)
|
|
604
|
-
input.downcase.strip.match?(/\A(?:y|yes|yeah|yep|sure|ok|okay|go ahead|proceed|continue|implement|do it|please do|make it so)\b/)
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
def proceed_request_worker(job, agent, session_store)
|
|
608
|
-
return runtime_output("Worker #{job.fetch('id')} is not ready to proceed.") unless request_ready?(job)
|
|
609
|
-
|
|
610
|
-
release_implementation_writer
|
|
611
|
-
manager = worker_manager(agent || build_session_agent_for_worker(job, session_store))
|
|
612
|
-
worker = manager.continue(
|
|
613
|
-
job.fetch("id"),
|
|
614
|
-
role: "implementation",
|
|
615
|
-
prompt: implementation_prompt_for_request(job),
|
|
616
|
-
title: "Implement #{job.fetch('title')}"
|
|
617
|
-
)
|
|
618
|
-
runtime_output("Worker #{worker.id} queued from request #{job.fetch('id')}: #{worker.title}")
|
|
619
|
-
wait_for_worker_session(worker)
|
|
620
|
-
load_worker_session(session_store, worker.session.path, worker.to_h, worker: worker) if worker.session&.path
|
|
621
|
-
rescue StandardError => e
|
|
622
|
-
runtime_output("Error: #{e.message}")
|
|
623
|
-
nil
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
def wait_for_worker_session(worker, timeout: 1.0)
|
|
627
|
-
deadline = Time.now + timeout
|
|
628
|
-
until worker.session&.path || Time.now >= deadline
|
|
629
|
-
sleep 0.02
|
|
630
|
-
end
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
def build_session_agent_for_worker(job, session_store)
|
|
634
|
-
conversation = Conversation.new(workspace_root: job["workspace_root"] || session_store&.cwd || current_workspace_root)
|
|
635
|
-
build_worker_agent(conversation, role: "request")
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
def request_ready?(job)
|
|
639
|
-
job["role"] == "request" && job["status"] == "ready"
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
def implementation_prompt_for_request(job)
|
|
643
|
-
<<~PROMPT
|
|
644
|
-
The user reviewed and approved this Kward request. Continue in the write-capable implementation lane.
|
|
645
|
-
|
|
646
|
-
Original request:
|
|
647
|
-
#{job["prompt"]}
|
|
648
|
-
|
|
649
|
-
Request review:
|
|
650
|
-
#{job["report"].to_s.empty? ? "No saved review text is available. Use the request session transcript for context if needed." : job["report"]}
|
|
651
|
-
|
|
652
|
-
Implement the approved next step. Make the smallest correct change, preserve existing style, and run focused verification when practical.
|
|
653
|
-
If you change files, commit the changes and report the commit hash. If no file changes are needed, explain why.
|
|
654
|
-
PROMPT
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
def open_background_worker_actions(job, session_store)
|
|
658
|
-
actions = ["Show"]
|
|
659
|
-
actions << "Proceed" if request_ready?(job)
|
|
660
|
-
actions << "Cancel" if %w[queued running].include?(job["status"])
|
|
661
|
-
actions << "Dismiss"
|
|
662
|
-
actions << "Back to list"
|
|
663
|
-
choice = @prompt.select("#{job.fetch('id')} — #{job.fetch('title')}", actions, title: "Worker", custom: false)
|
|
664
|
-
case choice
|
|
665
|
-
when "Show"
|
|
666
|
-
worker = @worker_manager&.find(job.fetch("id"))
|
|
667
|
-
path = job["session_path"] || worker&.session&.path
|
|
668
|
-
return runtime_output("Worker #{job.fetch('id')} session is not ready yet.") unless path
|
|
669
|
-
|
|
670
|
-
load_worker_session(session_store, path, job, worker: worker)
|
|
671
|
-
when "Proceed"
|
|
672
|
-
proceed_request_worker(job, nil, session_store)
|
|
673
|
-
when "Cancel"
|
|
674
|
-
@worker_manager&.cancel(job.fetch("id"))
|
|
675
|
-
runtime_output("Worker #{job.fetch('id')} cancelled.")
|
|
676
|
-
when "Dismiss"
|
|
677
|
-
dismiss_worker(job.fetch("id"))
|
|
678
|
-
runtime_output("Worker #{job.fetch('id')} dismissed.")
|
|
679
|
-
when "Back to list"
|
|
680
|
-
open_worker_list(nil, session_store)
|
|
681
|
-
end
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
def dismiss_worker(id)
|
|
685
|
-
@worker_manager&.archive(id)
|
|
686
|
-
rescue ArgumentError
|
|
687
|
-
nil
|
|
688
|
-
ensure
|
|
689
|
-
worker_store.archive(id)
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
def load_implementation_session(session_store, job)
|
|
693
|
-
return runtime_output("Implementation session unavailable.") unless session_store
|
|
694
|
-
|
|
695
|
-
stop_live_worker_view
|
|
696
|
-
@active_worker_role = "implementation"
|
|
697
|
-
set_visible_worker("implementation", status: "active")
|
|
698
|
-
load_session(session_store, job.fetch("session_path"), message: "Showing implementation worker")
|
|
699
|
-
rescue StandardError => e
|
|
700
|
-
runtime_output("Error: #{e.message}")
|
|
701
|
-
nil
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
def worker_choice_label(job)
|
|
705
|
-
error = job["status"] == "failed" && !job["error"].to_s.empty? ? " — #{job['error']}" : ""
|
|
706
|
-
"#{job.fetch('id')} [#{job.fetch('role')}/#{job.fetch('status')}] #{job.fetch('title')}#{error}"
|
|
707
|
-
end
|
|
708
|
-
|
|
709
|
-
def load_worker_session(session_store, path, job, worker: nil)
|
|
710
|
-
unless session_store
|
|
711
|
-
runtime_output(worker_report_text(job))
|
|
712
|
-
return nil
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
release_implementation_writer
|
|
716
|
-
agent = load_session(session_store, path, message: "Showing worker #{job.fetch('id')}")
|
|
717
|
-
release_implementation_writer
|
|
718
|
-
role = visible_session_role(job)
|
|
719
|
-
agent = build_worker_agent(agent.conversation, role: role)
|
|
720
|
-
@active_worker_role = role
|
|
721
|
-
set_visible_worker(job.fetch("id"), status: job["status"], worker: worker)
|
|
722
|
-
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
723
|
-
start_live_worker_view(worker, agent) if live_worker?(worker)
|
|
724
|
-
agent
|
|
725
|
-
rescue StandardError => e
|
|
726
|
-
runtime_output("Error: #{e.message}")
|
|
727
|
-
nil
|
|
728
|
-
end
|
|
729
|
-
|
|
730
|
-
def visible_session_role(job)
|
|
731
|
-
return "read_only" if job["id"] != "implementation" && job["role"] == "implementation"
|
|
732
|
-
|
|
733
|
-
job["role"] || "request"
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
def live_worker?(worker)
|
|
737
|
-
worker && %w[queued running].include?(worker.status)
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
def start_live_worker_view(worker, agent)
|
|
741
|
-
return unless prompt_interface?
|
|
742
|
-
|
|
743
|
-
stop_live_worker_view
|
|
744
|
-
renderer = live_worker_renderer(worker)
|
|
745
|
-
@live_worker_view = Workers::LiveView.new(worker: worker, agent: agent, renderer: renderer).start
|
|
746
|
-
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
747
|
-
runtime_output("Watching worker #{worker.id}; the view will update until it finishes.")
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
def stop_live_worker_view
|
|
751
|
-
@live_worker_view&.stop
|
|
752
|
-
@live_worker_view = nil
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
def live_worker_renderer(worker)
|
|
756
|
-
markdown_chunks = []
|
|
757
|
-
stream_state = {
|
|
758
|
-
streamed: false,
|
|
759
|
-
last_flush: monotonic_now,
|
|
760
|
-
stream_block_open: false,
|
|
761
|
-
markdown_streams: {},
|
|
762
|
-
defer_assistant_streaming: false
|
|
763
|
-
}
|
|
764
|
-
lambda do |event, agent|
|
|
765
|
-
if event == :flush
|
|
766
|
-
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: worker_finished?(worker))
|
|
767
|
-
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
768
|
-
next
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
notify_plugin_transcript_event(event, agent.respond_to?(:conversation) ? agent.conversation : nil)
|
|
772
|
-
handle_live_worker_event(event, markdown_chunks, stream_state)
|
|
773
|
-
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: worker_finished?(worker))
|
|
774
|
-
rescue StandardError => e
|
|
775
|
-
runtime_output("Worker view error: #{e.message}")
|
|
776
|
-
end
|
|
777
|
-
end
|
|
778
|
-
|
|
779
|
-
def handle_live_worker_event(event, markdown_chunks, stream_state)
|
|
780
|
-
case event
|
|
781
|
-
when Events::AssistantMessage
|
|
782
|
-
return if stream_state[:streamed]
|
|
783
|
-
|
|
784
|
-
render_assistant_message(event.message)
|
|
785
|
-
else
|
|
786
|
-
handle_interactive_event(event, markdown_chunks, stream_state)
|
|
787
|
-
end
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
def worker_finished?(worker)
|
|
791
|
-
%w[ready failed cancelled archived].include?(worker.status)
|
|
792
|
-
end
|
|
793
|
-
|
|
794
|
-
def worker_report_text(job)
|
|
795
|
-
lines = ["Worker #{job.fetch('id')} [#{job.fetch('status')}] #{job.fetch('title')}", ""]
|
|
796
|
-
if job["report"].to_s.empty?
|
|
797
|
-
lines << (job["error"].to_s.empty? ? "No report yet." : "Error: #{job['error']}")
|
|
798
|
-
else
|
|
799
|
-
lines << job["report"]
|
|
800
|
-
end
|
|
801
|
-
lines.join("\n")
|
|
802
|
-
end
|
|
803
|
-
|
|
804
244
|
end
|
|
805
245
|
end
|
|
806
246
|
end
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -404,7 +404,6 @@ module Kward
|
|
|
404
404
|
end
|
|
405
405
|
|
|
406
406
|
def start_tab_turn(tab, input, display_input: nil)
|
|
407
|
-
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
408
407
|
prepare_memory_context(tab.agent.conversation, input) if tab.agent.respond_to?(:conversation)
|
|
409
408
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
410
409
|
tab.status = "queued"
|
|
@@ -489,7 +488,7 @@ module Kward
|
|
|
489
488
|
tab.queued_inputs << "/exit"
|
|
490
489
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
491
490
|
when String
|
|
492
|
-
handle_tab_busy_input(tab, poll_result)
|
|
491
|
+
return { tab_action: :busy_command } if handle_tab_busy_input(tab, poll_result) == :active_tab_changed
|
|
493
492
|
end
|
|
494
493
|
return next_tab_queued_input(tab) if tab.idle? && !tab.queued_inputs.empty?
|
|
495
494
|
return :tab_idle if tab.idle?
|
|
@@ -512,6 +511,15 @@ module Kward
|
|
|
512
511
|
tab.queued_inputs << input
|
|
513
512
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
514
513
|
return
|
|
514
|
+
elsif busy_tab_command?(input)
|
|
515
|
+
handle_tab_command(input.to_s.strip.sub(%r{\A/tab(?:\s+|\z)}, ""), @session_store)
|
|
516
|
+
unless active_tab.equal?(tab)
|
|
517
|
+
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
518
|
+
return :active_tab_changed
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
restore_busy_input_prompt
|
|
522
|
+
return
|
|
515
523
|
elsif slash_command_input?(input)
|
|
516
524
|
# Slash commands are local control actions. Running or queuing them
|
|
517
525
|
# from the busy composer is surprising because the state they act on
|
|
@@ -532,6 +540,10 @@ module Kward
|
|
|
532
540
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
533
541
|
end
|
|
534
542
|
|
|
543
|
+
def busy_tab_command?(input)
|
|
544
|
+
input.to_s.strip.match?(%r{\A/tab(?:\s|\z)})
|
|
545
|
+
end
|
|
546
|
+
|
|
535
547
|
def refresh_active_tab
|
|
536
548
|
tab = active_tab
|
|
537
549
|
return unless tab
|
data/lib/kward/cli.rb
CHANGED
|
@@ -40,7 +40,6 @@ require_relative "session_tree_renderer"
|
|
|
40
40
|
require_relative "starter_pack_installer"
|
|
41
41
|
require_relative "steering"
|
|
42
42
|
require_relative "update_check"
|
|
43
|
-
require_relative "workers"
|
|
44
43
|
require_relative "tools/tool_call"
|
|
45
44
|
require_relative "tools/registry"
|
|
46
45
|
require_relative "telemetry/stats"
|
|
@@ -114,8 +113,6 @@ module Kward
|
|
|
114
113
|
@prompt_delimited = false
|
|
115
114
|
@requested_mode = "auto"
|
|
116
115
|
@skip_config = false
|
|
117
|
-
@experimental_workers = false
|
|
118
|
-
@foreground_turn_active = false
|
|
119
116
|
@pending_reasoning_config = nil
|
|
120
117
|
@pending_reasoning_config_mutex = Mutex.new
|
|
121
118
|
@color_enabled = ANSI.enabled?($stdout)
|
|
@@ -253,7 +250,7 @@ module Kward
|
|
|
253
250
|
raise ArgumentError, command_usage("rpc") unless @argv.length == 1
|
|
254
251
|
|
|
255
252
|
ensure_client!
|
|
256
|
-
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client
|
|
253
|
+
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client).run
|
|
257
254
|
return
|
|
258
255
|
end
|
|
259
256
|
|
|
@@ -473,25 +470,16 @@ module Kward
|
|
|
473
470
|
end
|
|
474
471
|
end
|
|
475
472
|
next if handled
|
|
476
|
-
request_handled, request_replacement = handle_request_worker_input(command_input, agent, session_store)
|
|
477
|
-
if request_handled
|
|
478
|
-
if replacement_agent?(request_replacement)
|
|
479
|
-
agent = active_tab ? replace_active_tab_agent(request_replacement) : request_replacement
|
|
480
|
-
end
|
|
481
|
-
next
|
|
482
|
-
end
|
|
483
473
|
next if shell_command_input?(command_input) && handle_interactive_shell_command(command_input, agent)
|
|
484
474
|
|
|
485
475
|
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
486
476
|
expanded_input = expand_prompt_template(input)
|
|
487
477
|
display_input = display_input || input if expanded_input
|
|
488
478
|
input = expanded_input || input
|
|
489
|
-
agent = refresh_implementation_writer(agent)
|
|
490
479
|
@footer_conversation = agent.conversation
|
|
491
480
|
begin
|
|
492
481
|
@rewind_return_leaf_id = nil
|
|
493
482
|
auto_name_active_session(display_input || input)
|
|
494
|
-
@foreground_turn_active = true if @active_worker_role == "implementation"
|
|
495
483
|
if active_tab
|
|
496
484
|
submit_tab_input(active_tab, input, display_input: display_input)
|
|
497
485
|
pending_inputs = []
|
|
@@ -503,9 +491,6 @@ module Kward
|
|
|
503
491
|
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
504
492
|
rescue StandardError => e
|
|
505
493
|
runtime_output("Error: #{e.message}")
|
|
506
|
-
ensure
|
|
507
|
-
@foreground_turn_active = false if @active_worker_role == "implementation"
|
|
508
|
-
release_implementation_writer if @active_worker_role == "implementation"
|
|
509
494
|
end
|
|
510
495
|
end
|
|
511
496
|
|
|
@@ -518,7 +503,6 @@ module Kward
|
|
|
518
503
|
ensure
|
|
519
504
|
begin
|
|
520
505
|
stop_tabs if respond_to?(:stop_tabs, true)
|
|
521
|
-
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
522
506
|
@prompt.close if prompt_interface?
|
|
523
507
|
ensure
|
|
524
508
|
cleanup_unused_sessions
|