feat: add manifest output upload limits

This commit is contained in:
2026-05-12 16:20:15 +09:00
parent 53b508abae
commit 8d2ca5d530
8 changed files with 191 additions and 26 deletions
+61 -3
View File
@@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::{
CompactionConfig, MemoryConfig, PodManifest, PodMeta, ScopeConfig, SkillsConfig,
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WorkerManifest,
CompactionConfig, FileUploadLimits, MemoryConfig, PodManifest, PodMeta, ScopeConfig,
SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WorkerManifest,
};
/// Partial-form Pod manifest. Every field is optional; one or more
@@ -81,6 +81,8 @@ pub struct WorkerManifestConfig {
pub reasoning: Option<ReasoningControl>,
#[serde(default)]
pub tool_output: ToolOutputLimitsPartial,
#[serde(default)]
pub file_upload: FileUploadLimitsPartial,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -91,6 +93,12 @@ pub struct ToolOutputLimitsPartial {
pub per_tool: HashMap<String, usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileUploadLimitsPartial {
#[serde(default)]
pub max_bytes: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PermissionConfigPartial {
#[serde(default)]
@@ -158,6 +166,9 @@ impl PodManifestConfig {
default_max_bytes: Some(defaults::TOOL_OUTPUT_MAX_BYTES),
per_tool: HashMap::new(),
},
file_upload: FileUploadLimitsPartial {
max_bytes: Some(defaults::FILE_UPLOAD_MAX_BYTES),
},
..Default::default()
},
..Default::default()
@@ -287,6 +298,7 @@ impl WorkerManifestConfig {
stop_sequences: upper.stop_sequences.or(self.stop_sequences),
reasoning: upper.reasoning.or(self.reasoning),
tool_output: self.tool_output.merge(upper.tool_output),
file_upload: self.file_upload.merge(upper.file_upload),
}
}
}
@@ -302,6 +314,14 @@ impl ToolOutputLimitsPartial {
}
}
impl FileUploadLimitsPartial {
fn merge(self, upper: Self) -> Self {
Self {
max_bytes: upper.max_bytes.or(self.max_bytes),
}
}
}
impl PermissionConfigPartial {
fn merge(mut self, upper: Self) -> Self {
self.rules.extend(upper.rules);
@@ -423,6 +443,13 @@ impl TryFrom<PodManifestConfig> for PodManifest {
.unwrap_or(defaults::TOOL_OUTPUT_MAX_BYTES),
per_tool: cfg.worker.tool_output.per_tool,
},
file_upload: FileUploadLimits {
max_bytes: cfg
.worker
.file_upload
.max_bytes
.unwrap_or(defaults::FILE_UPLOAD_MAX_BYTES),
},
};
if cfg.scope.allow.is_empty() {
@@ -858,6 +885,29 @@ mod tests {
assert_eq!(to.per_tool.get("Grep"), Some(&512));
}
#[test]
fn merge_file_upload_max_bytes_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
file_upload: FileUploadLimitsPartial {
max_bytes: Some(8192),
},
..Default::default()
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
file_upload: FileUploadLimitsPartial {
max_bytes: Some(54_321),
},
..Default::default()
},
..Default::default()
};
let merged = lower.merge(upper);
assert_eq!(merged.worker.file_upload.max_bytes, Some(54_321));
}
#[test]
fn merge_option_struct_field_wise() {
let lower = PodManifestConfig {
@@ -1000,12 +1050,16 @@ permission = "write"
}
#[test]
fn builtin_defaults_populates_tool_output_max_bytes() {
fn builtin_defaults_populates_worker_limit_defaults() {
let cfg = PodManifestConfig::builtin_defaults();
assert_eq!(
cfg.worker.tool_output.default_max_bytes,
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
);
assert_eq!(
cfg.worker.file_upload.max_bytes,
Some(defaults::FILE_UPLOAD_MAX_BYTES)
);
}
#[test]
@@ -1039,6 +1093,10 @@ permission = "write"
manifest.worker.tool_output.default_max_bytes,
defaults::TOOL_OUTPUT_MAX_BYTES
);
assert_eq!(
manifest.worker.file_upload.max_bytes,
defaults::FILE_UPLOAD_MAX_BYTES
);
}
#[test]
+5 -1
View File
@@ -8,7 +8,11 @@
/// Byte-size cap applied to any tool's `content` output when no
/// per-tool override is set. See [`crate::ToolOutputLimits`].
pub const TOOL_OUTPUT_MAX_BYTES: usize = 16 * 1024;
pub const TOOL_OUTPUT_MAX_BYTES: usize = 64 * 1024;
/// Byte-size cap applied to each submit-time FileRef upload / attachment.
/// See [`crate::FileUploadLimits`].
pub const FILE_UPLOAD_MAX_BYTES: usize = 256 * 1024;
/// Number of most-recent turns protected from pruning. See
/// [`crate::CompactionConfig::prune_protected_turns`].
+44 -8
View File
@@ -7,8 +7,8 @@ mod scope;
pub use cascade::{LayerLoadError, find_project_manifest_from, load_layer};
pub use config::{
CompactionConfigPartial, PermissionConfigPartial, PodManifestConfig, PodMetaConfig,
ResolveError, ToolOutputLimitsPartial, WorkerManifestConfig,
CompactionConfigPartial, FileUploadLimitsPartial, PermissionConfigPartial, PodManifestConfig,
PodMetaConfig, ResolveError, ToolOutputLimitsPartial, WorkerManifestConfig,
};
pub use model::{
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
@@ -183,10 +183,15 @@ pub struct WorkerManifest {
pub reasoning: Option<ReasoningControl>,
/// Byte-size caps applied to tool `content` before it reaches the
/// conversation history. The section is optional in TOML — when
/// omitted, `ToolOutputLimits::default()` (16KB default cap, no
/// omitted, `ToolOutputLimits::default()` (64 KiB default cap, no
/// per-tool overrides) is applied so truncation is on by default.
#[serde(default)]
pub tool_output: ToolOutputLimits,
/// Byte-size cap applied to submit-time FileRef uploads / attachments.
/// This is intentionally separate from tool-output truncation because
/// user-requested file attachments can usually tolerate a larger budget.
#[serde(default)]
pub file_upload: FileUploadLimits,
}
/// Byte-size caps applied to tool execution `content` before it enters
@@ -206,10 +211,26 @@ pub struct ToolOutputLimits {
pub per_tool: HashMap<String, usize>,
}
/// Byte-size cap for submit-time FileRef uploads / attachments.
///
/// This governs the `[File: <path>]` system-message attachment produced
/// when a user explicitly submits a `@<path>` reference. It does not affect
/// tool result truncation; see [`ToolOutputLimits`] for that path.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileUploadLimits {
/// Cap applied to each resolved FileRef body.
#[serde(default = "default_file_upload_max_bytes")]
pub max_bytes: usize,
}
fn default_tool_output_max_bytes() -> usize {
defaults::TOOL_OUTPUT_MAX_BYTES
}
fn default_file_upload_max_bytes() -> usize {
defaults::FILE_UPLOAD_MAX_BYTES
}
fn default_instruction() -> String {
defaults::DEFAULT_INSTRUCTION.to_string()
}
@@ -223,6 +244,14 @@ impl Default for ToolOutputLimits {
}
}
impl Default for FileUploadLimits {
fn default() -> Self {
Self {
max_bytes: default_file_upload_max_bytes(),
}
}
}
impl ToolOutputLimits {
/// Resolve the cap for a given tool name.
pub fn limit_for(&self, tool_name: &str) -> usize {
@@ -635,15 +664,19 @@ model_id = "claude-sonnet-4-20250514"
}
#[test]
fn omitted_tool_output_falls_back_to_default_16k() {
fn omitted_limits_fall_back_to_defaults() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let limits = &manifest.worker.tool_output;
assert_eq!(limits.default_max_bytes, 16 * 1024);
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
assert!(limits.per_tool.is_empty());
assert_eq!(
manifest.worker.file_upload.max_bytes,
defaults::FILE_UPLOAD_MAX_BYTES
);
}
#[test]
fn parse_tool_output_limits() {
fn parse_worker_output_limits() {
let toml = MINIMAL_REQUIRED.replace(
"[worker]\n",
"[worker]\n\
@@ -651,7 +684,9 @@ model_id = "claude-sonnet-4-20250514"
default_max_bytes = 8192\n\n\
[worker.tool_output.per_tool]\n\
Read = 32768\n\
Grep = 4096\n",
Grep = 4096\n\n\
[worker.file_upload]\n\
max_bytes = 12345\n",
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let limits = &manifest.worker.tool_output;
@@ -659,6 +694,7 @@ model_id = "claude-sonnet-4-20250514"
assert_eq!(limits.limit_for("Read"), 32768);
assert_eq!(limits.limit_for("Grep"), 4096);
assert_eq!(limits.limit_for("Unknown"), 8192);
assert_eq!(manifest.worker.file_upload.max_bytes, 12345);
}
#[test]
@@ -670,7 +706,7 @@ model_id = "claude-sonnet-4-20250514"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let limits = &manifest.worker.tool_output;
assert_eq!(limits.default_max_bytes, 16 * 1024);
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
assert!(limits.per_tool.is_empty());
}