ファイル参照を与えた際に自動的に読ませる実装

This commit is contained in:
2026-04-30 21:58:10 +09:00
parent bc81dc1513
commit d7bc7ab3dd
12 changed files with 399 additions and 36 deletions
+65 -5
View File
@@ -123,6 +123,10 @@ permission = "write"
"#;
async fn make_pod(client: MockClient) -> Pod<MockClient, FsStore> {
make_pod_with_pwd(client).await.0
}
async fn make_pod_with_pwd(client: MockClient) -> (Pod<MockClient, FsStore>, std::path::PathBuf) {
let manifest = PodManifest::from_toml(MANIFEST_TOML).unwrap();
let store_tmp = tempfile::tempdir().unwrap();
let store = FsStore::new(store_tmp.path()).await.unwrap();
@@ -137,7 +141,10 @@ async fn make_pod(client: MockClient) -> Pod<MockClient, FsStore> {
std::mem::forget(pwd_tmp);
let worker = Worker::new(client);
Pod::new(manifest, worker, store, pwd, scope).await.unwrap()
let pod = Pod::new(manifest, worker, store, pwd.clone(), scope)
.await
.unwrap();
(pod, pwd)
}
use pod::PodHandle;
@@ -405,6 +412,58 @@ async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
);
}
#[tokio::test]
async fn run_with_resolvable_file_ref_attaches_system_message_after_user() {
let client = MockClient::new(simple_text_events());
let client_for_assert = client.clone();
let (pod, pwd) = make_pod_with_pwd(client).await;
std::fs::write(pwd.join("notes.md"), "alpha\nbeta\n").unwrap();
let handle = spawn_controller(pod).await;
let segments = vec![
protocol::Segment::text("see "),
protocol::Segment::FileRef {
path: "notes.md".into(),
},
];
handle.send(Method::Run { input: segments }).await.unwrap();
// Wait for the turn to complete.
let mut rx = handle.subscribe();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
tokio::select! {
event = rx.recv() => match event {
Ok(Event::TurnEnd { .. }) => break,
Err(_) => break,
_ => {}
},
_ = tokio::time::sleep_until(deadline) => break,
}
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let requests = client_for_assert.captured_requests();
let items = &requests[0].items;
// The submit produces 2 history items: user message then file content.
let user_idx = items
.iter()
.position(|i| i.is_user_message())
.expect("user message present");
let next = items
.get(user_idx + 1)
.expect("attachment item present after user");
let next_text = next.as_text().unwrap_or_default();
assert!(
next_text.contains("[File: notes.md]"),
"expected file header, got: {next_text:?}"
);
assert!(
next_text.contains("alpha"),
"expected file body, got: {next_text:?}"
);
}
#[tokio::test]
async fn run_with_unresolved_segment_emits_alert_and_placeholder() {
let client = MockClient::new(simple_text_events());
@@ -448,11 +507,12 @@ async fn run_with_unresolved_segment_emits_alert_and_placeholder() {
.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.
// The user message keeps the literal `@<path>` token (matching what
// the user typed). Resolution failure surfaces via the Alert above;
// the LLM still sees the intent as a sigil-prefixed reference.
assert!(
user_text.contains("[unresolved file ref: src/lib.rs]"),
"placeholder missing, got: {user_text:?}"
user_text.contains("@src/lib.rs"),
"literal sigil missing, got: {user_text:?}"
);
}