You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/18 08:29:49 UTC
(maka) branch rust-preview updated: fix(files): read archived tool result continuations
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/rust-preview by this push:
new 95b1472333 fix(files): read archived tool result continuations
95b1472333 is described below
commit 95b14723334326f593dbb067170475d2bbe73e8f
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Oct 18 16:29:43 2026 +0800
fix(files): read archived tool result continuations
Resolve original tool-result resources under the active Invocation and Session,
reuse the shared pager with the Native response budget, and preserve source
projections without rerunning their tools. Verify actual continuation calls,
full Unicode content, Session isolation and authorized restart recovery.
Generated-by: Codex
---
plugins/agent/facts/src/archive.rs | 2 +-
plugins/agent/facts/src/read/tool_result.rs | 10 +-
plugins/agent/native/src/owner/tests/todo.rs | 167 +++++++++++++++++++++
plugins/workspace/workspace-files/native/README.md | 2 +
.../workspace-files/native/README.zh-CN.md | 2 +
.../workspace/workspace-files/native/src/lib.rs | 68 ++++++---
products/maka/bundle/src/installation.ts | 7 +-
products/maka/cli/notices.json | 2 +-
8 files changed, 231 insertions(+), 29 deletions(-)
diff --git a/plugins/agent/facts/src/archive.rs b/plugins/agent/facts/src/archive.rs
index 73efd3765a..dcbb8a1dbf 100644
--- a/plugins/agent/facts/src/archive.rs
+++ b/plugins/agent/facts/src/archive.rs
@@ -134,7 +134,7 @@ impl ArchivedPlaceholder {
.resolve()
.map_err(|_| "invalid archive address")?;
let page = request
- .tool_result_page_with_budget(
+ .tool_result_page(
&identity.tool_name,
body,
MAX_PAGE_CHARS.saturating_sub(overhead),
diff --git a/plugins/agent/facts/src/read/tool_result.rs b/plugins/agent/facts/src/read/tool_result.rs
index 34a132457c..862c3451ed 100644
--- a/plugins/agent/facts/src/read/tool_result.rs
+++ b/plugins/agent/facts/src/read/tool_result.rs
@@ -17,7 +17,7 @@
* under the License.
*/
-use super::{MAX_PAGE_CHARS, ReadError, ReadPage, ReadRequest, shell::read_archived_shell};
+use super::{ReadError, ReadPage, ReadRequest, shell::read_archived_shell};
use serde::Deserialize;
#[derive(Deserialize)]
@@ -39,14 +39,6 @@ impl ReadRequest {
&self,
tool_name: &str,
serialized: &str,
- ) -> Result<ReadPage, ReadError> {
- self.tool_result_page_with_budget(tool_name, serialized, MAX_PAGE_CHARS)
- }
-
- pub(crate) fn tool_result_page_with_budget(
- &self,
- tool_name: &str,
- serialized: &str,
budget: usize,
) -> Result<ReadPage, ReadError> {
if matches!(tool_name, "Shell" | "Read")
diff --git a/plugins/agent/native/src/owner/tests/todo.rs b/plugins/agent/native/src/owner/tests/todo.rs
index 5e9dfec74e..f84da8b85b 100644
--- a/plugins/agent/native/src/owner/tests/todo.rs
+++ b/plugins/agent/native/src/owner/tests/todo.rs
@@ -78,6 +78,173 @@ pub(super) async fn reopen(f: &mut Fixture) {
.unwrap();
f.app.ready().await.unwrap();
}
+#[tokio::test]
+async fn todo_archived_result_next_is_readable_by_the_selected_native_read_tool() {
+ tokio::time::timeout(Duration::from_secs(90), async {
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let setup = service_tools::Setup::files("readText").named("Read");
+ let policy = setup.policy();
+ let mut f = Fixture::configured(
+ &format!("http://{}/v1", listener.local_addr().unwrap()),
+ None,
+ None,
+ Some(setup),
+ |settings| {
+ settings.todo = true;
+ settings.permissions.access = WorkspaceAccess::ReadOnly;
+ },
+ )
+ .await;
+ // Declare the controlled provider's capacity, keeping this recovery
+ // proof independent of an unrelated automatic compaction conversation.
+ selection::edit(&f, |changes| {
+ changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
+ context_window: Some(1_000_000), input_limit: Some(900_000), ..Default::default()
+ })]));
+ }).await;
+ let session = f.create("Recover original checklist result").await;
+ let other = f.create("Cannot read another checklist archive").await;
+ let items: Vec<Value> = (0..32)
+ .map(|index| json!({"content":format!("{index:02} {}", "中文😀".repeat(if index == 0 { 2048 } else { 128 })),"status":"pending"}))
+ .collect();
+ // Independent literal rendering oracle: no Document::render or pager.
+ let expected = (0..32).map(|index| format!("{}. [pending] \"{index:02} {}\"", index + 1,
+ "中文😀".repeat(if index == 0 { 2048 } else { 128 }))).collect::<Vec<_>>().join("\n");
+ f.send(&session).await.unwrap();
+ let (mut socket, _) = listener.accept().await.unwrap();
+ let sent = request(&mut socket).await;
+ let selected_read = sent["tools"].as_array().unwrap().iter().any(|tool| tool["function"]["name"] == "Read");
+ service_tools::named_tool_response(&mut socket, "todo_write", json!({"todos":items})).await;
+
+ let (mut socket, _) = listener.accept().await.unwrap();
+ let sent = request(&mut socket).await;
+ let archived: Value = serde_json::from_str(last_tool(&sent)).unwrap();
+ let first_next = archived["page"]["next"].clone();
+ let mut pages = vec![archived["page"].clone()];
+ let mut recovery_results = Vec::new();
+ let mut next = first_next.clone();
+ while next.is_object() {
+ // Every page is requested through the actual model with exactly the
+ // prior tool's next object, never a manufactured cursor/store read.
+ service_tools::named_tool_response(&mut socket, "Read", next).await;
+ let (mut received, _) = listener.accept().await.unwrap();
+ let sent = request(&mut received).await;
+ let result: Value = serde_json::from_str(last_tool(&sent)).unwrap();
+ next = result["value"]["next"].clone();
+ if result["kind"] == "completed" {
+ pages.push(result["value"].clone());
+ }
+ recovery_results.push(result);
+ socket = received;
+ }
+ response(&mut socket).await;
+ f.drained().await;
+
+ f.send(&other).await.unwrap();
+ let (mut socket, _) = listener.accept().await.unwrap();
+ request(&mut socket).await;
+ service_tools::named_tool_response(&mut socket, "Read", first_next.clone()).await;
+ let (mut socket, _) = listener.accept().await.unwrap();
+ let sent = request(&mut socket).await;
+ let foreign: Value = serde_json::from_str(last_tool(&sent)).unwrap();
+ response(&mut socket).await;
+ f.drained().await;
+
+ let deployment = f.app.source().unwrap().deployment;
+ f.app.shutdown().await.unwrap();
+ let mut native = NativeRegistry::default();
+ maka_settings_native::provide(&mut native).unwrap();
+ provide_all(&mut native).unwrap();
+ f.app = Application::start_from_store(
+ f.store.clone(),
+ deployment,
+ native,
+ f.issuer.clone(),
+ Arc::new(Grant(f.issuer.clone(), Some(policy))),
+ Some(maka_host_js::factory(Default::default())),
+ ).await.unwrap();
+ f.app.ready().await.unwrap();
+ // send uses the current Application's public service. The old fixture
+ // owner is retired; final Application shutdown drains the new owner.
+ f.send(&session).await.unwrap();
+ let (mut socket, _) = listener.accept().await.unwrap();
+ request(&mut socket).await;
+ service_tools::named_tool_response(&mut socket, "Read", first_next.clone()).await;
+ let (mut socket, _) = listener.accept().await.unwrap();
+ let sent = request(&mut socket).await;
+ let restarted_raw = last_tool(&sent).to_owned();
+ response(&mut socket).await;
+ while !f.query("observe", json!({"sessionId":session})).await.unwrap()["run"].is_null() {
+ tokio::task::yield_now().await;
+ }
+ f.app.shutdown().await.unwrap();
+
+ let original = f.store.scoped_prefix(
+ maka_agent_facts::event::LogScope::Session { id: session.clone() },
+ 256,
+ 4 * 1024 * 1024,
+ ).await.unwrap();
+ let source_operation = original.events.iter().find_map(|event| match &event.event.fact {
+ Fact::ToolDispatched { operation_id, name, .. } if name == "todo_write" => Some(operation_id),
+ _ => None,
+ }).unwrap();
+ let source = original.events.iter().find(|event| matches!(&event.event.fact,
+ Fact::ToolSettled { operation_id, outcome: ToolOutcome::Succeeded { .. } }
+ if operation_id == source_operation)).unwrap();
+ let source_output = f.store.resolve_tool_result(&session, &source.event.id).await.unwrap();
+ f.config.shutdown().await.unwrap();
+
+ // All behavior assertions follow actual cleanup, including expected
+ // failure paths, so no failing recovery assertion abandons the Host.
+ let restarted: Value = serde_json::from_str(&restarted_raw)
+ .unwrap_or_else(|error| panic!("restarted Read returned non-JSON after cleanup: {error}; raw={restarted_raw:?}"));
+ assert!(selected_read, "the original model request must select the genuine Read binding");
+ assert_eq!(archived["kind"], "maka.archived_tool_result");
+ assert_eq!(archived["toolName"], "todo_write");
+ assert!(first_next.is_object(), "the oversized original must advertise next");
+ let maka_agent_facts::tool_output::ToolOutput::Json(document) = source_output else {
+ panic!("the original checklist output must remain JSON");
+ };
+ assert_eq!(document["items"], json!(items));
+ let Fact::ToolSettled { outcome: ToolOutcome::Succeeded { model_projection, .. }, .. } = &source.event.fact else { unreachable!() };
+ assert_eq!(model_projection, &maka_agent_facts::tool_output::DurableToolProjection::Text { text: expected.clone() });
+ assert_eq!(original.events.iter().filter(|event| matches!(&event.event.fact,
+ Fact::ToolDispatched { name, .. } if name == "todo_write")).count(), 1);
+ for result in &recovery_results {
+ assert_eq!(result["kind"], "completed", "the selected Read must recover the actual archive continuation: {result}");
+ assert!(serde_json::to_string(result).unwrap().encode_utf16().count()
+ <= maka_agent_facts::read::MAX_PAGE_CHARS, "recovery pages must not be archived again");
+ }
+ let mut recovered = String::new();
+ let mut omitted_newline = false;
+ for (index, page) in pages.iter().enumerate() {
+ let content = page["content"].as_str().unwrap();
+ recovered.push_str(content);
+ assert_eq!(page["totalLines"], 32);
+ if let Some(next) = pages.get(index + 1) {
+ let end_line = page["offset"].as_u64().unwrap() + content.bytes().filter(|byte| *byte == b'\n').count() as u64;
+ let next_line = next["offset"].as_u64().unwrap();
+ if next_line == end_line + 1 {
+ recovered.push('\n');
+ omitted_newline = true;
+ } else {
+ assert_eq!(next_line, end_line, "a page may skip only its boundary newline");
+ }
+ }
+ }
+ assert!(pages.iter().any(|page| page["partialLine"] == true), "the long first line must span actual pages");
+ assert!(omitted_newline, "complete-line pagination must exercise its delimiter convention");
+ assert_eq!(recovered.as_bytes(), expected.as_bytes(), "every original UTF-8 byte must be recoverable");
+ assert_eq!(foreign, json!({"kind":"failed","message":"Tool result was not found in this Session"}));
+ assert_eq!(restarted, recovery_results[0], "a new authorized Invocation after restart must read the same immutable page");
+ let source_archives = original.events.iter().filter(|event| matches!(&event.event.fact,
+ Fact::ToolResultArchived { placeholder } if placeholder.identity.runtime_event_id == source.event.id)).count();
+ let archives = original.events.iter().filter(|event| matches!(&event.event.fact, Fact::ToolResultArchived { .. })).count();
+ assert_eq!(archives, source_archives, "bounded recovery pages must not acquire their own archive replacements");
+ assert_eq!(source_archives, 1);
+ }).await.unwrap();
+}
+
#[tokio::test]
async fn todo_real_model_direct_and_code_mode_keep_full_list_isolation_and_restart() {
tokio::time::timeout(Duration::from_secs(90), async {
diff --git a/plugins/workspace/workspace-files/native/README.md b/plugins/workspace/workspace-files/native/README.md
index b025034751..f144f8798e 100644
--- a/plugins/workspace/workspace-files/native/README.md
+++ b/plugins/workspace/workspace-files/native/README.md
@@ -23,4 +23,6 @@
Exposes file operations for an explicitly selected workspace. `provide` registers the service and `attachments` handles captured file resources. The wrapper supplies the original call scope and directory authority to the file engine. File tools and commands share the session sandbox preset: reads may reach outside the workspace; workspace-write permits writes in the workspace and system temporary directories. Other writes require permission approval. The selected directory remains captur [...]
+`readText` also pages uploaded attachments and original tool-result resources visible to the calling Session. A selected Read tool uses its current live Invocation; resource addresses and continuations do not grant access to another Session. Archived results reuse the original model projection without executing the source tool again. Each archive page currently loads the complete original projection before paging, and cancellation waits for that storage operation to settle.
+
[Source](src/lib.rs) · [Cargo manifest](Cargo.toml)
diff --git a/plugins/workspace/workspace-files/native/README.zh-CN.md b/plugins/workspace/workspace-files/native/README.zh-CN.md
index dddcc1653d..a2f94d0695 100644
--- a/plugins/workspace/workspace-files/native/README.zh-CN.md
+++ b/plugins/workspace/workspace-files/native/README.zh-CN.md
@@ -23,4 +23,6 @@
为显式选择的工作区提供文件操作。`provide` 注册服务,`attachments` 处理捕获的文件资源。包装层向文件引擎传递原始调用作用域与目录权限。文件工具与命令共用会话沙箱策略:允许读取工作区外文件;workspace-write 默认允许写入工作区及系统临时目录,其他写入需要权限审批。所选目录仍通过捕获的句柄访问,审批不能绕过工作区元数据与 Host 私有路径保护。
+`readText` 也可分页读取调用方 Session 可见的上传附件与原始工具结果资源。已选择的 Read 工具使用当前活跃 Invocation 的权限;资源地址和续页地址不会授予其他 Session 的访问权。归档读取复用原始模型投影,不会再次执行源工具。当前每个归档页仍会先加载完整原始投影,取消也会等待该存储操作结束。
+
[源码](src/lib.rs) · [Cargo 配置](Cargo.toml)
diff --git a/plugins/workspace/workspace-files/native/src/lib.rs b/plugins/workspace/workspace-files/native/src/lib.rs
index 4c4f6eb61b..a83afd89d7 100644
--- a/plugins/workspace/workspace-files/native/src/lib.rs
+++ b/plugins/workspace/workspace-files/native/src/lib.rs
@@ -272,31 +272,65 @@ async fn execute(
if let Some(mut address) =
maka_agent_facts::read::ResourceAddress::parse(request.path()).map_err(failed)?
{
- if !matches!(
- address,
- maka_agent_facts::read::ResourceAddress::Attachment(_)
- ) {
- return Err(failed(
- "This Files service supports only attachment resources",
- ));
+ use maka_agent_facts::read::ResourceAddress;
+ match &address {
+ ResourceAddress::Task(_) => {
+ return Err(failed("This Files service does not support task resources"));
+ }
+ ResourceAddress::ToolResult(_) if image => {
+ return Err(failed("Use readText for archived tool results"));
+ }
+ _ => {}
}
+ let missing = match &address {
+ ResourceAddress::Attachment(_) => "Attachment was not found in this Session",
+ _ => "Tool result was not found in this Session",
+ };
+ cancelled(cancellation)?;
if !request.is_continuation() {
address = store
.resolve_read_resource(&tool_context.invocation.session_id, address)
.await
.map_err(failed)?
- .ok_or_else(|| failed("Attachment was not found in this Session"))?;
+ .ok_or_else(|| failed(missing))?;
+ cancelled(cancellation)?;
request.bind_resource(&address).map_err(failed)?;
}
- return attachments::read(
- store,
- &tool_context.invocation.session_id,
- address.id(),
- &request,
- image,
- cancellation,
- )
- .await;
+ match address {
+ ResourceAddress::Attachment(id) => {
+ return attachments::read(
+ store,
+ &tool_context.invocation.session_id,
+ &id,
+ &request,
+ image,
+ cancellation,
+ )
+ .await;
+ }
+ ResourceAddress::ToolResult(id) => {
+ cancelled(cancellation)?;
+ // Await the original store job: dropping its reply cannot
+ // cancel the physical read or settle this call's ticket.
+ let result = store
+ .read_tool_result(&tool_context.invocation.session_id, &id)
+ .await
+ .map_err(failed)?;
+ cancelled(cancellation)?;
+ let result = result
+ .ok_or_else(|| failed("Tool result was not found in this Session"))?;
+ let page = request
+ .tool_result_page(
+ &result.tool_name,
+ &result.serialized_result,
+ read_page_budget(),
+ )
+ .map_err(failed)?;
+ cancelled(cancellation)?;
+ return serde_json::to_value(page).map(Output::Json).map_err(failed);
+ }
+ ResourceAddress::Task(_) => unreachable!("task resource rejected above"),
+ }
}
}
diff --git a/products/maka/bundle/src/installation.ts b/products/maka/bundle/src/installation.ts
index 08330f63d3..413ccb481c 100644
--- a/products/maka/bundle/src/installation.ts
+++ b/products/maka/bundle/src/installation.ts
@@ -53,7 +53,12 @@ export function installation(
confirmation: 'preapproved',
nestable: ['files', 'processes', 'web', 'skills'].includes(service),
});
- add('files', 'readText', 'Read', 'Read text from a file in the selected workspace.');
+ add(
+ 'files',
+ 'readText',
+ 'Read',
+ 'Read text from a file in the selected workspace or a Session attachment/archive resource. Pass a returned page.next object unchanged to continue reading.',
+ );
add('files', 'readImage', 'ReadImage', 'Read an image in the selected workspace.', 'image');
add('files', 'glob', 'Glob', 'Find files in the selected workspace.');
add('files', 'grep', 'Grep', 'Search file contents in the selected workspace.');
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 41b3730410..ee9e6f0ec2 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -391,7 +391,7 @@
"products/maka/bundle/package.json": "dd17e0a82ab68c03cdfd8adb3e243d4c5e881530db66ff311cb642b3bdc1f7c0",
"products/maka/bundle/scripts/build.mjs": "db11d8cd6f29ef7ead087261848568da1ad3889a870a89f23fa969449d769d47",
"products/maka/bundle/src/composition.ts": "1615a7c723ec37679108d7e2da5d718068ed919b39b57dfde14fb8a5c3f7345c",
- "products/maka/bundle/src/installation.ts": "614e9ed4ee41c26f9a398da21b1c5a4b41d91a22080a472d64207c5826e491fc",
+ "products/maka/bundle/src/installation.ts": "df8f08d67cf6f8cb2367b1a5c1bdc61db694696a89f928a97d0f6895ed8ab95d",
"products/maka/cli/Cargo.toml": "2a41d9dd434d08bb62cfe784e32b9e9facd41c58bbe9eb1b809b3fde3a2ed446",
"products/maka/cli/build.rs": "852960c81f7f6ee03e882eabfa07603bcb8727b98f22a3e6e2552e6e375668cd",
"products/maka/cli/build/product.rs": "6302eefa7c818dbd73e75f7796d331542315b79032a62727fa43f53413c25766",
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org