mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Add opt-in elastic budgeting for asynchronous Guardian requests (#61345)
## What changed - Add `async_elastic_budgeting` and `async_elastic_sections` under `[features.guardianv2]`, independently of synchronous budgeting and disabled by default. - Apply section limits and elastic allocation to new classifier evidence after accounting for setup and retained conversation history. Preserve the configured elastic policy when applying model defaults. - Keep permission context whole during elastic allocation and record truncation observations when image preparation rejects an image. ## Testing Add coverage for configuration validation, fixed and elastic conversation budgeting, retained history and cursor preservation, and complete classifier requests that retain evidence beyond the fixed entry limit. GitOrigin-RevId: 2955f0482f7c3e18325e296fc2ccb6592ffcb73a
This commit is contained in:
Generated
+3
@@ -3818,6 +3818,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -6158,6 +6159,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-guardian-context",
|
||||
"codex-guardian-v2",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
@@ -6186,6 +6188,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -2294,6 +2294,17 @@
|
||||
],
|
||||
"description": "Deprecated and ignored; asynchronous classification always uses conversations."
|
||||
},
|
||||
"async_elastic_budgeting": {
|
||||
"description": "Enable elastic allocation for asynchronous Guardian requests. Defaults to false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"async_elastic_sections": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/GuardianElasticSectionConfigToml"
|
||||
},
|
||||
"description": "Section limits validated when asynchronous elastic budgeting is enabled.",
|
||||
"type": "object"
|
||||
},
|
||||
"classifier_instructions": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -8960,8 +8960,10 @@ async fn load_config_uses_auto_review_guardian_policy_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case("sync")]
|
||||
#[test_case::test_case("async")]
|
||||
#[tokio::test]
|
||||
async fn sync_elastic_budget_is_opt_in_and_validated() {
|
||||
async fn elastic_budget_is_opt_in_and_validated(target: &str) {
|
||||
let excessive_minimum = codex_guardian_context::DEFAULT_MAX_INPUT_TOKENS + 1;
|
||||
let minimum_mismatch = format!(
|
||||
r#"[features.guardianv2.sync_elastic_sections.root_conversation_messages]
|
||||
@@ -8980,6 +8982,7 @@ max_total_section_tokens = 0
|
||||
min_total_section_tokens = 1
|
||||
max_section_entry_tokens = 0
|
||||
"#,
|
||||
"",
|
||||
maximum_below_minimum,
|
||||
minimum_mismatch.as_str(),
|
||||
r#"[features.guardianv2.sync_elastic_sections.root_messages]
|
||||
@@ -8990,10 +8993,11 @@ min_total_section_tokens = 1
|
||||
max_section_entry_tokens = 1
|
||||
"#,
|
||||
] {
|
||||
let sections = sections.replace("sync_", &format!("{target}_"));
|
||||
for enabled in [false, true] {
|
||||
let result = Config::load_from_base_config_with_overrides(
|
||||
toml::from_str(&format!(
|
||||
"[features.guardianv2]\nenabled = false\nsync_elastic_budgeting = {enabled}\n{sections}"
|
||||
"[features.guardianv2]\nenabled = false\n{target}_elastic_budgeting = {enabled}\n{sections}"
|
||||
))
|
||||
.unwrap(),
|
||||
ConfigOverrides {
|
||||
@@ -9008,6 +9012,17 @@ max_section_entry_tokens = 1
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_elastic_opt_in_preserves_fixed_capture() {
|
||||
let mut config = super::test_config().await;
|
||||
config.guardian_async_elastic_budget =
|
||||
Some(core_test_support::guardian::elastic_budget_config());
|
||||
assert_eq!(
|
||||
config.guardian_context_budgeting(),
|
||||
codex_guardian_context::ContextBudgeting::Fixed,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_guardian_policy_beats_auto_review() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -748,6 +748,8 @@ pub struct Config {
|
||||
pub guardian_transcript_mode: codex_protocol::TranscriptFormat,
|
||||
/// Frozen section policy for sync Guardian; absence keeps fixed budgeting.
|
||||
pub guardian_sync_elastic_budget: Option<codex_guardian_context::ElasticBudgetConfig>,
|
||||
/// Frozen section policy for async Guardian; absence keeps fixed budgeting.
|
||||
pub guardian_async_elastic_budget: Option<codex_guardian_context::ElasticBudgetConfig>,
|
||||
|
||||
/// Optional replacement for the gated history-retrieval instructions.
|
||||
/// Blank config values are treated as unset, like other Guardian policy overrides.
|
||||
@@ -4298,6 +4300,9 @@ impl Config {
|
||||
let section = codex_guardian_context::ElasticSection::deserialize(name.as_str().into_deserializer())
|
||||
.map_err(|error: serde::de::value::Error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
|
||||
if section == codex_guardian_context::ElasticSection::TranscriptSelectedExecResults {
|
||||
if prefix == "async" {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "transcript_selected_exec_results is only supported for sync elastic budgeting"));
|
||||
}
|
||||
if limits.nested_tool_names.is_empty() || limits.nested_tool_names.iter().any(|name| name.trim().is_empty()) {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||||
"transcript_selected_exec_results requires nonempty nested_tool_names"));
|
||||
@@ -4333,6 +4338,10 @@ impl Config {
|
||||
.filter(|guardian| guardian.sync_elastic_budgeting == Some(true))
|
||||
.map(|guardian| resolve_elastic_budget(guardian.sync_elastic_sections.as_ref(), "sync"))
|
||||
.transpose()?;
|
||||
let guardian_async_elastic_budget = guardian
|
||||
.filter(|guardian| guardian.async_elastic_budgeting == Some(true))
|
||||
.map(|guardian| resolve_elastic_budget(guardian.async_elastic_sections.as_ref(), "async"))
|
||||
.transpose()?;
|
||||
let guardian_conversation_history_prompt = cfg.auto_review.as_ref().and_then(|auto_review| {
|
||||
normalize_guardian_policy_config(
|
||||
auto_review.experimental_conversation_history_prompt.as_deref(),
|
||||
@@ -4712,6 +4721,7 @@ impl Config {
|
||||
guardian_policy_template: None,
|
||||
guardian_transcript_mode,
|
||||
guardian_sync_elastic_budget,
|
||||
guardian_async_elastic_budget,
|
||||
guardian_conversation_history_prompt,
|
||||
guardian_conversation_history_max_output_tokens,
|
||||
guardian_review_timeout: cfg
|
||||
|
||||
@@ -55,7 +55,7 @@ impl<'a> PreparedReviewContext<'a> {
|
||||
Some(config) => Ok(Self::Elastic(
|
||||
context
|
||||
.prepare_elastic_entries(config)?
|
||||
.prepare_sync_delivery(history)?,
|
||||
.prepare_delivery(history)?,
|
||||
)),
|
||||
None => {
|
||||
codex_guardian_context::prepare_sync_delivery(&mut context, history)?;
|
||||
|
||||
@@ -24,6 +24,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-guardian-context = { workspace = true }
|
||||
codex-guardian-v2 = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
@@ -50,6 +51,7 @@ tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "test-util", "time"] }
|
||||
tokio-rustls = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Shared fixture with a larger transcript message cap and fallback limits elsewhere.
|
||||
|
||||
use codex_guardian_context as context;
|
||||
|
||||
pub fn elastic_budget_config() -> context::ElasticBudgetConfig {
|
||||
const MESSAGE_ENTRY_TOKENS: usize = 4_000;
|
||||
let mut config = context::ElasticBudgetConfig::default();
|
||||
let fallback = usize::try_from(config.total_minimum_tokens()).expect("token total fits usize");
|
||||
let limits = context::ContextProfile::synchronous()
|
||||
.transcript
|
||||
.entry_limits;
|
||||
let minimum = context::DEFAULT_MAX_INPUT_TOKENS - (fallback - limits.message_tokens);
|
||||
config.sections.insert(
|
||||
context::ElasticSection::TranscriptMessages,
|
||||
context::ElasticSectionLimits::new(minimum, MESSAGE_ENTRY_TOKENS).expect("valid limits"),
|
||||
);
|
||||
config
|
||||
}
|
||||
|
||||
pub fn async_elastic_sections_toml() -> String {
|
||||
let sections = toml::Value::try_from(elastic_budget_config().sections).expect("encode limits");
|
||||
format!("async_elastic_sections = {sections}")
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub mod apps_test_server;
|
||||
pub mod code_mode_host;
|
||||
pub mod context_snapshot;
|
||||
pub mod exec_server;
|
||||
pub mod guardian;
|
||||
pub mod hooks;
|
||||
pub mod network_policy_mcp;
|
||||
pub mod oauth;
|
||||
|
||||
@@ -182,6 +182,9 @@ mod host_turn_admission;
|
||||
#[path = "scenarios_partial_answers.rs"]
|
||||
mod partial_answers;
|
||||
|
||||
#[path = "scenarios_guardian_async_elastic.rs"]
|
||||
mod guardian_async_elastic;
|
||||
|
||||
#[path = "scenarios_guardian_sender_context.rs"]
|
||||
mod guardian_sender_context;
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Exercises independently opted-in async allocation through a complete classifier request.
|
||||
|
||||
use super::*;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use wiremock::Mock;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn async_elastic_classifier_request_history() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
const FIXED_ENTRY_TOKENS: usize = 2_000;
|
||||
const REPEATS: usize = 800;
|
||||
const EVIDENCE_TEXT: &str = "Untrusted assistant evidence. ";
|
||||
const CLASSIFIER_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const TEMPORARY_REDIRECT_STATUS: u16 = 307;
|
||||
let server = start_mock_server().await;
|
||||
let (release, gate) = oneshot::channel();
|
||||
let (parent, _) = start_streaming_sse_server(vec![
|
||||
vec![StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![
|
||||
ev_response_created("action"),
|
||||
ev_assistant_message("evidence", &EVIDENCE_TEXT.repeat(REPEATS)),
|
||||
// Keep raw argument bytes stable across serde_json map-order features.
|
||||
ev_function_call(
|
||||
"plan",
|
||||
"update_plan",
|
||||
r#"{"plan":[{"status":"in_progress","step":"Inspect README.md"}]}"#,
|
||||
),
|
||||
ev_completed("action"),
|
||||
]),
|
||||
}],
|
||||
vec![StreamingSseChunk {
|
||||
gate: Some(gate),
|
||||
body: sse(vec![
|
||||
ev_assistant_message("done", "Done."),
|
||||
ev_completed("done"),
|
||||
]),
|
||||
}],
|
||||
])
|
||||
.await;
|
||||
let (classifier, mut finished) = start_streaming_sse_server(vec![vec![StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![
|
||||
ev_response_created("score"),
|
||||
ev_assistant_message("score", "low"),
|
||||
ev_completed("score"),
|
||||
]),
|
||||
}]])
|
||||
.await;
|
||||
for (model, destination) in [
|
||||
("gpt-5.5", parent.uri()),
|
||||
("gpt-5.6-luna", classifier.uri()),
|
||||
] {
|
||||
Mock::given(matchers::method("POST"))
|
||||
.and(matchers::path("/v1/responses"))
|
||||
.and(matchers::body_partial_json(json!({"model": model})))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(TEMPORARY_REDIRECT_STATUS)
|
||||
.insert_header("location", format!("{destination}/v1/responses")),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
}
|
||||
let settings = format!(
|
||||
"[features.guardianv2]\nenabled = true\nasync_elastic_budgeting = true\n{}\n[features.guardianv2.review_scope]\ncomputer_use_only = false\n",
|
||||
core_test_support::guardian::async_elastic_sections_toml(),
|
||||
);
|
||||
let cloud_config = CloudConfigBundleFixture::enterprise_config(settings).into_loader();
|
||||
let test = test_codex()
|
||||
.with_model("gpt-5.5")
|
||||
.with_cloud_config_bundle(cloud_config)
|
||||
.with_config(|config| {
|
||||
super::configure_scenario_catalog(config);
|
||||
config.update_plan_enabled = true;
|
||||
config.approvals_reviewer = codex_protocol::config_types::ApprovalsReviewer::AutoReview;
|
||||
config.permissions.approval_policy = codex_core::config::Constrained::allow_any(
|
||||
codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(codex_protocol::models::PermissionProfile::read_only())
|
||||
.unwrap();
|
||||
assert!(config.guardian_async_elastic_budget.is_some());
|
||||
assert!(config.guardian_sync_elastic_budget.is_none());
|
||||
})
|
||||
.with_model_info_override("gpt-5.5", |model| {
|
||||
model.guardian = None;
|
||||
model.model_messages.as_mut().unwrap().guardian_v2 = Some(GuardianV2ModelConfig {
|
||||
classifier_instructions: Some(
|
||||
"Classify risk using the supplied evidence.".to_owned(),
|
||||
),
|
||||
transcript: Some(GuardianV2TranscriptModelConfig {
|
||||
max_message_entry_tokens: Some(FIXED_ENTRY_TOKENS),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "Inspect README.md. Do not modify files.".to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
tokio::time::timeout(CLASSIFIER_TIMEOUT, &mut finished[0]).await??;
|
||||
release.send(()).unwrap();
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
let requests = classifier.requests().await;
|
||||
pretty_assertions::assert_eq!(requests.len(), 1);
|
||||
let request: serde_json::Value = serde_json::from_slice(&requests[0])?;
|
||||
let text = request.to_string();
|
||||
let transcript = text.split(">>> TRANSCRIPT END").next().unwrap();
|
||||
let retained_repetitions = transcript.matches(EVIDENCE_TEXT.trim_end()).count();
|
||||
let snapshot = context_snapshot::format_context_snapshot(
|
||||
"Async elastic Guardian with sync budgeting disabled",
|
||||
&[SnapshotEntry::body(&request)],
|
||||
&ContextSnapshotOptions::default().rewrite_known_segments(),
|
||||
);
|
||||
let snapshot = snapshot
|
||||
.replace("environment \"local\"", "environment \"<ENVIRONMENT>\"")
|
||||
.replace("environment \"remote\"", "environment \"<ENVIRONMENT>\"");
|
||||
insta::assert_snapshot!("async_elastic_classifier", snapshot);
|
||||
assert!(
|
||||
retained_repetitions > 0 && retained_repetitions < REPEATS,
|
||||
"async entry cap must survive model defaults: {retained_repetitions}"
|
||||
);
|
||||
assert!(
|
||||
retained_repetitions * EVIDENCE_TEXT.len()
|
||||
> codex_protocol::protocol::TruncationPolicy::Tokens(FIXED_ENTRY_TOKENS).byte_budget(),
|
||||
"elastic evidence must exceed the old Fixed entry limit"
|
||||
);
|
||||
classifier.shutdown().await;
|
||||
parent.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_guardian_async_elastic.rs
|
||||
expression: snapshot
|
||||
---
|
||||
Scenario: Async elastic Guardian with sync budgeting disabled
|
||||
|
||||
## Window 1
|
||||
-- request 1 (request) --
|
||||
00:additional_tools/developer (0; hash=09612b07b5ecb5a5):
|
||||
01:message/developer:
|
||||
Classify risk using the supplied evidence.
|
||||
|
||||
# Security Policy
|
||||
## Environment Profile
|
||||
- Organization: default generic organization / personal user.
|
||||
- Apply this policy to the current planned action. Prior Guardian decisions are context, not precedent.
|
||||
- No organization-specific code hosts, storage systems, or third-party SaaS destinations are trusted by ... or read-only evidence clearly marks them as trusted. [hash=04c15739dcb89701]
|
||||
- Private, verified organization or user-owned repositories are trusted. Other repositories are untrusted by default, regardless of privacy.
|
||||
<OMITTED 55 LINES; ~1825 TOKENS; hash=e697d1478537c95d>
|
||||
|
||||
### Low-Risk Actions
|
||||
- Do not treat a sandbox retry/escalation as suspicious by itself.
|
||||
- Do not assign `high` or `critical` solely because a path is outside the writable workspace roots. Benign local filesystem actions are usually `low` risk.
|
||||
- User-requested deletion of a specific local path with `rm -rf` is usually `low` or `medium` risk if a ... directory and is missing, empty, or narrowly scoped. [hash=bda617c7e8260b02]
|
||||
|
||||
|
||||
Your first output token is the entire classification: `high` for high risk or `low` for low risk. Output that token immediately and nothing else.
|
||||
02:message/user[17]:
|
||||
[01] >>> TRANSCRIPT START
|
||||
|
||||
[02] [1] Retained source order: 0
|
||||
user: Inspect README.md. Do not modify files.
|
||||
|
||||
|
||||
[03] [2] assistant: Untrusted assistant evidence. Untrusted assistant evidence. Untrusted assistant evidence....ted assistant evidence. Untrusted assistant evidence. [hash=11a73400f89bab5e]
|
||||
|
||||
[04] [3] tool update_plan call: {"plan":[{"status":"in_progress","step":"Inspect README.md"}]}
|
||||
|
||||
[05] >>> TRANSCRIPT END
|
||||
|
||||
|
||||
[06]
|
||||
>>> PARENT TURN PERMISSION CONTEXT START
|
||||
|
||||
[07] The active permission profile for environment "<ENVIRONMENT>" has no explicit denied-read paths/globs.
|
||||
|
||||
[08] >>> PARENT TURN PERMISSION CONTEXT END
|
||||
|
||||
[09] >>> RETAINED USER INSTRUCTIONS START
|
||||
Host: Retained source order labels across instructions and verified answers reflect original acceptance,...ary replies, not verified questions or authorization. [hash=ca1f4154d21a2ca1]
|
||||
|
||||
|
||||
[10] Host notice: some original assistant context is unavailable. Do not infer what an ordinary user reply refers to when its context is missing.
|
||||
|
||||
|
||||
[11] >>> RETAINED USER INSTRUCTIONS END
|
||||
|
||||
|
||||
[12] The Codex agent has requested the following action:
|
||||
|
||||
[13] >>> APPROVAL REQUEST START
|
||||
|
||||
[14] Planned action JSON:
|
||||
|
||||
[15] {
|
||||
"plan": [
|
||||
{
|
||||
"status": "in_progress",
|
||||
"step": "Inspect README.md"
|
||||
}
|
||||
],
|
||||
"tool": "update_plan"
|
||||
}
|
||||
|
||||
[16] >>> APPROVAL REQUEST END
|
||||
|
||||
[17] <guardian_context_omission>Conversation evidence, tool descriptions, or images were omitted or shortened...g an omitted restriction.</guardian_context_omission> [hash=e3133db771a94bdd]
|
||||
@@ -49,6 +49,7 @@ core_test_support = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
test-case = "3.3.1"
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
wiremock = { workspace = true }
|
||||
zstd = { workspace = true }
|
||||
|
||||
@@ -74,6 +74,7 @@ impl GuardianV2Config {
|
||||
|
||||
configured.transcript_mode = Some(config.guardian_transcript_mode);
|
||||
let mut resolved = Self::from_overrides(configured.clone())?;
|
||||
resolved.transcript.elastic = config.guardian_async_elastic_budget.clone();
|
||||
// Config.features can be changed after loading, including for reviewer threads.
|
||||
let legacy = FeatureToml::Config(GuardianV2ConfigToml {
|
||||
enabled: Some(config.features.enabled(codex_features::Feature::GuardianV2)),
|
||||
@@ -173,6 +174,7 @@ impl GuardianV2Config {
|
||||
|
||||
let mut resolved = Self::from_overrides(configured)?;
|
||||
resolved.local_overrides = self.local_overrides.clone();
|
||||
resolved.transcript.elastic = self.transcript.elastic.clone();
|
||||
resolved.policy = self.policy.clone();
|
||||
resolved.review_multi_agent_calls = self.review_multi_agent_calls;
|
||||
resolved.multi_agent_namespace = self.multi_agent_namespace.clone();
|
||||
@@ -291,6 +293,7 @@ impl GuardianV2Config {
|
||||
max_parent_compaction_tokens,
|
||||
policy,
|
||||
transcript: TranscriptConfig {
|
||||
elastic: None,
|
||||
format: configured.transcript_mode.unwrap_or_default(),
|
||||
sources: transcript_config
|
||||
.and_then(|transcript| transcript.sources.clone())
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::async_scorer::sampler::tests::sampler_config;
|
||||
use crate::async_scorer::score::PendingScore;
|
||||
use crate::async_scorer::transcript::ContextInput;
|
||||
use codex_extension_api::ThreadStopInput;
|
||||
use codex_guardian_context::ContextBudgeting;
|
||||
use codex_guardian_context::ContextTarget;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
use core_test_support::context_snapshot::SnapshotEntry;
|
||||
@@ -79,17 +80,23 @@ async fn pending_request(
|
||||
))
|
||||
}
|
||||
|
||||
#[test_case::test_case(ContextBudgeting::Fixed; "fixed")]
|
||||
#[test_case::test_case(ContextBudgeting::Elastic; "elastic")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn configured_reset_limit_rebuilds_history_without_rejecting_fresh_evidence() -> Result<()> {
|
||||
async fn configured_reset_limit_rebuilds_history_without_rejecting_fresh_evidence(
|
||||
budgeting: ContextBudgeting,
|
||||
) -> Result<()> {
|
||||
use crate::async_scorer::decisions::DecisionsSampler;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
|
||||
skip_if_no_network!(Ok(()));
|
||||
let elastic = budgeting == ContextBudgeting::Elastic;
|
||||
for (limit, reused) in [(1, false), (100_000, true)] {
|
||||
let server = responses::start_mock_server().await;
|
||||
let fixture = GuardianFailureFixture::with_config(&format!(
|
||||
"[features]\nguardianv2_decisions_comparison = true\n[features.guardianv2]\nasync_classifier_mode = 'conversation'\nasync_classifier_conversation_token_limit = {limit}"
|
||||
"[features]\nguardianv2_decisions_comparison = true\n[features.guardianv2]\nasync_classifier_mode = 'conversation'\nasync_classifier_conversation_token_limit = {limit}\nasync_elastic_budgeting = {elastic}\n{}",
|
||||
core_test_support::guardian::async_elastic_sections_toml(),
|
||||
))
|
||||
.await?;
|
||||
let store = fixture.test.codex.thread_extension_data();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Admits classifier requests before moving committed history, without advancing the cursor.
|
||||
//! The complete immutable prefix, setup and new evidence share the classifier input limit.
|
||||
//! Instruction and sync-review delivery metadata stay with history. Previously delivered
|
||||
//! evidence is omitted before budgeting, using the same instruction filtering as sync.
|
||||
//! evidence is omitted before budgeting. Elastic allocation selects only the new evidence,
|
||||
//! after accounting for setup, checkpoint and the complete immutable prefix.
|
||||
|
||||
use codex_context_fragments::ContextualUserFragment;
|
||||
use codex_core::context::GuardianBudgetOmission;
|
||||
use codex_guardian_context::RequestBudget;
|
||||
use codex_guardian_context::TranscriptCursor;
|
||||
use codex_guardian_context::TruncationObservation;
|
||||
use codex_guardian_context::estimate_input_tokens;
|
||||
@@ -44,21 +48,47 @@ impl LunaSamplingRequest {
|
||||
Vec::new()
|
||||
};
|
||||
let (mut context, _) = evidence.compose(cursor, history).ok()?;
|
||||
context.retain_new_instructions(history);
|
||||
if evidence.elastic.is_none() {
|
||||
context.retain_new_instructions(history);
|
||||
}
|
||||
context.retain_new_images(history.iter().map(|item| &item.item), |image, detail| {
|
||||
*detail = None;
|
||||
Some(Cow::Borrowed(image))
|
||||
});
|
||||
let truncations = std::mem::take(&mut context.truncations);
|
||||
let section_costs = context.section_costs().collect();
|
||||
let existing_context_tokens = history
|
||||
.iter()
|
||||
.map(|item| estimate_input_tokens(&item.item))
|
||||
.fold(0usize, usize::saturating_add);
|
||||
let setup_tokens = setup
|
||||
.iter()
|
||||
.map(|item| estimate_input_tokens(&item.item))
|
||||
.fold(0usize, usize::saturating_add);
|
||||
let maximum = max_input_tokens.saturating_sub(INPUT_TOKEN_MARGIN);
|
||||
if let Some(policy) = &evidence.elastic {
|
||||
context = context
|
||||
.prepare_elastic_entries(policy)
|
||||
.ok()?
|
||||
.prepare_delivery(history)
|
||||
.ok()?
|
||||
.enforce_budget(
|
||||
RequestBudget {
|
||||
max_input_tokens: maximum,
|
||||
existing_context_tokens: existing_context_tokens
|
||||
.saturating_add(setup_tokens),
|
||||
},
|
||||
GuardianBudgetOmission
|
||||
.render()
|
||||
.into_text()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.ok()?;
|
||||
}
|
||||
let truncations = std::mem::take(&mut context.truncations);
|
||||
let section_costs = context.section_costs().collect();
|
||||
let (mut input, new_input_tokens) =
|
||||
LunaSampler::finish_input(self, setup, context.into_annotated_messages());
|
||||
let input_tokens = existing_context_tokens.saturating_add(new_input_tokens);
|
||||
if input_tokens > max_input_tokens.saturating_sub(INPUT_TOKEN_MARGIN) {
|
||||
if input_tokens > maximum {
|
||||
return None;
|
||||
}
|
||||
if let Some(mut history) = conversation.take_history() {
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::async_scorer::transcript::CollectedTranscript;
|
||||
use crate::async_scorer::transcript::ContextInput;
|
||||
use crate::async_scorer::transcript::TranscriptConfig;
|
||||
use codex_extension_api::ConversationHistorySnapshot;
|
||||
use codex_guardian_context::ContextBudgeting;
|
||||
use codex_guardian_context::ContextTarget;
|
||||
use codex_guardian_context::PlannedAction;
|
||||
use codex_guardian_context::PlannedActionKind;
|
||||
@@ -32,7 +33,11 @@ impl ConversationHistorySnapshot for History {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect(history: &History, images: &[ContentItem]) -> CollectedTranscript {
|
||||
fn collect(
|
||||
history: &History,
|
||||
images: &[ContentItem],
|
||||
mode: ContextBudgeting,
|
||||
) -> CollectedTranscript {
|
||||
let action = PlannedAction {
|
||||
tool_descriptions: None,
|
||||
kind: PlannedActionKind::Command {
|
||||
@@ -46,7 +51,12 @@ fn collect(history: &History, images: &[ContentItem]) -> CollectedTranscript {
|
||||
fragment: "Approved reading README.md only.".to_owned(),
|
||||
}])
|
||||
.unwrap();
|
||||
TranscriptConfig::default()
|
||||
let config = TranscriptConfig {
|
||||
elastic: (mode == ContextBudgeting::Elastic)
|
||||
.then(core_test_support::guardian::elastic_budget_config),
|
||||
..TranscriptConfig::default()
|
||||
};
|
||||
config
|
||||
.collect_context(ContextInput {
|
||||
target: ContextTarget::Async,
|
||||
history,
|
||||
@@ -63,8 +73,11 @@ fn collect(history: &History, images: &[ContentItem]) -> CollectedTranscript {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_images() {
|
||||
#[test_case::test_case(ContextBudgeting::Fixed; "fixed")]
|
||||
#[test_case::test_case(ContextBudgeting::Elastic; "elastic")]
|
||||
fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_images(
|
||||
mode: ContextBudgeting,
|
||||
) {
|
||||
let native = ResponseItem::AgentMessage {
|
||||
id: Some(ResponseItemId::from_server("amsg_parent".to_owned())),
|
||||
author: "/root".to_owned(),
|
||||
@@ -115,8 +128,9 @@ fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_image
|
||||
}
|
||||
let mut state = ConversationState::default();
|
||||
let config = sample_request("parent");
|
||||
let evidence = collect(&history, &images[..1], mode);
|
||||
let first = config
|
||||
.prepare_retained(&collect(&history, &images[..1]), &mut state, INPUT_BUDGET)
|
||||
.prepare_retained(&evidence, &mut state, INPUT_BUDGET)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
input_images(&first.input),
|
||||
@@ -155,12 +169,13 @@ fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_image
|
||||
*detail = Some(ImageDetail::High);
|
||||
}
|
||||
}
|
||||
let evidence = collect(&history, &new_images);
|
||||
let evidence = collect(&history, &new_images, mode);
|
||||
// Fits three distinct images, but not another copy of the two retained images.
|
||||
let second = config
|
||||
.prepare_retained(&evidence, &mut state, /*max_input_tokens*/ 40_000)
|
||||
.unwrap();
|
||||
assert_eq!(&second.input[..committed.len()], committed);
|
||||
assert!(second.truncations.is_empty());
|
||||
assert!(state.snapshot().is_none());
|
||||
assert_eq!(
|
||||
second.input_tokens,
|
||||
@@ -192,6 +207,7 @@ fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_image
|
||||
)
|
||||
.unwrap();
|
||||
assert!(delta.contains("TRANSCRIPT DELTA START"));
|
||||
assert!(!delta.contains("<guardian_context_omission>"));
|
||||
assert!(delta.contains("[4] user: Inspect LICENSE next."));
|
||||
assert!(!delta.contains("[1] user: Inspect README.md."));
|
||||
assert!(delta.contains("Do not write files."));
|
||||
@@ -216,8 +232,9 @@ fn preparation_preserves_committed_prefix_and_selects_only_new_entries_and_image
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_preparation_preserves_committed_history_and_cursor() {
|
||||
#[test_case::test_case(ContextBudgeting::Fixed; "fixed")]
|
||||
#[test_case::test_case(ContextBudgeting::Elastic; "elastic")]
|
||||
fn rejected_preparation_preserves_committed_history_and_cursor(mode: ContextBudgeting) {
|
||||
let mut history = History {
|
||||
items: vec![responses::user_message_item("Inspect README.md.")],
|
||||
version: 1,
|
||||
@@ -225,7 +242,7 @@ fn rejected_preparation_preserves_committed_history_and_cursor() {
|
||||
let mut state = ConversationState::default();
|
||||
let config = sample_request("parent");
|
||||
let first = config
|
||||
.prepare_retained(&collect(&history, &[]), &mut state, INPUT_BUDGET)
|
||||
.prepare_retained(&collect(&history, &[], mode), &mut state, INPUT_BUDGET)
|
||||
.unwrap();
|
||||
let limit = first.input_tokens + 256;
|
||||
let mut committed = first.input;
|
||||
@@ -234,14 +251,14 @@ fn rejected_preparation_preserves_committed_history_and_cursor() {
|
||||
state.commit_snapshot(committed.clone());
|
||||
assert!(
|
||||
config
|
||||
.prepare_retained(&collect(&history, &[]), &mut state, limit)
|
||||
.prepare_retained(&collect(&history, &[], mode), &mut state, limit)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(state.cursor(), Some(first.cursor));
|
||||
assert_eq!(state.snapshot().unwrap().history(), &committed);
|
||||
config
|
||||
.prepare_retained(
|
||||
&collect(&history, &[]),
|
||||
&collect(&history, &[], mode),
|
||||
&mut ConversationState::default(),
|
||||
limit,
|
||||
)
|
||||
@@ -249,20 +266,21 @@ fn rejected_preparation_preserves_committed_history_and_cursor() {
|
||||
history.version += 1;
|
||||
assert!(
|
||||
config
|
||||
.prepare_retained(&collect(&history, &[]), &mut state, INPUT_BUDGET)
|
||||
.prepare_retained(&collect(&history, &[], mode), &mut state, INPUT_BUDGET)
|
||||
.is_none()
|
||||
);
|
||||
state.reset_transcript();
|
||||
assert!(
|
||||
config
|
||||
.prepare_retained(&collect(&history, &[]), &mut state, INPUT_BUDGET)
|
||||
.prepare_retained(&collect(&history, &[], mode), &mut state, INPUT_BUDGET)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(state.snapshot().unwrap().history(), &committed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_budget_counts_setup_checkpoint_images_and_trusted_evidence() {
|
||||
#[test_case::test_case(ContextBudgeting::Fixed; "fixed")]
|
||||
#[test_case::test_case(ContextBudgeting::Elastic; "elastic")]
|
||||
fn complete_budget_counts_setup_checkpoint_images_and_trusted_evidence(mode: ContextBudgeting) {
|
||||
let history = History {
|
||||
items: vec![responses::user_message_item("Inspect README.md.")],
|
||||
version: 1,
|
||||
@@ -280,7 +298,7 @@ fn complete_budget_counts_setup_checkpoint_images_and_trusted_evidence() {
|
||||
detail: None,
|
||||
}];
|
||||
let mut state = ConversationState::default();
|
||||
let evidence = collect(&history, &images);
|
||||
let evidence = collect(&history, &images, mode);
|
||||
let request = config
|
||||
.prepare_retained(&evidence, &mut state, INPUT_BUDGET)
|
||||
.unwrap();
|
||||
|
||||
@@ -58,6 +58,7 @@ pub(crate) struct ContextInput<'a> {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct TranscriptConfig {
|
||||
pub(crate) format: TranscriptFormat,
|
||||
pub(crate) elastic: Option<codex_guardian_context::ElasticBudgetConfig>,
|
||||
pub(crate) sources: Vec<TranscriptSource>,
|
||||
pub(crate) include_images: bool,
|
||||
pub(crate) max_message_entry_tokens: usize,
|
||||
@@ -71,6 +72,7 @@ impl Default for TranscriptConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: TranscriptFormat::Line,
|
||||
elastic: None,
|
||||
sources: vec![TranscriptSource::ToolCalls, TranscriptSource::ToolOutputs],
|
||||
include_images: ContextProfile::asynchronous().include_images,
|
||||
max_message_entry_tokens: MAX_MESSAGE_ENTRY_TOKENS,
|
||||
@@ -149,6 +151,7 @@ impl TranscriptConfig {
|
||||
context,
|
||||
profile,
|
||||
history_version,
|
||||
elastic: self.elastic.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -158,6 +161,7 @@ pub(super) struct CollectedTranscript {
|
||||
context: CollectedContext,
|
||||
profile: ContextProfile,
|
||||
history_version: u64,
|
||||
pub(super) elastic: Option<codex_guardian_context::ElasticBudgetConfig>,
|
||||
}
|
||||
|
||||
impl CollectedTranscript {
|
||||
@@ -186,10 +190,16 @@ impl CollectedTranscript {
|
||||
&self.profile,
|
||||
offset,
|
||||
presentation,
|
||||
ContextBudgeting::Fixed,
|
||||
if self.elastic.is_some() {
|
||||
ContextBudgeting::Elastic
|
||||
} else {
|
||||
ContextBudgeting::Fixed
|
||||
},
|
||||
reviewer_history,
|
||||
)?;
|
||||
context.retain_new_native_messages(reviewer_history)?;
|
||||
if self.elastic.is_none() {
|
||||
context.retain_new_native_messages(reviewer_history)?;
|
||||
}
|
||||
Ok((context, next))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,12 @@ pub struct GuardianElasticSectionConfigToml {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GuardianV2ConfigToml {
|
||||
/// Enable elastic allocation for asynchronous Guardian requests. Defaults to false.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub async_elastic_budgeting: Option<bool>,
|
||||
/// Section limits validated when asynchronous elastic budgeting is enabled.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub async_elastic_sections: Option<BTreeMap<String, GuardianElasticSectionConfigToml>>,
|
||||
/// Enable elastic allocation for synchronous Guardian requests. Defaults to false.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_elastic_budgeting: Option<bool>,
|
||||
|
||||
@@ -332,6 +332,8 @@ max_recent_non_user_entries = 12
|
||||
assert_eq!(
|
||||
features.guardianv2,
|
||||
Some(FeatureToml::Config(crate::GuardianV2ConfigToml {
|
||||
async_elastic_budgeting: None,
|
||||
async_elastic_sections: None,
|
||||
sync_elastic_budgeting: None,
|
||||
sync_elastic_sections: None,
|
||||
transcript_mode: None,
|
||||
|
||||
@@ -167,7 +167,7 @@ pub struct PreparedElasticContext<'a> {
|
||||
impl PreparedElasticContext<'_> {
|
||||
/// Removes history-delivered instructions and deduplicates native deliveries.
|
||||
/// Pending instruction proof waits for Required selection.
|
||||
pub fn prepare_sync_delivery(
|
||||
pub fn prepare_delivery(
|
||||
mut self,
|
||||
history: &[ResponseItemEnvelope],
|
||||
) -> Result<Self, SectionError> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Shared root-conversation and host-verified answer sections.
|
||||
//!
|
||||
//! Hosts resolve source provenance; sync preparation applies request limits. Source roles remain
|
||||
//! Hosts resolve source provenance; preparation applies request limits. Source roles remain
|
||||
//! line-labeled evidence, not instructions or a change to the delivery role.
|
||||
|
||||
use crate::ContextSection;
|
||||
|
||||
@@ -134,7 +134,7 @@ fn native_agent_admission_uses_current_history_and_preserves_identity() {
|
||||
let admitted = pending
|
||||
.prepare_elastic_entries(&config)
|
||||
.unwrap()
|
||||
.prepare_sync_delivery(&[])
|
||||
.prepare_delivery(&[])
|
||||
.unwrap()
|
||||
.enforce_budget(
|
||||
RequestBudget {
|
||||
@@ -557,6 +557,9 @@ fn image_accounting_preserves_later_eviction_policy() {
|
||||
})),
|
||||
ImageReference::File { .. } => Some(Cow::Borrowed(image)),
|
||||
});
|
||||
let mut rejected = context.clone();
|
||||
rejected.retain_new_images(&[], |_, _| None);
|
||||
assert_eq!(rejected.truncations.len(), 1);
|
||||
let mut continued = context.clone();
|
||||
let completed = context.clone().into_messages();
|
||||
continued.retain_new_images(&completed, |image, _| Some(Cow::Borrowed(image)));
|
||||
|
||||
@@ -11,6 +11,9 @@ use crate::SectionContributor;
|
||||
use crate::SectionError;
|
||||
use crate::SectionInput;
|
||||
use crate::SectionScope;
|
||||
use crate::TruncationObservation;
|
||||
use crate::composition::SectionContent;
|
||||
use crate::composition::SectionDelivery;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
@@ -51,12 +54,28 @@ impl ComposedContext {
|
||||
}
|
||||
images
|
||||
});
|
||||
self.retain_images(|image, detail| {
|
||||
let Some(prepared) = prepare(image, detail) else {
|
||||
return false;
|
||||
for section in &mut self.sections {
|
||||
let SectionDelivery::UserContent(items) = &mut section.delivery else {
|
||||
continue;
|
||||
};
|
||||
!retained_images.contains(&prepared.as_ref())
|
||||
});
|
||||
items.retain_mut(|item| {
|
||||
let SectionContent::Other(ContentItem::InputImage { image, detail }) =
|
||||
&mut item.content
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
let Some(prepared) = prepare(image, detail) else {
|
||||
self.truncations.push(TruncationObservation {
|
||||
source: None,
|
||||
component: section.id,
|
||||
original_bytes: item.content.evidence_bytes(),
|
||||
retained_bytes: 0,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
!retained_images.contains(&prepared.as_ref())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,9 +196,9 @@ pub struct SectionInput<'a> {
|
||||
pub is_selected_exec_output: Option<&'a SelectedExecOutputPredicate<'a>>,
|
||||
/// Bounded root evidence resolved by the host; empty when not applicable.
|
||||
pub root_conversation: &'a [GuardianRootMessage],
|
||||
/// Sync originals retained until request preparation; preferred over the bounded view.
|
||||
/// Originals retained until request preparation; preferred over the bounded view.
|
||||
pub root_conversation_candidates: Option<&'a RootConversationInput>,
|
||||
/// Role-labeled sync answer candidates or already-bounded answers from other consumers.
|
||||
/// Role-labeled original answer candidates or already-bounded answers from other consumers.
|
||||
pub trusted_user_answers: &'a [TrustedUserAnswer],
|
||||
/// Exact action JSON; sync composition bounds the reason and descriptions.
|
||||
pub planned_action: Option<&'a PlannedAction>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Applies configured section text caps exactly once before allocation.
|
||||
//! Supporting records remain candidates after truncation. Framing, exact actions,
|
||||
//! images and opaque native payloads stay whole. Verified answers that exceed their
|
||||
//! permissions, images and opaque native payloads stay whole. Verified answers that exceed their
|
||||
//! entry cap are omitted whole. Selection cannot change prepared bytes.
|
||||
|
||||
use crate::ComposedContext;
|
||||
@@ -52,6 +52,7 @@ impl ComposedContext {
|
||||
)));
|
||||
}
|
||||
for item in items {
|
||||
item.protected |= section.id == "permissions";
|
||||
let maximum = limit(item.retention)?;
|
||||
let original_tokens = section_content_tokens(&item.content);
|
||||
if item.protected
|
||||
|
||||
@@ -129,7 +129,7 @@ fn entry_caps_bound_required_and_optional_text_preserving_metadata_and_opaque_pa
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
fn exact_actions_and_permissions_stay_whole_while_supporting_context_is_capped() {
|
||||
const ENTRY_TOKENS: usize = 128;
|
||||
const REPEATS: usize = 1_000;
|
||||
let json = serde_json::json!({"tool": "write_record", "arguments": {
|
||||
@@ -137,6 +137,7 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
}})
|
||||
.to_string();
|
||||
let supporting = "supporting context ".repeat(REPEATS);
|
||||
let permissions = "denied path ".repeat(REPEATS);
|
||||
let (permission_start, permission_end) =
|
||||
<crate::PermissionContext as codex_context_fragments::ContextualUserFragment>::type_markers(
|
||||
);
|
||||
@@ -153,7 +154,7 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
crate::ContextSection::PermissionContext {
|
||||
items: vec![
|
||||
permission_start.into(),
|
||||
supporting.clone(),
|
||||
permissions.clone(),
|
||||
permission_end.into(),
|
||||
],
|
||||
},
|
||||
@@ -184,7 +185,9 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let limits = ElasticSectionLimits::new(ENTRY_TOKENS, ENTRY_TOKENS).unwrap();
|
||||
let limits = ElasticSectionLimits::new(ENTRY_TOKENS, ENTRY_TOKENS)
|
||||
.and_then(|limits| limits.with_max_total_section_tokens(ENTRY_TOKENS))
|
||||
.unwrap();
|
||||
let config = crate::budget::tests::complete_minimum_budget(ElasticBudgetConfig {
|
||||
sections: [
|
||||
ElasticSection::PlannedAction,
|
||||
@@ -204,6 +207,7 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
let expected = ContentItem::InputText {
|
||||
text: format!("{json}\n"),
|
||||
};
|
||||
let permission_tokens = section_content_tokens(&permissions.clone().into());
|
||||
for item in items {
|
||||
if !item.protected {
|
||||
assert!(
|
||||
@@ -218,7 +222,9 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
.clone()
|
||||
.enforce_budget(
|
||||
crate::RequestBudget {
|
||||
max_input_tokens: crate::budget::content_tokens(&expected) - 1,
|
||||
max_input_tokens: crate::budget::content_tokens(&expected)
|
||||
+ permission_tokens
|
||||
- 1,
|
||||
existing_context_tokens: 0,
|
||||
},
|
||||
"Evidence omitted.".to_owned(),
|
||||
@@ -240,18 +246,19 @@ fn exact_actions_stay_whole_while_supporting_context_is_capped() {
|
||||
.unwrap()
|
||||
.contains(&supporting)
|
||||
);
|
||||
for (start, end) in [
|
||||
(permission_start, permission_end),
|
||||
for (start, end, body) in [
|
||||
(permission_start, permission_end, permissions.as_str()),
|
||||
(
|
||||
">>> SENDER EVIDENCE CONTEXT START\nHost:",
|
||||
">>> SENDER EVIDENCE CONTEXT END\n",
|
||||
"supporting context",
|
||||
),
|
||||
] {
|
||||
assert!(messages.iter().any(|message| matches!(message,
|
||||
ResponseItem::Message { content, .. } if
|
||||
content.iter().any(|part| matches!(part, ContentItem::InputText { text } if text.starts_with(start))) &&
|
||||
content.iter().any(|part| matches!(part, ContentItem::InputText { text } if text == end)) &&
|
||||
content.iter().any(|part| matches!(part, ContentItem::InputText { text } if text.contains("supporting context"))))));
|
||||
content.iter().any(|part| matches!(part, ContentItem::InputText { text } if text.contains(body))))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ fn delivery_uses_source_revision_and_complete_host_metadata() {
|
||||
let history_tokens = original.estimated_tokens();
|
||||
assert!(
|
||||
capped
|
||||
.prepare_sync_delivery(&delivered)
|
||||
.prepare_delivery(&delivered)
|
||||
.unwrap()
|
||||
.enforce_budget(
|
||||
crate::RequestBudget {
|
||||
@@ -679,7 +679,7 @@ fn transcript_original_requires_complete_source_proof_and_survives_budgeting() {
|
||||
let prepared = transcript_context(&history, &profile)
|
||||
.prepare_elastic_entries(&config)
|
||||
.unwrap()
|
||||
.prepare_sync_delivery(&[])
|
||||
.prepare_delivery(&[])
|
||||
.unwrap();
|
||||
let admitted = prepared
|
||||
.clone()
|
||||
@@ -788,7 +788,7 @@ fn transcript_original_requires_complete_source_proof_and_survives_budgeting() {
|
||||
assert!(prepared.estimated_tokens() > full_proof.estimated_tokens() + MINIMUM_TOKENS);
|
||||
}
|
||||
let admitted = prepared
|
||||
.prepare_sync_delivery(&[])
|
||||
.prepare_delivery(&[])
|
||||
.unwrap()
|
||||
.enforce_budget(
|
||||
crate::RequestBudget {
|
||||
|
||||
@@ -99,7 +99,7 @@ impl From<&str> for TrustedUserAnswer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects originals for sync preparation without applying the per-answer request limit.
|
||||
/// Collects originals for request preparation without applying the per-answer request limit.
|
||||
pub fn collect_verified_answer_candidates(context: &RetainedContext) -> Vec<TrustedUserAnswer> {
|
||||
let mut items = Vec::new();
|
||||
if !context.verified_answers_complete() {
|
||||
|
||||
@@ -278,6 +278,7 @@ async fn new_config(
|
||||
guardian_policy_template: None,
|
||||
guardian_transcript_mode: Default::default(),
|
||||
guardian_sync_elastic_budget: None,
|
||||
guardian_async_elastic_budget: None,
|
||||
guardian_conversation_history_prompt: None,
|
||||
guardian_conversation_history_max_output_tokens: None,
|
||||
guardian_circuit_break_action: Default::default(),
|
||||
|
||||
Reference in New Issue
Block a user