ask-agent 0.36.0 → 0.37.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 +27 -0
- data/lib/ask/agent/loop.rb +7 -4
- data/lib/ask/agent/memory_extractor.rb +1 -2
- data/lib/ask/agent/session.rb +76 -1
- data/lib/ask/agent/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: e22262139bd8142b9e45f22e831d59360f9465aad8fea93247352591b742d6c3
|
|
4
|
+
data.tar.gz: 1486719187105e25f794bff150fd9aa59e902096a40c15fdb822b2e7061d9bac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2eaacd8c0a7a32392546ccde9d218d271afa22a3a2d1c154bfc3665691060b9cc4e7ab8d658c25eb2b2e79cccb1ad3bbbf10cd65e36c4324e2fc408f8eb8d973
|
|
7
|
+
data.tar.gz: d9abb9f16f260d5ece0c403346ae0f1cb5a9edadb3e9fad0a2a56fd05c66748eb0070f16efa8441aa55275c5fc1aff724d43f0d4d6809751b932978272563fcf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## [0.37.0] — 2026-08-07
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Steer — concurrency-safe message injection.**
|
|
6
|
+
`Session#steer(message, expected_turn_id:)` lets any thread (web, CLI,
|
|
7
|
+
another agent) inject a message safely:
|
|
8
|
+
- **`:stale`** — `expected_turn_id` doesn't match the current turn id
|
|
9
|
+
(the caller was looking at an older state); the message is rejected.
|
|
10
|
+
- **`:queued`** — a turn is running; the message is held and dispatched
|
|
11
|
+
as the next user message at the next turn boundary (the loop now
|
|
12
|
+
resolves each recursive turn's message from a steer source). No more
|
|
13
|
+
abort-and-retry.
|
|
14
|
+
- **`:steered`** — the session is idle; the message enters the
|
|
15
|
+
conversation and the next run processes it. Queued leftovers drain at
|
|
16
|
+
the next run start.
|
|
17
|
+
- `Session#turn_id` tracks the running turn (bumped on `TurnStart`);
|
|
18
|
+
`Session#queued_steers` reports pending messages.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **No more duplicate tail checkpoints.** The loop persists after every
|
|
23
|
+
turn and `run()` persists again on the way out, so every run previously
|
|
24
|
+
appended a redundant checkpoint (seqs 1,2,3 for two turns). `persist!`
|
|
25
|
+
now skips the checkpoint when the message count and turn count are
|
|
26
|
+
unchanged — `checkpoint_history` is exact.
|
|
27
|
+
|
|
1
28
|
## [0.36.0] — 2026-08-07
|
|
2
29
|
|
|
3
30
|
### Added
|
data/lib/ask/agent/loop.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Ask
|
|
|
17
17
|
@max_consecutive_tool_turns = max_consecutive_tool_turns
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil, persist: nil, tool_call_repair: nil)
|
|
20
|
+
def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil, persist: nil, tool_call_repair: nil, steer_source: nil)
|
|
21
21
|
raise MaxTurnsExceeded if @turn_count >= @max_turns
|
|
22
22
|
|
|
23
23
|
event_emitter.emit(Events::TurnStart.new)
|
|
@@ -157,10 +157,12 @@ module Ask
|
|
|
157
157
|
# Aborted while tools ran? Skip the follow-up LLM call.
|
|
158
158
|
return response.content.to_s if aborted?(event_emitter)
|
|
159
159
|
|
|
160
|
-
# Recursive call — LLM processes tool results
|
|
160
|
+
# Recursive call — LLM processes tool results. When a steer source
|
|
161
|
+
# is provided, queued steer messages become the next user message
|
|
162
|
+
# instead of an empty continuation.
|
|
161
163
|
run_turn(
|
|
162
164
|
chat: chat,
|
|
163
|
-
message: "",
|
|
165
|
+
message: steer_source ? steer_source.call.to_s : "",
|
|
164
166
|
tools: tools,
|
|
165
167
|
tool_executor: tool_executor,
|
|
166
168
|
compactor: compactor,
|
|
@@ -168,7 +170,8 @@ module Ask
|
|
|
168
170
|
event_emitter: event_emitter,
|
|
169
171
|
session_id: session_id,
|
|
170
172
|
persist: persist,
|
|
171
|
-
tool_call_repair: tool_call_repair
|
|
173
|
+
tool_call_repair: tool_call_repair,
|
|
174
|
+
steer_source: steer_source
|
|
172
175
|
)
|
|
173
176
|
end
|
|
174
177
|
|
|
@@ -5,8 +5,7 @@ require "json"
|
|
|
5
5
|
module Ask
|
|
6
6
|
module Agent
|
|
7
7
|
# Extracts durable facts from a finished session's transcript and writes
|
|
8
|
-
# them to the session's {Memory} — the "learning" half of durable memory
|
|
9
|
-
# (codex two-phase pattern, phase 1).
|
|
8
|
+
# them to the session's {Memory} — the "learning" half of durable memory.
|
|
10
9
|
#
|
|
11
10
|
# One LLM call with a structured-output prompt: the model reads the
|
|
12
11
|
# memory-relevant messages and returns a JSON list of durable facts.
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -41,6 +41,13 @@ module Ask
|
|
|
41
41
|
@pending_mutex = Mutex.new
|
|
42
42
|
@followup_pending = false
|
|
43
43
|
@turn_count = 0
|
|
44
|
+
# Concurrency-safe steering: turn id bumped at every
|
|
45
|
+
# TurnStart; steers arriving mid-turn are queued and dispatched at
|
|
46
|
+
# the next turn boundary.
|
|
47
|
+
@turn_id = 0
|
|
48
|
+
@queued_steers = []
|
|
49
|
+
@steer_mutex = Mutex.new
|
|
50
|
+
on(Events::TurnStart) { @turn_id += 1 }
|
|
44
51
|
@created_at = Time.now
|
|
45
52
|
@_no_tools_instructed = false
|
|
46
53
|
|
|
@@ -197,6 +204,10 @@ module Ask
|
|
|
197
204
|
@_no_tools_instructed = true
|
|
198
205
|
end
|
|
199
206
|
|
|
207
|
+
# Leftover queued steers from a previous run become user messages
|
|
208
|
+
# before this run starts.
|
|
209
|
+
drain_leftover_steers
|
|
210
|
+
|
|
200
211
|
begin
|
|
201
212
|
@tool_executor.telemetry = @telemetry
|
|
202
213
|
|
|
@@ -210,6 +221,7 @@ module Ask
|
|
|
210
221
|
event_emitter: self,
|
|
211
222
|
session_id: @id,
|
|
212
223
|
tool_call_repair: @tool_call_repair,
|
|
224
|
+
steer_source: method(:drain_one_steer),
|
|
213
225
|
persist: @state ? method(:persist!) : nil
|
|
214
226
|
)
|
|
215
227
|
|
|
@@ -517,6 +529,20 @@ module Ask
|
|
|
517
529
|
|
|
518
530
|
# --- Plan mode ---
|
|
519
531
|
|
|
532
|
+
# Pop the next queued steer (called by the loop at each turn
|
|
533
|
+
# boundary); returns "" when nothing is queued.
|
|
534
|
+
def drain_one_steer
|
|
535
|
+
@steer_mutex.synchronize { @queued_steers.shift }.to_s
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Move any queued steers left over from a previous run into the
|
|
539
|
+
# conversation (the session was idle, so they are dispatched now).
|
|
540
|
+
def drain_leftover_steers
|
|
541
|
+
while (message = drain_one_steer) != ""
|
|
542
|
+
@chat.add_message(role: :user, content: message)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
520
546
|
# Extract durable facts from this session's transcript into memory
|
|
521
547
|
# (memory_learning: true). Best-effort — extraction never breaks the
|
|
522
548
|
# session; failures are swallowed.
|
|
@@ -582,6 +608,45 @@ module Ask
|
|
|
582
608
|
)
|
|
583
609
|
end
|
|
584
610
|
|
|
611
|
+
# --- Steer (concurrency-safe message injection) ---
|
|
612
|
+
|
|
613
|
+
# @return [Integer] id of the turn currently running (or the last
|
|
614
|
+
# completed turn when idle)
|
|
615
|
+
attr_reader :turn_id
|
|
616
|
+
|
|
617
|
+
# Inject a message into the session safely, from any thread (web, CLI,
|
|
618
|
+
# another agent):
|
|
619
|
+
#
|
|
620
|
+
# - **:stale** — the caller's `expected_turn_id` does not match the
|
|
621
|
+
# current turn id (the caller was looking at an older state).
|
|
622
|
+
# - **:queued** — a turn is running; the message is held and dispatched
|
|
623
|
+
# as the next user message at the next turn boundary.
|
|
624
|
+
# - **:steered** — the session is idle; the message is added to the
|
|
625
|
+
# conversation and processed by the next run.
|
|
626
|
+
#
|
|
627
|
+
# @param message [String]
|
|
628
|
+
# @param expected_turn_id [Integer, nil] the turn id the caller
|
|
629
|
+
# believes is current; nil skips the check
|
|
630
|
+
# @return [Hash] {status: :stale|:queued|:steered, turn_id: Integer}
|
|
631
|
+
def steer(message, expected_turn_id: nil)
|
|
632
|
+
@steer_mutex.synchronize do
|
|
633
|
+
if expected_turn_id && expected_turn_id != @turn_id
|
|
634
|
+
return { status: :stale, turn_id: @turn_id }
|
|
635
|
+
end
|
|
636
|
+
if @running
|
|
637
|
+
@queued_steers << message.to_s
|
|
638
|
+
return { status: :queued, turn_id: @turn_id }
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
@chat.add_message(role: :user, content: message.to_s)
|
|
642
|
+
{ status: :steered, turn_id: @turn_id }
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# @return [Integer] steers queued and not yet dispatched
|
|
646
|
+
def queued_steers
|
|
647
|
+
@steer_mutex.synchronize { @queued_steers.size }
|
|
648
|
+
end
|
|
649
|
+
|
|
585
650
|
# --- Async (pending) tools ---
|
|
586
651
|
|
|
587
652
|
# Registers a pending tool call (called by the loop when a tool
|
|
@@ -895,7 +960,17 @@ module Ask
|
|
|
895
960
|
}
|
|
896
961
|
}
|
|
897
962
|
@state.set(@id, payload)
|
|
898
|
-
|
|
963
|
+
# Checkpoint only when the conversation actually changed since the
|
|
964
|
+
# last one: the loop persists after every turn and run() persists
|
|
965
|
+
# again on the way out, so without this check every run would append
|
|
966
|
+
# a duplicate tail checkpoint.
|
|
967
|
+
if @checkpoints
|
|
968
|
+
head = @checkpoint_store.load(@id)
|
|
969
|
+
head_messages = head ? (head["messages"] || head[:messages] || []) : []
|
|
970
|
+
head_turn = head ? (head.dig("metadata", "turn_count") || head.dig(:metadata, :turn_count)) : nil
|
|
971
|
+
unchanged = head_messages.size == payload[:messages].size && head_turn == @turn_count
|
|
972
|
+
@checkpoint_store.checkpoint(@id, payload) unless unchanged
|
|
973
|
+
end
|
|
899
974
|
end
|
|
900
975
|
|
|
901
976
|
def try_auto_meta_agent
|
data/lib/ask/agent/version.rb
CHANGED