Handle standalone tool outputs as external context (#39791)

## What changed

- Treat standalone `function_call_output` items without a `call_id` as external context, including items injected into new and forked threads, and mark the thread memory mode polluted when `memories.disable_on_external_context` is enabled.
- Include these outputs in guardian transcripts with their namespaced tool name and a placeholder for non-text content.
- Allow image generation to reuse images from standalone and otherwise unpaired function or custom-tool outputs.

## Testing

- Cover injected thread items, external-context detection, guardian transcript rendering, and recent-image selection.

GitOrigin-RevId: ca4e8c16a4b731e1256a5956908d00f785b4feb1
This commit is contained in:
rka-oai authored and copyberry committed 2026-08-20 19:46:29 +00:00
1 parent 43526b8561
commit aead844f64
10 files changed
+170 -49

No files matched your search

+24 -1
View File
@@ -42,6 +42,7 @@ use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnEnvironment;
use crate::session_prefix::format_inter_agent_completion_message;
use crate::skills_load_input_from_config;
use crate::stream_events_utils::mark_thread_memory_mode_polluted_if_external_context;
use crate::turn_metadata::TurnMetadataState;
use crate::turn_timing::now_unix_timestamp_ms;
use async_channel::Receiver;
@@ -1346,12 +1347,13 @@ impl Session {
let mut state = self.state.lock().await;
state.set_next_turn_is_first(!has_prior_user_turns);
}
match conversation_history {
let turn_context = match conversation_history {
InitialHistory::New | InitialHistory::Cleared => {
// Defer initial context insertion until the first real turn starts so
// turn/start overrides can be merged before we write model-visible context.
self.set_previous_turn_settings(/*previous_turn_settings*/ None)
.await;
None
}
InitialHistory::Resumed(resumed_history) => {
let turn_context = self.new_default_turn().await;
@@ -1401,6 +1403,7 @@ impl Session {
if !is_subagent {
let _ = self.flush_rollout().await;
}
None
}
InitialHistory::Forked(mut rollout_items) => {
let turn_context = self.new_default_turn().await;
@@ -1449,6 +1452,19 @@ impl Session {
if !is_subagent {
let _ = self.flush_rollout().await;
}
Some(turn_context)
}
};
if let Some(turn_context) = turn_context
&& turn_context.config.memories.disable_on_external_context
{
let history = self.clone_history().await;
if let Some(item) = history
.raw_items()
.find(|item| matches!(item, ResponseItem::FunctionCallOutput { call_id: None, .. }))
{
mark_thread_memory_mode_polluted_if_external_context(self, &turn_context, item)
.await;
}
}
}
@@ -3087,6 +3103,13 @@ impl Session {
let rollout_items: Vec<RolloutItem> =
items.into_iter().map(RolloutItem::ResponseItem).collect();
self.persist_rollout_items(&rollout_items).await;
if turn_context.config.memories.disable_on_external_context
&& let Some(item) = response_items
.iter()
.find(|item| matches!(item, ResponseItem::FunctionCallOutput { call_id: None, .. }))
{
mark_thread_memory_mode_polluted_if_external_context(self, turn_context, item).await;
}
self.send_raw_response_items(turn_context, &response_items)
.await;
}