kward 0.68.0 → 0.69.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +48 -0
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile.lock +8 -2
  6. data/README.md +32 -25
  7. data/Rakefile +14 -1
  8. data/doc/authentication.md +74 -56
  9. data/doc/code-search.md +55 -28
  10. data/doc/configuration.md +18 -0
  11. data/doc/extensibility.md +89 -128
  12. data/doc/getting-started.md +52 -54
  13. data/doc/memory.md +51 -118
  14. data/doc/personas.md +417 -0
  15. data/doc/plugins.md +55 -97
  16. data/doc/releasing.md +3 -1
  17. data/doc/rpc.md +1 -1
  18. data/doc/usage.md +125 -144
  19. data/doc/web-search.md +80 -14
  20. data/exe/kward +2 -0
  21. data/lib/kward/agent.rb +1 -1
  22. data/lib/kward/cli/commands.rb +10 -3
  23. data/lib/kward/cli/compaction.rb +3 -3
  24. data/lib/kward/cli/interactive_turn.rb +3 -1
  25. data/lib/kward/cli/memory_commands.rb +16 -16
  26. data/lib/kward/cli/plugins.rb +3 -3
  27. data/lib/kward/cli/prompt_interface.rb +15 -13
  28. data/lib/kward/cli/rendering.rb +35 -46
  29. data/lib/kward/cli/runtime_helpers.rb +13 -2
  30. data/lib/kward/cli/sessions.rb +21 -21
  31. data/lib/kward/cli/settings.rb +49 -43
  32. data/lib/kward/cli/slash_commands.rb +6 -4
  33. data/lib/kward/cli/stats.rb +2 -2
  34. data/lib/kward/cli/sysprompt.rb +57 -0
  35. data/lib/kward/cli/tool_summaries.rb +5 -1
  36. data/lib/kward/cli.rb +14 -2
  37. data/lib/kward/cli_transcript_formatter.rb +36 -5
  38. data/lib/kward/compactor.rb +2 -2
  39. data/lib/kward/config_files.rb +45 -10
  40. data/lib/kward/conversation.rb +41 -9
  41. data/lib/kward/memory/manager.rb +131 -14
  42. data/lib/kward/message_access.rb +6 -0
  43. data/lib/kward/model/context_usage.rb +11 -10
  44. data/lib/kward/model/model_info.rb +18 -1
  45. data/lib/kward/model/payloads.rb +89 -10
  46. data/lib/kward/model/stream_parser.rb +258 -25
  47. data/lib/kward/prompt_interface/question_prompt.rb +1 -1
  48. data/lib/kward/prompt_interface/transcript_renderer.rb +20 -11
  49. data/lib/kward/prompts.rb +61 -7
  50. data/lib/kward/rpc/server.rb +7 -2
  51. data/lib/kward/rpc/session_manager.rb +18 -2
  52. data/lib/kward/rpc/session_metrics.rb +2 -2
  53. data/lib/kward/rpc/transcript_normalizer.rb +47 -0
  54. data/lib/kward/session_store.rb +40 -1
  55. data/lib/kward/starter_pack_installer.rb +2 -2
  56. data/lib/kward/tools/fetch_content.rb +41 -0
  57. data/lib/kward/tools/fetch_raw.rb +40 -0
  58. data/lib/kward/tools/registry.rb +9 -2
  59. data/lib/kward/tools/search/web.rb +3 -3
  60. data/lib/kward/tools/search/web_fetch.rb +202 -0
  61. data/lib/kward/tools/tool_call.rb +2 -0
  62. data/lib/kward/version.rb +1 -1
  63. data/templates/default/fulldoc/html/css/kward.css +1501 -0
  64. data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
  65. data/templates/default/fulldoc/html/js/kward.js +296 -0
  66. data/templates/default/fulldoc/html/setup.rb +8 -0
  67. data/templates/default/layout/html/breadcrumb.erb +11 -0
  68. data/templates/default/layout/html/layout.erb +141 -0
  69. data/templates/default/layout/html/setup.rb +139 -0
  70. metadata +14 -1
data/doc/web-search.md CHANGED
@@ -1,28 +1,94 @@
1
1
  # Web search
2
2
 
