kward 0.84.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +17 -2
- data/doc/composer.md +1 -1
- data/doc/configuration.md +27 -4
- data/doc/extensibility.md +2 -1
- data/doc/git.md +3 -1
- data/doc/pan.md +6 -0
- data/doc/permissions.md +4 -4
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +7 -1
- data/doc/session-management.md +5 -4
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/usage.md +4 -1
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/lib/kward/agent.rb +24 -11
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/prompt_interface.rb +25 -5
- data/lib/kward/cli/rendering.rb +3 -0
- data/lib/kward/cli/runtime_helpers.rb +121 -27
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +45 -4
- data/lib/kward/cli/tabs.rb +167 -27
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +26 -24
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -1008
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +2 -2
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/server.rb +10 -7
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/composer_state.rb +1 -1
- data/lib/kward/prompt_interface/editor/controller.rb +1 -1
- data/lib/kward/prompt_interface/editor/runner.rb +1 -1
- data/lib/kward/prompt_interface/editor/state.rb +1 -1
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +9 -2
- data/lib/kward/prompt_interface.rb +60 -11
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +96 -13
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/fulldoc/html/css/kward.css +125 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/layout/html/layout.erb +16 -4
- metadata +67 -48
- data/lib/kward/tab_driver.rb +0 -90
- data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
- /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
- /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
- /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
- /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
- /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
- /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Frontend-neutral external transport contracts.
|
|
2
|
+
require "logger"
|
|
3
|
+
require_relative "../deep_copy"
|
|
4
|
+
|
|
5
|
+
# Namespace for frontend-neutral external transport contracts.
|
|
6
|
+
module Kward
|
|
7
|
+
module Transport
|
|
8
|
+
STREAMING_MODES = %i[native edit aggregate none].freeze
|
|
9
|
+
|
|
10
|
+
Actor = Data.define(:id, :display_name, :metadata)
|
|
11
|
+
Attachment = Data.define(:name, :mime_type, :data, :url, :metadata)
|
|
12
|
+
ConversationKey = Data.define(:transport_id, :external_id)
|
|
13
|
+
InboundMessage = Data.define(
|
|
14
|
+
:conversation,
|
|
15
|
+
:message_id,
|
|
16
|
+
:actor,
|
|
17
|
+
:text,
|
|
18
|
+
:attachments,
|
|
19
|
+
:reply_context,
|
|
20
|
+
:idempotency_key,
|
|
21
|
+
:metadata
|
|
22
|
+
)
|
|
23
|
+
TurnEvent = Data.define(:type, :session_id, :turn_id, :sequence, :payload)
|
|
24
|
+
InteractionRequest = Data.define(
|
|
25
|
+
:id,
|
|
26
|
+
:session_id,
|
|
27
|
+
:turn_id,
|
|
28
|
+
:kind,
|
|
29
|
+
:prompt,
|
|
30
|
+
:choices,
|
|
31
|
+
:expires_at,
|
|
32
|
+
:metadata
|
|
33
|
+
)
|
|
34
|
+
Capabilities = Data.define(:inbound, :outbound, :streaming, :limits)
|
|
35
|
+
ExecutionProfile = Data.define(
|
|
36
|
+
:id,
|
|
37
|
+
:tool_mode,
|
|
38
|
+
:allowed_tools,
|
|
39
|
+
:disabled_tools,
|
|
40
|
+
:plugin_commands,
|
|
41
|
+
:approval_mode,
|
|
42
|
+
:memory,
|
|
43
|
+
:attachments,
|
|
44
|
+
:workspace_mode,
|
|
45
|
+
:prompt_context
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
TOOL_MODES = %i[all none allowlist].freeze
|
|
49
|
+
APPROVAL_MODES = %i[default ask deny].freeze
|
|
50
|
+
MEMORY_MODES = %i[default none].freeze
|
|
51
|
+
WORKSPACE_MODES = %i[session fixed].freeze
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def actor(id:, display_name: nil, metadata: {})
|
|
56
|
+
Actor.new(
|
|
57
|
+
id: required_string(id, "actor id"),
|
|
58
|
+
display_name: optional_string(display_name),
|
|
59
|
+
metadata: frozen_copy(metadata)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def attachment(name: nil, mime_type:, data: nil, url: nil, metadata: {})
|
|
64
|
+
raise ArgumentError, "attachment requires data or url" if data.nil? && url.nil?
|
|
65
|
+
raise ArgumentError, "attachment cannot contain both data and url" unless data.nil? || url.nil?
|
|
66
|
+
|
|
67
|
+
Attachment.new(
|
|
68
|
+
name: optional_string(name),
|
|
69
|
+
mime_type: required_string(mime_type, "attachment MIME type"),
|
|
70
|
+
data: data&.dup&.freeze,
|
|
71
|
+
url: url && required_string(url, "attachment URL"),
|
|
72
|
+
metadata: frozen_copy(metadata)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def conversation_key(transport_id:, external_id:)
|
|
77
|
+
ConversationKey.new(
|
|
78
|
+
transport_id: required_string(transport_id, "transport id"),
|
|
79
|
+
external_id: required_string(external_id, "external conversation id")
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def inbound_message(conversation:, message_id:, actor:, text: "", attachments: [], reply_context: {}, idempotency_key:, metadata: {})
|
|
84
|
+
unless conversation.is_a?(ConversationKey)
|
|
85
|
+
raise ArgumentError, "conversation must be a Transport::ConversationKey"
|
|
86
|
+
end
|
|
87
|
+
unless actor.is_a?(Actor)
|
|
88
|
+
raise ArgumentError, "actor must be a Transport::Actor"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
InboundMessage.new(
|
|
92
|
+
conversation: conversation,
|
|
93
|
+
message_id: required_string(message_id, "message id"),
|
|
94
|
+
actor: actor,
|
|
95
|
+
text: text.to_s.freeze,
|
|
96
|
+
attachments: frozen_copy(Array(attachments)),
|
|
97
|
+
reply_context: frozen_copy(reply_context),
|
|
98
|
+
idempotency_key: required_string(idempotency_key, "idempotency key"),
|
|
99
|
+
metadata: frozen_copy(metadata)
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def turn_event(type:, session_id:, turn_id:, sequence:, payload: {})
|
|
104
|
+
TurnEvent.new(
|
|
105
|
+
type: required_string(type, "turn event type"),
|
|
106
|
+
session_id: required_string(session_id, "session id"),
|
|
107
|
+
turn_id: required_string(turn_id, "turn id"),
|
|
108
|
+
sequence: Integer(sequence),
|
|
109
|
+
payload: frozen_copy(payload)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def interaction_request(id:, session_id:, turn_id:, kind:, prompt:, choices: [], expires_at: nil, metadata: {})
|
|
114
|
+
InteractionRequest.new(
|
|
115
|
+
id: required_string(id, "interaction id"),
|
|
116
|
+
session_id: required_string(session_id, "session id"),
|
|
117
|
+
turn_id: required_string(turn_id, "turn id"),
|
|
118
|
+
kind: required_string(kind, "interaction kind"),
|
|
119
|
+
prompt: required_string(prompt, "interaction prompt"),
|
|
120
|
+
choices: frozen_copy(Array(choices)),
|
|
121
|
+
expires_at: expires_at,
|
|
122
|
+
metadata: frozen_copy(metadata)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def execution_profile(
|
|
127
|
+
id:,
|
|
128
|
+
tool_mode: :all,
|
|
129
|
+
allowed_tools: nil,
|
|
130
|
+
disabled_tools: nil,
|
|
131
|
+
plugin_commands: true,
|
|
132
|
+
approval_mode: :default,
|
|
133
|
+
memory: :default,
|
|
134
|
+
attachments: true,
|
|
135
|
+
workspace_mode: :session,
|
|
136
|
+
prompt_context: nil
|
|
137
|
+
)
|
|
138
|
+
id = required_string(id, "execution profile id")
|
|
139
|
+
tool_mode = tool_mode.to_sym
|
|
140
|
+
approval_mode = approval_mode.to_sym
|
|
141
|
+
memory = memory.to_sym
|
|
142
|
+
workspace_mode = workspace_mode.to_sym
|
|
143
|
+
raise ArgumentError, "unsupported tool mode: #{tool_mode}" unless TOOL_MODES.include?(tool_mode)
|
|
144
|
+
raise ArgumentError, "unsupported approval mode: #{approval_mode}" unless APPROVAL_MODES.include?(approval_mode)
|
|
145
|
+
raise ArgumentError, "unsupported memory mode: #{memory}" unless MEMORY_MODES.include?(memory)
|
|
146
|
+
raise ArgumentError, "unsupported workspace mode: #{workspace_mode}" unless WORKSPACE_MODES.include?(workspace_mode)
|
|
147
|
+
raise ArgumentError, "allowlist tool mode requires allowed_tools" if tool_mode == :allowlist && Array(allowed_tools).empty?
|
|
148
|
+
raise ArgumentError, "none tool mode cannot define allowed_tools" if tool_mode == :none && allowed_tools
|
|
149
|
+
raise ArgumentError, "allowed_tools and disabled_tools cannot both be set" if allowed_tools && disabled_tools
|
|
150
|
+
|
|
151
|
+
ExecutionProfile.new(
|
|
152
|
+
id: id,
|
|
153
|
+
tool_mode: tool_mode,
|
|
154
|
+
allowed_tools: frozen_symbols(allowed_tools),
|
|
155
|
+
disabled_tools: frozen_symbols(disabled_tools),
|
|
156
|
+
plugin_commands: !!plugin_commands,
|
|
157
|
+
approval_mode: approval_mode,
|
|
158
|
+
memory: memory,
|
|
159
|
+
attachments: !!attachments,
|
|
160
|
+
workspace_mode: workspace_mode,
|
|
161
|
+
prompt_context: prompt_context.nil? ? nil : prompt_context.to_s.freeze
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def capabilities(inbound: [], outbound: [], streaming: :none, limits: {})
|
|
166
|
+
streaming = streaming.to_sym
|
|
167
|
+
raise ArgumentError, "unsupported streaming mode: #{streaming}" unless STREAMING_MODES.include?(streaming)
|
|
168
|
+
|
|
169
|
+
Capabilities.new(
|
|
170
|
+
inbound: frozen_symbols(inbound),
|
|
171
|
+
outbound: frozen_symbols(outbound),
|
|
172
|
+
streaming: streaming,
|
|
173
|
+
limits: frozen_copy(limits)
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def required_string(value, name)
|
|
178
|
+
value = value.to_s
|
|
179
|
+
raise ArgumentError, "#{name} is required" if value.empty?
|
|
180
|
+
|
|
181
|
+
value.freeze
|
|
182
|
+
end
|
|
183
|
+
private_class_method :required_string
|
|
184
|
+
|
|
185
|
+
def optional_string(value)
|
|
186
|
+
value.nil? ? nil : value.to_s.freeze
|
|
187
|
+
end
|
|
188
|
+
private_class_method :optional_string
|
|
189
|
+
|
|
190
|
+
def frozen_symbols(values)
|
|
191
|
+
Array(values).map { |value| value.to_sym }.uniq.freeze
|
|
192
|
+
end
|
|
193
|
+
private_class_method :frozen_symbols
|
|
194
|
+
|
|
195
|
+
def frozen_copy(value)
|
|
196
|
+
DeepCopy.freeze(DeepCopy.dup(value))
|
|
197
|
+
end
|
|
198
|
+
private_class_method :frozen_copy
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -13,6 +13,7 @@ module Kward
|
|
|
13
13
|
# without changing transport plugins.
|
|
14
14
|
class Gateway
|
|
15
15
|
POLL_INTERVAL = 0.05
|
|
16
|
+
INTERACTION_ROUTE_LIMIT = 1_000
|
|
16
17
|
TERMINAL_STATUSES = %w[completed canceled failed].freeze
|
|
17
18
|
|
|
18
19
|
def initialize(session_manager:, transport_id:, storage: nil, poll_interval: POLL_INTERVAL)
|
|
@@ -22,6 +23,7 @@ module Kward
|
|
|
22
23
|
@poll_interval = poll_interval
|
|
23
24
|
@subscriptions = []
|
|
24
25
|
@interaction_subscribers = []
|
|
26
|
+
@interaction_routes = {}
|
|
25
27
|
@mutex = Mutex.new
|
|
26
28
|
@session_manager.subscribe_events { |method, payload| handle_runtime_event(method, payload) } if @session_manager.respond_to?(:subscribe_events)
|
|
27
29
|
end
|
|
@@ -73,19 +75,16 @@ module Kward
|
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
def answer_transport_interaction(session_id:, request_id:, answer:)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
78
|
+
route = @mutex.synchronize { @interaction_routes.delete(request_id.to_s) }
|
|
79
|
+
case route
|
|
80
|
+
when :plugin_ui
|
|
81
|
+
@session_manager.answer_plugin_ui(session_id: session_id, request_id: request_id, value: answer)
|
|
82
|
+
when :tool_approval
|
|
83
|
+
@session_manager.answer_tool_approval(session_id: session_id, approval_request_id: request_id, approved: answer == true)
|
|
84
|
+
when :question
|
|
85
|
+
answer_transport_question(session_id, request_id, answer)
|
|
82
86
|
else
|
|
83
|
-
|
|
84
|
-
@session_manager.answer_question(
|
|
85
|
-
session_id: session_id,
|
|
86
|
-
question_request_id: request_id,
|
|
87
|
-
answers: answers
|
|
88
|
-
)
|
|
87
|
+
answer == true || answer == false ? answer_transport_approval(session_id, request_id, answer) : answer_transport_question(session_id, request_id, answer)
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
|
|
@@ -122,6 +121,7 @@ module Kward
|
|
|
122
121
|
current = @subscriptions
|
|
123
122
|
@subscriptions = []
|
|
124
123
|
@interaction_subscribers = []
|
|
124
|
+
@interaction_routes = {}
|
|
125
125
|
current
|
|
126
126
|
end
|
|
127
127
|
subscriptions.each(&:kill)
|
|
@@ -168,31 +168,17 @@ module Kward
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def handle_runtime_event(method, payload)
|
|
171
|
-
request =
|
|
172
|
-
when "ui/question"
|
|
173
|
-
questions = Array(payload[:questions] || payload["questions"])
|
|
174
|
-
Transport.interaction_request(
|
|
175
|
-
id: payload[:questionRequestId] || payload["questionRequestId"],
|
|
176
|
-
session_id: payload[:sessionId] || payload["sessionId"],
|
|
177
|
-
turn_id: payload[:turnId] || payload["turnId"] || "unknown",
|
|
178
|
-
kind: :question,
|
|
179
|
-
prompt: questions.map { |question| question[:question] || question["question"] }.join("\\n"),
|
|
180
|
-
choices: questions
|
|
181
|
-
)
|
|
182
|
-
when "tool/approvalRequested"
|
|
183
|
-
Transport.interaction_request(
|
|
184
|
-
id: payload[:approvalRequestId] || payload["approvalRequestId"],
|
|
185
|
-
session_id: payload[:sessionId] || payload["sessionId"],
|
|
186
|
-
turn_id: payload[:turnId] || payload["turnId"] || "unknown",
|
|
187
|
-
kind: :tool_approval,
|
|
188
|
-
prompt: "Allow #{payload[:toolName] || payload["toolName"]}?",
|
|
189
|
-
choices: [{ id: "approve", label: "Approve" }, { id: "deny", label: "Deny" }],
|
|
190
|
-
metadata: { tool_call_id: payload[:toolCallId] || payload["toolCallId"], args: payload[:args] || payload["args"] }
|
|
191
|
-
)
|
|
192
|
-
end
|
|
171
|
+
request, route = interaction_for_runtime_event(method, payload)
|
|
193
172
|
return unless request
|
|
194
173
|
|
|
195
|
-
subscribers = @mutex.synchronize
|
|
174
|
+
subscribers = @mutex.synchronize do
|
|
175
|
+
current = @interaction_subscribers.dup
|
|
176
|
+
if route && !current.empty?
|
|
177
|
+
@interaction_routes[request.id.to_s] = route
|
|
178
|
+
@interaction_routes.shift while @interaction_routes.length > INTERACTION_ROUTE_LIMIT
|
|
179
|
+
end
|
|
180
|
+
current
|
|
181
|
+
end
|
|
196
182
|
subscribers.each do |subscriber|
|
|
197
183
|
begin
|
|
198
184
|
subscriber.call(request)
|
|
@@ -202,6 +188,64 @@ module Kward
|
|
|
202
188
|
end
|
|
203
189
|
end
|
|
204
190
|
|
|
191
|
+
def interaction_for_runtime_event(method, payload)
|
|
192
|
+
session_id = payload[:sessionId] || payload["sessionId"]
|
|
193
|
+
turn_id = payload[:turnId] || payload["turnId"] || "unknown"
|
|
194
|
+
case method
|
|
195
|
+
when "ui/question"
|
|
196
|
+
questions = Array(payload[:questions] || payload["questions"])
|
|
197
|
+
[Transport.interaction_request(
|
|
198
|
+
id: payload[:questionRequestId] || payload["questionRequestId"],
|
|
199
|
+
session_id: session_id,
|
|
200
|
+
turn_id: turn_id,
|
|
201
|
+
kind: :question,
|
|
202
|
+
prompt: questions.map { |question| question[:question] || question["question"] }.join("\\n"),
|
|
203
|
+
choices: questions
|
|
204
|
+
), :question]
|
|
205
|
+
when "ui/request"
|
|
206
|
+
plugin_ui_interaction(payload, session_id, turn_id)
|
|
207
|
+
when "tool/approvalRequested"
|
|
208
|
+
[Transport.interaction_request(
|
|
209
|
+
id: payload[:approvalRequestId] || payload["approvalRequestId"],
|
|
210
|
+
session_id: session_id,
|
|
211
|
+
turn_id: turn_id,
|
|
212
|
+
kind: :tool_approval,
|
|
213
|
+
prompt: "Allow #{payload[:toolName] || payload["toolName"]}?",
|
|
214
|
+
choices: [{ id: "approve", label: "Approve" }, { id: "deny", label: "Deny" }],
|
|
215
|
+
metadata: { tool_call_id: payload[:toolCallId] || payload["toolCallId"], args: payload[:args] || payload["args"] }
|
|
216
|
+
), :tool_approval]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def plugin_ui_interaction(payload, session_id, turn_id)
|
|
221
|
+
kind = (payload[:kind] || payload["kind"]).to_s
|
|
222
|
+
details = payload[:payload] || payload["payload"] || {}
|
|
223
|
+
prompt = details[:message] || details["message"] || details[:title] || details["title"]
|
|
224
|
+
choices = details[:options] || details["options"] || []
|
|
225
|
+
if kind == "confirm"
|
|
226
|
+
choices = [{ label: "Yes", value: true }, { label: "No", value: false }]
|
|
227
|
+
end
|
|
228
|
+
default = details.key?(:default) ? details[:default] : details["default"]
|
|
229
|
+
[Transport.interaction_request(
|
|
230
|
+
id: payload[:requestId] || payload["requestId"],
|
|
231
|
+
session_id: session_id,
|
|
232
|
+
turn_id: turn_id,
|
|
233
|
+
kind: kind,
|
|
234
|
+
prompt: prompt,
|
|
235
|
+
choices: choices,
|
|
236
|
+
metadata: { title: details[:title] || details["title"], placeholder: details[:placeholder] || details["placeholder"], default: default }.compact
|
|
237
|
+
), :plugin_ui]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def answer_transport_approval(session_id, request_id, answer)
|
|
241
|
+
@session_manager.answer_tool_approval(session_id: session_id, approval_request_id: request_id, approved: answer)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def answer_transport_question(session_id, request_id, answer)
|
|
245
|
+
answers = answer.is_a?(Array) ? answer : [{ question: request_id, answer: answer.to_s }]
|
|
246
|
+
@session_manager.answer_question(session_id: session_id, question_request_id: request_id, answers: answers)
|
|
247
|
+
end
|
|
248
|
+
|
|
205
249
|
def profile_options(options, profile)
|
|
206
250
|
return options unless profile
|
|
207
251
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "thread"
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../plugins/chat_runtime"
|
|
4
4
|
|
|
5
5
|
module Kward
|
|
6
6
|
module Transport
|
|
@@ -45,9 +45,10 @@ module Kward
|
|
|
45
45
|
raise ArgumentError, "plugin chat turns do not support #{streaming_behavior} streaming"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
chat = fetch_chat(chat_id)
|
|
48
49
|
turn = @runtime.start_turn(
|
|
49
50
|
chat_id: chat_id,
|
|
50
|
-
input: @runtime.input_with_attachments(input, normalize_attachments(attachments)),
|
|
51
|
+
input: @runtime.input_with_attachments(input, normalize_attachments(attachments), capabilities: chat.type.capabilities),
|
|
51
52
|
display_input: input.to_s,
|
|
52
53
|
context: { actor: actor }
|
|
53
54
|
)
|
data/lib/kward/transport.rb
CHANGED
|
@@ -1,203 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
require_relative "deep_copy"
|
|
3
|
-
|
|
4
|
-
# Namespace for frontend-neutral external transport contracts.
|
|
5
|
-
module Kward
|
|
6
|
-
module Transport
|
|
7
|
-
STREAMING_MODES = %i[native edit aggregate none].freeze
|
|
8
|
-
|
|
9
|
-
Actor = Data.define(:id, :display_name, :metadata)
|
|
10
|
-
Attachment = Data.define(:name, :mime_type, :data, :url, :metadata)
|
|
11
|
-
ConversationKey = Data.define(:transport_id, :external_id)
|
|
12
|
-
InboundMessage = Data.define(
|
|
13
|
-
:conversation,
|
|
14
|
-
:message_id,
|
|
15
|
-
:actor,
|
|
16
|
-
:text,
|
|
17
|
-
:attachments,
|
|
18
|
-
:reply_context,
|
|
19
|
-
:idempotency_key,
|
|
20
|
-
:metadata
|
|
21
|
-
)
|
|
22
|
-
TurnEvent = Data.define(:type, :session_id, :turn_id, :sequence, :payload)
|
|
23
|
-
InteractionRequest = Data.define(
|
|
24
|
-
:id,
|
|
25
|
-
:session_id,
|
|
26
|
-
:turn_id,
|
|
27
|
-
:kind,
|
|
28
|
-
:prompt,
|
|
29
|
-
:choices,
|
|
30
|
-
:expires_at,
|
|
31
|
-
:metadata
|
|
32
|
-
)
|
|
33
|
-
Capabilities = Data.define(:inbound, :outbound, :streaming, :limits)
|
|
34
|
-
ExecutionProfile = Data.define(
|
|
35
|
-
:id,
|
|
36
|
-
:tool_mode,
|
|
37
|
-
:allowed_tools,
|
|
38
|
-
:disabled_tools,
|
|
39
|
-
:plugin_commands,
|
|
40
|
-
:approval_mode,
|
|
41
|
-
:memory,
|
|
42
|
-
:attachments,
|
|
43
|
-
:workspace_mode,
|
|
44
|
-
:prompt_context
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
TOOL_MODES = %i[all none allowlist].freeze
|
|
48
|
-
APPROVAL_MODES = %i[default ask deny].freeze
|
|
49
|
-
MEMORY_MODES = %i[default none].freeze
|
|
50
|
-
WORKSPACE_MODES = %i[session fixed].freeze
|
|
51
|
-
|
|
52
|
-
module_function
|
|
53
|
-
|
|
54
|
-
def actor(id:, display_name: nil, metadata: {})
|
|
55
|
-
Actor.new(
|
|
56
|
-
id: required_string(id, "actor id"),
|
|
57
|
-
display_name: optional_string(display_name),
|
|
58
|
-
metadata: frozen_copy(metadata)
|
|
59
|
-
)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def attachment(name: nil, mime_type:, data: nil, url: nil, metadata: {})
|
|
63
|
-
raise ArgumentError, "attachment requires data or url" if data.nil? && url.nil?
|
|
64
|
-
raise ArgumentError, "attachment cannot contain both data and url" unless data.nil? || url.nil?
|
|
65
|
-
|
|
66
|
-
Attachment.new(
|
|
67
|
-
name: optional_string(name),
|
|
68
|
-
mime_type: required_string(mime_type, "attachment MIME type"),
|
|
69
|
-
data: data&.dup&.freeze,
|
|
70
|
-
url: url && required_string(url, "attachment URL"),
|
|
71
|
-
metadata: frozen_copy(metadata)
|
|
72
|
-
)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def conversation_key(transport_id:, external_id:)
|
|
76
|
-
ConversationKey.new(
|
|
77
|
-
transport_id: required_string(transport_id, "transport id"),
|
|
78
|
-
external_id: required_string(external_id, "external conversation id")
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def inbound_message(conversation:, message_id:, actor:, text: "", attachments: [], reply_context: {}, idempotency_key:, metadata: {})
|
|
83
|
-
unless conversation.is_a?(ConversationKey)
|
|
84
|
-
raise ArgumentError, "conversation must be a Transport::ConversationKey"
|
|
85
|
-
end
|
|
86
|
-
unless actor.is_a?(Actor)
|
|
87
|
-
raise ArgumentError, "actor must be a Transport::Actor"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
InboundMessage.new(
|
|
91
|
-
conversation: conversation,
|
|
92
|
-
message_id: required_string(message_id, "message id"),
|
|
93
|
-
actor: actor,
|
|
94
|
-
text: text.to_s.freeze,
|
|
95
|
-
attachments: frozen_copy(Array(attachments)),
|
|
96
|
-
reply_context: frozen_copy(reply_context),
|
|
97
|
-
idempotency_key: required_string(idempotency_key, "idempotency key"),
|
|
98
|
-
metadata: frozen_copy(metadata)
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def turn_event(type:, session_id:, turn_id:, sequence:, payload: {})
|
|
103
|
-
TurnEvent.new(
|
|
104
|
-
type: required_string(type, "turn event type"),
|
|
105
|
-
session_id: required_string(session_id, "session id"),
|
|
106
|
-
turn_id: required_string(turn_id, "turn id"),
|
|
107
|
-
sequence: Integer(sequence),
|
|
108
|
-
payload: frozen_copy(payload)
|
|
109
|
-
)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def interaction_request(id:, session_id:, turn_id:, kind:, prompt:, choices: [], expires_at: nil, metadata: {})
|
|
113
|
-
InteractionRequest.new(
|
|
114
|
-
id: required_string(id, "interaction id"),
|
|
115
|
-
session_id: required_string(session_id, "session id"),
|
|
116
|
-
turn_id: required_string(turn_id, "turn id"),
|
|
117
|
-
kind: required_string(kind, "interaction kind"),
|
|
118
|
-
prompt: required_string(prompt, "interaction prompt"),
|
|
119
|
-
choices: frozen_copy(Array(choices)),
|
|
120
|
-
expires_at: expires_at,
|
|
121
|
-
metadata: frozen_copy(metadata)
|
|
122
|
-
)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def execution_profile(
|
|
126
|
-
id:,
|
|
127
|
-
tool_mode: :all,
|
|
128
|
-
allowed_tools: nil,
|
|
129
|
-
disabled_tools: nil,
|
|
130
|
-
plugin_commands: true,
|
|
131
|
-
approval_mode: :default,
|
|
132
|
-
memory: :default,
|
|
133
|
-
attachments: true,
|
|
134
|
-
workspace_mode: :session,
|
|
135
|
-
prompt_context: nil
|
|
136
|
-
)
|
|
137
|
-
id = required_string(id, "execution profile id")
|
|
138
|
-
tool_mode = tool_mode.to_sym
|
|
139
|
-
approval_mode = approval_mode.to_sym
|
|
140
|
-
memory = memory.to_sym
|
|
141
|
-
workspace_mode = workspace_mode.to_sym
|
|
142
|
-
raise ArgumentError, "unsupported tool mode: #{tool_mode}" unless TOOL_MODES.include?(tool_mode)
|
|
143
|
-
raise ArgumentError, "unsupported approval mode: #{approval_mode}" unless APPROVAL_MODES.include?(approval_mode)
|
|
144
|
-
raise ArgumentError, "unsupported memory mode: #{memory}" unless MEMORY_MODES.include?(memory)
|
|
145
|
-
raise ArgumentError, "unsupported workspace mode: #{workspace_mode}" unless WORKSPACE_MODES.include?(workspace_mode)
|
|
146
|
-
raise ArgumentError, "allowlist tool mode requires allowed_tools" if tool_mode == :allowlist && Array(allowed_tools).empty?
|
|
147
|
-
raise ArgumentError, "none tool mode cannot define allowed_tools" if tool_mode == :none && allowed_tools
|
|
148
|
-
raise ArgumentError, "allowed_tools and disabled_tools cannot both be set" if allowed_tools && disabled_tools
|
|
149
|
-
|
|
150
|
-
ExecutionProfile.new(
|
|
151
|
-
id: id,
|
|
152
|
-
tool_mode: tool_mode,
|
|
153
|
-
allowed_tools: frozen_symbols(allowed_tools),
|
|
154
|
-
disabled_tools: frozen_symbols(disabled_tools),
|
|
155
|
-
plugin_commands: !!plugin_commands,
|
|
156
|
-
approval_mode: approval_mode,
|
|
157
|
-
memory: memory,
|
|
158
|
-
attachments: !!attachments,
|
|
159
|
-
workspace_mode: workspace_mode,
|
|
160
|
-
prompt_context: prompt_context.nil? ? nil : prompt_context.to_s.freeze
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def capabilities(inbound: [], outbound: [], streaming: :none, limits: {})
|
|
165
|
-
streaming = streaming.to_sym
|
|
166
|
-
raise ArgumentError, "unsupported streaming mode: #{streaming}" unless STREAMING_MODES.include?(streaming)
|
|
167
|
-
|
|
168
|
-
Capabilities.new(
|
|
169
|
-
inbound: frozen_symbols(inbound),
|
|
170
|
-
outbound: frozen_symbols(outbound),
|
|
171
|
-
streaming: streaming,
|
|
172
|
-
limits: frozen_copy(limits)
|
|
173
|
-
)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def required_string(value, name)
|
|
177
|
-
value = value.to_s
|
|
178
|
-
raise ArgumentError, "#{name} is required" if value.empty?
|
|
179
|
-
|
|
180
|
-
value.freeze
|
|
181
|
-
end
|
|
182
|
-
private_class_method :required_string
|
|
183
|
-
|
|
184
|
-
def optional_string(value)
|
|
185
|
-
value.nil? ? nil : value.to_s.freeze
|
|
186
|
-
end
|
|
187
|
-
private_class_method :optional_string
|
|
188
|
-
|
|
189
|
-
def frozen_symbols(values)
|
|
190
|
-
Array(values).map { |value| value.to_sym }.uniq.freeze
|
|
191
|
-
end
|
|
192
|
-
private_class_method :frozen_symbols
|
|
193
|
-
|
|
194
|
-
def frozen_copy(value)
|
|
195
|
-
DeepCopy.freeze(DeepCopy.dup(value))
|
|
196
|
-
end
|
|
197
|
-
private_class_method :frozen_copy
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
1
|
+
require_relative "transport/contracts"
|
|
201
2
|
require_relative "transport/store"
|
|
202
3
|
require_relative "transport/host"
|
|
203
4
|
require_relative "transport/manager"
|
data/lib/kward/version.rb
CHANGED
|
@@ -133,6 +133,16 @@ module Kward
|
|
|
133
133
|
run_git(path, "diff", "--name-only", "--diff-filter=U").lines(chomp: true)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
def merge_conflict_marker_paths(path, cached: true)
|
|
137
|
+
arguments = ["diff"]
|
|
138
|
+
arguments << "--cached" if cached
|
|
139
|
+
arguments << "--check"
|
|
140
|
+
output, _status = capture_git(path, *arguments)
|
|
141
|
+
output.lines.filter_map do |line|
|
|
142
|
+
line[/\A(.+?):\d+: leftover conflict marker/, 1]
|
|
143
|
+
end.uniq
|
|
144
|
+
end
|
|
145
|
+
|
|
136
146
|
def abort_merge(path)
|
|
137
147
|
raise Error, "No merge is in progress." unless merge_in_progress?(path)
|
|
138
148
|
|
|
@@ -140,6 +150,24 @@ module Kward
|
|
|
140
150
|
true
|
|
141
151
|
end
|
|
142
152
|
|
|
153
|
+
def continue_merge(path)
|
|
154
|
+
raise Error, "No merge is in progress." unless merge_in_progress?(path)
|
|
155
|
+
|
|
156
|
+
run_git(path, "add", "--all")
|
|
157
|
+
conflicts = unmerged_paths(path)
|
|
158
|
+
unless conflicts.empty?
|
|
159
|
+
raise Error, "Merge still has unresolved conflicts: #{conflicts.join(", ")}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
marker_paths = merge_conflict_marker_paths(path)
|
|
163
|
+
unless marker_paths.empty?
|
|
164
|
+
raise Error, "Merge still contains conflict markers: #{marker_paths.join(", ")}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
run_git(path, "commit", "--no-edit")
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
143
171
|
def create(repository_root:, origin_root:, path:, branch:, base: "HEAD")
|
|
144
172
|
repository_root = canonical_existing_path(repository_root)
|
|
145
173
|
origin_root = canonical_existing_path(origin_root)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
-
require_relative "local_command_runner"
|
|
3
|
-
require_relative "sandbox"
|
|
4
|
-
require_relative "
|
|
2
|
+
require_relative "../pty/local_command_runner"
|
|
3
|
+
require_relative "../sandbox"
|
|
4
|
+
require_relative "../sessions/diff"
|
|
5
5
|
|
|
6
6
|
require_relative "path_guard"
|
|
7
7
|
|