kward 0.67.0 → 0.68.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +5 -5
  5. data/doc/authentication.md +24 -1
  6. data/doc/configuration.md +9 -2
  7. data/doc/extensibility.md +1 -1
  8. data/doc/getting-started.md +4 -6
  9. data/doc/plugins.md +0 -2
  10. data/doc/releasing.md +7 -8
  11. data/doc/rpc.md +6 -6
  12. data/doc/usage.md +5 -2
  13. data/doc/web-search.md +2 -2
  14. data/kward.gemspec +4 -0
  15. data/lib/kward/agent.rb +29 -2
  16. data/lib/kward/ansi.rb +3 -0
  17. data/lib/kward/auth/anthropic_oauth.rb +291 -0
  18. data/lib/kward/auth/file.rb +2 -0
  19. data/lib/kward/auth/github_oauth.rb +3 -0
  20. data/lib/kward/auth/openai_oauth.rb +4 -0
  21. data/lib/kward/auth/openrouter_api_key.rb +2 -0
  22. data/lib/kward/cancellation.rb +3 -0
  23. data/lib/kward/cli/auth_commands.rb +82 -0
  24. data/lib/kward/cli/commands.rb +222 -0
  25. data/lib/kward/cli/compaction.rb +25 -0
  26. data/lib/kward/cli/doctor.rb +121 -0
  27. data/lib/kward/cli/interactive_turn.rb +225 -0
  28. data/lib/kward/cli/memory_commands.rb +133 -0
  29. data/lib/kward/cli/plugins.rb +112 -0
  30. data/lib/kward/cli/prompt_interface.rb +132 -0
  31. data/lib/kward/cli/rendering.rb +389 -0
  32. data/lib/kward/cli/runtime_helpers.rb +159 -0
  33. data/lib/kward/cli/sessions.rb +376 -0
  34. data/lib/kward/cli/settings.rb +663 -0
  35. data/lib/kward/cli/slash_commands.rb +112 -0
  36. data/lib/kward/cli/stats.rb +64 -0
  37. data/lib/kward/cli/tool_summaries.rb +153 -0
  38. data/lib/kward/cli.rb +38 -2790
  39. data/lib/kward/cli_transcript_formatter.rb +4 -7
  40. data/lib/kward/clipboard.rb +1 -0
  41. data/lib/kward/compaction/file_operation_tracker.rb +3 -0
  42. data/lib/kward/compactor.rb +29 -7
  43. data/lib/kward/config_files.rb +33 -24
  44. data/lib/kward/conversation.rb +70 -5
  45. data/lib/kward/events.rb +2 -0
  46. data/lib/kward/export_path.rb +2 -0
  47. data/lib/kward/image_attachments.rb +2 -0
  48. data/lib/kward/markdown_transcript.rb +2 -0
  49. data/lib/kward/memory/manager.rb +13 -0
  50. data/lib/kward/message_access.rb +23 -2
  51. data/lib/kward/message_text.rb +45 -0
  52. data/lib/kward/model/chat_invocation.rb +2 -0
  53. data/lib/kward/model/client.rb +295 -77
  54. data/lib/kward/model/context_overflow.rb +2 -0
  55. data/lib/kward/model/context_usage.rb +3 -0
  56. data/lib/kward/model/model_info.rb +143 -4
  57. data/lib/kward/model/payloads.rb +166 -13
  58. data/lib/kward/model/retry_message.rb +2 -0
  59. data/lib/kward/model/stream_parser.rb +129 -0
  60. data/lib/kward/pan/server.rb +3 -1
  61. data/lib/kward/plugin_registry.rb +12 -0
  62. data/lib/kward/private_file.rb +2 -0
  63. data/lib/kward/prompt_interface/banner.rb +3 -0
  64. data/lib/kward/prompt_interface/composer_controller.rb +262 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
  66. data/lib/kward/prompt_interface/composer_state.rb +221 -0
  67. data/lib/kward/prompt_interface/key_handler.rb +365 -0
  68. data/lib/kward/prompt_interface/layout.rb +31 -0
  69. data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
  70. data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
  71. data/lib/kward/prompt_interface/question_prompt.rb +328 -0
  72. data/lib/kward/prompt_interface/runtime_state.rb +59 -0
  73. data/lib/kward/prompt_interface/screen.rb +186 -0
  74. data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
  75. data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
  76. data/lib/kward/prompt_interface/stream_state.rb +65 -0
  77. data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
  78. data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
  79. data/lib/kward/prompt_interface.rb +69 -1832
  80. data/lib/kward/prompts/commands.rb +2 -0
  81. data/lib/kward/prompts/templates.rb +3 -0
  82. data/lib/kward/prompts.rb +2 -0
  83. data/lib/kward/question_contract.rb +66 -0
  84. data/lib/kward/resources/avatar_kward_logo.rb +2 -0
  85. data/lib/kward/resources/pixel_logo.rb +2 -0
  86. data/lib/kward/rpc/attachment_normalizer.rb +60 -0
  87. data/lib/kward/rpc/auth_manager.rb +65 -11
  88. data/lib/kward/rpc/config_manager.rb +11 -0
  89. data/lib/kward/rpc/prompt_bridge.rb +5 -26
  90. data/lib/kward/rpc/redactor.rb +3 -0
  91. data/lib/kward/rpc/runtime_payloads.rb +4 -1
  92. data/lib/kward/rpc/server.rb +37 -10
  93. data/lib/kward/rpc/session_manager.rb +123 -347
  94. data/lib/kward/rpc/session_metrics.rb +68 -0
  95. data/lib/kward/rpc/session_tree.rb +48 -0
  96. data/lib/kward/rpc/session_tree_rows.rb +208 -0
  97. data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
  98. data/lib/kward/rpc/tool_metadata.rb +3 -0
  99. data/lib/kward/rpc/transcript_normalizer.rb +3 -0
  100. data/lib/kward/rpc/transport.rb +3 -0
  101. data/lib/kward/session_diff.rb +2 -0
  102. data/lib/kward/session_store.rb +125 -31
  103. data/lib/kward/session_trash.rb +1 -0
  104. data/lib/kward/session_tree_renderer.rb +8 -41
  105. data/lib/kward/session_tree_tool_display.rb +56 -0
  106. data/lib/kward/skills/registry.rb +3 -0
  107. data/lib/kward/starter_pack_installer.rb +1 -0
  108. data/lib/kward/steering.rb +2 -0
  109. data/lib/kward/telemetry/logger.rb +3 -0
  110. data/lib/kward/telemetry/stats.rb +3 -0
  111. data/lib/kward/tools/ask_user_question.rb +20 -32
  112. data/lib/kward/tools/base.rb +8 -0
  113. data/lib/kward/tools/code_search.rb +5 -0
  114. data/lib/kward/tools/edit_file.rb +5 -0
  115. data/lib/kward/tools/list_directory.rb +5 -0
  116. data/lib/kward/tools/read_file.rb +5 -0
  117. data/lib/kward/tools/read_skill.rb +5 -0
  118. data/lib/kward/tools/registry.rb +33 -2
  119. data/lib/kward/tools/run_shell_command.rb +5 -0
  120. data/lib/kward/tools/search/code.rb +7 -0
  121. data/lib/kward/tools/search/web.rb +17 -14
  122. data/lib/kward/tools/tool_call.rb +25 -5
  123. data/lib/kward/tools/web_search.rb +7 -1
  124. data/lib/kward/tools/write_file.rb +5 -0
  125. data/lib/kward/transcript_export.rb +2 -0
  126. data/lib/kward/version.rb +2 -1
  127. data/lib/kward/workspace.rb +45 -5
  128. metadata +43 -1
