Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/contribute-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ runtime or bindings.
- `RunRequest.relay_session_root` is forwarded as the typed `AgentRunRequest.relay_session_root` field. Keep Rust, Python, TypeScript, and schema representations in parity. Context keys do not control Relay propagation; preserve the adapter fallback for unusable UUIDs.
- OpenShell task commands serialize public SDK config/request/handle/result mappings. Keep forwarding buffered and preserve task-generated IDs, normalized failed results, telemetry references, and declared artifacts. Do not normalize an SDK result as an adapter result. Task paths remain POSIX strings; host artifact destinations stay separate. Unknown outcomes bind the immutable sandbox identity through reattachment and must not be replayed.
- Preserve normalized `runtime.timeout_seconds` across host and task SDKs: positive finite values enforce the invocation deadline; omission/null disables the total invocation deadline. Keep lifecycle, health, and cleanup budgets separate, and do not replace disabled invocation deadlines with transport defaults.
- Keep `runtime.response_limits` SDK-owned across host/task execution. Preserve inclusive byte/file budgets, explicit limit diagnostics, and transport allowances derived from configured budgets. Recheck convenience-run responses after cleanup adds stop events or errors, and preserve structured collection-time limit failures. Do not add adapter capability requirements or independent compressed-file ceilings. Use `tests/e2e/response-limits-checklist.md` when changing these paths.
- Update docs and examples in the same branch

## Minimum Acceptance
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ NeMo Fabric gives applications, evaluation systems, and rollout platforms a cons
- **Switch harnesses:** Reuse the consumer integration while adapters handle harness-specific behavior.
- **Compare outcomes:** Collect normalized results, errors, artifacts, lifecycle events, and telemetry references.

Invocation response and artifact budgets are configured under
[`runtime.response_limits`](docs/sdk/python.mdx#response-and-artifact-limits),
with the same policy for host and OpenShell execution.

## Supported Agent Harnesses

NeMo Fabric includes adapters for the following harnesses and execution targets. Select a harness name for its adapter documentation:
Expand Down
2 changes: 1 addition & 1 deletion adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ and additive extension maps because their support does not vary by adapter:
| `models.<role>.max_tokens` | No | No | Yes | Yes | Yes: passed through LiteLLM | No | Yes | Yes | No | No | No | No | No | No | No | No | Yes: translated to the selected API protocol |
| `instructions.system` | `replace`, `append` | `replace` with base instructions | `replace` | `replace` | `replace` | `replace` | `replace` | `replace`, `append` | `replace` with Cline override prompt | `replace`, `append` | `replace` with OpenCode base instructions | `replace` with Pi base instructions | `replace`, `append` | `replace` | No | `append` | `replace` |
| `runtime.input_schema`, `.output_schema` | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core |
| `runtime.artifacts`, `.timeout_seconds` | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core |
| `runtime.artifacts`, `.timeout_seconds`, `.response_limits` | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core |
| `runtime.max_turns` | Yes | No | Yes; maps to LangGraph supersteps | Yes; iteration limit | Yes | No | No | Yes; per invocation | No | No | No | No | No | Yes; maps to build-agent steps | No | No | No |
| `environment.provider`, `.control_location`, `.ownership` | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core |
| `environment.workspace`, `.artifacts`, `.env`, `.env_names` | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core | Core |
Expand Down
1 change: 1 addition & 0 deletions crates/fabric-cli/src/presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ fn config(
output_schema: "message".to_string(),
artifacts: None,
timeout_seconds: None,
response_limits: None,
max_turns: None,
extensions: BTreeMap::new(),
},
Expand Down
1 change: 1 addition & 0 deletions crates/fabric-cli/templates/rust/main.rs.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn build_config() -> FabricConfig {
output_schema: "message".to_string(),
artifacts: None,
timeout_seconds: {{TIMEOUT_SECONDS}},
response_limits: None,
max_turns: {{MAX_TURNS}},
extensions: BTreeMap::new(),
},
Expand Down
128 changes: 128 additions & 0 deletions crates/fabric-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,9 @@ pub struct RuntimeConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("exclusiveMinimum" = 0.0))]
pub timeout_seconds: Option<f64>,
/// Response and declared-artifact budgets enforced by the SDK.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_limits: Option<ResponseLimits>,
/// Maximum number of harness turns within one invocation.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = u32::MAX))]
Expand All @@ -1068,6 +1071,53 @@ pub struct RuntimeConfig {
pub extensions: BTreeMap<String, Value>,
}