3
- Web search lets the agent search the live web. Use it when you need current facts, official docs, release notes, bug reports, pricing pages, or recent announcements.
3
+ Use web search when the answer depends on current or external information:
4
+
5
+ - current framework or dependency docs,
6
+ - release notes and migration guides,
7
+ - security advisories,
8
+ - pricing or provider pages,
9
+ - bug reports and issue discussions,
10
+ - a specific URL you want Kward to inspect.
4
11
 
5
12
  Example prompts:
6
13
 
7
14
  ```text
8
- Research the current Rails release notes and summarize migration risks.
15
+ Check the current Rails release notes and summarize migration risks for this project.
9
16
  Find the official OpenRouter docs for model configuration.
10
17
  Check whether this dependency has a recent security advisory.
18
+ Read this URL and explain the setup steps: https://example.com/docs
19
+ ```
20
+
21
+ ## How Kward researches
22
+
23
+ Kward has three web tools:
24
+
25
+ 1. `web_search` finds candidate sources.
26
+ 2. `fetch_content` reads human-readable pages.
27
+ 3. `fetch_raw` reads machine-readable files such as JSON, YAML, XML, RSS, OpenAPI specs, or plain text.
28
+
29
+ A good research flow is:
30
+
31
+ ```text
32
+ Search for the official docs, fetch the relevant page, then answer with the source URL.
33
+ ```
34
+
35
+ Kward should search first, then fetch important pages before relying on them.
36
+
37
+ ## Network behavior
38
+
39
+ Web tools are advertised to the model by default. Queries and fetched URLs are sent over the network to the selected provider or target host.
40
+
41
+ In automatic mode, provider fallback is:
42
+
43
+ 1. Exa API when `EXA_API_KEY` is configured, otherwise keyless Exa MCP.
44
+ 2. Perplexity API when configured and model-provider fallback is allowed.
45
+ 3. Gemini API with Google Search grounding when configured and model-provider fallback is allowed.
46
+ 4. DuckDuckGo HTML search, then bundled public SearXNG instances.
47
+
48
+ You do not need an API key for basic web search, but keys can improve limits or provider choice.
49
+
50
+ ## Disable web tools
51
+
52
+ Hide all web tools:
53
+
54
+ ```json
55
+ {
56
+ "web_search": {
57
+ "enabled": false
58
+ }
59
+ }
11
60
  ```
12
61
 
13
- The `web_search` tool is advertised by default so the agent can use current sources when needed. In `auto` mode the provider fallback order is:
62
+ Use this when working on private projects where no prompt should trigger external lookup.
63
+
64
+ ## Tool details
65
+
66
+ ### `web_search`
67
+
68
+ Finds candidate sources. Arguments:
69
+
70
+ - `queries`: one to four search strings.
71
+ - `max_results`: results per query, default 5, capped at 20.
72
+ - `provider`: optional `auto`, `exa`, `perplexity`, `gemini`, or `duckduckgo`.
73
+ - `recency_filter`: optional `day`, `week`, `month`, or `year`.
74
+ - `domain_filter`: optional domains to include, or domains prefixed with `-` to exclude.
75
+
76
+ ### `fetch_content`
77
+
78
+ Reads a specific HTTP or HTTPS page and extracts readable text. Use it for docs pages, articles, issues, and release notes.
79
+
80
+ Arguments:
81
+
82
+ - `url`
83
+ - `max_bytes`: default 16384, capped at 131072.
84
+ - `extract`: optional `auto`, `text`, or `markdown`.
14
85
 
15
- 1. Exa API when `EXA_API_KEY` is configured, otherwise keyless Exa MCP (`https://mcp.exa.ai/mcp`)
16
- 2. Perplexity API when configured and `allow_model_providers` is true
17
- 3. Gemini API with Google Search grounding when configured and `allow_model_providers` is true
18
- 4. DuckDuckGo HTML search, then bundled public SearXNG instances
86
+ ### `fetch_raw`
19
87
 
20
- Queries are sent over the network to the selected provider. API keys are never bundled with Kward; configure your own keys only if you want higher limits or alternate providers. Set `web_search.enabled` to `false` to hide the tool. Direct `provider: perplexity` or `provider: gemini` requests still use those providers when keys are configured.
88
+ Reads a specific HTTP or HTTPS resource without readability extraction. Use it for JSON, YAML, XML, RSS, OpenAPI specs, and plain text.
21
89
 
22
- Supported arguments:
90
+ Arguments:
23
91
 