@@ -1,8 +1,13 @@
1
+ require_relative "../question_contract"
1
2
  require_relative "base"
2
3
 
4
+ # Namespace for the Kward CLI agent runtime.
3
5
  module Kward
6
+ # Model-callable tool wrappers and their argument schemas.
4
7
  module Tools
8
+ # Tool wrapper for structured clarification questions to the user.
5
9
  class AskUserQuestion < Base
10
+ # Builds the tool schema and stores the execution dependency.
6
11
  def initialize(prompt:)
7
12
  @prompt = prompt
8
13
  super(
@@ -42,6 +47,7 @@ module Kward
42
47
  )
43
48
  end
44
49
 
50
+ # Executes the tool and returns model-facing output text.
45
51
  def call(args, _conversation, cancellation: nil)
46
52
  return "Error: ask_user_question requires interactive prompt support." unless @prompt.respond_to?(:ask_user_question)
47
53
 
@@ -67,40 +73,22 @@ module Kward
67
73
  end
68
74
 
69
75
  def validated_questions(args)
70
- questions = argument(args, :questions)
71
- return "Error: ask_user_question requires questions." unless questions.is_a?(Array)
72
- return "Error: ask_user_question requires 1 to 4 questions." unless questions.length.between?(1, 4)
73
-
74
- questions.map.with_index(1) do |question, index|
75
- return "Error: question #{index} must be an object." unless question.respond_to?(:key?)
76
- return "Error: question #{index} uses unsupported multiSelect." if question.key?("multiSelect") || question.key?(:multiSelect)
77
-
78
- text = question_value(question, :question).to_s.strip
79
- header = question_value(question, :header).to_s.strip
80
- options = question_value(question, :options)
81
- return "Error: question #{index} requires question and header." if text.empty? || header.empty?
82
- return "Error: question #{index} requires 2 to 4 options." unless options.is_a?(Array) && options.length.between?(2, 4)
83
-
84
- normalized_options = options.map.with_index(1) do |option, option_index|
85
- return "Error: question #{index} option #{option_index} must be an object." unless option.respond_to?(:key?)
86
- return "Error: question #{index} option #{option_index} uses unsupported preview." if option.key?("preview") || option.key?(:preview)
87
-
88
- label = question_value(option, :label).to_s.strip
89
- description = question_value(option, :description).to_s.strip
90
- return "Error: question #{index} option #{option_index} requires label and description." if label.empty? || description.empty?
91
-
92
- { label: label, description: description }
93
- end
94
-
95
- { question: text, header: header, options: normalized_options }
96
- end
76
+ QuestionContract.normalize_questions(argument(args, :questions))
77
+ rescue ArgumentError => e
78
+ "Error: #{tool_error_message(e.message)}."
97
79
  end
