kward 0.71.0 → 0.73.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +93 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +4 -0
  6. data/doc/agent-tools.md +15 -6
  7. data/doc/authentication.md +22 -1
  8. data/doc/code-search.md +42 -2
  9. data/doc/configuration.md +106 -3
  10. data/doc/context-budgeting.md +136 -0
  11. data/doc/context-tools.md +16 -3
  12. data/doc/editor.md +415 -0
  13. data/doc/extensibility.md +16 -7
  14. data/doc/files.md +100 -0
  15. data/doc/getting-started.md +25 -18
  16. data/doc/git.md +123 -0
  17. data/doc/memory.md +24 -4
  18. data/doc/personas.md +34 -5
  19. data/doc/plugins.md +72 -1
  20. data/doc/releasing.md +37 -9
  21. data/doc/rpc.md +75 -5
  22. data/doc/session-management.md +35 -1
  23. data/doc/shell.md +332 -0
  24. data/doc/tabs.md +122 -0
  25. data/doc/troubleshooting.md +77 -1
  26. data/doc/usage.md +79 -7
  27. data/doc/web-search.md +12 -4
  28. data/doc/workspace-tools.md +51 -12
  29. data/examples/plugins/space_invaders.rb +377 -0
  30. data/lib/kward/agent.rb +1 -1
  31. data/lib/kward/ansi.rb +62 -23
  32. data/lib/kward/cli/commands.rb +33 -2
  33. data/lib/kward/cli/git.rb +150 -0
  34. data/lib/kward/cli/interactive_turn.rb +73 -9
  35. data/lib/kward/cli/plugins.rb +54 -4
  36. data/lib/kward/cli/prompt_interface.rb +32 -1
  37. data/lib/kward/cli/rendering.rb +4 -1
  38. data/lib/kward/cli/runtime_helpers.rb +268 -4
  39. data/lib/kward/cli/sessions.rb +2 -2
  40. data/lib/kward/cli/settings.rb +217 -9
  41. data/lib/kward/cli/slash_commands.rb +628 -2
  42. data/lib/kward/cli/tabs.rb +725 -0
  43. data/lib/kward/cli/tool_summaries.rb +6 -0
  44. data/lib/kward/cli.rb +150 -26
  45. data/lib/kward/clipboard.rb +2 -3
  46. data/lib/kward/compactor.rb +7 -19
  47. data/lib/kward/config_files.rb +145 -1
  48. data/lib/kward/context_budget_meter.rb +44 -0
  49. data/lib/kward/conversation.rb +12 -4
  50. data/lib/kward/editor_mode.rb +25 -0
  51. data/lib/kward/ekwsh.rb +559 -0
  52. data/lib/kward/image_attachments.rb +3 -1
  53. data/lib/kward/interactive_pty_runner.rb +151 -0
  54. data/lib/kward/local_command_runner.rb +155 -0
  55. data/lib/kward/local_pty_command_runner.rb +171 -0
  56. data/lib/kward/model/context_usage.rb +2 -2
  57. data/lib/kward/model/payloads.rb +2 -5
  58. data/lib/kward/plugin_registry.rb +61 -0
  59. data/lib/kward/project_files.rb +52 -0
  60. data/lib/kward/prompt_history.rb +84 -0
  61. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  62. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  63. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  64. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  65. data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
  66. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  67. data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
  68. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  69. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  70. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  71. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  73. data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
  74. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
  75. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  76. data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
  77. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  78. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  79. data/lib/kward/prompt_interface/editor/state.rb +1271 -0
  80. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  81. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
  82. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  83. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  84. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  85. data/lib/kward/prompt_interface/git_prompt.rb +288 -0
  86. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  87. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  88. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  89. data/lib/kward/prompt_interface/key_handler.rb +451 -57
  90. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  91. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  92. data/lib/kward/prompt_interface/question_prompt.rb +99 -56
  93. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  94. data/lib/kward/prompt_interface/screen.rb +19 -3
  95. data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
  96. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  97. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  98. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  99. data/lib/kward/prompt_interface.rb +366 -222
  100. data/lib/kward/prompts/commands.rb +9 -0
  101. data/lib/kward/prompts.rb +2 -0
  102. data/lib/kward/rpc/memory_methods.rb +83 -0
  103. data/lib/kward/rpc/server.rb +169 -83
  104. data/lib/kward/rpc/session_manager.rb +45 -121
  105. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  106. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  107. data/lib/kward/rpc/tool_metadata.rb +11 -0
  108. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  109. data/lib/kward/scratchpad_runner.rb +56 -0
  110. data/lib/kward/session_diff.rb +20 -3
  111. data/lib/kward/session_naming.rb +11 -0
  112. data/lib/kward/session_store.rb +44 -0
  113. data/lib/kward/session_tree_nodes.rb +136 -0
  114. data/lib/kward/session_tree_renderer.rb +9 -131
  115. data/lib/kward/tab_store.rb +47 -0
  116. data/lib/kward/terminal_keys.rb +84 -0
  117. data/lib/kward/terminal_sequences.rb +42 -0
  118. data/lib/kward/text_boundary.rb +25 -0
  119. data/lib/kward/tools/context_budget_stats.rb +54 -0
  120. data/lib/kward/tools/context_for_task.rb +204 -0
  121. data/lib/kward/tools/read_file.rb +8 -4
  122. data/lib/kward/tools/registry.rb +62 -16
  123. data/lib/kward/tools/tool_call.rb +10 -0
  124. data/lib/kward/version.rb +1 -1
  125. data/lib/kward/workers/git_guard.rb +93 -0
  126. data/lib/kward/workers/job.rb +99 -0
  127. data/lib/kward/workers/live_view.rb +49 -0
  128. data/lib/kward/workers/manager.rb +288 -0
  129. data/lib/kward/workers/queue_runner.rb +166 -0
  130. data/lib/kward/workers/queue_store.rb +112 -0
  131. data/lib/kward/workers/store.rb +72 -0
  132. data/lib/kward/workers/tool_policy.rb +23 -0
  133. data/lib/kward/workers/worker.rb +82 -0
  134. data/lib/kward/workers/write_lock.rb +38 -0
  135. data/lib/kward/workers.rb +10 -0
  136. data/lib/kward/workspace.rb +125 -87
  137. data/templates/default/fulldoc/html/css/kward.css +140 -36
  138. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  139. data/templates/default/fulldoc/html/setup.rb +1 -0
  140. data/templates/default/kward_navigation.rb +12 -1
  141. data/templates/default/layout/html/layout.erb +23 -34
  142. data/templates/default/layout/html/setup.rb +6 -0
  143. metadata +67 -1
