Skip to content

Commit 9528b99

Browse files
committed
refactor: streamline RL trajectory recording
1 parent 4df29f6 commit 9528b99

4 files changed

Lines changed: 98 additions & 82 deletions

File tree

core/src/agent/llm_turn.rs

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use super::{AgentEvent, AgentLoop};
33
use crate::hooks::{
44
ErrorType, GenerateEndEvent, GenerateStartEvent, HookEvent, TokenUsageInfo, ToolCallInfo,
55
};
6-
use crate::llm::{LlmResponse, Message, ToolCall};
6+
use crate::llm::{LlmResponse, Message, ToolCall, ToolDefinition};
77
use anyhow::Context;
88
use std::time::Duration;
99
use tokio::sync::mpsc;
@@ -14,6 +14,16 @@ pub(super) struct LlmTurnOutput {
1414
pub(super) tool_calls: Vec<ToolCall>,
1515
}
1616

17+
struct LlmCallRequest<'a> {
18+
turn: usize,
19+
messages: &'a [Message],
20+
system: Option<&'a str>,
21+
tools: &'a [ToolDefinition],
22+
session_id: Option<&'a str>,
23+
event_tx: &'a Option<mpsc::Sender<AgentEvent>>,
24+
cancel_token: &'a tokio_util::sync::CancellationToken,
25+
}
26+
1727
impl AgentLoop {
1828
pub(super) async fn execute_llm_turn(
1929
&self,
@@ -49,14 +59,15 @@ impl AgentLoop {
4959

5060
let llm_start = std::time::Instant::now();
5161
let response = self
52-
.call_llm_with_circuit_breaker(
62+
.call_llm_with_circuit_breaker(LlmCallRequest {
5363
turn,
54-
&state.messages,
55-
augmented_system.as_deref(),
64+
messages: &state.messages,
65+
system: augmented_system.as_deref(),
66+
tools: &selected_tools,
5667
session_id,
5768
event_tx,
5869
cancel_token,
59-
)
70+
})
6071
.await?;
6172

6273
state.record_usage(&response.usage);
@@ -122,19 +133,14 @@ impl AgentLoop {
122133

123134
async fn call_llm_with_circuit_breaker(
124135
&self,
125-
turn: usize,
126-
messages: &[Message],
127-
system: Option<&str>,
128-
session_id: Option<&str>,
129-
event_tx: &Option<mpsc::Sender<AgentEvent>>,
130-
cancel_token: &tokio_util::sync::CancellationToken,
136+
request: LlmCallRequest<'_>,
131137
) -> anyhow::Result<LlmResponse> {
132138
// Consult the host's BudgetGuard once per turn (not per retry).
133139
// A `Deny` bails out before the LLM is touched; a `SoftLimit`
134140
// surfaces a BudgetThresholdHit event and proceeds.
135141
if let Some(guard) = &self.config.budget_guard {
136-
let sid = session_id.unwrap_or("");
137-
let estimate = estimate_prompt_tokens(messages, system);
142+
let sid = request.session_id.unwrap_or("");
143+
let estimate = estimate_prompt_tokens(request.messages, request.system);
138144
match guard.check_before_llm(sid, estimate).await {
139145
crate::budget::BudgetDecision::Allow => {}
140146
crate::budget::BudgetDecision::SoftLimit {
@@ -143,7 +149,7 @@ impl AgentLoop {
143149
limit,
144150
message,
145151
} => {
146-
if let Some(tx) = event_tx {
152+
if let Some(tx) = request.event_tx {
147153
let _ = tx
148154
.send(AgentEvent::BudgetThresholdHit {
149155
resource,
@@ -156,7 +162,7 @@ impl AgentLoop {
156162
}
157163
}
158164
crate::budget::BudgetDecision::Deny { resource, reason } => {
159-
if let Some(tx) = event_tx {
165+
if let Some(tx) = request.event_tx {
160166
let _ = tx
161167
.send(AgentEvent::BudgetThresholdHit {
162168
resource: resource.clone(),
@@ -178,23 +184,31 @@ impl AgentLoop {
178184
loop {
179185
attempt += 1;
180186
let result = self
181-
.call_llm(messages, system, event_tx, cancel_token)
187+
.call_llm(
188+
request.messages,
189+
request.system,
190+
request.tools,
191+
request.event_tx,
192+
request.cancel_token,
193+
)
182194
.await;
183195
match result {
184196
Ok(response) => {
185197
if let Some(guard) = &self.config.budget_guard {
186198
guard
187-
.record_after_llm(session_id.unwrap_or(""), &response.usage)
199+
.record_after_llm(request.session_id.unwrap_or(""), &response.usage)
188200
.await;
189201
}
190202
return Ok(response);
191203
}
192-
Err(error) if cancel_token.is_cancelled() => {
204+
Err(error) if request.cancel_token.is_cancelled() => {
193205
anyhow::bail!(error);
194206
}
195-
Err(error) if attempt < threshold && (event_tx.is_none() || attempt == 1) => {
207+
Err(error)
208+
if attempt < threshold && (request.event_tx.is_none() || attempt == 1) =>
209+
{
196210
tracing::warn!(
197-
turn = turn,
211+
turn = request.turn,
198212
attempt = attempt,
199213
threshold = threshold,
200214
error = %error,
@@ -211,15 +225,15 @@ impl AgentLoop {
211225
} else {
212226
format!("LLM call failed: {}", error)
213227
};
214-
tracing::error!(turn = turn, attempt = attempt, "{}", msg);
228+
tracing::error!(turn = request.turn, attempt = attempt, "{}", msg);
215229
self.fire_on_error(
216-
session_id.unwrap_or(""),
230+
request.session_id.unwrap_or(""),
217231
ErrorType::LlmFailure,
218232
&msg,
219-
serde_json::json!({"turn": turn, "attempt": attempt}),
233+
serde_json::json!({"turn": request.turn, "attempt": attempt}),
220234
)
221235
.await;
222-
self.emit_error(event_tx, msg.clone()).await;
236+
self.emit_error(request.event_tx, msg.clone()).await;
223237
anyhow::bail!(msg);
224238
}
225239
}
@@ -365,23 +379,22 @@ impl AgentLoop {
365379
/// Streaming events (`TextDelta`, `ToolStart`) are forwarded to `event_tx`
366380
/// as they arrive. Non-streaming mode simply awaits the complete response.
367381
///
368-
/// Tool definitions are selected per turn by the centralized tool selector.
382+
/// Tool definitions are selected once per turn by the centralized tool selector.
369383
///
370384
/// Returns `Err` on any LLM API failure. The circuit breaker in
371385
/// `execute_loop` wraps this call with retry logic for non-streaming mode.
372386
async fn call_llm(
373387
&self,
374388
messages: &[Message],
375389
system: Option<&str>,
390+
tools: &[ToolDefinition],
376391
event_tx: &Option<mpsc::Sender<AgentEvent>>,
377392
cancel_token: &tokio_util::sync::CancellationToken,
378393
) -> anyhow::Result<LlmResponse> {
379-
let tools = crate::tools::select_tools_for_messages(&self.config.tools, messages);
380-
381394
if event_tx.is_some() {
382395
let mut stream_rx = match self
383396
.llm_client
384-
.complete_streaming(messages, system, &tools, cancel_token.clone())
397+
.complete_streaming(messages, system, tools, cancel_token.clone())
385398
.await
386399
{
387400
Ok(rx) => rx,
@@ -396,7 +409,7 @@ impl AgentLoop {
396409
);
397410
return self
398411
.llm_client
399-
.complete(messages, system, &tools)
412+
.complete(messages, system, tools)
400413
.await
401414
.with_context(|| {
402415
format!(
@@ -447,7 +460,7 @@ impl AgentLoop {
447460
final_response.context("Stream ended without final response")
448461
} else {
449462
self.llm_client
450-
.complete(messages, system, &tools)
463+
.complete(messages, system, tools)
451464
.await
452465
.context("LLM call failed")
453466
}

core/src/agent/loop_runtime.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,15 @@ impl AgentLoop {
100100
let augmented_system = turn_context.augmented_system;
101101

102102
self.config.rl_trajectory_recorder.record_execution_start(
103-
session_id.unwrap_or(""),
104-
&self.tool_context.workspace,
105-
effective_prompt,
106-
history,
107-
augmented_system.as_deref(),
108-
self.config.max_tool_rounds,
109-
&format!("{:?}", self.config.planning_mode),
103+
crate::rl_trajectory::ExecutionStartRecord {
104+
session_id: session_id.unwrap_or(""),
105+
workspace: &self.tool_context.workspace,
106+
prompt: effective_prompt,
107+
history,
108+
system_prompt: augmented_system.as_deref(),
109+
max_tool_rounds: self.config.max_tool_rounds,
110+
planning_mode: &format!("{:?}", self.config.planning_mode),
111+
},
110112
);
111113

112114
// Add user message

core/src/agent_api/tests.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2700,15 +2700,15 @@ async fn test_session_options_with_rl_trajectory_records_jsonl() {
27002700
session
27012701
.config
27022702
.rl_trajectory_recorder
2703-
.record_execution_start(
2704-
"sess-rl",
2705-
std::path::Path::new("/tmp/test-ws-rl-trajectory"),
2706-
"abcdef",
2707-
&[],
2708-
None,
2709-
16,
2710-
"disabled",
2711-
);
2703+
.record_execution_start(crate::rl_trajectory::ExecutionStartRecord {
2704+
session_id: "sess-rl",
2705+
workspace: std::path::Path::new("/tmp/test-ws-rl-trajectory"),
2706+
prompt: "abcdef",
2707+
history: &[],
2708+
system_prompt: None,
2709+
max_tool_rounds: 16,
2710+
planning_mode: "disabled",
2711+
});
27122712

27132713
let content = std::fs::read_to_string(&trajectory_path).unwrap();
27142714
let record: serde_json::Value = serde_json::from_str(content.lines().next().unwrap()).unwrap();

core/src/rl_trajectory.rs

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ pub struct RlTrajectoryRecorder {
161161
inner: Option<Arc<RlTrajectoryRecorderInner>>,
162162
}
163163

164+
pub struct ExecutionStartRecord<'a> {
165+
pub session_id: &'a str,
166+
pub workspace: &'a Path,
167+
pub prompt: &'a str,
168+
pub history: &'a [Message],
169+
pub system_prompt: Option<&'a str>,
170+
pub max_tool_rounds: usize,
171+
pub planning_mode: &'a str,
172+
}
173+
164174
struct RlTrajectoryRecorderInner {
165175
config: RlTrajectoryConfig,
166176
context: RlTrajectoryContext,
@@ -228,29 +238,20 @@ impl RlTrajectoryRecorder {
228238
self.inner.is_some()
229239
}
230240

231-
pub fn record_execution_start(
232-
&self,
233-
session_id: &str,
234-
workspace: &Path,
235-
prompt: &str,
236-
history: &[Message],
237-
system_prompt: Option<&str>,
238-
max_tool_rounds: usize,
239-
planning_mode: &str,
240-
) {
241+
pub fn record_execution_start(&self, record: ExecutionStartRecord<'_>) {
241242
let Some(inner) = &self.inner else {
242243
return;
243244
};
244245
let payload = json!({
245-
"workspace": workspace.display().to_string(),
246-
"prompt": inner.capture_text(prompt),
247-
"history_message_count": history.len(),
248-
"history": inner.capture_messages(history),
249-
"system_prompt": system_prompt.map(|s| inner.capture_text(s)),
250-
"max_tool_rounds": max_tool_rounds,
251-
"planning_mode": planning_mode,
246+
"workspace": record.workspace.display().to_string(),
247+
"prompt": inner.capture_text(record.prompt),
248+
"history_message_count": record.history.len(),
249+
"history": inner.capture_messages(record.history),
250+
"system_prompt": record.system_prompt.map(|s| inner.capture_text(s)),
251+
"max_tool_rounds": record.max_tool_rounds,
252+
"planning_mode": record.planning_mode,
252253
});
253-
inner.record("execution_start", session_id, payload);
254+
inner.record("execution_start", record.session_id, payload);
254255
}
255256

256257
pub fn record_llm_request(
@@ -587,15 +588,15 @@ mod tests {
587588
let recorder =
588589
RlTrajectoryRecorder::from_config(Some(RlTrajectoryConfig::new(&path))).unwrap();
589590

590-
recorder.record_execution_start(
591-
"sess-1",
592-
Path::new("/workspace"),
593-
"solve task",
594-
&[],
595-
Some("system"),
596-
64,
597-
"disabled",
598-
);
591+
recorder.record_execution_start(ExecutionStartRecord {
592+
session_id: "sess-1",
593+
workspace: Path::new("/workspace"),
594+
prompt: "solve task",
595+
history: &[],
596+
system_prompt: Some("system"),
597+
max_tool_rounds: 64,
598+
planning_mode: "disabled",
599+
});
599600
recorder.record_tool_result("sess-1", 1, "tool-1", "bash", "ok", 0, 3, &None, None);
600601

601602
let lines = std::fs::read_to_string(path).unwrap();
@@ -615,15 +616,15 @@ mod tests {
615616
))
616617
.unwrap();
617618

618-
recorder.record_execution_start(
619-
"sess-1",
620-
Path::new("/workspace"),
621-
"abcdef",
622-
&[],
623-
None,
624-
64,
625-
"auto",
626-
);
619+
recorder.record_execution_start(ExecutionStartRecord {
620+
session_id: "sess-1",
621+
workspace: Path::new("/workspace"),
622+
prompt: "abcdef",
623+
history: &[],
624+
system_prompt: None,
625+
max_tool_rounds: 64,
626+
planning_mode: "auto",
627+
});
627628

628629
let text = std::fs::read_to_string(path).unwrap();
629630
let record: Value = serde_json::from_str(text.lines().next().unwrap()).unwrap();

0 commit comments

Comments
 (0)