98
80
 
99
- def question_value(object, key)
100
- return object[key] if object.key?(key)
101
- return object[key.to_s] if object.key?(key.to_s)
102
-
103
- nil
81
+ def tool_error_message(message)
82
+ case message
83
+ when "questions must be an array"
84
+ "ask_user_question requires questions"
85
+ when "ui/question requires 1-4 questions"
86
+ "ask_user_question requires 1 to 4 questions"
87
+ else
88
+ message.gsub("2-4", "2 to 4")
89
+ .gsub("multiSelect is unsupported", "uses unsupported multiSelect")
90
+ .gsub("preview is unsupported", "uses unsupported preview")
91
+ end
104
92
  end
105
93
  end
106
94
  end
@@ -1,8 +1,13 @@
1
+ # Namespace for the Kward CLI agent runtime.
1
2
  module Kward
3
+ # Model-callable tool wrappers and their argument schemas.
2
4
  module Tools
5
+ # Base class for model-callable tools and their JSON schemas.
3
6
  class Base
7
+ # @return [String] function name exposed to the model
4
8
  attr_reader :name
5
9
 
10
+ # Creates a tool schema definition shared by all concrete tool wrappers.
6
11
  def initialize(name, description, properties: {}, required: [])
7
12
  @name = name
8
13
  @description = description
@@ -10,6 +15,7 @@ module Kward
10
15
  @required = required
11
16
  end
12
17
 
18
+ # Returns the strict JSON schema advertised to model providers.
13
19
  def schema