/// Inclusive budgets for a buffered invocation result and declared artifact files.
/// Omitted fields retain the default budget. These limits are owned by the SDK,
/// independently of the harness and execution environment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct ResponseLimits {
/// Maximum compact UTF-8 JSON bytes in the normalized RunResult.
#[schemars(range(min = 1))]
pub max_response_bytes: u32,
/// Maximum aggregate uncompressed bytes in unique declared artifact files.
#[schemars(range(min = 1))]
pub max_artifact_bytes: u32,
/// Maximum number of unique declared artifact files.
#[schemars(range(min = 1))]
pub max_artifact_files: u32,
/// Maximum uncompressed bytes in one declared artifact file.
#[schemars(range(min = 1))]
pub max_artifact_file_bytes: u32,
}

impl Default for ResponseLimits {
fn default() -> Self {
Self {
max_response_bytes: 4 * 1024 * 1024,
max_artifact_bytes: 2 * 1024 * 1024,
max_artifact_files: 16,
max_artifact_file_bytes: 1024 * 1024,
}
}
}

impl ResponseLimits {
// Transport envelopes and compression are implementation details, not extra
// user budgets. Allow JSON escaping/decimal byte arrays plus bounded metadata.
pub(crate) fn response_transport_bytes(&self) -> usize {
(self.max_response_bytes as usize)
.saturating_mul(6)
.saturating_add(1024 * 1024)
}

pub(crate) fn artifact_transport_bytes(&self) -> usize {
(self.max_artifact_bytes as usize)
.saturating_mul(8)
.saturating_add(self.response_transport_bytes())
}
}

fn default_input_schema() -> String {
"text".to_string()
}
Expand Down Expand Up @@ -1924,6 +1974,21 @@ pub(crate) fn validate_config(config: &FabricConfig) -> Result<()> {
}
}
}
if let Some(limits) = config.runtime.response_limits {
for (name, value) in [
("max_response_bytes", limits.max_response_bytes),
("max_artifact_bytes", limits.max_artifact_bytes),
("max_artifact_files", limits.max_artifact_files),
("max_artifact_file_bytes", limits.max_artifact_file_bytes),
] {
if value == 0 {
return invalid_config(
format!("runtime.response_limits.{name}"),
"must be greater than zero",
);
}
}
}
if config.runtime.max_turns == Some(0) {
return invalid_config("runtime.max_turns", "must be greater than zero");
}
Expand Down Expand Up @@ -4512,6 +4577,69 @@ mod tests {
.expect("typed config")
}

#[test]
fn transport_budgets_saturate_at_the_platform_ceiling() {
let limits = ResponseLimits {
max_response_bytes: u32::MAX,
max_artifact_bytes: u32::MAX,
..Default::default()
};
let response = u128::from(u32::MAX) * 6 + 1024 * 1024;
let artifact = u128::from(u32::MAX) * 8 + response;
assert_eq!(
limits.response_transport_bytes(),
response.min(usize::MAX as u128) as usize
);
assert_eq!(
limits.artifact_transport_bytes(),
artifact.min(usize::MAX as u128) as usize
);
}

#[test]
fn response_limit_defaults_validation_and_unknown_fields() {
let limits: ResponseLimits =
serde_json::from_value(serde_json::json!({"max_response_bytes": 8})).unwrap();
assert_eq!(limits.max_response_bytes, 8);
assert_eq!(limits.max_artifact_files, 16);
assert_eq!(limits.max_artifact_bytes, 2 * 1024 * 1024);
assert_eq!(limits.max_artifact_file_bytes, 1024 * 1024);
assert!(
serde_json::from_value::<ResponseLimits>(serde_json::json!({"max_response_byte": 8}))
.is_err()
);
for field in [
"max_response_bytes",
"max_artifact_bytes",
"max_artifact_files",
"max_artifact_file_bytes",
] {
for value in [
serde_json::json!(0),
serde_json::json!(-1),
serde_json::json!(1.5),
serde_json::json!(true),
serde_json::json!("12"),
Value::Null,
] {
let mut config = typed_config("test.host");
let value = serde_json::from_value(serde_json::json!({field: value}));
match value {
Err(_) => {}
Ok(limits) => {
config.runtime.response_limits = Some(limits);
let error = validate_config(&config).expect_err("invalid limit");
assert!(
error
.to_string()
.contains(&format!("runtime.response_limits.{field}"))
);
}
}
}
}
}

fn config_with_model(adapter_id: &str, provider: &str) -> FabricConfig {
let mut config = typed_config(adapter_id);
config.models.insert(
Expand Down
Loading
Loading