Compare commits

...

16 Commits

Author SHA1 Message Date
Akshay Nathan
ea735b69da Merge branch 'main' into codex/stateful-apply-patch-streaming 2026-05-04 15:18:11 -07:00
Akshay Nathan
b459ba0f4d Gate streaming apply_patch parser 2026-05-04 11:15:50 -07:00
Akshay Nathan
66e5d99eb2 Merge remote-tracking branch 'origin/main' into codex/stateful-apply-patch-streaming 2026-05-04 10:52:59 -07:00
Akshay Nathan
d9f639ba6d Revert "Avoid duplicate apply_patch progress events"
This reverts commit ebd79231a5.
2026-04-30 14:06:09 -07:00
Akshay Nathan
ebd79231a5 Avoid duplicate apply_patch progress events 2026-04-30 14:04:40 -07:00
Akshay Nathan
8b9888d60b Restore apply_patch parser unit tests 2026-04-30 13:30:52 -07:00
Akshay Nathan
57e8dd5e7b Merge remote-tracking branch 'origin/main' into codex/stateful-apply-patch-streaming 2026-04-30 13:26:59 -07:00
Akshay Nathan
209bc225a5 Remove apply_patch parser comparator 2026-04-30 12:44:43 -07:00
Akshay Nathan
903c56aa87 Match streaming apply_patch parser errors 2026-04-30 12:42:56 -07:00
Akshay Nathan
6bd78a51b5 Fix streaming apply_patch parser mismatches 2026-04-29 10:15:49 -07:00
pakrym-oai
9a05898cf0 Add repro for divergent successful apply_patch parse 2026-04-27 21:02:28 -07:00
pakrym-oai
9136dee011 Add reduced repro tests for apply_patch parser mismatches 2026-04-27 17:45:02 -07:00
Akshay Nathan
f2924bf70c codex: fix CI failure on PR #19160 2026-04-24 21:54:29 -07:00
Akshay Nathan
47964c77db Merge remote-tracking branch 'origin/main' into codex/stateful-apply-patch-streaming
# Conflicts:
#	codex-rs/app-server-protocol/schema/json/ServerNotification.json
#	codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
#	codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
#	codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
#	codex-rs/core/src/tools/handlers/apply_patch.rs
#	codex-rs/core/src/tools/handlers/apply_patch_tests.rs
2026-04-24 17:24:16 -07:00
Akshay Nathan
0782c6050e Wire the PatchUpdated events through app_server (#18289)
Wires patch_updated events through app_server. These events are parsed
and streamed while apply_patch is being written by the model. Also adds 500ms of buffering to the patch_updated events in the diff_consumer.

The eventual goal is to use this to display better progress indicators in
the codex app.
2026-04-24 17:22:14 -07:00
Akshay Nathan
4c0c1b7eee Make apply_patch streaming parser stateful 2026-04-24 16:46:44 -07:00
13 changed files with 173 additions and 25 deletions

View File

@@ -19,6 +19,7 @@ use crate::IoError;
use crate::MaybeApplyPatchVerified;
use crate::parser::Hunk;
use crate::parser::ParseError;
use crate::parser::ParsePatchMode;
use crate::parser::parse_patch;
use crate::unified_diff_from_chunks;
use std::str::Utf8Error;
@@ -102,17 +103,19 @@ fn extract_apply_patch_from_shell(
}
// TODO: make private once we remove tests in lib.rs
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
pub fn maybe_parse_apply_patch(argv: &[String], mode: ParsePatchMode) -> MaybeApplyPatch {
match argv {
// Direct invocation: apply_patch <patch>
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => match parse_patch(body) {
Ok(source) => MaybeApplyPatch::Body(source),
Err(e) => MaybeApplyPatch::PatchParseError(e),
},
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => {
match parse_patch(body, mode) {
Ok(source) => MaybeApplyPatch::Body(source),
Err(e) => MaybeApplyPatch::PatchParseError(e),
}
}
// Shell heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
_ => match parse_shell_script(argv) {
Some((shell, script)) => match extract_apply_patch_from_shell(shell, script) {
Ok((body, workdir)) => match parse_patch(&body) {
Ok((body, workdir)) => match parse_patch(&body, mode) {
Ok(mut source) => {
source.workdir = workdir;
MaybeApplyPatch::Body(source)
@@ -133,6 +136,7 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
/// patch.
pub async fn maybe_parse_apply_patch_verified(
argv: &[String],
mode: ParsePatchMode,
cwd: &AbsolutePathBuf,
fs: &dyn ExecutorFileSystem,
sandbox: Option<&codex_exec_server::FileSystemSandboxContext>,
@@ -140,17 +144,17 @@ pub async fn maybe_parse_apply_patch_verified(
// Detect a raw patch body passed directly as the command or as the body of a shell
// script. In these cases, report an explicit error rather than applying the patch.
if let [body] = argv
&& parse_patch(body).is_ok()
&& parse_patch(body, mode).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
if let Some((_, script)) = parse_shell_script(argv)
&& parse_patch(script).is_ok()
&& parse_patch(script, mode).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
match maybe_parse_apply_patch(argv) {
match maybe_parse_apply_patch(argv, mode) {
MaybeApplyPatch::Body(ApplyPatchArgs {
patch,
hunks,
@@ -377,6 +381,7 @@ fn extract_apply_patch_from_bash(
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_patch;
use crate::unified_diff_from_chunks;
use assert_matches::assert_matches;
use codex_exec_server::LOCAL_FS;
@@ -437,7 +442,7 @@ mod tests {
}
fn assert_match_args(args: Vec<String>, expected_workdir: Option<&str>) {
match maybe_parse_apply_patch(&args) {
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
assert_eq!(workdir.as_deref(), expected_workdir);
assert_eq!(hunks, expected_single_add());
@@ -454,7 +459,7 @@ mod tests {
fn assert_not_match(script: &str) {
let args = args_bash(script);
assert_matches!(
maybe_parse_apply_patch(&args),
maybe_parse_apply_patch(&args, ParsePatchMode::Legacy),
MaybeApplyPatch::NotApplyPatch
);
}
@@ -467,6 +472,7 @@ mod tests {
assert_matches!(
maybe_parse_apply_patch_verified(
&args,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
LOCAL_FS.as_ref(),
/*sandbox*/ None,
@@ -484,6 +490,7 @@ mod tests {
assert_matches!(
maybe_parse_apply_patch_verified(
&args,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
LOCAL_FS.as_ref(),
/*sandbox*/ None,
@@ -504,7 +511,7 @@ mod tests {
"#,
]);
match maybe_parse_apply_patch(&args) {
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
assert_eq!(
hunks,
@@ -529,7 +536,7 @@ mod tests {
"#,
]);
match maybe_parse_apply_patch(&args) {
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
assert_eq!(
hunks,
@@ -568,7 +575,7 @@ mod tests {
PATCH"#,
]);
match maybe_parse_apply_patch(&args) {
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
assert_eq!(workdir, None);
assert_eq!(
@@ -689,7 +696,7 @@ PATCH"#,
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -728,7 +735,7 @@ PATCH"#,
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -773,6 +780,7 @@ PATCH"#,
let result = maybe_parse_apply_patch_verified(
&argv,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
LOCAL_FS.as_ref(),
/*sandbox*/ None,
@@ -827,6 +835,7 @@ PATCH"#,
let result = maybe_parse_apply_patch_verified(
&argv,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
LOCAL_FS.as_ref(),
/*sandbox*/ None,

View File

@@ -19,6 +19,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
pub use parser::Hunk;
pub use parser::ParseError;
use parser::ParseError::*;
pub use parser::ParsePatchMode;
pub use parser::UpdateFileChunk;
pub use parser::parse_patch;
use similar::TextDiff;
@@ -183,13 +184,14 @@ impl ApplyPatchAction {
/// Applies the patch and prints the result to stdout/stderr.
pub async fn apply_patch(
patch: &str,
mode: ParsePatchMode,
cwd: &AbsolutePathBuf,
stdout: &mut impl std::io::Write,
stderr: &mut impl std::io::Write,
fs: &dyn ExecutorFileSystem,
sandbox: Option<&FileSystemSandboxContext>,
) -> Result<(), ApplyPatchError> {
let hunks = match parse_patch(patch) {
let hunks = match parser::parse_patch(patch, mode) {
Ok(source) => source.hunks,
Err(e) => {
match &e {
@@ -640,6 +642,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -700,6 +703,7 @@ mod tests {
apply_patch(
&patch,
ParsePatchMode::Legacy,
&cwd,
&mut stdout,
&mut stderr,
@@ -743,6 +747,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -779,6 +784,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -819,6 +825,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -868,6 +875,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -926,6 +934,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -970,6 +979,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -1013,6 +1023,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -1057,7 +1068,7 @@ mod tests {
+QUX"#,
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let update_file_chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
@@ -1104,7 +1115,7 @@ mod tests {
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -1145,7 +1156,7 @@ mod tests {
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -1184,7 +1195,7 @@ mod tests {
path.display()
));
let patch = parse_patch(&patch).unwrap();
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match patch.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -1234,7 +1245,7 @@ mod tests {
let patch = wrap_patch(&patch_body);
// Extract chunks then build the unified diff.
let parsed = parse_patch(&patch).unwrap();
let parsed = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
let chunks = match parsed.hunks.as_slice() {
[Hunk::UpdateFile { chunks, .. }] => chunks,
_ => panic!("Expected a single UpdateFile hunk"),
@@ -1269,6 +1280,7 @@ mod tests {
let mut stderr = Vec::new();
apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,
@@ -1309,6 +1321,7 @@ g
let mut stderr = Vec::new();
let result = apply_patch(
&patch,
ParsePatchMode::Legacy,
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
&mut stdout,
&mut stderr,

View File

@@ -123,7 +123,25 @@ pub struct UpdateFileChunk {
pub is_end_of_file: bool,
}
pub fn parse_patch(patch: &str) -> Result<ApplyPatchArgs, ParseError> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParsePatchMode {
Legacy,
Streaming,
}
pub fn parse_patch(patch: &str, mode: ParsePatchMode) -> Result<ApplyPatchArgs, ParseError> {
if mode == ParsePatchMode::Streaming {
let patch = normalize_patch_text(patch)?;
let mut parser = crate::streaming_parser::StreamingPatchParser::default();
parser.push_delta(&patch)?;
let hunks = parser.finish()?;
return Ok(ApplyPatchArgs {
hunks,
patch,
workdir: None,
});
}
let mode = if PARSE_IN_STRICT_MODE {
ParseMode::Strict
} else {
@@ -132,6 +150,12 @@ pub fn parse_patch(patch: &str) -> Result<ApplyPatchArgs, ParseError> {
parse_patch_text(patch, mode)
}
fn normalize_patch_text(patch: &str) -> Result<String, ParseError> {
let lines: Vec<&str> = patch.trim().lines().collect();
let (patch_lines, _) = check_patch_boundaries_lenient(&lines)?;
Ok(patch_lines.join("\n"))
}
enum ParseMode {
/// Parse the patch text argument as is.
Strict,

View File

@@ -67,6 +67,7 @@ pub fn run_main() -> i32 {
};
match runtime.block_on(crate::apply_patch(
&patch_arg,
crate::ParsePatchMode::Legacy,
&cwd,
&mut stdout,
&mut stderr,

View File

@@ -116,6 +116,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
};
match runtime.block_on(codex_apply_patch::apply_patch(
&patch_arg,
codex_apply_patch::ParsePatchMode::Legacy,
&cwd,
&mut stdout,
&mut stderr,

View File

@@ -364,6 +364,9 @@
"apply_patch_streaming_events": {
"type": "boolean"
},
"apply_patch_streaming_parser": {
"type": "boolean"
},
"apps": {
"type": "boolean"
},
@@ -3853,6 +3856,9 @@
"apply_patch_streaming_events": {
"type": "boolean"
},
"apply_patch_streaming_parser": {
"type": "boolean"
},
"apps": {
"type": "boolean"
},

View File

@@ -35,6 +35,7 @@ use crate::tools::sandboxing::ToolCtx;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange;
use codex_apply_patch::Hunk;
use codex_apply_patch::ParsePatchMode;
use codex_apply_patch::StreamingPatchParser;
use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
@@ -373,8 +374,14 @@ impl ToolHandler for ApplyPatchHandler {
.environment
.is_remote()
.then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None));
let parse_mode = if turn.features.enabled(Feature::ApplyPatchStreamingParser) {
ParsePatchMode::Streaming
} else {
ParsePatchMode::Legacy
};
match codex_apply_patch::maybe_parse_apply_patch_verified(
&command,
parse_mode,
&cwd,
fs.as_ref(),
sandbox.as_ref(),
@@ -405,6 +412,7 @@ impl ToolHandler for ApplyPatchHandler {
let req = ApplyPatchRequest {
action: apply.action,
parse_mode,
file_paths,
changes,
exec_approval_requirement: apply.exec_approval_requirement,
@@ -479,8 +487,19 @@ pub(crate) async fn intercept_apply_patch(
.primary()
.filter(|env| env.environment.is_remote())
.map(|_| turn.file_system_sandbox_context(/*additional_permissions*/ None));
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, sandbox.as_ref())
.await
let parse_mode = if turn.features.enabled(Feature::ApplyPatchStreamingParser) {
ParsePatchMode::Streaming
} else {
ParsePatchMode::Legacy
};
match codex_apply_patch::maybe_parse_apply_patch_verified(
command,
parse_mode,
cwd,
fs,
sandbox.as_ref(),
)
.await
{
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
session
@@ -513,6 +532,7 @@ pub(crate) async fn intercept_apply_patch(
let req = ApplyPatchRequest {
action: apply.action,
parse_mode,
file_paths: approval_keys,
changes,
exec_approval_requirement: apply.exec_approval_requirement,

View File

@@ -222,6 +222,7 @@ async fn approval_keys_include_move_destination() {
let argv = vec!["apply_patch".to_string(), patch.to_string()];
let action = match codex_apply_patch::maybe_parse_apply_patch_verified(
&argv,
codex_apply_patch::ParsePatchMode::Legacy,
&cwd,
LOCAL_FS.as_ref(),
/*sandbox*/ None,

View File

@@ -18,6 +18,7 @@ use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::with_cached_approval;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ParsePatchMode;
use codex_exec_server::FileSystemSandboxContext;
use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
@@ -38,6 +39,7 @@ use std::time::Instant;
#[derive(Debug)]
pub struct ApplyPatchRequest {
pub action: ApplyPatchAction,
pub parse_mode: ParsePatchMode,
pub file_paths: Vec<AbsolutePathBuf>,
pub changes: std::collections::HashMap<PathBuf, FileChange>,
pub exec_approval_requirement: ExecApprovalRequirement,
@@ -201,6 +203,7 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
let mut stderr = Vec::new();
let result = codex_apply_patch::apply_patch(
&req.action.patch,
req.parse_mode,
&req.action.cwd,
&mut stdout,
&mut stderr,

View File

@@ -50,6 +50,7 @@ fn guardian_review_request_includes_patch_context() {
let expected_patch = action.patch.clone();
let request = ApplyPatchRequest {
action,
parse_mode: ParsePatchMode::Legacy,
file_paths: vec![path.clone()],
changes: HashMap::from([(
path.to_path_buf(),
@@ -88,6 +89,7 @@ fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
let expected_patch = action.patch.clone();
let req = ApplyPatchRequest {
action,
parse_mode: ParsePatchMode::Legacy,
file_paths: vec![path],
changes: HashMap::new(),
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
@@ -127,6 +129,7 @@ fn file_system_sandbox_context_uses_active_attempt() {
};
let req = ApplyPatchRequest {
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
parse_mode: ParsePatchMode::Legacy,
file_paths: vec![path.clone()],
changes: HashMap::new(),
exec_approval_requirement: ExecApprovalRequirement::Skip {
@@ -184,6 +187,7 @@ fn no_sandbox_attempt_has_no_file_system_context() {
.abs();
let req = ApplyPatchRequest {
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
parse_mode: ParsePatchMode::Legacy,
file_paths: vec![path.clone()],
changes: HashMap::new(),
exec_approval_requirement: ExecApprovalRequirement::Skip {

View File

@@ -219,6 +219,51 @@ D delete.txt
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn apply_patch_cli_uses_streaming_parser_when_feature_enabled() -> Result<()> {
skip_if_no_network!(Ok(()));
let harness = apply_patch_harness_with(|builder| {
builder.with_model("gpt-5.4").with_config(|config| {
config
.features
.enable(Feature::ApplyPatchStreamingParser)
.expect("enable apply_patch streaming parser");
config.suppress_unstable_features_warning = true;
})
})
.await?;
let patch = "*** Begin Patch\n*** Add File: streaming-parser.txt\n+enabled\n*** End Patch";
let call_id = "apply-streaming-parser";
mount_apply_patch(
&harness,
call_id,
patch,
"done",
ApplyPatchModelOutput::Function,
)
.await;
harness
.submit("please apply streaming parser patch")
.await?;
let out = harness
.apply_patch_output(call_id, ApplyPatchModelOutput::Function)
.await;
assert_regex_match(
r"(?s)^Exit code: 0.*Success\. Updated the following files:\nA streaming-parser\.txt\n?$",
&out,
);
assert_eq!(
harness.read_file_text("streaming-parser.txt").await?,
"enabled\n"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[test_case(ApplyPatchModelOutput::Freeform)]
#[test_case(ApplyPatchModelOutput::Function)]

View File

@@ -99,6 +99,8 @@ pub enum Feature {
ApplyPatchFreeform,
/// Stream structured progress while apply_patch input is being generated.
ApplyPatchStreamingEvents,
/// Use the streaming apply_patch parser for completed patch verification and application.
ApplyPatchStreamingParser,
/// Allow exec tools to request additional permissions while staying sandboxed.
ExecPermissionApprovals,
/// Expose the built-in request_permissions tool.
@@ -815,6 +817,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ApplyPatchStreamingParser,
key: "apply_patch_streaming_parser",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ExecPermissionApprovals,
key: "exec_permission_approvals",

View File

@@ -110,6 +110,19 @@ fn request_permissions_is_under_development() {
assert_eq!(Feature::ExecPermissionApprovals.default_enabled(), false);
}
#[test]
fn apply_patch_streaming_parser_is_under_development() {
assert_eq!(
feature_for_key("apply_patch_streaming_parser"),
Some(Feature::ApplyPatchStreamingParser)
);
assert_eq!(
Feature::ApplyPatchStreamingParser.stage(),
Stage::UnderDevelopment
);
assert_eq!(Feature::ApplyPatchStreamingParser.default_enabled(), false);
}
#[test]
fn request_permissions_tool_is_under_development() {
assert_eq!(