14
20
  {
15
21
  type: "function",
@@ -28,6 +34,7 @@ module Kward
28
34
 
29
35
  private
30
36
 
37
+ # Reads a tool argument while accepting symbol or string keys from restored calls.
31
38
  def argument(args, key, default = nil)
32
39
  return args[key] if args.key?(key)
33
40
  return args[key.to_s] if args.key?(key.to_s)
@@ -35,6 +42,7 @@ module Kward
35
42
  default
36
43
  end
37
44
 
45
+ # Detects successful AGENTS.md writes so callers can refresh prompt context.
38
46
  def agents_file_changed?(workspace, path, result)
39
47
  result.to_s.start_with?("Wrote ", "Edited ") && File.basename(path.to_s) == "AGENTS.md" && workspace.resolved_path(path) == File.join(workspace.root.to_s, "AGENTS.md")
40
48
  rescue StandardError
@@ -1,8 +1,12 @@
1
1
  require_relative "base"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
4
6
  module Tools
7
+ # Package lookup and GitHub repository cache/search implementation.
5
8
  class CodeSearch < Base
9
+ # Builds the tool schema and stores the execution dependency.
6
10
  def initialize(code_search:)
7
11
  @code_search = code_search
8
12
  super(
@@ -56,6 +60,7 @@ module Kward
56
60
  )
57
61
  end
58
62
 
63
+ # Executes the tool and returns model-facing output text.
59
64
  def call(args, _conversation, cancellation: nil)
60
65
  cancellation&.raise_if_cancelled!
61
66
  @code_search.call(args)
@@ -1,8 +1,12 @@
1
1
  require_relative "base"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
4
6
  module Tools
7
+ # Tool wrapper for exact block replacement edits.
5
8
  class EditFile < Base
9
+ # Builds the tool schema and stores the execution dependency.
6
10
  def initialize(workspace:)
7
11
  @workspace = workspace
8
12
  super(
@@ -28,6 +32,7 @@ module Kward
28
32
  )
29
33
  end
30
34
 
35
+ # Executes the tool and returns model-facing output text.
31
36
  def call(args, conversation, cancellation: nil)
32
37
  path = argument(args, :path, "")
33
38
  edits = argument(args, :edits, [])
@@ -1,8 +1,12 @@
1
1
  require_relative "base"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
4
6
  module Tools
7
+ # Tool wrapper for listing workspace directory entries.
5
8
  class ListDirectory < Base
9
+ # Builds the tool schema and stores the execution dependency.
6
10
  def initialize(workspace:)
7
11
  @workspace = workspace
8
12
  super(
@@ -13,6 +17,7 @@ module Kward
13
17
  )
14
18
  end
15
19
 
20
+ # Executes the tool and returns model-facing output text.
16
21
  def call(args, _conversation, cancellation: nil)
17
22
  @workspace.list_directory(argument(args, :path, "."))
18
23
  end
@@ -1,8 +1,12 @@
1
1
  require_relative "base"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
4
6
  module Tools
7
+ # Tool wrapper for bounded workspace file reads.
5
8
  class ReadFile < Base
9
+ # Builds the tool schema and stores the execution dependency.
6
10
  def initialize(workspace:)
7
11
  @workspace = workspace
8
12
  super(
@@ -17,6 +21,7 @@ module Kward
17
21
  )
18
22
  end
19
23
 
24
+ # Executes the tool and returns model-facing output text.
20
25
  def call(args, conversation, cancellation: nil)
21
26
  path = argument(args, :path, "")
22
27
  offset = argument(args, :offset)
@@ -1,9 +1,13 @@
1
1
  require_relative "base"
2
2
  require_relative "../config_files"
3
3
 
4
+ # Namespace for the Kward CLI agent runtime.
4
5
  module Kward
6
+ # Model-callable tool wrappers and their argument schemas.
5
7
  module Tools
8
+ # Tool wrapper for reading configured skill instructions.
6
9
  class ReadSkill < Base
10
+ # Builds the tool schema and stores the execution dependency.
7
11
  def initialize
8
12
  super(
9
13
  "read_skill",
@@ -16,6 +20,7 @@ module Kward
16
20
  )
17
21
  end
18
22
 
23
+ # Executes the tool and returns model-facing output text.
19
24
  def call(args, _conversation, cancellation: nil)
20
25
  name = argument(args, :name, "")
21
26
  path = argument(args, :path)
@@ -13,15 +13,42 @@ require_relative "search/web"
13
13
  require_relative "tool_call"
14
14
  require_relative "../workspace"
15
15
 
16
+ # Namespace for the Kward CLI agent runtime.
16
17
  module Kward
17
18
  # Exposes local workspace, search, skill, and interaction tools to the model
18
- # and dispatches approved tool calls into the active conversation.
19
+ # and dispatches tool calls into the active conversation.
20
+ #
21
+ # `ToolRegistry` is the boundary between model-requested function calls and
22
+ # Ruby tool objects. It owns schema exposure and transcript persistence for
23
+ # tool results; individual tools own validation and side effects. Keep frontend
24
+ # policy outside this class by passing dependencies such as `workspace` and
25
+ # `prompt` from CLI or RPC setup.
26
+ #
27
+ # A tool may exist in `@tools` but not be advertised in `schemas`. This allows
28
+ # restored transcripts or compatibility callers to dispatch known tools while
29
+ # config and frontend capability checks decide what the model can request next.
30
+ #
31
+ # Tool schemas are the strict output contract advertised to models and clients.
32
+ # Incoming calls are intentionally more tolerant: extra fields are ignored by
33
+ # individual tools, and legacy-compatible shapes are accepted where already
34
+ # supported. Required fields and invalid required values should still return
35
+ # explicit tool errors.
19
36
  class ToolRegistry
20
37
  # Tool schemas advertised to the model for the current frontend and config.
21
38
  #
22
- # @return [Array<Hash>]
39
+ # @return [Array<Hash>] tool schemas currently advertised to the model
23
40
  attr_reader :schemas
24
41
 
42
+ # Builds tool objects and the schema list for the current frontend/config.
43
+ #
44
+ # @param workspace [Workspace] filesystem/shell boundary used by local tools
45
+ # @param prompt [Object, nil] interactive prompt bridge; must implement
46
+ # `ask_user_question` before that tool is advertised
47
+ # @param web_search [WebSearch] live web search implementation
48
+ # @param code_search [CodeSearch] public source/package search implementation
49
+ # @param web_search_enabled [Boolean, nil] override for web search exposure
50
+ # @param skills [Array<ConfigFiles::Skill>, nil] override discovered skills
51
+ # @param ask_user_question_enabled [Boolean, nil] override question exposure
25
52
  def initialize(workspace: Workspace.new, prompt: nil, web_search: WebSearch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil)
26
53
  @workspace = workspace
27
54
  @prompt = prompt
@@ -37,6 +64,10 @@ module Kward
37
64
  # Executes a model-requested tool call and appends the result to the
38
65
  # conversation transcript.
39
66
  #
67
+ # Unknown tools are recorded as tool results instead of raising. That keeps
68
+ # the conversation valid for the model and lets the assistant recover by
69
+ # choosing an advertised tool on the next turn.
70
+ #
40
71
  # @param tool_call [Hash] model tool call payload
41
72
  # @param conversation [Conversation] active conversation
42
73
  # @return [String] tool output content appended to the conversation
@@ -1,9 +1,13 @@
1
1
  require_relative "base"
2
2
  require_relative "../workspace"
3
3
 
4
+ # Namespace for the Kward CLI agent runtime.
4
5
  module Kward
6
+ # Model-callable tool wrappers and their argument schemas.
5
7
  module Tools
8
+ # Tool wrapper for bounded shell commands in the workspace.
6
9
  class RunShellCommand < Base
10
+ # Builds the tool schema and stores the execution dependency.
7
11
  def initialize(workspace:)
8
12
  @workspace = workspace
9
13
  super(
@@ -17,6 +21,7 @@ module Kward
17
21
  )
18
22
  end
19
23
 
24
+ # Executes the tool and returns model-facing output text.
20
25
  def call(args, _conversation, cancellation: nil)
21
26
  command = argument(args, :command, "")
22
27
  timeout_seconds = argument(args, :timeout_seconds, Workspace::DEFAULT_COMMAND_TIMEOUT_SECONDS)
@@ -7,7 +7,9 @@ require "pathname"
7
7
  require "uri"
8
8
  require_relative "../../config_files"
9
9
 
10
+ # Namespace for the Kward CLI agent runtime.
10
11
  module Kward
12
+ # Package lookup and GitHub repository cache/search implementation.
11
13
  class CodeSearch
12
14
  DEFAULT_MAX_RESULTS = 10
13
15
  MAX_MAX_RESULTS = 50
@@ -21,6 +23,7 @@ module Kward
21
23
  ECOSYSTEMS = %w[rubygems npm pypi crates go].freeze
22
24
  ACTIONS = %w[package_search github_search repo_clone repo_search repo_read list_cache refresh_cache clear_cache].freeze
23
25
 
26
+ # Creates an object for code search and repository cache operations.
24
27
  def initialize(cache_root: nil, http_client: NetHttpClient.new, git_runner: GitRunner.new, max_output_bytes: MAX_OUTPUT_BYTES)
25
28
  @cache_root = File.expand_path(cache_root || ConfigFiles.code_search_cache_dir)
26
29
  @http_client = http_client
@@ -46,6 +49,7 @@ module Kward
46
49
  "Error: code_search failed: #{redact(e.message)}"
47
50
  end
48
51
 
52
+ # HTTP adapter used by code search registry/package lookups.
49
53
  class NetHttpClient
50
54
  def get_json(url, headers: {})
51
55
  JSON.parse(get_text(url, headers: headers.merge("Accept" => "application/json")))
@@ -65,6 +69,7 @@ module Kward
65
69
  end
66
70
  end
67
71
 
72
+ # Git command adapter used by repository cache operations.
68
73
  class GitRunner
69
74
  def run(*args, chdir: nil)
70
75
  command = ["git", *args]
@@ -410,6 +415,7 @@ module Kward
410
415
  text[%r{https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?}]
411
416
  end
412
417
 
418
+ # Returns GitHub API headers, including an optional token when configured.
413
419
  def github_headers
414
420
  token = ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
415
421
  token.to_s.empty? ? {} : { "Authorization" => "Bearer #{token}" }
@@ -422,6 +428,7 @@ module Kward
422
428
  nil
423
429
  end
424
430
 
431
+ # Returns a cache file path relative to the cloned repository root.
425
432
  def relative_path(root, path)
426
433
  Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
427
434
  end
@@ -5,7 +5,9 @@ require "nokogiri"
5
5
  require "uri"
6
6
  require_relative "../../config_files"
7
7
 
8
+ # Namespace for the Kward CLI agent runtime.
8
9
  module Kward
10
+ # Live web-search implementation with provider fallbacks.
9
11
  class WebSearch
10
12
  DEFAULT_MAX_RESULTS = 5
11
13
  MAX_MAX_RESULTS = 20
@@ -27,11 +29,12 @@ module Kward
27
29
  "https://search.inetol.net",
28
30
  "https://searx.tiekoetter.com"
29
31
  ].freeze
