Let extensions contribute World State sections (#30100)

## Why

#29856 already owns the durable thread intent and exact environment
binding. This PR adds only the small missing extension boundary: an
extension can contribute one named World State section, while core still
owns persistence, diffing, and model-visible fragment types.

This lets skills stay in the skills extension instead of moving their
runtime into core.

## Shape

```text
extension-owned state
        |
        | contribute section id + JSON snapshot + renderer
        v
core World State
        |
        | compare with the previous snapshot
        v
no message, or one incremental model-visible update
```

The extension API is deliberately small:

```rust
fn contribute_world_state(...) -> Vec<WorldStateSectionContribution>
```

Core adapts the rendered result to `ContextualUserFragment`, records the
snapshot, and keeps the existing compaction/resume behavior.

## What changes

- Adds extension-owned World State section contributions.
- Calls those contributors from the existing per-step World State
builder.
- Restores durable selected capability roots into extension thread state
on resume.
- Keeps the actual model-context fragment and rollout machinery in core.

## What does not change

- No skill or MCP implementation moves out of its extension.
- No new file watcher, generation, or RPC.
- No generic migration of existing World State sections.
- No change to the stable environment-ID assumption from #29856.

## Example

```text
step 1 snapshot: skills = []
step 2 snapshot: skills = [executor-demo:deploy]

core asks the skills extension to render only that change.
```

## Stack

1. **This PR:** let extensions contribute World State sections.
2. Project executor skills through the skills extension.
3. Pin one MCP runtime to each model step.
4. Project selected MCP/app/connector metadata by environment
availability.
5. One end-to-end integration scenario.
This commit is contained in:
jif authored and GitHub committed 2026-06-25 22:23:51 +01:00
1 parent db541f4553
commit c9e6d9783d
9 files changed
+273 -5

No files matched your search

@@ -0,0 +1,122 @@
use std::sync::Arc;
use codex_protocol::ThreadId;
use codex_protocol::protocol::TurnEnvironmentSelection;
use serde_json::Value;
use crate::ExtensionData;
/// Host state available while an extension contributes one sampling step's World State.
pub struct WorldStateContributionInput<'a> {
pub thread_id: ThreadId,
pub turn_id: &'a str,
pub environments: &'a [TurnEnvironmentSelection],
pub session_store: &'a ExtensionData,
pub thread_store: &'a ExtensionData,
pub turn_store: &'a ExtensionData,
}
/// What the harness knows about the previous value of one extension-owned section.
pub enum PreviousWorldStateSection<'a> {
Absent,
Unknown,
Known(&'a Value),
}
/// Plain model-visible data rendered by an extension-owned World State section.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenderedWorldStateFragment {
role: &'static str,
markers: (&'static str, &'static str),
body: String,
}
impl RenderedWorldStateFragment {
pub fn new(
role: &'static str,
markers: (&'static str, &'static str),
body: impl Into<String>,
) -> Self {
Self {
role,
markers,
body: body.into(),
}
}
pub fn role(&self) -> &'static str {
self.role
}
pub fn markers(&self) -> (&'static str, &'static str) {
self.markers
}
pub fn body(&self) -> &str {
&self.body
}
}
type RenderDiff = dyn for<'a> Fn(PreviousWorldStateSection<'a>) -> Option<RenderedWorldStateFragment>
+ Send
+ Sync;
type LegacyFragmentMatcher = dyn Fn(&str, &str) -> bool + Send + Sync;
/// One extension-owned World State section captured for a sampling step.
///
/// The extension owns the stable ID, comparison snapshot, and diff rendering. The harness owns
/// persistence and the concrete model-context fragment envelope.
#[derive(Clone)]
pub struct WorldStateSectionContribution {
id: &'static str,
snapshot: Value,
render_diff: Arc<RenderDiff>,
matches_legacy_fragment: Arc<LegacyFragmentMatcher>,
}
impl WorldStateSectionContribution {
pub fn new(
id: &'static str,
snapshot: Value,
render_diff: impl for<'a> Fn(
PreviousWorldStateSection<'a>,
) -> Option<RenderedWorldStateFragment>
+ Send
+ Sync
+ 'static,
) -> Self {
Self {
id,
snapshot,
render_diff: Arc::new(render_diff),
matches_legacy_fragment: Arc::new(|_, _| false),
}
}
pub fn with_legacy_matcher(
mut self,
matcher: impl Fn(&str, &str) -> bool + Send + Sync + 'static,
) -> Self {
self.matches_legacy_fragment = Arc::new(matcher);
self
}
pub fn id(&self) -> &'static str {
self.id
}
pub fn snapshot(&self) -> &Value {
&self.snapshot
}
pub fn render_diff(
&self,
previous: PreviousWorldStateSection<'_>,
) -> Option<RenderedWorldStateFragment> {
(self.render_diff)(previous)
}
pub fn matches_legacy_fragment(&self, role: &str, text: &str) -> bool {
(self.matches_legacy_fragment)(role, text)
}
}