24
- - `queries`: one to four search strings
25
- - `max_results`: results per query, default 5, capped at 20
26
- - `provider`: optional `auto`, `exa`, `perplexity`, `gemini`, or `duckduckgo`
27
- - `recency_filter`: optional `day`, `week`, `month`, or `year`
28
- - `domain_filter`: optional list of included domains, or excluded domains prefixed with `-`
92
+ - `url`
93
+ - `max_bytes`: default 16384, capped at 131072.
94
+ - `accept`: optional HTTP `Accept` header.
data/exe/kward CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+
3
5
  require "kward"
4
6
 
5
7
  Kward::CLI.new.run
data/lib/kward/agent.rb CHANGED
@@ -213,7 +213,7 @@ module Kward
213
213
  end
214
214
  ChatInvocation.call(
215
215
  @client,
216
- @conversation.messages,
216
+ @conversation.context_messages,
217
217
  {
218
218
  tools: @tool_registry.schemas,
219
219
  on_reasoning_delta: reasoning_delta,
@@ -50,8 +50,9 @@ module Kward
50
50
  #{command.call("kward")} #{option.call('"Explain this project"')} Run a one-shot prompt
51
51
  #{command.call("kward login")} Sign in or save provider credentials
52
52
  #{command.call("kward auth status")} Show saved credential status
53
- #{command.call("kward init")} Install starter prompts and AGENTS.md
53
+ #{command.call("kward init")} Install starter prompts and PRINCIPLES.md
54
54
  #{command.call("kward doctor")} Check local Kward setup
55
+ #{command.call("kward sysprompt")} Inspect the effective system prompt
55
56
  #{command.call("kward pan")} Start Pan mode web UI
56
57
  #{command.call("kward rpc")} Start the experimental JSON-RPC backend
57
58
 
@@ -60,8 +61,9 @@ module Kward
60
61
  #{command.call("version")} Show the installed Kward version
61
62
  #{command.call("login")} [anthropic|openrouter|github] Sign in with OpenAI, Anthropic, OpenRouter, or GitHub
62
63
  #{command.call("auth status|logout")} Show or clear saved credentials
63
- #{command.call("init")} Install starter prompts and AGENTS.md
64
+ #{command.call("init")} Install starter prompts and PRINCIPLES.md
64
65
  #{command.call("doctor")} Check local Kward setup
66
+ #{command.call("sysprompt")} [--raw] Inspect the effective system prompt
65
67
  #{command.call("stats tokens")} [range] [options] Export local token telemetry as CSV
66
68
  #{command.call("pan")} Start Pan mode web UI
67
69
  #{command.call("rpc")} Run the JSON-RPC backend for UI clients
@@ -106,7 +108,7 @@ module Kward
106
108
  },
107
109
  "init" => {
108
110
  usage: "kward init",
109
- description: "Install starter prompts and base AGENTS.md into your config directory.",
111
+ description: "Install starter prompts and base PRINCIPLES.md into your config directory.",
110
112
  examples: ["kward init"]
111
113
  },
112
114
  "doctor" => {
@@ -114,6 +116,11 @@ module Kward
114
116
  description: "Check local Kward configuration, workspace, auth hints, and writable directories.",
115
117
  examples: ["kward doctor", "kward --working-directory ~/code/project doctor"]
116
118
  },
119
+ "sysprompt" => {
120
+ usage: "kward sysprompt [--raw]",
121
+ description: "Inspect the effective system prompt for a new conversation in the current workspace.",
122
+ examples: ["kward sysprompt", "kward sysprompt --raw", "kward --working-directory ~/code/project sysprompt"]
123
+ },
117
124
  "stats" => {
118
125
  usage: "kward stats tokens [range] [--bucket second|minute|hour|day|week|month|year] [--output path]",
119
126
  description: "Export local token telemetry as CSV.",
@@ -12,12 +12,12 @@ module Kward
12
12
  client: @client,
13
13
  tool_result_summarizer: lambda { |tool_call, content| tool_result_summary(tool_call, content) }
14
14
  ).compact(custom_instructions: argument)
15
- @prompt.say("\nCompacted context: #{result.old_message_count} messages -> #{result.new_message_count} messages.\n")
15
+ runtime_output("Compacted context: #{result.old_message_count} messages -> #{result.new_message_count} messages.")
16
16
  render_transcript_block("Assistant", result.summary)
17
17
  rescue Compactor::NothingToCompact, Compactor::AlreadyCompacted, Compactor::EmptySummary => e
18
- @prompt.say("\n#{e.message}\n")
18
+ runtime_output(e.message)
19
19
  rescue StandardError => e
20
- @prompt.say("\nCompaction error: #{e.message}\n")
20
+ runtime_output("Compaction error: #{e.message}")
21
21
  end
22
22
 
23
23
  end
@@ -95,6 +95,9 @@ module Kward
95
95
  when Events::AssistantDelta
96
96
  stream_state[:streamed] = true
97
97
  append_markdown_delta(markdown_chunks, "Assistant", event.delta)
98
+ when Events::Steering
99
+ finish_interactive_markdown_deltas(markdown_chunks, stream_state)
100
+ print_user_transcript(event.input)
98
101
  when Events::SteeringApplied
99
102
  @prompt.clear_steered_count if @prompt.respond_to?(:clear_steered_count)
100
103
  when Events::Retry
@@ -104,7 +107,6 @@ module Kward
104
107
  when Events::ToolCall
105
108
  stream_state[:streamed] = true
106
109
  finish_interactive_markdown_deltas(markdown_chunks, stream_state)
107
- print_tool_call(event.tool_call)
108
110
  when Events::ToolResult
109
111
  stream_state[:streamed] = true
110
112
  finish_interactive_markdown_deltas(markdown_chunks, stream_state)
@@ -18,53 +18,53 @@ module Kward
18
18
  when "enable"
19
19
  manager.enable
20
20
  agent.conversation.refresh_system_message!
21
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Memory enabled.\n")
21
+ runtime_output("Memory enabled.")
22
22
  when "disable"
23
23
  manager.disable
24
24
  agent.conversation.memory_context = nil
25
25
  agent.conversation.refresh_system_message!
26
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Memory disabled.\n")
26
+ runtime_output("Memory disabled.")
27
27
  when "auto-summary"
28
28
  case rest.to_s.strip
29
29
  when "enable", "on"
30
30
  manager.auto_summary_enable
31
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Memory auto-summary enabled.\n")
31
+ runtime_output("Memory auto-summary enabled.")
32
32
  when "disable", "off"
33
33
  manager.auto_summary_disable
34
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Memory auto-summary disabled.\n")
34
+ runtime_output("Memory auto-summary disabled.")
35
35
  else
36
- @prompt.say("\nUsage: /memory auto-summary enable|disable\n")
36
+ runtime_output("Usage: /memory auto-summary enable|disable")
37
37
  end
38
38
  when "core"
39
39
  record = manager.add_core(unquote_argument(rest))
40
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Added core memory #{record["id"]}.\n")
40
+ runtime_output("Added core memory #{record["id"]}.")
41
41
  when "add"
42
42
  record = manager.add_soft(unquote_argument(rest), scope: "workspace:#{agent.conversation.workspace_root}")
43
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Added soft memory #{record["id"]}.\n")
43
+ runtime_output("Added soft memory #{record["id"]}.")
44
44
  when "list"
45
- @prompt.say("\n#{format_memory_list(manager.hierarchy(workspace_root: agent.conversation.workspace_root))}\n")
45
+ runtime_output(format_memory_list(manager.hierarchy(workspace_root: agent.conversation.workspace_root)))
46
46
  when "forget"
47
47
  forgotten = manager.forget_memory(rest.to_s.strip)
48
- @prompt.say("\n#{forgotten ? "Forgot #{rest.to_s.strip}." : "No memory found for #{rest.to_s.strip}."}\n")
48
+ runtime_output(forgotten ? "Forgot #{rest.to_s.strip}." : "No memory found for #{rest.to_s.strip}.")
49
49
  when "promote"
50
50
  record = manager.promote_memory(rest.to_s.strip)
51
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Promoted memory #{record["id"]}.\n")
51
+ runtime_output("Promoted memory #{record["id"]}.")
52
52
  when "relax"
53
53
  record = manager.relax_core(rest.to_s.strip, workspace_root: agent.conversation.workspace_root)
54
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Relaxed memory #{record["id"]}.\n")
54
+ runtime_output("Relaxed memory #{record["id"]}.")
55
55
  when "inspect"
56
- @prompt.say("\n#{JSON.pretty_generate(manager.inspect_memory)}\n")
56
+ runtime_output(JSON.pretty_generate(manager.inspect_memory))
57
57
  when "why"
58
58
  explanation = agent.conversation.last_memory_retrieval || manager.explain_retrieval
59
- @prompt.say("\n#{format_memory_why(explanation)}\n")
59
+ runtime_output(format_memory_why(explanation))
60
60
  when "summarize", "learn"
61
61
  records = summarize_memory(agent.conversation, manager: manager)
62
- @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Learned #{records.length} soft #{records.length == 1 ? "memory" : "memories"}.\n")
62
+ runtime_output("Learned #{records.length} soft #{records.length == 1 ? "memory" : "memories"}.")
63
63
  else
64
- @prompt.say("\nUsage: /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize\n")
64
+ runtime_output("Usage: /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize")
65
65
  end
66
66
  rescue StandardError => e
67
- @prompt.say("\nMemory command failed: #{e.message}\n")
67
+ runtime_output("Memory command failed: #{e.message}")
68
68
  end
69
69
 
70
70
  def summarize_memory(conversation, manager: Memory::Manager.new)
@@ -26,7 +26,7 @@ module Kward
26
26
  @plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
27
27
  conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
28
28
  conversation.refresh_system_message! if conversation.respond_to?(:refresh_system_message!)
29
- @prompt.say("\nPlugins reloaded.\n")
29
+ runtime_output("Plugins reloaded.")
30
30
  end
31
31
 
32
32
  def reserved_slash_command_names
@@ -62,7 +62,7 @@ module Kward
62
62
  command.handler.call(argument, context)
63
63
  [true, nil]
64
64
  rescue StandardError => e
65
- @prompt.say("\nPlugin command /#{name} error: #{e.message}\n")
65
+ runtime_output("Plugin command /#{name} error: #{e.message}")
66
66
  [true, nil]
67
67
  end
68
68
 
@@ -72,7 +72,7 @@ module Kward
72
72
  args: args,
73
73
  session: @active_session,
74
74
  workspace_root: conversation.workspace_root,
75
- say_callback: lambda { |message| @prompt.say("\n#{message}\n") }
75
+ say_callback: lambda { |message| runtime_output(message) }
76
76
  )
77
77
  end
78
78
 
@@ -52,10 +52,12 @@ module Kward
52
52
  end
53
53
 
54
54
  def prompt_footer_renderer
55
- renderer = plugin_registry.footer_renderer
56
- return nil unless renderer
55
+ return nil unless plugin_registry.footer_renderer
57
56
 
58
57
  lambda do
58
+ renderer = plugin_registry.footer_renderer
59
+ next "" unless renderer
60
+
59
61
  context = plugin_context(current_footer_conversation, "")
60
62
  renderer.call(context).to_s
61
63
  rescue StandardError => e
@@ -65,9 +67,10 @@ module Kward
65
67
  end
66
68
 
67
69
  def composer_status_text
68
- provider = @client.respond_to?(:current_provider) ? @client.current_provider : "Codex"
69
- model = @client.respond_to?(:current_model) ? @client.current_model : ModelInfo::DEFAULT_OPENAI_MODEL
70
- reasoning = @client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT
70
+ conversation = current_footer_conversation
71
+ provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
72
+ model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : ModelInfo::DEFAULT_OPENAI_MODEL)
73
+ reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)
71
74
  reasoning = "n/a" unless ModelInfo.reasoning_supported?(provider, model) && !reasoning.to_s.empty?