30
- PROVIDERS = %w[auto exa perplexity gemini legacy duckduckgo].freeze
32
+ PROVIDERS = %w[auto exa perplexity gemini duckduckgo].freeze
31
33
 
32
34
  Result = Struct.new(:title, :url, :excerpt, :provider, keyword_init: true)
33
35
  SearchResponse = Struct.new(:answer, :results, :provider, :note, keyword_init: true)
34
36
 
37
+ # Creates an object for web search provider operations.
35
38
  def initialize(http_client: NetHttpClient.new, searxng_instances: PUBLIC_SEARXNG_INSTANCES, max_output_bytes: MAX_OUTPUT_BYTES, config: nil)
36
39
  @http_client = http_client
37
40
  @searxng_instances = searxng_instances
@@ -92,8 +95,8 @@ module Kward
92
95
  perplexity_search(query, options)
93
96
  when "gemini"
94
97
  gemini_search(query, options)
95
- when "legacy"
96
- legacy_search(query, options)
98
+ when "duckduckgo"
99
+ duckduckgo_provider_search(query, options)
97
100
  end
98
101
  return [response, errors.empty? ? nil : errors.join("; ")] if successful_response?(response)
99
102
  errors << "#{provider}: no results"
@@ -105,6 +108,7 @@ module Kward
105
108
  [nil, errors.join("; ")]
