Add experimental turn additional context (#24154)

## Summary

Adds experimental `additionalContext` support to `turn/start` and
`turn/steer` so clients can provide ephemeral external context, such as
browser or automation state, without turning that plumbing into a
visible user prompt or triggering user-prompt lifecycle behavior.

## API Shape

The parameter shape is:

```ts
additionalContext?: Record<string, {
  value: string
  kind: "untrusted" | "application"
}> | null
```

Example:

```json
{
  "additionalContext": {
    "browser_info": {
      "value": "Active tab is CI failures.",
      "kind": "untrusted"
    },
    "automation_info": {
      "value": "CI rerun is in progress.",
      "kind": "application"
    }
  }
}
```

The keys are opaque and caller-defined.

## Context Injection

When provided, accepted entries are inserted into model context as
hidden contextual message items, not as visible thread user-message
items.

`kind: "untrusted"` entries are inserted with role `user`:

```text
<external_${key}>${value}</external_${key}>
```

`kind: "application"` entries are inserted with role `developer`:

```text
<${key}>${value}</${key}>
```

Values are not escaped. Each value is truncated to 1k approximate tokens
before wrapping.

For `turn/start`, accepted additional context is inserted before normal
user input. For `turn/steer`, additional context is merged only when the
steer includes non-empty user input; context-only steers still reject as
empty input.

## Dedupe Strategy

`AdditionalContextStore` lives on session state and stores the latest
complete additional-context map.

Each `turn/start` or non-empty `turn/steer` treats its
`additionalContext` as the current complete set of values. Entries are
injected only when the key is new or the exact entry for that key
changed, including `value` or `kind`. After merging, the store is
replaced with the provided map, so omitted keys are removed from the
retained set and can be injected again later if reintroduced.

Omitting `additionalContext`, passing `null`, or passing an empty object
resets the store to empty and injects nothing.

## What Changed

- Threads experimental v2 `additionalContext` through app-server into
core turn start and steer handling.
- Adds separate contextual fragment types for untrusted user-role
context and application developer-role context.
- Uses pending response input items so additional context can be
combined with normal user input without treating it as prompt text.
- Adds integration coverage for start/steer flow, role routing,
dedupe/reset behavior, deletion/re-add behavior, hook-blocked input
behavior, empty context-only steer rejection, external-fragment marker
matching, and truncation.
This commit is contained in:
pakrym-oai authored and GitHub committed 2026-05-26 13:02:34 -07:00
1 parent cd934c8bcb
commit 768848ab6f
108 files changed
+1583 -57

No files matched your search

+15 -1
View File
@@ -10,6 +10,7 @@ use tracing::debug_span;
use tracing::info_span;
use crate::session::SteerInputError;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::session::SessionSettingsUpdate;
@@ -194,6 +195,7 @@ pub(super) async fn user_input_or_turn_inner(
environments,
final_output_json_schema,
responsesapi_client_metadata,
additional_context,
thread_settings,
} = op
else {
@@ -224,6 +226,7 @@ pub(super) async fn user_input_or_turn_inner(
let accepted_items = match sess
.steer_input(
items.clone(),
additional_context.clone(),
/*expected_turn_id*/ None,
responsesapi_client_metadata.clone(),
)
@@ -246,9 +249,20 @@ pub(super) async fn user_input_or_turn_inner(
)
.await;
let accepted_items = items.clone();
let additional_context_input = {
let mut state = sess.state.lock().await;
state.additional_context.merge(additional_context)
};
let mut task_input = additional_context_input
.into_iter()
.map(TurnInput::ResponseInputItem)
.collect::<Vec<_>>();
if !items.is_empty() {
task_input.push(TurnInput::UserInput(items));
}
sess.spawn_task(
Arc::clone(&current_context),
items,
task_input,
crate::tasks::RegularTask::new(),
)
.await;