72
75
  text = "#{provider} #{model} · #{reasoning}"
73
76
  parts = []
@@ -97,19 +100,18 @@ module Kward
97
100
  ANSI.colorize("#{value}%", color, enabled: @color_enabled)
98
101
  end
99
102
 
100
- def composer_context_window
101
- provider = @client.respond_to?(:current_provider) ? @client.current_provider : "Codex"
102
- model = @client.respond_to?(:current_model) ? @client.current_model : ModelInfo::DEFAULT_OPENAI_MODEL
103
- provider = ModelInfo.provider_label(provider)
104
- @client.respond_to?(:current_context_window) ? @client.current_context_window : ModelInfo.context_window(provider, model)
103
+ def composer_context_window(provider = nil, model = nil)
104
+ provider ||= current_footer_conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
105
+ model ||= current_footer_conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : ModelInfo::DEFAULT_OPENAI_MODEL)
106
+ ModelInfo.context_window(ModelInfo.provider_label(provider), model)
105
107
  end
106
108
 
107
109
  def composer_context_usage(provider, model)
108
- context_window = composer_context_window
110
+ context_window = composer_context_window(provider, model)
109
111
  context_parts = if @client.respond_to?(:current_context_parts)
110
- @client.current_context_parts(current_footer_conversation.messages, footer_tool_schemas)
112
+ @client.current_context_parts(current_footer_conversation.context_messages, footer_tool_schemas)
111
113
  else
