feat: execute non-isolated macros on the live REPL context

Make the macro isolated field live (design §3, §9 step 5):
isolated: false now runs the interpolated steps via run_repl_command
on the live RequestContext — session-recorded, conversation-visible,
with mutating steps persisting by design — while isolated: true keeps
the forked execution byte-for-byte unchanged.

- Add macro_non_isolated companion field beside macro_flag; both
  fork-propagation sites mirror it verbatim.
- RAII MacroModeGuard wraps the whole &mut RequestContext (DerefMut
  passthrough) and restores flag+mode on every exit path, including a
  failing step; steps remain fail-fast.
- Reject nested macro invocation when the current mode is non-isolated
  ("nested macros not allowed in non-isolated mode"); an isolated
  macro's step may still run a non-isolated macro inline on its fork.
- use_agent now suppresses the agent's default session only for
  isolated macros; a non-isolated .agent step engages it as if typed.
This commit is contained in:
2026-08-21 11:55:13 -06:00
parent e94bd450cd
commit 5478c5a239
2 changed files with 435 additions and 2 deletions
+87 -1
View File
@@ -233,6 +233,10 @@ pub struct RequestContext {
pub app: Arc<AppState>,
pub macro_flag: bool,
/// Companion to `macro_flag`: true while a non-isolated macro is running
/// its steps directly on this live context. Isolated macros execute on a
/// forked context and leave this false.
pub macro_non_isolated: bool,
pub info_flag: bool,
pub working_mode: WorkingMode,
@@ -270,6 +274,7 @@ impl RequestContext {
Self {
app,
macro_flag: false,
macro_non_isolated: false,
info_flag: false,
working_mode,
model: Default::default(),
@@ -324,6 +329,7 @@ impl RequestContext {
Ok(Self {
app,
macro_flag: false,
macro_non_isolated: false,
info_flag,
working_mode,
model,
@@ -373,6 +379,7 @@ impl RequestContext {
Self {
app: Arc::clone(&self.app),
macro_flag: self.macro_flag,
macro_non_isolated: self.macro_non_isolated,
info_flag: self.info_flag,
working_mode: self.working_mode,
model: self.model.clone(),
@@ -412,6 +419,7 @@ impl RequestContext {
Self {
app,
macro_flag: parent.macro_flag,
macro_non_isolated: parent.macro_non_isolated,
info_flag: parent.info_flag,
working_mode: WorkingMode::Cmd,
model: parent.model.clone(),
@@ -2480,6 +2488,14 @@ impl RequestContext {
Ok(())
}
/// Whether a non-isolated macro is currently running its steps on this
/// context. Macro invocations are rejected in this mode: the nested
/// macro's steps would interleave with the outer macro's on the live
/// session.
pub fn in_non_isolated_macro(&self) -> bool {
self.macro_flag && self.macro_non_isolated
}
/// The resolved macro set for the active context (workspace + global
/// discovery, effective `enabled_macros` allowlist, built-in shadowing).
pub fn macro_policy(&self) -> MacroPolicy {
@@ -4016,8 +4032,11 @@ impl RequestContext {
// Graph agents manage their own state; never engage a session,
// not even an inherited app-level `agent_session` default.
// Isolated macros suppress an inherited default too — their forked
// context has no session to return to. A non-isolated macro's `.agent`
// step engages it exactly as if the user had typed the command.
let session_name = session_name.map(|v| v.to_string()).or_else(|| {
if self.macro_flag || is_graph_agent {
if (self.macro_flag && !self.macro_non_isolated) || is_graph_agent {
None
} else {
agent.agent_session().map(|v| v.to_string())
@@ -6566,6 +6585,73 @@ mod tests {
);
}
#[test]
#[serial]
fn use_agent_suppresses_inherited_session_in_isolated_macro() {
let _guard = TestConfigDirGuard::new();
let mut ctx = create_test_ctx();
ctx.macro_flag = true;
ctx.update_app_config(|app| app.agent_session = Some("inherited".to_string()));
let app = ctx.app.config.clone();
let agent_name = format!(
"test_agent_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let agent_dir = paths::agent_data_dir(&agent_name);
create_dir_all(&agent_dir).unwrap();
write(
agent_dir.join("config.yaml"),
format!("name: {agent_name}\ninstructions: hi\n"),
)
.unwrap();
let abort = utils::create_abort_signal();
run_async(ctx.use_agent(&app, &agent_name, None, abort)).unwrap();
assert!(
ctx.session.is_none(),
"an isolated macro must keep suppressing the agent's default session"
);
}
#[test]
#[serial]
fn use_agent_engages_inherited_session_in_non_isolated_macro() {
let _guard = TestConfigDirGuard::new();
let mut ctx = create_test_ctx();
ctx.macro_flag = true;
ctx.macro_non_isolated = true;
ctx.update_app_config(|app| app.agent_session = Some("inherited".to_string()));
let app = ctx.app.config.clone();
let agent_name = format!(
"test_agent_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let agent_dir = paths::agent_data_dir(&agent_name);
create_dir_all(&agent_dir).unwrap();
write(
agent_dir.join("config.yaml"),
format!("name: {agent_name}\ninstructions: hi\n"),
)
.unwrap();
let abort = utils::create_abort_signal();
run_async(ctx.use_agent(&app, &agent_name, None, abort)).unwrap();
assert!(
ctx.session.is_some(),
"a non-isolated macro's agent step must engage the default session as if typed"
);
}
fn first_file(dir: &Path) -> Option<PathBuf> {
for entry in read_dir(dir).ok()?.flatten() {
let path = entry.path();