@@ -25,6 +25,15 @@ module Kward
25
25
  { name: "model", description: "Select the default model.", argument_hint: "" },
26
26
  { name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
27
27
  { name: "reload", description: "Reload installed plugins.", argument_hint: "" },
28
+ { name: "workers", description: "Open the worker pipeline.", argument_hint: "[new|do <task>]" },
29
+ { name: "queue", description: "Manage the tab-backed worker queue.", argument_hint: "[add|list|open|run|suspend|resume]" },
30
+ { name: "git", description: "Review uncommitted changes and commit them.", argument_hint: "" },
31
+ { name: "diff", description: "Open the file changes recorded in this session.", argument_hint: "" },
32
+ { name: "files", description: "Browse project files.", argument_hint: "" },
33
+ { name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
34
+ { name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
35
+ { name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
36
+ { name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
28
37
  { name: "status", description: "Show the current status message.", argument_hint: "" },
29
38
  { name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
30
39
  { name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
data/lib/kward/prompts.rb CHANGED
@@ -35,6 +35,8 @@ module Kward
35
35
  You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
36
36
 
37
37
  For web research, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on.
38
+
39
+ Manage code context deliberately. Prefer context_for_task, summarize_file_structure, and read_file mode="outline"/"preview" before broad reads. Escalate to read_file mode="range" for exact lines, and use mode="full" only when focused context is insufficient. Use context_budget_stats when asked about context savings.
38
40
  PROMPT
39
41
  end
40
42
 
@@ -0,0 +1,83 @@
1
+ require_relative "../memory/manager"
2
+
3
+ # Namespace for the Kward CLI agent runtime.
4
+ module Kward
5
+ # JSON-RPC backend namespace used by UI clients.
6
+ module RPC
7
+ # Memory-related RPC method implementations mixed into SessionManager.
8
+ module MemoryMethods
9
+ def memory_manager
10
+ Memory::Manager.for_config_dir(@config_dir)
11
+ end
12
+
13
+ def memory_status
14
+ manager = memory_manager
15
+ { enabled: manager.enabled?, autoSummary: manager.auto_summary_enabled?, paths: manager.paths }
16
+ end
17
+
18
+ def memory_enable
19
+ memory_manager.enable
20
+ { enabled: true }
21
+ end
22
+
23
+ def memory_disable
24
+ memory_manager.disable
25
+ { enabled: false }
26
+ end
27
+
28
+ def memory_auto_summary_enable
29
+ memory_manager.auto_summary_enable
30
+ { autoSummary: true }
31
+ end
32
+
33
+ def memory_auto_summary_disable
34
+ memory_manager.auto_summary_disable
35
+ { autoSummary: false }
36
+ end
37
+
38
+ def memory_list(include_inactive: false, workspace_root: Dir.pwd)
39
+ memory_manager.hierarchy(include_inactive: include_inactive, workspace_root: workspace_root)
40
+ end
41
+
42
+ def memory_add(text:, scope: nil, tags: [])
43
+ { memory: memory_manager.add_soft(text, scope: scope || "global", tags: tags) }
44
+ end
45
+
46
+ def memory_add_core(text:, scope: nil, tags: [])
47
+ { memory: memory_manager.add_core(text, scope: scope || "global", tags: tags) }
48
+ end
49
+
50
+ def memory_forget(id:)
51
+ { forgotten: memory_manager.forget_memory(id) }
52
+ end
53
+
54
+ def memory_promote(id:)
55
+ { memory: memory_manager.promote_memory(id) }
56
+ end
57
+
58
+ def memory_relax(id:, workspace_root: Dir.pwd)
59
+ { memory: memory_manager.relax_core(id, workspace_root: workspace_root) }
60
+ end
61
+
62
+ def memory_inspect
63
+ memory_manager.inspect_memory
64
+ end
65
+
66
+ def memory_why(session_id: nil)
67
+ if session_id
68
+ rpc_session = fetch_session(session_id)
69
+ return rpc_session.conversation.last_memory_retrieval || memory_manager.explain_retrieval
70
+ end
71
+
72
+ memory_manager.explain_retrieval
73
+ end
74
+
75
+ def memory_summarize(session_id:)
76
+ rpc_session = fetch_session(session_id)
77
+ records = memory_manager.summarize_conversation(rpc_session.conversation, client: @client)
78
+ persist_memory_state(rpc_session)
79
+ { memories: records }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -5,6 +5,7 @@ require_relative "../memory/manager"
5
5
  require_relative "../plugin_registry"
6
6
  require_relative "../prompts/commands"
7
7
  require_relative "../tools/registry"
8
+ require_relative "../workers"
8
9
  require_relative "../workspace"
9
10
  require_relative "../telemetry/logger"
10
11
  require_relative "../telemetry/stats"
@@ -43,13 +44,20 @@ module Kward
43
44
  invalid_params: -32_602,
44
45
  internal_error: -32_603
45
46
  }.freeze
47
+ PROTOCOL_METHODS = ["initialize", "shutdown"].freeze
48
+ WORKSPACE_METHODS = ["workspace/validate", "workspace/info"].freeze
49
+ TOOL_METHODS = ["tools/list"].freeze
50
+ PROMPT_METHODS = ["prompts/list", "prompts/expand"].freeze
46
51
  SESSION_METHODS = [
47
52
  "sessions/create", "sessions/resume", "sessions/list", "sessions/rename",
48
53
  "sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork",
49
54
  "sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
50
55
  "sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
51
56
  ].freeze
57
+ TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events"].freeze
52
58
  MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
59
+ RUNTIME_METHODS = ["runtime/state", "runtime/stats"].freeze
60
+ RUNTIME_SETTING_METHODS = ["runtime/updateSetting", "runtime/reload"].freeze
53
61
  AUTH_METHODS = [
54
62
  "auth/status", "auth/providers", "auth/loginWithApiKey", "auth/logoutProvider",
55
63
  "auth/loginWithOAuth", "auth/startOpenAILogin", "auth/submitOpenAICode", "auth/loginStatus"
@@ -60,14 +68,48 @@ module Kward
60
68
  "memory/forget", "memory/promote", "memory/relax", "memory/inspect",
61
69
  "memory/why", "memory/summarize"
62
70
  ].freeze
71
+ WORKER_METHODS = ["workers/list", "workers/show"].freeze
72
+ COMMAND_METHODS = ["commands/list", "commands/run"].freeze
73
+ STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
74
+ CONFIG_METHODS = ["config/read", "config/update"].freeze
75
+ LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
76
+ UI_METHODS = ["ui/answerQuestion"].freeze
77
+ SESSION_EVENT_NOTIFICATION = "session/event"
78
+ SESSION_UPDATED_NOTIFICATION = "session/updated"
79
+ TURN_EVENT_NOTIFICATION = "turn/event"
80
+ UI_QUESTION_NOTIFICATION = "ui/question"
81
+ UI_FOOTER_NOTIFICATION = "ui/footer"
82
+ METHOD_GROUPS = {
83
+ protocol: PROTOCOL_METHODS,
84
+ workspace: WORKSPACE_METHODS,
85
+ tools: TOOL_METHODS,
86
+ prompts: PROMPT_METHODS,
87
+ sessions: SESSION_METHODS,
88
+ turns: TURN_METHODS,
89
+ models: MODEL_METHODS,
90
+ runtime: RUNTIME_METHODS,
91
+ runtime_settings: RUNTIME_SETTING_METHODS,
92
+ auth: AUTH_METHODS,
93
+ memory: MEMORY_METHODS,
94
+ workers: WORKER_METHODS,
95
+ commands: COMMAND_METHODS,
96
+ startup_resources: STARTUP_RESOURCE_METHODS,
97
+ config: CONFIG_METHODS,
98
+ logging: LOGGING_METHODS,
99
+ ui: UI_METHODS
100
+ }.freeze
101
+ RPC_METHODS = METHOD_GROUPS.values.flatten.freeze
63
102
 
64
103
  # Creates the RPC server and its stateful managers.
65
- def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new)
104
+ def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new, experimental_workers: false)
66
105
  @transport = Transport.new(input: input, output: output)
67
106
  @error_output = error_output
107
+ @client = client
68
108
  @config_manager = ConfigManager.new
69
109
  @session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
70
110
  @auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
111
+ @worker_store = Workers::Store.new
112
+ @experimental_workers = experimental_workers
71
113
  @shutdown = false
72
114
  end
73
115
 
@@ -88,7 +130,7 @@ module Kward
88
130
  end
89
131
  end
90
132
  ensure
91
- @session_manager.cleanup_unused_sessions
133
+ @session_manager.shutdown_sessions
92
134
  end
93
135
 
94
136
  # Sends a redacted JSON-RPC notification to the client.
@@ -149,139 +191,145 @@ module Kward
149
191
  def dispatch(method, params)
150
192
  params = stringify_keys(params || {})
151
193
  case method
152
- when "initialize"
194
+ when PROTOCOL_METHODS[0]
153
195
  initialize_result
154
- when "shutdown"
196
+ when PROTOCOL_METHODS[1]
155
197
  @shutdown = true
156
198
  { ok: true }
157
- when "workspace/validate"
199
+ when WORKSPACE_METHODS[0]
158
200
  { root: @session_manager.validate_workspace_root(params["workspaceRoot"] || Dir.pwd) }
159
- when "workspace/info"
201
+ when WORKSPACE_METHODS[1]
160
202
  workspace_info(params["workspaceRoot"] || Dir.pwd)
161
- when "tools/list"
203
+ when TOOL_METHODS[0]
162
204
  { tools: ToolRegistry.new(workspace: configured_workspace).schemas }
163
- when "prompts/list"
205
+ when PROMPT_METHODS[0]
164
206
  prompts_list
165
- when "prompts/expand"
207
+ when PROMPT_METHODS[1]
166
208
  prompts_expand(params)
167
- when "models/list"
209
+ when MODEL_METHODS[0]
168
210
  models_list
169
- when "models/current"
211
+ when MODEL_METHODS[1]
170
212
  models_current
171
- when "models/set"
213
+ when MODEL_METHODS[2]
172
214
  models_set(params)
173
- when "reasoning/set"
215
+ when MODEL_METHODS[3]
174
216
  reasoning_set(params)
175
- when "runtime/state"
217
+ when RUNTIME_METHODS[0]
176
218
  @session_manager.runtime_state(session_id: params.fetch("sessionId"))
177
- when "runtime/stats"
219
+ when RUNTIME_METHODS[1]
178
220
  @session_manager.runtime_stats(session_id: params.fetch("sessionId"))
179
- when "runtime/updateSetting"
221
+ when RUNTIME_SETTING_METHODS[0]
180
222
  runtime_update_setting(params)
181
- when "runtime/reload"
223
+ when RUNTIME_SETTING_METHODS[1]
182
224
  runtime_reload(params)
183
- when "commands/list"
225
+ when COMMAND_METHODS[0]
184
226
  commands_list(params)
185
- when "commands/run"
227
+ when COMMAND_METHODS[1]
186
228
  commands_run(params)
187
- when "resources/startup"
229
+ when STARTUP_RESOURCE_METHODS[0]
188
230
  startup_resources(params)
189
- when "config/read"
231
+ when CONFIG_METHODS[0]
190
232
  { path: @config_manager.config_path, config: @config_manager.read(redacted: params.fetch("redacted", true)) }
191
- when "config/update"
233
+ when CONFIG_METHODS[1]
192
234
  config_update(params)
193
- when "logging/stats"
235
+ when LOGGING_METHODS[0]
194
236
  logging_stats(params)
195
- when "logging/tokenCsv"
237
+ when LOGGING_METHODS[1]
196
238
  logging_token_csv(params)
197
- when "memory/status"
239
+ when MEMORY_METHODS[0]
198
240
  @session_manager.memory_status
199
- when "memory/enable"
241
+ when MEMORY_METHODS[1]
200
242
  @session_manager.memory_enable
201
- when "memory/disable"
243
+ when MEMORY_METHODS[2]
202
244
  @session_manager.memory_disable
203
- when "memory/autoSummary/enable"
245
+ when MEMORY_METHODS[3]
204
246
  @session_manager.memory_auto_summary_enable
205
- when "memory/autoSummary/disable"
247
+ when MEMORY_METHODS[4]
206
248
  @session_manager.memory_auto_summary_disable
207
- when "memory/list"
249
+ when MEMORY_METHODS[5]
208
250
  @session_manager.memory_list(include_inactive: params["includeInactive"] || false, workspace_root: params["workspaceRoot"] || Dir.pwd)
209
- when "memory/add"
251
+ when MEMORY_METHODS[6]
210
252
  @session_manager.memory_add(text: params.fetch("text"), scope: params["scope"], tags: params["tags"] || [])
211
- when "memory/addCore"
253
+ when MEMORY_METHODS[7]
212
254
  @session_manager.memory_add_core(text: params.fetch("text"), scope: params["scope"], tags: params["tags"] || [])
213
- when "memory/forget"
255
+ when MEMORY_METHODS[8]
214
256
  @session_manager.memory_forget(id: params.fetch("id"))
215
- when "memory/promote"
257
+ when MEMORY_METHODS[9]
216
258
  @session_manager.memory_promote(id: params.fetch("id"))
217
- when "memory/relax"
259
+ when MEMORY_METHODS[10]
218
260
  @session_manager.memory_relax(id: params.fetch("id"), workspace_root: params["workspaceRoot"] || Dir.pwd)
219
- when "memory/inspect"
261
+ when MEMORY_METHODS[11]
220
262
  @session_manager.memory_inspect
221
- when "memory/why"
263
+ when MEMORY_METHODS[12]
222
264
  @session_manager.memory_why(session_id: params["sessionId"])
223
- when "memory/summarize"
265
+ when MEMORY_METHODS[13]
224
266
  @session_manager.memory_summarize(session_id: params.fetch("sessionId"))
225
- when "auth/status"
267
+ when WORKER_METHODS[0]
268
+ require_experimental_workers!
269
+ workers_list(params)
270
+ when WORKER_METHODS[1]
271
+ require_experimental_workers!
272
+ workers_show(params)
273
+ when AUTH_METHODS[0]
226
274
  @auth_manager.status
227
- when "auth/providers"
275
+ when AUTH_METHODS[1]
228
276
  @auth_manager.providers
229
- when "auth/loginWithApiKey"
277
+ when AUTH_METHODS[2]
230
278
  auth_login_with_api_key(params)
231
- when "auth/logoutProvider"
279
+ when AUTH_METHODS[3]
232
280
  auth_logout_provider(params)
233
- when "auth/loginWithOAuth"
281
+ when AUTH_METHODS[4]
234
282
  @auth_manager.login_with_oauth(provider_id: params.fetch("providerId"), timeout_seconds: params["timeoutSeconds"] || 120)
235
- when "auth/startOpenAILogin"
283
+ when AUTH_METHODS[5]
236
284
  @auth_manager.start_openai_login(timeout_seconds: params["timeoutSeconds"] || 120)
237
- when "auth/submitOpenAICode"
285
+ when AUTH_METHODS[6]
238
286
  @auth_manager.submit_openai_code(login_id: params.fetch("loginId"), code: params.fetch("code"))
239
- when "auth/loginStatus"
287
+ when AUTH_METHODS[7]
240
288
  @auth_manager.login_status(login_id: params.fetch("loginId"))
241
- when "sessions/create"
289
+ when SESSION_METHODS[0]
242
290
  @session_manager.create_session(workspace_root: params["workspaceRoot"] || Dir.pwd, name: params["name"], resume_last: params["resumeLast"] != false)
243
- when "sessions/resume"
291
+ when SESSION_METHODS[1]
244
292
  @session_manager.resume_session(path: params.fetch("path"), workspace_root: params["workspaceRoot"])
245
- when "sessions/list"
293
+ when SESSION_METHODS[2]
246
294
  { sessions: @session_manager.list_sessions(workspace_root: params["workspaceRoot"] || Dir.pwd, limit: params["limit"], current_session_path: params["currentSessionPath"]) }
247
- when "sessions/rename"
295
+ when SESSION_METHODS[3]
248
296
  @session_manager.rename_session(session_id: params.fetch("sessionId"), name: params["name"])
249
- when "sessions/clone"
297
+ when SESSION_METHODS[4]
250
298
  @session_manager.clone_session(session_id: params.fetch("sessionId"))
251
- when "sessions/compact"
299
+ when SESSION_METHODS[5]
252
300
  @session_manager.compact_session(session_id: params.fetch("sessionId"), custom_instructions: params["customInstructions"] || "")
253
- when "sessions/forkMessages"
301
+ when SESSION_METHODS[6]
254
302
  @session_manager.fork_messages(session_id: params.fetch("sessionId"))
255
- when "sessions/fork"
303
+ when SESSION_METHODS[7]
256
304
  @session_manager.fork_session(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"))
257
- when "sessions/tree"
305
+ when SESSION_METHODS[8]
258
306
  @session_manager.session_tree(session_id: params.fetch("sessionId"))
259
- when "sessions/tree/setLabel"
307
+ when SESSION_METHODS[9]
260
308
  @session_manager.set_tree_label(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"), label: params["label"])
261
- when "sessions/tree/navigate"
309
+ when SESSION_METHODS[10]
262
310
  @session_manager.navigate_tree(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"), summarize: params["summarize"], custom_instructions: params["customInstructions"])
263
- when "sessions/export"
311
+ when SESSION_METHODS[11]
264
312
  @session_manager.export_session(session_id: params.fetch("sessionId"), path: params["path"], format: params["format"])
265
- when "sessions/delete"
313
+ when SESSION_METHODS[12]
266
314
  @session_manager.delete_session(session_id: params.fetch("sessionId"))
267
- when "sessions/close"
315
+ when SESSION_METHODS[13]
268
316
  @session_manager.close_session(session_id: params.fetch("sessionId"))
269
- when "sessions/transcript"
317
+ when SESSION_METHODS[14]
270
318
  @session_manager.transcript(session_id: params.fetch("sessionId"))
271
- when "turns/start"
319
+ when TURN_METHODS[0]
272
320
  @session_manager.start_turn(
273
321
  session_id: params.fetch("sessionId"),
274
322
  input: params.fetch("input"),
275
323
  streaming_behavior: params["streamingBehavior"],
276
324
  attachments: params["attachments"] || []
277
325
  )
278
- when "turns/cancel"
326
+ when TURN_METHODS[1]
279
327
  @session_manager.cancel_turn(turn_id: params.fetch("turnId"))
280
- when "turns/status"
328
+ when TURN_METHODS[2]
281
329
  @session_manager.turn_status(turn_id: params.fetch("turnId"))
282
- when "turns/events"
330
+ when TURN_METHODS[3]
283
331
  @session_manager.turn_events(turn_id: params.fetch("turnId"), after_sequence: params["afterSequence"] || 0)
284
- when "ui/answerQuestion"
332
+ when UI_METHODS[0]
285
333
  @session_manager.answer_question(session_id: params.fetch("sessionId"), question_request_id: params.fetch("questionRequestId"), answers: params.fetch("answers"))
286
334
  else
287
335
  raise NoMethodError, method
@@ -313,13 +361,13 @@ module Kward
313
361
  mode: "explicit",
314
362
  persistence: "jsonl",
315
363
  methods: SESSION_METHODS,
316
- startupResume: { supported: true, method: "sessions/create", parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
364
+ startupResume: { supported: true, method: SESSION_METHODS[0], parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
317
365
  list: { supported: true, source: "rpc", ancestry: true, treeFields: true },
318
- fork: { supported: true, methods: ["sessions/forkMessages", "sessions/fork"], entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
319
- compact: { supported: true, method: "sessions/compact", notification: "session/event", events: ["compactionStart", "compactionEnd"] },
366
+ fork: { supported: true, methods: SESSION_METHODS.values_at(6, 7), entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
367
+ compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
320
368
  import: { supported: false },
321
- tree: { supported: true, method: "sessions/tree", labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "tauren-tree-items-v1" },
322
- updates: { supported: false, notification: "session/updated" }
369
+ tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "tauren-tree-items-v1" },
370
+ updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION }
323
371
  },
324
372
  turns: {
325
373
  mode: "async",
@@ -338,19 +386,19 @@ module Kward
338
386
  eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT }
339
387
  },
340
388
  events: {
341
- notification: "turn/event",
389
+ notification: TURN_EVENT_NOTIFICATION,
342
390
  assistantText: "assistantDelta",
343
391
  reasoning: { start: false, delta: true, end: false },
344
392
  modelRetry: { supported: true, event: "modelRetry" },
345
393
  steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
346
- tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: false, workspaceGuardrails: workspace_guardrails_enabled? },
394
+ tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
347
395
  errors: true,
348
396
  sessionUpdates: false
349
397
  },
350
398
  attachments: {
351
399
  input: {
352
400
  supported: true,
353
- method: "turns/start",
401
+ method: TURN_METHODS[0],
354
402
  encoding: "base64",
355
403
  mimeTypes: SessionManager::RPC_IMAGE_MIME_TYPES,
356
404
  maxBytes: SessionManager::RPC_ATTACHMENT_MAX_BYTES
@@ -365,13 +413,13 @@ module Kward
365
413
  },
366
414
  runtime: {
367
415
  supported: true,
368
- methods: ["runtime/state", "runtime/stats"],
416
+ methods: RUNTIME_METHODS,
369
417
  state: { supported: true },
370
418
  stats: { messageCounts: true, tokens: false, cost: false, contextUsage: true, contextUsageEstimated: true }
371
419
  },
372
420
  runtimeSettings: {
373
421
  supported: true,
374
- methods: ["runtime/updateSetting", "runtime/reload"],
422
+ methods: RUNTIME_SETTING_METHODS,
375
423
  settings: ["defaultModel", "defaultThinkingLevel"]
376
424
  },
377
425
  auth: {
@@ -384,17 +432,20 @@ module Kward
384
432
  logout: true
385
433
  },
386
434
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
387
- commands: { supported: true, methods: ["commands/list", "commands/run"], method: "commands/list", runMethod: "commands/run", sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
388
- startupResources: { supported: true, method: "resources/startup" },
435
+ workers: workers_capability,
436
+ commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
437
+ startupResources: { supported: true, method: STARTUP_RESOURCE_METHODS.first },
389
438
  starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
439
+ shell: { supported: false, reason: "interactiveTuiOnly" },
440
+ scratchpad: { supported: false, reason: "interactiveTuiOnly" },
390
441
  extensionUi: {
391
- question: { supported: true, notification: "ui/question", method: "ui/answerQuestion", maxQuestions: 4, multiSelect: false, preview: false },
442
+ question: { supported: true, notification: UI_QUESTION_NOTIFICATION, method: UI_METHODS.first, maxQuestions: 4, multiSelect: false, preview: false },
392
443
  select: false,
393
444
  confirm: false,
394
445
  input: false,
395
446
  editor: false,
396
447
  widgets: false,
397
- footer: { supported: true, notification: "ui/footer" },
448
+ footer: { supported: true, notification: UI_FOOTER_NOTIFICATION },
398
449
  custom: false,
399
450
  terminalInput: false
400
451
  },
@@ -412,9 +463,9 @@ module Kward
412
463
  logging: {
413
464
  supported: true,
414
465
  defaultEnabled: false,
415
- methods: ["logging/stats", "logging/tokenCsv"],
416
- stats: { supported: true, method: "logging/stats", defaultRange: TelemetryStats::DEFAULT_RANGE, units: %w[minutes hours days weeks months years] },
417
- usageCsv: { supported: true, method: "logging/tokenCsv", defaultRange: TelemetryStats::DEFAULT_RANGE, buckets: %w[second minute hour day week month year] },
466
+ methods: LOGGING_METHODS,
467
+ stats: { supported: true, method: LOGGING_METHODS[0], defaultRange: TelemetryStats::DEFAULT_RANGE, units: %w[minutes hours days weeks months years] },
468
+ usageCsv: { supported: true, method: LOGGING_METHODS[1], defaultRange: TelemetryStats::DEFAULT_RANGE, buckets: %w[second minute hour day week month year] },
418
469
  config: "logging",
419
470
  envPrefix: "KWARD_LOGGING",
420
471
  directory: File.join(ConfigFiles.config_dir, "logs"),
@@ -426,6 +477,18 @@ module Kward
426
477
  }
427
478
  end
428
479
 
480
+ def workers_capability
481
+ return { supported: false, reason: "experimentalWorkersFlagRequired", flag: "--experimental-workers" } unless @experimental_workers
482
+
483
+ { supported: true, methods: WORKER_METHODS, roles: ["implementation", "request"], statuses: Workers::Worker::STATUSES, transcriptStorage: "sessions", metadataStorage: "json" }
484
+ end
485
+
486
+ def require_experimental_workers!
487
+ return if @experimental_workers
488
+
489
+ raise NoMethodError, "workers require --experimental-workers"
490
+ end
491
+
429
492
  def workspace_info(root)
430
493
  root = @session_manager.validate_workspace_root(root)
431
494
  { root: root, basename: File.basename(root), writable: File.writable?(root) }
@@ -571,6 +634,20 @@ module Kward
571
634
  { sections: sections }
572
635
  end
573
636
 
637
+ def workers_list(params)
638
+ include_archived = params["includeArchived"] == true
639
+ workers = @worker_store.list(include_archived: include_archived)
640
+ { workers: workers }
641
+ end
642
+
643
+ def workers_show(params)
644
+ id = params.fetch("id").to_s.delete_prefix("#")
645
+ worker = @worker_store.find(id)
646
+ return { worker: worker } if worker
647
+
648
+ raise ArgumentError, "Unknown worker: #{id}"
649
+ end
650
+
574
651
  def auth_login_with_api_key(params)
575
652
  result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"))
576
653
  @session_manager.refresh_client_config
@@ -602,6 +679,15 @@ module Kward
602
679
  end
603
680
 
604
681
  def provider_model_from(value)
682
+ if value.is_a?(Hash)
683
+ provider = value["provider"] || value[:provider] || @session_manager.current_model[:provider]
684
+ model = value["model"] || value[:model] || value["id"] || value[:id]
685
+ model = model.to_s.strip
686
+ raise ArgumentError, "Model must be a non-empty string" if model.empty?
687
+
688
+ return [provider, model]
689
+ end
690
+
605
691
  text = value.to_s.strip
606
692
  raise ArgumentError, "Model must be a non-empty string" if text.empty?
607
693