kward 0.75.0 → 0.76.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 +12 -0
- data/Gemfile.lock +2 -2
- data/doc/configuration.md +20 -8
- data/doc/plugins.md +1 -0
- data/doc/rpc.md +3 -0
- data/lib/kward/agent.rb +5 -0
- data/lib/kward/cli/interactive_turn.rb +3 -0
- data/lib/kward/cli/prompt_interface.rb +9 -17
- data/lib/kward/cli/rendering.rb +7 -3
- data/lib/kward/cli_transcript_formatter.rb +21 -11
- data/lib/kward/config_files.rb +3 -0
- data/lib/kward/events.rb +1 -0
- data/lib/kward/model/client.rb +12 -9
- data/lib/kward/model/model_info.rb +14 -5
- data/lib/kward/model/stream_parser.rb +12 -12
- data/lib/kward/pan/index.html.erb +1 -0
- data/lib/kward/pan/server.rb +2 -0
- data/lib/kward/plugin_registry.rb +2 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +1 -1
- data/lib/kward/rpc/server.rb +1 -1
- data/lib/kward/rpc/session_manager.rb +2 -0
- data/lib/kward/rpc/transcript_normalizer.rb +10 -10
- data/lib/kward/update_check.rb +12 -7
- data/lib/kward/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd9108129c3abd1aebeb3d1adb3e9011fbaf4e96c60a4f2ff55505497ff4fd0c
|
|
4
|
+
data.tar.gz: 14177ca73389b1e2df9239706f20b879d22322ec89e72f31a81aa573915b0b91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 787448090be2990a36e9d6d5c1911ce3bc5f6031f7c946a6d5d3edde3b78a43742227df6257d64c04e15def86a579e91366db602187200f1c08d241ae705d3b4
|
|
7
|
+
data.tar.gz: 9a18e14824b3f38d2d1e030adf706629a4e797cd49a8aa1c23786fb53d174e4e81a3d1462c6e7a15de4efa1df517f0aaed7eba89fab1d94f162ba066fff59743
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to Kward will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.76.0] - 2026-07-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added GPT-5.6 Sol, Terra, and Luna to the OpenAI/Codex model choices and made GPT-5.6 Sol the default OpenAI/Codex model.
|
|
12
|
+
- Added Claude Sonnet 5 and Fable 5 to the Anthropic model choices and made Claude Sonnet 5 the default Anthropic model.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fixed Codex reasoning summaries so separate summary messages render as separate blocks with one empty line between transcript blocks.
|
|
17
|
+
- Fixed interactive update notices so stale checks refresh before the startup banner and new configs expose the `updates.check` setting.
|
|
18
|
+
|
|
7
19
|
## [0.75.0] - 2026-07-09
|
|
8
20
|
|
|
9
21
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kward (0.
|
|
4
|
+
kward (0.76.0)
|
|
5
5
|
base64
|
|
6
6
|
nokogiri
|
|
7
7
|
tiktoken_ruby
|
|
@@ -146,7 +146,7 @@ CHECKSUMS
|
|
|
146
146
|
html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd
|
|
147
147
|
io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
|
|
148
148
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
149
|
-
kward (0.
|
|
149
|
+
kward (0.76.0)
|
|
150
150
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
151
151
|
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
|
152
152
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
data/doc/configuration.md
CHANGED
|
@@ -9,7 +9,7 @@ Small examples:
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
11
|
"provider": "openrouter",
|
|
12
|
-
"openrouter_model": "openai/gpt-5.
|
|
12
|
+
"openrouter_model": "openai/gpt-5.6-sol"
|
|
13
13
|
}
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -36,6 +36,16 @@ See [MCP servers](mcp.md) for connecting local Model Context Protocol servers su
|
|
|
36
36
|
|
|
37
37
|
By default, Kward loads user-level skills but skips project-level skills from the workspace. Set `skills.trust_project` to `true`, or use `/settings` → `Tools & Search` → `Trust project skills`, when you trust the repository's `.kward/skills` or `.agents/skills` directories. See [Skills](skills.md) for skill locations and examples.
|
|
38
38
|
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"updates": {
|
|
42
|
+
"check": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Kward checks RubyGems for newer `kward` versions on the interactive startup screen. Results are cached under `~/.kward/cache/update_check.json` so startup does not hit RubyGems every time. Set `updates.check` to `false`, or set `KWARD_DISABLE_UPDATE_CHECK=1`, to disable the check.
|
|
48
|
+
|
|
39
49
|
```json
|
|
40
50
|
{
|
|
41
51
|
"memory": {
|
|
@@ -131,10 +141,10 @@ Model settings:
|
|
|
131
141
|
|
|
132
142
|
```json
|
|
133
143
|
{
|
|
134
|
-
"model": "gpt-5.
|
|
135
|
-
"openai_model": "gpt-5.
|
|
136
|
-
"openrouter_model": "openai/gpt-5.
|
|
137
|
-
"anthropic_model": "claude-sonnet-
|
|
144
|
+
"model": "gpt-5.6-sol",
|
|
145
|
+
"openai_model": "gpt-5.6-sol",
|
|
146
|
+
"openrouter_model": "openai/gpt-5.6-sol",
|
|
147
|
+
"anthropic_model": "claude-sonnet-5",
|
|
138
148
|
"copilot_model": "gpt-5-mini",
|
|
139
149
|
"reasoning_effort": "medium",
|
|
140
150
|
"openai_reasoning_effort": "medium",
|
|
@@ -151,12 +161,14 @@ Set `codex_show_raw_reasoning` to `true` to display raw Codex `reasoning_text` w
|
|
|
151
161
|
|
|
152
162
|
Defaults:
|
|
153
163
|
|
|
154
|
-
- OpenAI/Codex: `gpt-5.
|
|
155
|
-
- OpenRouter: `openai/gpt-5.
|
|
156
|
-
- Anthropic: `claude-sonnet-
|
|
164
|
+
- OpenAI/Codex: `gpt-5.6-sol`
|
|
165
|
+
- OpenRouter: `openai/gpt-5.6-sol`
|
|
166
|
+
- Anthropic: `claude-sonnet-5`
|
|
157
167
|
- Copilot: `gpt-5-mini`
|
|
158
168
|
- Reasoning effort: `medium`
|
|
159
169
|
|
|
170
|
+
The Anthropic model choices include `claude-fable-5`, but Fable availability depends on the logged-in account and organization. Selecting it without access returns an Anthropic provider error.
|
|
171
|
+
|
|
160
172
|
The interactive `/model` picker reads cached OpenRouter models when available. Run `kward openrouter refresh` to fetch text-capable models available to the configured OpenRouter API key and cache them under `~/.kward/cache/openrouter_models.json`. Run `kward openrouter list` to inspect the cached model ids.
|
|
161
173
|
|
|
162
174
|
## Environment overrides
|
data/doc/plugins.md
CHANGED
data/doc/rpc.md
CHANGED
|
@@ -357,6 +357,7 @@ Known event types:
|
|
|
357
357
|
- `steeringApplied`
|
|
358
358
|
- `turnStarted`
|
|
359
359
|
- `reasoningDelta`
|
|
360
|
+
- `reasoningBoundary`
|
|
360
361
|
- `assistantDelta`
|
|
361
362
|
- `assistantMessage`
|
|
362
363
|
- `modelRetry`
|
|
@@ -368,6 +369,8 @@ Known event types:
|
|
|
368
369
|
- `error`
|
|
369
370
|
- `turnFinished`
|
|
370
371
|
|
|
372
|
+
`reasoningBoundary` has an empty payload and marks the end of one reasoning summary message, allowing clients to render the next `reasoningDelta` as a separate block.
|
|
373
|
+
|
|
371
374
|
Lifecycle payloads include `status` for `turnQueued`, `turnStarted`, and `turnFinished`. Exactly one terminal `turnFinished` is emitted per turn with `status` set to `completed`, `failed`, or `canceled`; failed turns include a sanitized `{ "message": "...", "code": "...", "fatal": false }` error payload.
|
|
372
375
|
|
|
373
376
|
`modelRetry` is emitted before Kward retries a transient model request failure. Its payload includes `provider`, `model`, `attempt`, `maxAttempts`, `delaySeconds`, and `error`.
|
data/lib/kward/agent.rb
CHANGED
|
@@ -215,6 +215,10 @@ module Kward
|
|
|
215
215
|
on_reasoning_delta&.call(delta)
|
|
216
216
|
yield Events::ReasoningDelta.new(delta: delta) if block_given?
|
|
217
217
|
end
|
|
218
|
+
reasoning_boundary = lambda do
|
|
219
|
+
cancellation&.raise_if_cancelled!
|
|
220
|
+
yield Events::ReasoningBoundary.new if block_given?
|
|
221
|
+
end
|
|
218
222
|
assistant_delta = lambda do |delta|
|
|
219
223
|
cancellation&.raise_if_cancelled!
|
|
220
224
|
yield Events::AssistantDelta.new(delta: delta) if block_given?
|
|
@@ -244,6 +248,7 @@ module Kward
|
|
|
244
248
|
{
|
|
245
249
|
tools: request[:tools] || request["tools"],
|
|
246
250
|
on_reasoning_delta: reasoning_delta,
|
|
251
|
+
on_reasoning_boundary: reasoning_boundary,
|
|
247
252
|
on_assistant_delta: assistant_delta,
|
|
248
253
|
on_retry: retry_callback,
|
|
249
254
|
cancellation: cancellation,
|
|
@@ -116,6 +116,9 @@ module Kward
|
|
|
116
116
|
when Events::ReasoningDelta
|
|
117
117
|
stream_state[:streamed] = true
|
|
118
118
|
append_markdown_delta(markdown_chunks, "Reasoning", event.delta)
|
|
119
|
+
when Events::ReasoningBoundary
|
|
120
|
+
stream_state[:streamed] = true
|
|
121
|
+
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
119
122
|
when Events::AssistantDelta
|
|
120
123
|
stream_state[:streamed] = true
|
|
121
124
|
append_markdown_delta(markdown_chunks, "Assistant", event.delta)
|
|
@@ -70,13 +70,12 @@ module Kward
|
|
|
70
70
|
def print_visual_banner
|
|
71
71
|
return unless @prompt.respond_to?(:print_visual_banner)
|
|
72
72
|
|
|
73
|
-
@prompt.print_visual_banner(startup_info_screen)
|
|
74
|
-
refresh_startup_update_check if prompt_interface?
|
|
73
|
+
@prompt.print_visual_banner(startup_info_screen(refresh_update_check: prompt_interface?))
|
|
75
74
|
end
|
|
76
75
|
|
|
77
|
-
def startup_info_screen
|
|
76
|
+
def startup_info_screen(refresh_update_check: false)
|
|
78
77
|
[
|
|
79
|
-
startup_status_line,
|
|
78
|
+
startup_status_line(refresh_update_check: refresh_update_check),
|
|
80
79
|
*startup_update_notice_lines,
|
|
81
80
|
"",
|
|
82
81
|
startup_info_line("Workspace", startup_workspace_label),
|
|
@@ -119,8 +118,8 @@ module Kward
|
|
|
119
118
|
filenames.empty? ? "none" : filenames.join(", ")
|
|
120
119
|
end
|
|
121
120
|
|
|
122
|
-
def startup_status_line
|
|
123
|
-
color = startup_update_notice ? :yellow : :green
|
|
121
|
+
def startup_status_line(refresh_update_check: false)
|
|
122
|
+
color = startup_update_notice(refresh: refresh_update_check) ? :yellow : :green
|
|
124
123
|
"#{ANSI.colorize("●", color, enabled: @color_enabled)} Kward v#{Kward::VERSION} is online."
|
|
125
124
|
end
|
|
126
125
|
|
|
@@ -134,18 +133,11 @@ module Kward
|
|
|
134
133
|
]
|
|
135
134
|
end
|
|
136
135
|
|
|
137
|
-
def startup_update_notice
|
|
138
|
-
return @startup_update_notice if defined?(@startup_update_notice)
|
|
136
|
+
def startup_update_notice(refresh: false)
|
|
137
|
+
return @startup_update_notice if defined?(@startup_update_notice) && !refresh
|
|
139
138
|
|
|
140
|
-
@startup_update_check
|
|
141
|
-
@startup_update_notice = @startup_update_check.notice
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def refresh_startup_update_check
|
|
145
|
-
checker = @startup_update_check || UpdateCheck.new(current_version: Kward::VERSION)
|
|
146
|
-
return unless checker.enabled?
|
|
147
|
-
|
|
148
|
-
Thread.new { checker.refresh_if_stale }
|
|
139
|
+
@startup_update_check ||= UpdateCheck.new(current_version: Kward::VERSION)
|
|
140
|
+
@startup_update_notice = @startup_update_check.notice(refresh: refresh)
|
|
149
141
|
end
|
|
150
142
|
|
|
151
143
|
def startup_info_line(label, value)
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -38,8 +38,9 @@ module Kward
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def render_reasoning(message)
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
CLITranscriptFormatter.reasoning_blocks(message).each do |reasoning|
|
|
42
|
+
render_transcript_block("Reasoning", reasoning)
|
|
43
|
+
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def render_assistant_message(message)
|
|
@@ -66,7 +67,7 @@ module Kward
|
|
|
66
67
|
def render_transcript_block(label, content)
|
|
67
68
|
return if content.to_s.empty?
|
|
68
69
|
|
|
69
|
-
rendered = render_markdown_transcript(content)
|
|
70
|
+
rendered = render_markdown_transcript(content).sub(/\n+\z/, "")
|
|
70
71
|
if prompt_interface?
|
|
71
72
|
print_block_delta(label, rendered)
|
|
72
73
|
finish_stream_block
|
|
@@ -84,6 +85,9 @@ module Kward
|
|
|
84
85
|
when Events::ReasoningDelta
|
|
85
86
|
append_markdown_delta(markdown_chunks, "Reasoning", event.delta)
|
|
86
87
|
:streamed
|
|
88
|
+
when Events::ReasoningBoundary
|
|
89
|
+
flush_markdown_deltas(markdown_chunks)
|
|
90
|
+
:streamed
|
|
87
91
|
when Events::AssistantDelta
|
|
88
92
|
append_markdown_delta(markdown_chunks, "Assistant", event.delta)
|
|
89
93
|
:assistant_streamed
|
|
@@ -10,25 +10,31 @@ module Kward
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def reasoning(message)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
reasoning_blocks(message).join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reasoning_blocks(message)
|
|
17
|
+
response_blocks = response_items(message).flat_map do |item|
|
|
18
|
+
next [] unless MessageAccess.value(item, :type) == "reasoning"
|
|
19
|
+
|
|
20
|
+
summary = response_item_text_parts(MessageAccess.value(item, :summary))
|
|
21
|
+
summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
|
|
22
|
+
end
|
|
23
|
+
return response_blocks unless response_blocks.empty?
|
|
15
24
|
|
|
16
25
|
content = MessageAccess.content(message)
|
|
17
26
|
if content.is_a?(Array)
|
|
18
|
-
|
|
27
|
+
blocks = content.filter_map do |part|
|
|
19
28
|
type = MessageAccess.value(part, :type)
|
|
20
29
|
next unless ["thinking", "reasoning"].include?(type)
|
|
21
30
|
|
|
22
31
|
MessageAccess.value(part, :thinking) || MessageAccess.value(part, :reasoning) || MessageAccess.value(part, :text)
|
|
23
|
-
end.
|
|
24
|
-
return
|
|
32
|
+
end.map(&:to_s).reject(&:empty?)
|
|
33
|
+
return blocks unless blocks.empty?
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
response_item_text(MessageAccess.value(item, :summary)).empty? ? response_item_text(MessageAccess.value(item, :content)) : response_item_text(MessageAccess.value(item, :summary))
|
|
31
|
-
end.join("\n")
|
|
36
|
+
direct = MessageAccess.value(message, :reasoning_summary).to_s
|
|
37
|
+
direct.empty? ? [] : [direct]
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def content_text(content)
|
|
@@ -113,11 +119,15 @@ module Kward
|
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
def response_item_text(parts)
|
|
122
|
+
response_item_text_parts(parts).join
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def response_item_text_parts(parts)
|
|
116
126
|
Array(parts).filter_map do |part|
|
|
117
127
|
next unless part.is_a?(Hash)
|
|
118
128
|
|
|
119
129
|
MessageAccess.value(part, :text) || MessageAccess.value(part, :refusal)
|
|
120
|
-
end.
|
|
130
|
+
end.map(&:to_s).reject(&:empty?)
|
|
121
131
|
end
|
|
122
132
|
|
|
123
133
|
def image_part_reference(part)
|
data/lib/kward/config_files.rb
CHANGED
data/lib/kward/events.rb
CHANGED
|
@@ -3,6 +3,7 @@ module Kward
|
|
|
3
3
|
# Frontend-neutral event objects emitted during agent turns.
|
|
4
4
|
module Events
|
|
5
5
|
ReasoningDelta = Struct.new(:delta, keyword_init: true)
|
|
6
|
+
ReasoningBoundary = Data.define
|
|
6
7
|
AssistantDelta = Struct.new(:delta, keyword_init: true)
|
|
7
8
|
AssistantMessage = Struct.new(:message, keyword_init: true)
|
|
8
9
|
Retry = Struct.new(:provider, :model, :attempt, :max_attempts, :delay_seconds, :error, :request_bytes, keyword_init: true)
|
data/lib/kward/model/client.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Kward
|
|
|
94
94
|
@openrouter_models = nil
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
def chat(messages, tools: [], on_reasoning_delta: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil)
|
|
97
|
+
def chat(messages, tools: [], on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil)
|
|
98
98
|
cancellation&.raise_if_cancelled!
|
|
99
99
|
requested_provider = provider
|
|
100
100
|
url, token, resolved_provider, account_id = credentials(provider: requested_provider)
|
|
@@ -126,6 +126,7 @@ module Kward
|
|
|
126
126
|
request_body: request_body,
|
|
127
127
|
current_model: current_model,
|
|
128
128
|
on_reasoning_delta: on_reasoning_delta,
|
|
129
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
129
130
|
on_assistant_delta: on_assistant_delta,
|
|
130
131
|
cancellation: cancellation,
|
|
131
132
|
max_tokens: max_tokens
|
|
@@ -257,7 +258,7 @@ module Kward
|
|
|
257
258
|
}
|
|
258
259
|
end
|
|
259
260
|
|
|
260
|
-
def chat_provider_request(provider:, url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
261
|
+
def chat_provider_request(provider:, url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_reasoning_boundary:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
261
262
|
case provider
|
|
262
263
|
when "Codex"
|
|
263
264
|
chat_codex_provider(
|
|
@@ -269,6 +270,7 @@ module Kward
|
|
|
269
270
|
request_body: request_body,
|
|
270
271
|
current_model: current_model,
|
|
271
272
|
on_reasoning_delta: on_reasoning_delta,
|
|
273
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
272
274
|
on_assistant_delta: on_assistant_delta,
|
|
273
275
|
cancellation: cancellation,
|
|
274
276
|
max_tokens: max_tokens
|
|
@@ -306,7 +308,7 @@ module Kward
|
|
|
306
308
|
end
|
|
307
309
|
end
|
|
308
310
|
|
|
309
|
-
def chat_codex_provider(url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
311
|
+
def chat_codex_provider(url:, token:, account_id:, messages:, tools:, request_body:, current_model:, on_reasoning_delta:, on_reasoning_boundary:, on_assistant_delta:, cancellation:, max_tokens:)
|
|
310
312
|
message = codex_chat(
|
|
311
313
|
url,
|
|
312
314
|
token,
|
|
@@ -315,6 +317,7 @@ module Kward
|
|
|
315
317
|
tools,
|
|
316
318
|
request_body: request_body,
|
|
317
319
|
on_reasoning_delta: on_reasoning_delta,
|
|
320
|
+
on_reasoning_boundary: on_reasoning_boundary,
|
|
318
321
|
on_assistant_delta: on_assistant_delta,
|
|
319
322
|
cancellation: cancellation,
|
|
320
323
|
max_tokens: max_tokens
|
|
@@ -649,7 +652,7 @@ module Kward
|
|
|
649
652
|
role.to_s == "user" ? "user" : "agent"
|
|
650
653
|
end
|
|
651
654
|
|
|
652
|
-
def codex_chat(url, token, account_id, messages, tools, request_body: nil, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil, max_tokens: nil)
|
|
655
|
+
def codex_chat(url, token, account_id, messages, tools, request_body: nil, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil, max_tokens: nil)
|
|
653
656
|
request = Net::HTTP::Post.new(url)
|
|
654
657
|
request["Authorization"] = "Bearer #{token}"
|
|
655
658
|
request["ChatGPT-Account-Id"] = account_id if account_id
|
|
@@ -669,7 +672,7 @@ module Kward
|
|
|
669
672
|
raise RequestError.new(provider: "Codex", code: response.code, body: redact(body, token))
|
|
670
673
|
end
|
|
671
674
|
|
|
672
|
-
message = parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
675
|
+
message = parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation)
|
|
673
676
|
end
|
|
674
677
|
end
|
|
675
678
|
|
|
@@ -681,12 +684,12 @@ module Kward
|
|
|
681
684
|
raise e
|
|
682
685
|
end
|
|
683
686
|
|
|
684
|
-
def parse_codex_sse(body, on_reasoning_delta: nil, on_assistant_delta: nil)
|
|
685
|
-
ModelStreamParser.parse_codex_sse(body, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
687
|
+
def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil)
|
|
688
|
+
ModelStreamParser.parse_codex_sse(body, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
686
689
|
end
|
|
687
690
|
|
|
688
|
-
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil)
|
|
689
|
-
ModelStreamParser.parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, cancellation: cancellation, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
691
|
+
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil)
|
|
692
|
+
ModelStreamParser.parse_codex_sse_stream(response, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, cancellation: cancellation, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
|
|
690
693
|
end
|
|
691
694
|
|
|
692
695
|
def parse_anthropic_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil)
|
|
@@ -4,13 +4,15 @@ require_relative "../config_files"
|
|
|
4
4
|
module Kward
|
|
5
5
|
# Static and configured model metadata helpers.
|
|
6
6
|
module ModelInfo
|
|
7
|
-
DEFAULT_OPENAI_MODEL = "gpt-5.
|
|
7
|
+
DEFAULT_OPENAI_MODEL = "gpt-5.6-sol"
|
|
8
8
|
DEFAULT_COPILOT_MODEL = "gpt-5-mini"
|
|
9
|
-
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-
|
|
9
|
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5"
|
|
10
10
|
DEFAULT_REASONING_EFFORT = "medium"
|
|
11
|
-
OPENAI_MODEL_CHOICES = %w[gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
11
|
+
OPENAI_MODEL_CHOICES = %w[gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
12
12
|
ANTHROPIC_MODEL_CHOICES = %w[
|
|
13
|
+
claude-fable-5
|
|
13
14
|
claude-opus-4-8
|
|
15
|
+
claude-sonnet-5
|
|
14
16
|
claude-sonnet-4-6
|
|
15
17
|
claude-haiku-4-5
|
|
16
18
|
claude-opus-4-7
|
|
@@ -46,6 +48,10 @@ module Kward
|
|
|
46
48
|
["none", "None"],
|
|
47
49
|
*REASONING_EFFORT_CHOICES
|
|
48
50
|
].freeze
|
|
51
|
+
OPENAI_MAX_REASONING_EFFORT_CHOICES = [
|
|
52
|
+
*OPENAI_REASONING_EFFORT_CHOICES,
|
|
53
|
+
["max", "Max"]
|
|
54
|
+
].freeze
|
|
49
55
|
ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES = [
|
|
50
56
|
["low", "Low"],
|
|
51
57
|
["medium", "Medium"],
|
|
@@ -70,6 +76,7 @@ module Kward
|
|
|
70
76
|
].freeze
|
|
71
77
|
|
|
72
78
|
CODEX_CONTEXT_WINDOWS = [
|
|
79
|
+
[/\Agpt-5\.6/, 1_050_000],
|
|
73
80
|
[/\Agpt-5\.5/, 400_000],
|
|
74
81
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
75
82
|
[/\Agpt-5\.4/, 1_050_000],
|
|
@@ -87,6 +94,7 @@ module Kward
|
|
|
87
94
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
88
95
|
].freeze
|
|
89
96
|
OPENAI_CONTEXT_WINDOWS = [
|
|
97
|
+
[/\Agpt-5\.6/, 1_050_000],
|
|
90
98
|
[/\Agpt-5\.5/, 1_050_000],
|
|
91
99
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
92
100
|
[/\Agpt-5\.4/, 1_050_000],
|
|
@@ -104,7 +112,7 @@ module Kward
|
|
|
104
112
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
105
113
|
].freeze
|
|
106
114
|
ANTHROPIC_CONTEXT_WINDOWS = [
|
|
107
|
-
[/\Aclaude-(?:fable|mythos)-5(?:\z|-)/, 1_000_000],
|
|
115
|
+
[/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)/, 1_000_000],
|
|
108
116
|
[/\Aclaude-opus-4-(?:6|7|8)(?:\z|-)/, 1_000_000],
|
|
109
117
|
[/\Aclaude-sonnet-4-6(?:\z|-)/, 1_000_000],
|
|
110
118
|
[/\Aclaude-(?:haiku|opus|sonnet)-4-5(?:\z|-)/, 200_000],
|
|
@@ -382,13 +390,14 @@ module Kward
|
|
|
382
390
|
def openai_reasoning_effort_choices(id)
|
|
383
391
|
text = id.to_s.delete_prefix("openai/")
|
|
384
392
|
return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/)
|
|
393
|
+
return OPENAI_MAX_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.6(?:\z|-)/)
|
|
385
394
|
|
|
386
395
|
OPENAI_REASONING_EFFORT_CHOICES
|
|
387
396
|
end
|
|
388
397
|
|
|
389
398
|
def anthropic_reasoning_effort_choices(id)
|
|
390
399
|
text = normalize_anthropic_model(id)
|
|
391
|
-
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
400
|
+
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
392
401
|
return ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:opus-4-6|sonnet-4-6)(?:\z|-)/)
|
|
393
402
|
return ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-opus-4-5(?:\z|-)/)
|
|
394
403
|
|
|
@@ -45,10 +45,10 @@ module Kward
|
|
|
45
45
|
raise "Copilot returned invalid SSE JSON: #{e.message}"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def parse_codex_sse(body, on_reasoning_delta: nil, on_assistant_delta: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
48
|
+
def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
49
49
|
state = codex_sse_state(show_raw_reasoning: show_raw_reasoning)
|
|
50
50
|
body.split(/\r?\n\r?\n/).each do |block|
|
|
51
|
-
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
51
|
+
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
52
52
|
end
|
|
53
53
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
54
54
|
codex_sse_message(state)
|
|
@@ -61,7 +61,7 @@ module Kward
|
|
|
61
61
|
# Deltas are yielded as soon as complete SSE blocks arrive so interactive
|
|
62
62
|
# frontends can render streamed assistant and reasoning text without waiting
|
|
63
63
|
# for the provider to close the response.
|
|
64
|
-
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_assistant_delta: nil, cancellation: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
64
|
+
def parse_codex_sse_stream(response, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil, show_raw_reasoning: false, usage_normalizer: nil, request_error_class: nil)
|
|
65
65
|
state = codex_sse_state(show_raw_reasoning: show_raw_reasoning)
|
|
66
66
|
buffer = +""
|
|
67
67
|
|
|
@@ -72,11 +72,11 @@ module Kward
|
|
|
72
72
|
delimiter = Regexp.last_match[0]
|
|
73
73
|
block = buffer[0...index]
|
|
74
74
|
buffer = buffer[(index + delimiter.length)..] || +""
|
|
75
|
-
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
75
|
+
process_codex_sse_block(block, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
cancellation&.raise_if_cancelled!
|
|
79
|
-
process_codex_sse_block(buffer, state, on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class) unless buffer.empty?
|
|
79
|
+
process_codex_sse_block(buffer, state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, usage_normalizer: usage_normalizer, request_error_class: request_error_class) unless buffer.empty?
|
|
80
80
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
81
81
|
codex_sse_message(state)
|
|
82
82
|
rescue JSON::ParserError => e
|
|
@@ -239,7 +239,7 @@ module Kward
|
|
|
239
239
|
}
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
def process_codex_sse_block(block, state, on_reasoning_delta: nil, on_assistant_delta: nil, usage_normalizer: nil, request_error_class: nil)
|
|
242
|
+
def process_codex_sse_block(block, state, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, usage_normalizer: nil, request_error_class: nil)
|
|
243
243
|
data = block.lines.filter_map { |line| line.start_with?("data:") ? line.delete_prefix("data:").strip : nil }.join("\n")
|
|
244
244
|
return if data.empty? || data == "[DONE]"
|
|
245
245
|
|
|
@@ -256,7 +256,7 @@ module Kward
|
|
|
256
256
|
when "response.reasoning_summary_text.delta"
|
|
257
257
|
codex_reasoning_delta(state, event["delta"], on_reasoning_delta: on_reasoning_delta)
|
|
258
258
|
when "response.reasoning_summary_part.done"
|
|
259
|
-
codex_reasoning_part_done(state, on_reasoning_delta: on_reasoning_delta)
|
|
259
|
+
codex_reasoning_part_done(state, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary)
|
|
260
260
|
when "response.reasoning_text.delta"
|
|
261
261
|
codex_raw_reasoning_delta(state, event["delta"], on_reasoning_delta: on_reasoning_delta)
|
|
262
262
|
when "response.reasoning_text.done"
|
|
@@ -391,16 +391,16 @@ module Kward
|
|
|
391
391
|
append_codex_raw_reasoning_text(state, text, on_reasoning_delta: on_reasoning_delta) unless text.empty?
|
|
392
392
|
end
|
|
393
393
|
|
|
394
|
-
def codex_reasoning_part_done(state, on_reasoning_delta: nil)
|
|
394
|
+
def codex_reasoning_part_done(state, on_reasoning_delta: nil, on_reasoning_boundary: nil)
|
|
395
395
|
item = active_codex_item(state)
|
|
396
396
|
return unless item&.fetch("type", nil) == "reasoning"
|
|
397
397
|
return if item["summary"].to_a.empty?
|
|
398
398
|
|
|
399
399
|
flush_codex_reasoning_delta(state, on_reasoning_delta: on_reasoning_delta)
|
|
400
|
-
text = "\n\n"
|
|
401
|
-
|
|
402
|
-
state[:reasoning_summary] <<
|
|
403
|
-
|
|
400
|
+
item["summary"].last["text"] = "#{item["summary"].last["text"].to_s.rstrip}\n\n"
|
|
401
|
+
state[:reasoning_summary].rstrip!
|
|
402
|
+
state[:reasoning_summary] << "\n\n"
|
|
403
|
+
on_reasoning_boundary&.call
|
|
404
404
|
end
|
|
405
405
|
|
|
406
406
|
def append_codex_reasoning_text(state, text, on_reasoning_delta: nil)
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
currentReasoning.textContent += JSON.parse(event.data).delta;
|
|
101
101
|
transcript.scrollTop = transcript.scrollHeight;
|
|
102
102
|
});
|
|
103
|
+
events.addEventListener('reasoning_boundary', () => { currentReasoning = null; });
|
|
103
104
|
events.addEventListener('assistant_delta', event => {
|
|
104
105
|
if (!currentAssistant) currentAssistant = entry('assistant', 'Assistant', '');
|
|
105
106
|
currentAssistant.textContent += JSON.parse(event.data).delta;
|
data/lib/kward/pan/server.rb
CHANGED
|
@@ -166,6 +166,8 @@ module Kward
|
|
|
166
166
|
case event
|
|
167
167
|
when Events::ReasoningDelta
|
|
168
168
|
broadcast("reasoning_delta", { delta: event.delta.to_s })
|
|
169
|
+
when Events::ReasoningBoundary
|
|
170
|
+
broadcast("reasoning_boundary", {})
|
|
169
171
|
when Events::AssistantDelta
|
|
170
172
|
broadcast("assistant_delta", { delta: event.delta.to_s })
|
|
171
173
|
when Events::Retry
|
|
@@ -454,6 +454,8 @@ module Kward
|
|
|
454
454
|
case event.class.name
|
|
455
455
|
when "Kward::Events::ReasoningDelta"
|
|
456
456
|
transcript_event("reasoning_delta", delta: event.delta)
|
|
457
|
+
when "Kward::Events::ReasoningBoundary"
|
|
458
|
+
transcript_event("reasoning_boundary")
|
|
457
459
|
when "Kward::Events::AssistantDelta"
|
|
458
460
|
transcript_event("assistant_delta", delta: event.delta)
|
|
459
461
|
when "Kward::Events::AssistantMessage"
|
|
@@ -15,7 +15,7 @@ module Kward
|
|
|
15
15
|
@stream_state.start_block(label)
|
|
16
16
|
end
|
|
17
17
|
write_transcript_text_locked(delta) unless delta.empty?
|
|
18
|
-
write_transcript_text_locked("\n") if finish && @stream_state.block
|
|
18
|
+
write_transcript_text_locked("\n") if finish && @stream_state.block && !@transcript_buffer.end_with?("\n")
|
|
19
19
|
@stream_state.finish_block if finish
|
|
20
20
|
restore_composer_cursor_locked unless @restoring_transcript
|
|
21
21
|
end
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -426,7 +426,7 @@ module Kward
|
|
|
426
426
|
events: {
|
|
427
427
|
notification: TURN_EVENT_NOTIFICATION,
|
|
428
428
|
assistantText: "assistantDelta",
|
|
429
|
-
reasoning: { start: false, delta: true, end: false },
|
|
429
|
+
reasoning: { start: false, delta: true, boundary: true, end: false },
|
|
430
430
|
modelRetry: { supported: true, event: "modelRetry" },
|
|
431
431
|
steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
|
|
432
432
|
tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
|
|
@@ -1256,6 +1256,8 @@ module Kward
|
|
|
1256
1256
|
case event
|
|
1257
1257
|
when Events::ReasoningDelta
|
|
1258
1258
|
emit_turn_event(turn, "reasoningDelta", { delta: event.delta })
|
|
1259
|
+
when Events::ReasoningBoundary
|
|
1260
|
+
emit_turn_event(turn, "reasoningBoundary", {})
|
|
1259
1261
|
when Events::AssistantDelta
|
|
1260
1262
|
emit_turn_event(turn, "assistantDelta", { delta: event.delta })
|
|
1261
1263
|
when Events::AssistantMessage
|
|
@@ -157,18 +157,19 @@ module Kward
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
def response_item_content(message)
|
|
160
|
-
response_items(message).
|
|
161
|
-
next unless item.is_a?(Hash)
|
|
160
|
+
response_items(message).flat_map do |item|
|
|
161
|
+
next [] unless item.is_a?(Hash)
|
|
162
162
|
|
|
163
163
|
case ToolCall.value(item, :type).to_s
|
|
164
164
|
when "reasoning"
|
|
165
|
-
thinking
|
|
166
|
-
{ type: "thinking", thinking: thinking } unless thinking.empty?
|
|
165
|
+
reasoning_item_blocks(item).map { |thinking| { type: "thinking", thinking: thinking } }
|
|
167
166
|
when "message"
|
|
168
|
-
next if ToolCall.value(item, :phase).to_s == "commentary"
|
|
167
|
+
next [] if ToolCall.value(item, :phase).to_s == "commentary"
|
|
169
168
|
|
|
170
169
|
text = response_message_item_text(item)
|
|
171
|
-
{ type: "text", text: text }
|
|
170
|
+
text.empty? ? [] : [{ type: "text", text: text }]
|
|
171
|
+
else
|
|
172
|
+
[]
|
|
172
173
|
end
|
|
173
174
|
end
|
|
174
175
|
end
|
|
@@ -177,10 +178,9 @@ module Kward
|
|
|
177
178
|
MessageAccess.response_items(message)
|
|
178
179
|
end
|
|
179
180
|
|
|
180
|
-
def
|
|
181
|
-
summary = ToolCall.value(item, :summary)
|
|
182
|
-
|
|
183
|
-
response_text_parts(summary).empty? ? response_text_parts(content).join("\n\n") : response_text_parts(summary).join("\n\n")
|
|
181
|
+
def reasoning_item_blocks(item)
|
|
182
|
+
summary = response_text_parts(ToolCall.value(item, :summary))
|
|
183
|
+
summary.empty? ? response_text_parts(ToolCall.value(item, :content)) : summary
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
def response_message_item_text(item)
|
data/lib/kward/update_check.rb
CHANGED
|
@@ -29,15 +29,11 @@ module Kward
|
|
|
29
29
|
true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def notice
|
|
32
|
+
def notice(refresh: false)
|
|
33
33
|
return nil unless enabled?
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return nil if dismissed_version == latest_version
|
|
38
|
-
return nil unless newer_than_current?(latest_version)
|
|
39
|
-
|
|
40
|
-
Notice.new(latest_version: latest_version)
|
|
35
|
+
refresh_if_stale if refresh
|
|
36
|
+
notice_from_cache
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
def refresh_if_stale
|
|
@@ -69,6 +65,15 @@ module Kward
|
|
|
69
65
|
cache["dismissed_version"].to_s
|
|
70
66
|
end
|
|
71
67
|
|
|
68
|
+
def notice_from_cache
|
|
69
|
+
latest_version = cache["latest_version"].to_s
|
|
70
|
+
return nil if latest_version.empty?
|
|
71
|
+
return nil if dismissed_version == latest_version
|
|
72
|
+
return nil unless newer_than_current?(latest_version)
|
|
73
|
+
|
|
74
|
+
Notice.new(latest_version: latest_version)
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
def stale?
|
|
73
78
|
checked_at = Time.iso8601(cache["checked_at"].to_s)
|
|
74
79
|
(@now - checked_at) >= CHECK_INTERVAL_SECONDS
|
data/lib/kward/version.rb
CHANGED