submitをvec segmentを受け付ける形に変更
This commit is contained in:
@@ -57,7 +57,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Session: {}", pod.session_id());
|
||||
|
||||
// 4. Run a prompt
|
||||
let result = pod.run("What is the capital of France?").await?;
|
||||
let result = pod.run_text("What is the capital of France?").await?;
|
||||
match result {
|
||||
PodRunResult::Finished => println!("(finished)"),
|
||||
PodRunResult::Paused => println!("(paused)"),
|
||||
|
||||
@@ -93,9 +93,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Send a run method
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "What is the capital of France?".into(),
|
||||
})
|
||||
.send(Method::run_text("What is the capital of France?"))
|
||||
.await?;
|
||||
|
||||
// Wait for completion
|
||||
|
||||
@@ -284,7 +284,7 @@ impl PodController {
|
||||
// render the turn header + user line from a
|
||||
// single source of truth.
|
||||
let _ = event_tx.send(Event::UserMessage {
|
||||
text: input.clone(),
|
||||
segments: input.clone(),
|
||||
});
|
||||
let was_paused = status_before == PodStatus::Paused;
|
||||
shared_state.set_status(PodStatus::Running);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
use llm_worker::Item;
|
||||
use llm_worker::llm_client::client::LlmClient;
|
||||
use protocol::Segment;
|
||||
use session_store::Store;
|
||||
|
||||
use crate::pod::{Pod, PodError, PodRunResult};
|
||||
@@ -25,7 +26,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
/// rationale around synthetic tool results.
|
||||
pub async fn interrupt_and_run(
|
||||
&mut self,
|
||||
input: impl Into<String>,
|
||||
input: Vec<Segment>,
|
||||
) -> Result<PodRunResult, PodError> {
|
||||
let tool_result_summary = self
|
||||
.prompts()
|
||||
|
||||
+83
-3
@@ -28,7 +28,7 @@ use crate::runtime::dir;
|
||||
use crate::runtime::scope_lock::{self, ScopeAllocationGuard, ScopeLockError};
|
||||
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
||||
use crate::compact::usage_tracker::UsageTracker;
|
||||
use protocol::{Event, AlertLevel, AlertSource};
|
||||
use protocol::{AlertLevel, AlertSource, Event, Segment};
|
||||
use tokio::sync::broadcast;
|
||||
use async_trait::async_trait;
|
||||
use llm_worker::interceptor::PreRequestAction;
|
||||
@@ -553,27 +553,107 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convenience: run with a single `Segment::Text`.
|
||||
///
|
||||
/// Equivalent to `run(vec![Segment::text(s)])`. The dumb-client
|
||||
/// counterpart of [`protocol::Method::run_text`]; primarily for
|
||||
/// tests and tools that have only a string in hand.
|
||||
pub async fn run_text(
|
||||
&mut self,
|
||||
s: impl Into<String>,
|
||||
) -> Result<PodRunResult, PodError> {
|
||||
self.run(vec![Segment::text(s)]).await
|
||||
}
|
||||
|
||||
/// Send user input and run until the LLM turn completes.
|
||||
///
|
||||
/// `input` is a typed segment list (see [`protocol::Segment`]). The
|
||||
/// Pod flattens it into a single user-message string for the
|
||||
/// underlying Worker, expanding paste content inline and surfacing
|
||||
/// alerts for any segment kind the current Pod has no resolver for
|
||||
/// (file refs, knowledge refs, workflow invocations, unknown
|
||||
/// variants from a newer client).
|
||||
///
|
||||
/// If the between-turns compaction threshold is exceeded mid-run,
|
||||
/// the Worker is aborted, history is compacted, and execution resumes
|
||||
/// automatically.
|
||||
pub async fn run(&mut self, input: impl Into<String>) -> Result<PodRunResult, PodError> {
|
||||
pub async fn run(&mut self, input: Vec<Segment>) -> Result<PodRunResult, PodError> {
|
||||
self.ensure_interceptor_installed();
|
||||
self.ensure_system_prompt_materialized()?;
|
||||
self.ensure_session_head().await?;
|
||||
|
||||
let flattened = self.flatten_segments(&input);
|
||||
|
||||
let history_before = self.worker.as_ref().unwrap().history().len();
|
||||
|
||||
// lock → run → unlock
|
||||
let worker = self.worker.take().expect("worker taken during run");
|
||||
let mut locked = worker.lock();
|
||||
let result = locked.run(input).await;
|
||||
let result = locked.run(flattened).await;
|
||||
self.worker = Some(locked.unlock());
|
||||
|
||||
self.handle_worker_result(result, history_before).await
|
||||
}
|
||||
|
||||
/// Flatten a typed segment list into the single string the Worker
|
||||
/// receives as the user message. Inlines text and paste content;
|
||||
/// substitutes `[unresolved <kind>: <key>]` placeholders for
|
||||
/// segments that have no resolver, and emits a user-facing alert so
|
||||
/// neither the LLM nor the human is blind to the dropped intent.
|
||||
fn flatten_segments(&self, segments: &[Segment]) -> String {
|
||||
let mut out = String::new();
|
||||
for seg in segments {
|
||||
match seg {
|
||||
Segment::Text { content } => out.push_str(content),
|
||||
Segment::Paste { content, .. } => out.push_str(content),
|
||||
Segment::FileRef { path } => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
format!(
|
||||
"file ref @{path} cannot be resolved \
|
||||
(resolver not yet implemented); passed to LLM as placeholder"
|
||||
),
|
||||
);
|
||||
out.push_str(&format!("[unresolved file ref: {path}]"));
|
||||
}
|
||||
Segment::KnowledgeRef { slug } => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
format!(
|
||||
"knowledge ref #{slug} cannot be resolved \
|
||||
(resolver not yet implemented); passed to LLM as placeholder"
|
||||
),
|
||||
);
|
||||
out.push_str(&format!("[unresolved knowledge ref: {slug}]"));
|
||||
}
|
||||
Segment::WorkflowInvoke { slug } => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
format!(
|
||||
"workflow /{slug} cannot be resolved \
|
||||
(resolver not yet implemented); passed to LLM as placeholder"
|
||||
),
|
||||
);
|
||||
out.push_str(&format!("[unresolved workflow invoke: {slug}]"));
|
||||
}
|
||||
Segment::Unknown => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
"received unknown segment kind from a newer client; \
|
||||
passed to LLM as placeholder"
|
||||
.into(),
|
||||
);
|
||||
out.push_str("[unknown input segment]");
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Run a turn triggered by `Method::Notify` while the Pod is idle.
|
||||
///
|
||||
/// Unlike [`run`](Self::run), no user message is appended to
|
||||
|
||||
@@ -358,7 +358,12 @@ async fn send_run_and_confirm(socket: &Path, input: String) -> Result<(), SendRu
|
||||
let (r, w) = stream.into_split();
|
||||
let mut writer = JsonLineWriter::new(w);
|
||||
let mut reader = JsonLineReader::new(r);
|
||||
tokio::time::timeout(SOCKET_OP_TIMEOUT, writer.write(&Method::Run { input }))
|
||||
tokio::time::timeout(
|
||||
SOCKET_OP_TIMEOUT,
|
||||
writer.write(&Method::Run {
|
||||
input: vec![protocol::Segment::text(input)],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| SendRunError::Io("write timed out".into()))?
|
||||
.map_err(|e| SendRunError::Io(format!("write: {e}")))?;
|
||||
|
||||
@@ -424,7 +424,7 @@ async fn send_run(socket: &Path, task: &str) -> Result<(), ToolError> {
|
||||
let (_reader, writer) = stream.into_split();
|
||||
let mut w = JsonLineWriter::new(writer);
|
||||
w.write(&Method::Run {
|
||||
input: task.to_string(),
|
||||
input: vec![protocol::Segment::text(task)],
|
||||
})
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionFailed(format!("send Method::Run: {e}")))?;
|
||||
|
||||
@@ -192,7 +192,7 @@ async fn post_run_compact_success_broadcasts_start_and_done() {
|
||||
let (tx, mut rx) = broadcast::channel::<Event>(64);
|
||||
pod.attach_event_tx(tx);
|
||||
|
||||
pod.run("first").await.unwrap();
|
||||
pod.run_text("first").await.unwrap();
|
||||
// Drain run events so only compact events remain in `rx`.
|
||||
let _ = drain(&mut rx);
|
||||
|
||||
@@ -248,12 +248,12 @@ async fn mid_turn_compact_success_broadcasts_start_and_done() {
|
||||
pod.attach_event_tx(tx);
|
||||
|
||||
// First run populates usage_history above the request threshold.
|
||||
pod.run("first").await.unwrap();
|
||||
pod.run_text("first").await.unwrap();
|
||||
let _ = drain(&mut rx);
|
||||
|
||||
// Second run: pre_llm_request yields immediately, Worker returns
|
||||
// Yielded, handle_worker_result routes into do_compact_and_resume.
|
||||
pod.run("second").await.unwrap();
|
||||
pod.run_text("second").await.unwrap();
|
||||
|
||||
let events = drain(&mut rx);
|
||||
let kinds: Vec<&str> = events
|
||||
@@ -291,7 +291,7 @@ async fn post_run_compact_failure_broadcasts_start_and_failed() {
|
||||
let (tx, mut rx) = broadcast::channel::<Event>(64);
|
||||
pod.attach_event_tx(tx);
|
||||
|
||||
pod.run("first").await.unwrap();
|
||||
pod.run_text("first").await.unwrap();
|
||||
let _ = drain(&mut rx);
|
||||
|
||||
// Best-effort: returns Ok(()) even on failure, but emits CompactFailed.
|
||||
|
||||
@@ -170,9 +170,7 @@ async fn run_updates_shared_state_to_idle_after_completion() {
|
||||
let handle = spawn_controller(pod).await;
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "Hello".into(),
|
||||
})
|
||||
.send(Method::run_text("Hello"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -189,9 +187,7 @@ async fn run_populates_history() {
|
||||
let handle = spawn_controller(pod).await;
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "Hello".into(),
|
||||
})
|
||||
.send(Method::run_text("Hello"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -212,9 +208,7 @@ async fn events_are_broadcast() {
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "Hello".into(),
|
||||
})
|
||||
.send(Method::run_text("Hello"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -265,17 +259,13 @@ async fn double_run_returns_error() {
|
||||
|
||||
// Send first run
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "first".into(),
|
||||
})
|
||||
.send(Method::run_text("first"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Immediately send second run (should get error)
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "second".into(),
|
||||
})
|
||||
.send(Method::run_text("second"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -363,6 +353,119 @@ async fn cancel_without_run_returns_error() {
|
||||
assert!(saw_not_running, "should see not_running error");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
let client_for_assert = client.clone();
|
||||
let pod = make_pod(client).await;
|
||||
let handle = spawn_controller(pod).await;
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
// Mixed input: plain text + a paste chip + trailing text. Pod must
|
||||
// flatten this into one user-message string (paste content inlined,
|
||||
// no `[Clipboard ...]` label leaking to the LLM); the
|
||||
// `Event::UserMessage` re-broadcast must carry the typed segments
|
||||
// unchanged so other clients can re-render the chip.
|
||||
let segments = vec![
|
||||
protocol::Segment::text("see "),
|
||||
protocol::Segment::Paste {
|
||||
id: 7,
|
||||
chars: 11,
|
||||
lines: 2,
|
||||
content: "line1\nline2".into(),
|
||||
},
|
||||
protocol::Segment::text(" thanks"),
|
||||
];
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: segments.clone(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
let mut user_event_segments: Option<Vec<protocol::Segment>> = None;
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = rx.recv() => match event {
|
||||
Ok(Event::UserMessage { segments }) => user_event_segments = Some(segments),
|
||||
Ok(Event::TurnEnd { .. }) => break,
|
||||
Err(_) => break,
|
||||
_ => {}
|
||||
},
|
||||
_ = tokio::time::sleep_until(deadline) => break,
|
||||
}
|
||||
}
|
||||
let echoed = user_event_segments.expect("UserMessage event missing");
|
||||
assert_eq!(echoed.len(), 3, "all three segments must round-trip");
|
||||
assert!(matches!(echoed[1], protocol::Segment::Paste { id: 7, .. }));
|
||||
|
||||
// The Worker received a single user message whose text is the
|
||||
// flattened body — paste content inlined, no chip label.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
let requests = client_for_assert.captured_requests();
|
||||
assert_eq!(requests.len(), 1, "one LLM call expected");
|
||||
let user_text = requests[0]
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|i| i.as_text().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
assert!(user_text.contains("see line1\nline2 thanks"), "got: {user_text:?}");
|
||||
assert!(!user_text.contains("[Clipboard"), "label must not leak: {user_text:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_with_unresolved_segment_emits_alert_and_placeholder() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
let client_for_assert = client.clone();
|
||||
let pod = make_pod(client).await;
|
||||
let handle = spawn_controller(pod).await;
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
let segments = vec![
|
||||
protocol::Segment::text("look at "),
|
||||
protocol::Segment::FileRef { path: "src/lib.rs".into() },
|
||||
];
|
||||
handle
|
||||
.send(Method::Run { input: segments })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
let mut saw_alert_for_file_ref = false;
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = rx.recv() => match event {
|
||||
Ok(Event::Alert(a)) if a.message.contains("file ref @src/lib.rs") => {
|
||||
saw_alert_for_file_ref = true;
|
||||
}
|
||||
Ok(Event::TurnEnd { .. }) => break,
|
||||
Err(_) => break,
|
||||
_ => {}
|
||||
},
|
||||
_ = tokio::time::sleep_until(deadline) => break,
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
saw_alert_for_file_ref,
|
||||
"an Alert mentioning the unresolved file ref must be emitted"
|
||||
);
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
let requests = client_for_assert.captured_requests();
|
||||
let user_text = requests[0]
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|i| i.as_text().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
// LLM context carries a placeholder so the model can ask for the
|
||||
// missing content rather than silently miss the user's intent.
|
||||
assert!(
|
||||
user_text.contains("[unresolved file ref: src/lib.rs]"),
|
||||
"placeholder missing, got: {user_text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
@@ -425,9 +528,7 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "start".into(),
|
||||
})
|
||||
.send(Method::run_text("start"))
|
||||
.await
|
||||
.unwrap();
|
||||
handle
|
||||
@@ -491,9 +592,7 @@ async fn socket_run_receives_events() {
|
||||
|
||||
// Send run method via socket
|
||||
writer
|
||||
.write(&Method::Run {
|
||||
input: "Hello".into(),
|
||||
})
|
||||
.write(&Method::run_text("Hello"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -641,9 +740,7 @@ async fn pause_then_resume_transitions_and_preserves_history_consistency() {
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "hello".into(),
|
||||
})
|
||||
.send(Method::run_text("hello"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -754,9 +851,7 @@ async fn paused_then_run_closes_orphan_tool_use_for_next_request() {
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "first".into(),
|
||||
})
|
||||
.send(Method::run_text("first"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -789,9 +884,7 @@ async fn paused_then_run_closes_orphan_tool_use_for_next_request() {
|
||||
// `Pod::interrupt_and_run`, which closes the orphan + injects a
|
||||
// system note before the fresh user message.
|
||||
handle
|
||||
.send(Method::Run {
|
||||
input: "new request".into(),
|
||||
})
|
||||
.send(Method::run_text("new request"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
|
||||
@@ -185,7 +185,10 @@ async fn send_to_pod_delivers_run_method() {
|
||||
|
||||
let method = received.await.unwrap().expect("expected a method");
|
||||
match method {
|
||||
Method::Run { input } => assert_eq!(input, "hello there"),
|
||||
Method::Run { input } => match input.as_slice() {
|
||||
[protocol::Segment::Text { content }] => assert_eq!(content, "hello there"),
|
||||
other => panic!("expected single Text segment, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected Run, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,10 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
||||
// Verify the tool delivered Method::Run to the socket.
|
||||
let method = received.await.unwrap().expect("expected one Method line");
|
||||
match method {
|
||||
Method::Run { input } => assert_eq!(input, "hello"),
|
||||
Method::Run { input } => match input.as_slice() {
|
||||
[protocol::Segment::Text { content }] => assert_eq!(content, "hello"),
|
||||
other => panic!("expected single Text segment, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected Run, got {other:?}"),
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ async fn materialise_on_first_turn_populates_worker() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
pod.run("hi").await.unwrap();
|
||||
pod.run_text("hi").await.unwrap();
|
||||
let rendered = pod
|
||||
.worker()
|
||||
.get_system_prompt()
|
||||
@@ -180,7 +180,7 @@ async fn session_start_state_captures_rendered_prompt() {
|
||||
let (mut pod, pwd) = make_pod_with_body("hello cwd={{ cwd }}", client)
|
||||
.await
|
||||
.unwrap();
|
||||
pod.run("hi").await.unwrap();
|
||||
pod.run_text("hi").await.unwrap();
|
||||
|
||||
let entries = pod.store().read_all(pod.session_id()).await.unwrap();
|
||||
let first = entries.first().expect("at least one entry");
|
||||
@@ -199,7 +199,7 @@ async fn session_start_state_captures_rendered_prompt() {
|
||||
async fn render_failure_propagates_as_pod_error() {
|
||||
let client = MockClient::new(vec![single_text_events("ok")]);
|
||||
let (mut pod, _pwd) = make_pod_with_body("{{ ghost }}", client).await.unwrap();
|
||||
let err = pod.run("hi").await.unwrap_err();
|
||||
let err = pod.run_text("hi").await.unwrap_err();
|
||||
assert!(matches!(err, PodError::SystemPromptRender { .. }));
|
||||
}
|
||||
|
||||
@@ -212,9 +212,9 @@ async fn materialise_runs_only_once_across_turns() {
|
||||
let (mut pod, _pwd) = make_pod_with_body("fixed prompt {{ cwd }}", client)
|
||||
.await
|
||||
.unwrap();
|
||||
pod.run("one").await.unwrap();
|
||||
pod.run_text("one").await.unwrap();
|
||||
let first = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
pod.run("two").await.unwrap();
|
||||
pod.run_text("two").await.unwrap();
|
||||
let second = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert_eq!(first, second);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ async fn agents_md_is_injected_as_trailing_section_when_present() {
|
||||
let (mut pod, pwd) = make_pod_with_body("BODY", client).await.unwrap();
|
||||
std::fs::write(pwd.join("AGENTS.md"), "# project rules\nbe kind").unwrap();
|
||||
|
||||
pod.run("hi").await.unwrap();
|
||||
pod.run_text("hi").await.unwrap();
|
||||
let rendered = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert!(rendered.starts_with("BODY"));
|
||||
assert!(rendered.contains("## Project instructions (AGENTS.md)"));
|
||||
@@ -237,7 +237,7 @@ async fn agents_md_is_injected_as_trailing_section_when_present() {
|
||||
async fn agents_md_absent_omits_trailing_section() {
|
||||
let client = MockClient::new(vec![single_text_events("ok")]);
|
||||
let (mut pod, _pwd) = make_pod_with_body("BODY", client).await.unwrap();
|
||||
pod.run("hi").await.unwrap();
|
||||
pod.run_text("hi").await.unwrap();
|
||||
let rendered = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert!(!rendered.contains("## Project instructions"));
|
||||
assert!(!rendered.contains("AGENTS.md"));
|
||||
@@ -246,20 +246,20 @@ async fn agents_md_absent_omits_trailing_section() {
|
||||
#[tokio::test]
|
||||
async fn agents_md_not_reread_after_compact() {
|
||||
let client = MockClient::new(vec![
|
||||
single_text_events("a"), // pod.run("first")
|
||||
single_text_events("b"), // pod.run("second")
|
||||
single_text_events("a"), // pod.run_text("first")
|
||||
single_text_events("b"), // pod.run_text("second")
|
||||
write_summary_tool_use_events("call-1", "compacted summary"), // compact worker: tool_use
|
||||
single_text_events("done"), // compact worker: close
|
||||
single_text_events("c"), // pod.run("third")
|
||||
single_text_events("c"), // pod.run_text("third")
|
||||
]);
|
||||
let (mut pod, pwd) = make_pod_with_body("BODY", client).await.unwrap();
|
||||
let agents_path = pwd.join("AGENTS.md");
|
||||
std::fs::write(&agents_path, "original").unwrap();
|
||||
|
||||
pod.run("first").await.unwrap();
|
||||
pod.run_text("first").await.unwrap();
|
||||
let before = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert!(before.contains("original"));
|
||||
pod.run("second").await.unwrap();
|
||||
pod.run_text("second").await.unwrap();
|
||||
|
||||
// Mutate the file after the first turn — must not affect the cached
|
||||
// system prompt either on a subsequent turn or across compaction.
|
||||
@@ -269,7 +269,7 @@ async fn agents_md_not_reread_after_compact() {
|
||||
assert!(after_compact.contains("original"));
|
||||
assert!(!after_compact.contains("mutated"));
|
||||
|
||||
pod.run("third").await.unwrap();
|
||||
pod.run_text("third").await.unwrap();
|
||||
let after_third = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert!(after_third.contains("original"));
|
||||
assert!(!after_third.contains("mutated"));
|
||||
@@ -278,25 +278,25 @@ async fn agents_md_not_reread_after_compact() {
|
||||
#[tokio::test]
|
||||
async fn compact_preserves_system_prompt() {
|
||||
let client = MockClient::new(vec![
|
||||
single_text_events("a"), // pod.run("first")
|
||||
single_text_events("b"), // pod.run("second")
|
||||
single_text_events("a"), // pod.run_text("first")
|
||||
single_text_events("b"), // pod.run_text("second")
|
||||
write_summary_tool_use_events("call-1", "compacted summary"), // compact worker: tool_use
|
||||
single_text_events("done"), // compact worker: close
|
||||
single_text_events("c"), // pod.run("third")
|
||||
single_text_events("c"), // pod.run_text("third")
|
||||
]);
|
||||
let (mut pod, _pwd) = make_pod_with_body("SP cwd={{ cwd }}", client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pod.run("first").await.unwrap();
|
||||
pod.run_text("first").await.unwrap();
|
||||
let before = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
pod.run("second").await.unwrap();
|
||||
pod.run_text("second").await.unwrap();
|
||||
|
||||
pod.compact(0).await.unwrap();
|
||||
|
||||
let after = pod.worker().get_system_prompt().unwrap().to_string();
|
||||
assert_eq!(before, after);
|
||||
|
||||
pod.run("third").await.unwrap();
|
||||
pod.run_text("third").await.unwrap();
|
||||
assert_eq!(pod.worker().get_system_prompt().unwrap(), after.as_str());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user