106
109
  end
107
110
 
111
+ # Returns the configured provider fallback order for a query.
108
112
  def provider_order(provider)
109
113
  case provider
110
114
  when "auto"
@@ -113,10 +117,10 @@ module Kward
113
117
  order << "perplexity" if api_key("perplexity")
114
118
  order << "gemini" if api_key("gemini")
115
119
  end
116
- order << "legacy"
120
+ order << "duckduckgo"
117
121
  order
118
122
  when "duckduckgo"
119
- ["legacy"]
123
+ ["duckduckgo"]
120
124
  else
121
125
  [provider]
122
126
  end
@@ -352,15 +356,15 @@ module Kward
352
356
  SearchResponse.new(answer: answer, results: results, provider: "gemini")
353
357
  end
354
358
 
355
- def legacy_search(query, options)
356
- legacy_query = query_with_domain_filter(query, options[:domain_filter])
357
- results, error = legacy_search_query(legacy_query, options[:max_results], options[:recency_filter])
359
+ def duckduckgo_provider_search(query, options)
360
+ duckduckgo_query = query_with_domain_filter(query, options[:domain_filter])
361
+ results, error = duckduckgo_provider_query(duckduckgo_query, options[:max_results], options[:recency_filter])
358
362
  raise error if results.empty? && error