112
- { provider: provider, model: model, messages: current_footer_conversation.messages, tools: footer_tool_schemas }
114
+ { provider: provider, model: model, messages: current_footer_conversation.context_messages, tools: footer_tool_schemas }
113
115
  end
114
116
  @context_usage.call(
115
117
  provider: provider,
@@ -8,7 +8,7 @@ module Kward
8
8
 
9
9
  def render_conversation_transcript(conversation)
10
10
  tool_calls_by_id = {}
11
- @prompt.say("\n#{colored("Transcript", :cyan, :bold)}\n")
11
+ @prompt.say("\n#{colored("Transcript", :gray, :bold)}\n")
12
12
  conversation.messages.each do |message|
13
13
  role = message_role(message)
14
14
  next if role == "system"
@@ -26,7 +26,6 @@ module Kward
26
26
  render_assistant_message(message)
27
27
  message_tool_calls(message).each do |tool_call|
28
28
  tool_calls_by_id[tool_call_id(tool_call)] = tool_call
29
- render_tool_call(tool_call)
30
29
  end
31
30
  when "tool"
32
31
  render_tool_message(message, tool_calls_by_id)
@@ -44,7 +43,7 @@ module Kward
44
43
  end
45
44
 
46
45
  def render_assistant_message(message)
47
- content = CLITranscriptFormatter.content_text(message_content(message))
46
+ content = CLITranscriptFormatter.assistant_content_text(message)
48
47
  return if content.empty?
49
48
 
50
49
  render_transcript_block("Assistant", content)
@@ -55,20 +54,12 @@ module Kward
55
54
  render_tool_result(tool_call, message_content(message).to_s)
56
55
  end
57
56
 
58
- def render_tool_call(tool_call)
59
- if prompt_interface?
60
- print_tool_call(tool_call)
61
- else
62
- @prompt.say("\n#{colored("Tool>", :magenta, :bold)}\n#{tool_command(tool_call)}\n")
63
- end
64
- end
65
-
66
57
  def render_tool_result(tool_call, content)
67
58
  summary = limit_tool_output_lines(tool_result_summary(tool_call, content), INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
68
59
  if prompt_interface?
69
60
  print_tool_result(tool_call, content, line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
70
61
  else
71
- @prompt.say("\n#{colored("Tool output>", :cyan, :bold)}\n#{summary}\n")
62
+ @prompt.say("\n#{colored("Tool>", *tool_label_styles(content))}\n#{summary}\n")
72
63
  end
73
64
  end
74
65
 
@@ -80,7 +71,7 @@ module Kward
80
71
  print_block_delta(label, rendered)
81
72
  finish_stream_block
82
73
  else
83
- @prompt.say("\n#{colored("#{transcript_label(label)}>", label_color(label), :bold)}\n#{rendered}\n")
74
+ @prompt.say("\n#{colored("#{transcript_label(label)}>", *label_styles(label))}\n#{rendered}\n")
84
75
  end
85
76
  end
86
77
 
@@ -102,7 +93,6 @@ module Kward
102
93
  :streamed
103
94
  when Events::ToolCall
104
95
  flush_markdown_deltas(markdown_chunks)
105
- print_tool_call(event.tool_call)
106
96
  :streamed
107
97
  when Events::ToolResult
108
98
  flush_markdown_deltas(markdown_chunks)
@@ -304,24 +294,6 @@ module Kward
304
294
  RetryMessage.format(event)
305
295
  end
306
296
 
307
- # Writes the tool call output for the terminal CLI flow.
308
- def print_tool_call(tool_call)
309
- if prompt_interface?
310
- if @prompt.respond_to?(:write_stream_block)
311
- @prompt.write_stream_block("Tool", "#{tool_command(tool_call)}\n", finish: true)
312
- else
313
- @prompt.start_stream_block("Tool")
314
- @prompt.write_delta("#{tool_command(tool_call)}\n")
315
- @prompt.finish_stream_block
316
- end
317
- else
318
- start_stream_block("Tool")
319
- puts tool_command(tool_call)
320
- $stdout.flush
321
- @stream_block = nil
322
- end
323
- end
324
-
325
297
  # Writes the tool result output for the terminal CLI flow.
326
298
  def print_tool_result(tool_call, content, line_limit: nil)
327
299
  summary = tool_result_summary(tool_call, content)
@@ -329,14 +301,14 @@ module Kward
329
301
  if prompt_interface?
330
302
  summary = summary.end_with?("\n") ? summary : "#{summary}\n"
331
303
  if @prompt.respond_to?(:write_stream_block)
332
- @prompt.write_stream_block("Tool output", summary, finish: true)
304
+ @prompt.write_stream_block("Tool", summary, finish: true)
333
305
  else
334
- @prompt.start_stream_block("Tool output")
306
+ @prompt.start_stream_block("Tool")
335
307
  @prompt.write_delta(summary)
336
308
  @prompt.finish_stream_block
337
309
  end
338
310
  else
339
- start_stream_block("Tool output")
311
+ start_stream_block(tool_stream_label(content))
340
312
  print summary
341
313
  puts unless summary.end_with?("\n")
342
314
  $stdout.flush
@@ -348,7 +320,7 @@ module Kward
348
320
  return if @stream_block == label
349
321
 
350
322
  puts if @stream_block
351
- puts "\n#{colored("#{transcript_label(label)}>", label_color(label), :bold)}"
323
+ puts "\n#{colored("#{transcript_label(label)}>", *label_styles(label))}"
352
324
  @stream_block = label
353
325
  end
354
326
 
@@ -366,24 +338,41 @@ module Kward
366
338
  end
367
339
 
368
340
  def transcript_label(label)
369
- label == "Assistant" ? assistant_prompt_name : label
341
+ case label
342
+ when "Assistant"
343
+ assistant_prompt_name
344
+ when "Tool failed"
345
+ "Tool"
346
+ else
347
+ label
348
+ end
370
349
  end
371
350
 
372
- def label_color(label)
351
+ def label_styles(label)
373
352
  case label
374
- when "Reasoning"
375
- :yellow
353
+ when "Reasoning", "Compaction summary"
354
+ [:gray, :bold]
376
355
  when "Assistant", "Kward"
377
- :green
378
- when "Tool"
379
- :magenta
380
- when "Tool output"
381
- :cyan
356
+ [:green, :bold]
357
+ when "Tool", "Tool output"
358
+ [:cyan, :bold]
359
+ when "Tool failed"
360
+ [:red, :bold]
361
+ when "Retry"
362
+ [:yellow, :bold]
382
363
  else
383
- :blue
364
+ [:gray, :bold]
384
365
  end
385
366
  end
386
367
 
368
+ def tool_stream_label(content)
369
+ tool_result_failed?(content) ? "Tool failed" : "Tool"
370
+ end
371
+
372
+ def tool_label_styles(content)
373
+ label_styles(tool_stream_label(content))
374
+ end
375
+
387
376
  end
388
377
  end
389
378
  end
@@ -31,6 +31,17 @@ module Kward
31
31
  @assistant_prompt || "Assistant>"
32
32
  end
33
33
 
34
+ def runtime_output_prompt
35
+ "Runtime>"
36
+ end
37
+
38
+ def runtime_output(text)
39
+ content = text.to_s.chomp
40
+ label = colored(runtime_output_prompt, :gray, :bold)
41
+ separator = content.include?("\n") ? "\n" : " "
42
+ @prompt.say("\n#{label}#{separator}#{content}\n")
43
+ end
44
+
34
45
  def build_interactive_agent(conversation)
35
46
  conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
36
47
  workspace = configured_workspace(root: conversation.workspace_root)
@@ -47,13 +58,13 @@ module Kward
47
58
  def handle_interactive_shell_command(input, agent)
48
59
  command = input.to_s.sub(/\A!\s*/, "")
49
60
  if command.strip.empty?
50
- @prompt.say("\nShell command is required after !\n")
61
+ runtime_output("Shell command is required after !")
51
62
  return true
52
63
  end
53
64
 
54
65
  run_busy_local_command_and_requeue(activity: "running") do
55
66
  result = configured_workspace(root: interactive_workspace_root(agent)).run_shell_command(command)
56
- @prompt.say("\n#{colored("Shell>", :green, :bold)} #{command}\n#{result}\n")
67
+ @prompt.say("\n#{colored("Shell>", :cyan, :bold)} #{command}\n#{result}\n")
57
68
  end
58
69
  true
59
70
  end