359
363
 
360
- SearchResponse.new(answer: "", results: results, provider: results.first&.provider || "legacy", note: error)
364
+ SearchResponse.new(answer: "", results: results, provider: results.first&.provider || "duckduckgo", note: error)
361
365
  end
362
366
 
363
- def legacy_search_query(query, max_results, recency_filter)
367
+ def duckduckgo_provider_query(query, max_results, recency_filter)
364
368
  begin
365
369
  duckduckgo_results = duckduckgo_search(query, max_results, recency_filter)
366
370
  return [duckduckgo_results, nil] unless duckduckgo_results.empty?
@@ -576,6 +580,7 @@ module Kward
576
580
  text
577
581
  end
578
582
 
583
+ # Returns browser-like headers used for HTML search fallbacks.
579
584
  def browser_headers(accept)
580
585
  {
581
586
  "Accept" => accept,
@@ -604,19 +609,16 @@ module Kward
604
609
  end
605
610
 
606
611
  def web_config
607
- value = config["web_search"] || config["webSearch"] || config["web_research"] || config["webResearch"] || {}
608
- value.is_a?(Hash) ? value : {}
612
+ ConfigFiles.web_search_config(config)
609
613
  end
610
614
 
611
615
  def config_value(key)
612
616
  snake = key.to_s
613
617
  camel = snake.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
614
618
  prefixed = "web_search_#{snake}"
615
- legacy_prefixed = "web_research_#{snake}"
616
619
  return web_config[snake] if web_config.key?(snake)
617
620
  return web_config[camel] if web_config.key?(camel)
618
621
  return config[prefixed] if config.key?(prefixed)
619
- return config[legacy_prefixed] if config.key?(legacy_prefixed)
620
622
  return config[snake] if config.key?(snake)
621
623
  return config[camel] if config.key?(camel)
622
624
 
@@ -711,6 +713,7 @@ module Kward
711
713
  { "day" => "d", "week" => "w", "month" => "m", "year" => "y" }[filter]
712
714
  end
713
715
 
716
+ # HTTP adapter used by web-search providers and fallbacks.
714
717
  class NetHttpClient
715
718
  Response = Struct.new(:code, :body, keyword_init: true)
716
719
 
@@ -1,6 +1,14 @@
1
1
  require "json"
2
+ require_relative "../message_access"
2
3
 
4
+ # Namespace for the Kward CLI agent runtime.
3
5
  module Kward
6
+ # Reads and normalizes model tool-call hashes.
7
+ #
8
+ # Tool calls arrive from several providers and may be restored from session
9
+ # files. This module keeps provider/string/symbol compatibility in one place
10
+ # and exposes small helpers used by the agent loop, tool registry, transcript
11
+ # formatters, and RPC event normalizers.
4
12
  module ToolCall
5
13
  TOOL_NAME_MAP = {
6
14
  "read_file" => "read",
@@ -8,6 +16,7 @@ module Kward
8
16
  "write_file" => "write",
9
17
  "run_shell_command" => "bash",
10
18
  "list_directory" => "list_directory",
19
+ "code_search" => "code_search",
11
20
  "web_search" => "web_search",
12
21
  "read_skill" => "read_skill",
13
22
  "ask_user_question" => "ask_user_question"
@@ -15,19 +24,27 @@ module Kward
15
24
 
16
25
  module_function
17
26
 
27
+ # @return [String, nil] provider tool-call id
18
28
  def id(tool_call)
19
29
  value(tool_call, :id)
20
30
  end
21
31
 
32
+ # @return [String, nil] requested tool/function name
22
33
  def name(tool_call)
23
34
  value(function(tool_call), :name)
24
35
  end
25
36
 
37
+ # Returns the short name used in compact UI labels.
38
+ #
39
+ # @return [String] display label such as `read`, `edit`, or `bash`
26
40
  def display_name(tool_call)
27
41
  raw_name = name(tool_call)
28
42
  normalized_name(raw_name) || raw_name || "unknown_tool"
29
43
  end
30
44
 
45
+ # Parses the requested tool arguments.
46
+ #
47
+ # @return [Hash] decoded argument object, or an empty hash for invalid JSON
31
48
  def arguments(tool_call)
32
49
  parse_arguments(raw_arguments(tool_call))
33
50
  end
@@ -44,6 +61,10 @@ module Kward
44
61
  TOOL_NAME_MAP[name.to_s]
45
62
  end
46
63
 
64
+ # Converts provider argument payloads into hashes.
65
+ #
66
+ # Providers normally send JSON strings, while tests and compatibility callers
67
+ # may pass hashes directly.
47
68
  def parse_arguments(arguments)
48
69
  return {} if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
49
70
  return arguments if arguments.is_a?(Hash)
@@ -53,6 +74,9 @@ module Kward
53
74
  {}
54
75
  end
55
76
 
77
+ # Recursively converts snake_case hash keys to camelCase symbols.
78
+ #
79
+ # @return [Hash] camelized copy of `args`
56
80
  def camelize_args(args)
57
81
  return {} unless args.is_a?(Hash)
58
82
 
@@ -62,11 +86,7 @@ module Kward
62
86
  end
63
87
 
64
88
  def value(object, key)
65
- return nil unless object.respond_to?(:key?)
66
- return object[key] if object.key?(key)
67
- return object[key.to_s] if object.key?(key.to_s)
68
-
69
- nil
89
+ MessageAccess.value(object, key)
70
90
  end
71
91
 
72
92
  def camelize_value(item)
@@ -1,8 +1,13 @@
1
1
  require_relative "base"
2
+ require_relative "search/web"
2
3
 
4
+ # Namespace for the Kward CLI agent runtime.
3
5
  module Kward
6
+ # Model-callable tool wrappers and their argument schemas.
4
7
  module Tools
8
+ # Live web-search implementation with provider fallbacks.
5
9
  class WebSearch < Base
10
+ # Builds the tool schema and stores the execution dependency.
6
11
  def initialize(web_search:)
7
12
  @web_search = web_search
8
13
  super(
@@ -22,7 +27,7 @@ module Kward
22
27
  },
23
28
  provider: {
24
29
  type: "string",
25
- enum: %w[auto exa perplexity gemini legacy duckduckgo],
30
+ enum: Kward::WebSearch::PROVIDERS,
26
31
  description: "Provider override; default auto."
27
32
  },
28
33
  recency_filter: {
@@ -40,6 +45,7 @@ module Kward
40
45
  )
41
46
  end
42
47
 
48
+ # Executes the tool and returns model-facing output text.
43
49
  def call(args, _conversation, cancellation: nil)
44
50
  @web_search.search(args)
45
51
  end
@@ -1,8 +1,12 @@
1
1
  require_relative "base"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
4
6
  module Tools
7
+ # Tool wrapper for guarded full-file writes.
5
8
  class WriteFile < Base
9
+ # Builds the tool schema and stores the execution dependency.
6
10
  def initialize(workspace:)
7
11
  @workspace = workspace
8
12
  super(
@@ -16,6 +20,7 @@ module Kward
16
20
  )
17
21
  end
18
22
 
23
+ # Executes the tool and returns model-facing output text.
19
24
  def call(args, conversation, cancellation: nil)
20
25
  path = argument(args, :path, "")
21
26
  content = argument(args, :content, "")
@@ -1,7 +1,9 @@
1
1
  require "cgi"
2
2
  require_relative "markdown_transcript"
3
3
 
4
+ # Namespace for the Kward CLI agent runtime.
4
5
  module Kward
6
+ # Serializes conversations for transcript export formats.
5
7
  class TranscriptExport
6
8
  SUPPORTED_FORMATS = ["markdown", "html"].freeze
7
9
 
data/lib/kward/version.rb CHANGED
@@ -1,4 +1,5 @@
1
+ # Namespace for the Kward CLI agent runtime.
1
2
  module Kward
2
3
  # Current gem version.
3
- VERSION = "0.67.0"
4
+ VERSION = "0.68.0"
4
5
  end