Studio: real Codex parallel-call tab UI + in-process SDK spoof

Two paired changes that finally make the Codex parallel-calls fan-out
visible as actual clickable tabs in the chat surface, plus a credit-
free spoof that lets the whole pipeline run in dev / CI without ever
touching the upstream API.

1. Real tab UI (frontend).

   The chat-adapter used to render the per-worker outputs as inline
   `[Codex tab 1/N] ...` text blocks in the assistant message body,
   which collapsed into one big run-on block once more than a handful
   of tokens had streamed. Now each `codex_*` SSE event is folded into
   `codexParallelState` and re-published as the `args.state` of a
   single tool-call part with `toolName === "codex_parallel"`. The
   assistant-ui surface dispatches that to the new
   `CodexParallelToolUI` wrapper, which mounts the existing
   `CodexParallelTabs` component -- one tab per worker, one Synthesis
   tab, click to switch. The stable `toolCallId` keeps assistant-ui
   updating the SAME card across stream yields rather than spawning
   new cards.

   `renderCodexTabsBlock` now returns the empty string so the message
   body no longer contains the labelled-text fallback (kept the
   function name so the rest of the adapter's `renderFullContent` /
   pin-signature paths are untouched).

2. Credit-free Codex SDK spoof (backend).

   New `studio/backend/core/inference/codex_spoof.py` exposes a drop-in
   subset of the upstream `openai_codex` surface (`AsyncCodex`,
   `AppServerConfig`, `ApprovalMode.deny_all`, `SandboxMode.read_only`,
   thread with `turn().stream()` + `run_streaming()` + `run()`) and
   emits deterministic per-tab streaming events tagged with the worker
   index, so flipping between tabs in the UI shows visibly distinct
   text. Activated by `UNSLOTH_CODEX_SPOOF=1`; `_import_codex` installs
   the spoof into `sys.modules` under both `openai_codex` and
   `codex_app_server` and the rest of the provider keeps running
   unchanged. OFF by default; production is unaffected.

   Six new tests cover the spoof itself (module install, env-flag
   gating, delta + completion event shape, per-tab tagging, provider
   import path, safety-kwargs resolution against the spoof). 69/69
   tests pass with and without the flag; TypeScript clean.
This commit is contained in:
Daniel Han 2026-05-27 13:30:53 +00:00
commit f01011e4dd
6 changed files with 533 additions and 60 deletions

View file

@ -358,7 +358,16 @@ def _import_codex() -> Any:
name resolves, so this branch is reached only when (a) the user
explicitly forces the provider via a stale stored config or (b)
the install state changes between status probe and chat submit.
When ``UNSLOTH_CODEX_SPOOF=1`` is set we install the in-process
spoof under ``openai_codex`` so the rest of the provider runs
end-to-end (with deterministic per-tab replies and no upstream
credit usage). The flag is OFF by default in production.
"""
from core.inference import codex_spoof
if codex_spoof.is_spoof_enabled():
codex_spoof.install_as_openai_codex()
for name in _SDK_MODULE_NAMES:
if importlib.util.find_spec(name) is not None:
return importlib.import_module(name)

View file

@ -0,0 +1,269 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Local-only Codex SDK spoof for credit-free dev / CI runs.
Activated by ``UNSLOTH_CODEX_SPOOF=1`` -- the gate in ``codex_provider``
swaps in this module's symbols for ``openai_codex`` so the rest of the
provider can run end-to-end (thread_start, turn().stream(), run_streaming,
run(), AppServerConfig, ApprovalMode.deny_all, SandboxMode.read_only)
without ever touching the real CLI or upstream API.
The fake stream emits one ``message.delta`` per visible token plus a
trailing ``ItemCompletedNotification(item=agentMessage)`` so both the
delta path and the completion-only fallback in
``_stream_thread_run`` exercise their real branches.
The replies are deterministic and tagged with the model + tab index so
the parallel-calls fan-out shows visibly distinct text per tab, which
is the point of the tab UI demo. The spoof intentionally does NOT
emit command / file / tool deltas -- those would be denylisted by
``_coerce_text`` and never reach the user, and we want the demo to
show the same shape Codex normally streams: pure agent text.
This file is import-safe: it has no side effects on import. It MUST
never be selected unless the env flag is set explicitly.
"""
from __future__ import annotations
import asyncio
import os
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, AsyncIterator, Optional
SPOOF_ENV_VAR = "UNSLOTH_CODEX_SPOOF"
def is_spoof_enabled() -> bool:
"""Return True when the env flag is set to an explicit truthy value."""
return os.environ.get(SPOOF_ENV_VAR, "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
class ApprovalMode(str, Enum):
deny_all = "deny_all"
auto_review = "auto_review"
class SandboxMode(str, Enum):
read_only = "read_only"
workspace_write = "workspace_write"
@dataclass
class AppServerConfig:
env: Optional[dict[str, str]] = None
codex_bin: Optional[str] = None
extra: dict[str, Any] = field(default_factory=dict)
@dataclass
class _AgentMessage:
type: str = "agentMessage"
text: str = ""
@dataclass
class _ItemRoot:
root: _AgentMessage
@dataclass
class _ItemCompletedNotification:
"""Mirrors openai_codex.api.ItemCompletedNotification shape.
The class name is matched verbatim by ``_completed_agent_message_text``
so the completion-only fallback in ``_stream_thread_run`` recognises
these payloads.
"""
item: _ItemRoot
type: str = "ItemCompletedNotification"
def _tab_id_from_system(system: Optional[str]) -> int:
"""Pull the synthetic ``[tab N]`` marker the provider prepends to
each parallel worker's system prompt (when present), else 0."""
if not system:
return 0
for line in system.splitlines():
line = line.strip()
if line.startswith("[tab ") and line.endswith("]"):
try:
return int(line[len("[tab "): -1].split("/")[0])
except ValueError:
pass
return 0
def _spoof_response_text(model: str, prompt: str, tab_id: int) -> str:
"""Deterministic but visibly per-tab response.
Format keeps each parallel worker's reply distinct so when the user
clicks between tabs they see different text -- the whole point of
the tab UI demo.
"""
prompt_clean = (prompt or "").strip().replace("\n", " ")
if len(prompt_clean) > 120:
prompt_clean = prompt_clean[:117] + "..."
tab_suffix = f" (worker {tab_id})" if tab_id else ""
return (
f"[spoof reply from {model}{tab_suffix}] "
f"You said: {prompt_clean!r}. "
f"This response is generated by the local Codex spoof "
f"(UNSLOTH_CODEX_SPOOF=1) -- no upstream tokens were used."
)
class _TurnStream:
"""Async iterator returned by ``Turn.stream()``.
Emits a sequence of dict-shaped ``message.delta`` events (one word at
a time, so the chat-adapter's streaming surface gets exercised) and
closes with an ``ItemCompletedNotification`` carrying the same final
text. Matches the dual delta + completion shape the real upstream
SDK emits.
"""
def __init__(self, text: str, delay_s: float = 0.01) -> None:
self._text = text
self._delay_s = delay_s
self._iter: Optional[AsyncIterator[Any]] = None
def __aiter__(self) -> "_TurnStream":
return self
async def _generate(self) -> AsyncIterator[Any]:
# One word at a time gives a visible streaming effect in the UI
# without flooding the SSE channel.
words = self._text.split(" ")
for i, word in enumerate(words):
chunk = (" " + word) if i > 0 else word
yield {"type": "message.delta", "delta": chunk}
if self._delay_s > 0:
await asyncio.sleep(self._delay_s)
# Final completion event -- the canonical SDK always emits this,
# and ``_stream_thread_run`` uses it as its fallback when no
# deltas arrived (so worth keeping even when deltas did stream).
yield _ItemCompletedNotification(item=_ItemRoot(root=_AgentMessage(text=self._text)))
async def __anext__(self) -> Any:
if self._iter is None:
self._iter = self._generate()
return await self._iter.__anext__()
# Some SDK revs let callers ``async with stream:``. Treat as a no-op.
async def __aenter__(self) -> "_TurnStream":
return self
async def __aexit__(self, *_exc: Any) -> None:
return None
class _Turn:
def __init__(self, text: str) -> None:
self._text = text
def stream(self) -> _TurnStream:
return _TurnStream(self._text)
class _Thread:
def __init__(self, model: str, system: Optional[str]) -> None:
self._model = model
self._system = system
self._tab_id = _tab_id_from_system(system)
# Canonical path: ``thread.turn(prompt).stream()``.
def turn(self, prompt: str) -> _Turn:
text = _spoof_response_text(self._model, prompt, self._tab_id)
return _Turn(text)
# Legacy path: ``async for event in thread.run_streaming(prompt)``.
def run_streaming(self, prompt: str) -> _TurnStream:
text = _spoof_response_text(self._model, prompt, self._tab_id)
return _TurnStream(text)
# Buffered fallback: ``await thread.run(prompt)`` returning a result
# whose ``.text`` (or ``.final_response``) is the answer.
async def run(self, prompt: str) -> Any:
from types import SimpleNamespace
text = _spoof_response_text(self._model, prompt, self._tab_id)
await asyncio.sleep(0)
return SimpleNamespace(text=text, final_response=text)
class AsyncCodex:
"""Spoof drop-in for ``openai_codex.AsyncCodex``.
Accepts the same ``config=AppServerConfig(...)`` constructor signature
Studio passes through. ``thread_start`` returns a ``_Thread`` whose
turn / run / run_streaming methods emit deterministic streams.
"""
def __init__(self, config: Optional[AppServerConfig] = None, **_kw: Any) -> None:
self._config = config or AppServerConfig()
self._started_at = time.time()
async def thread_start(
self,
*,
model: str,
base_instructions: Optional[str] = None,
system: Optional[str] = None,
approval_mode: Optional[ApprovalMode] = None,
sandbox: Optional[SandboxMode] = None,
**_extra: Any,
) -> _Thread:
await asyncio.sleep(0)
# Either kwarg path is accepted -- the real provider tries
# ``base_instructions`` first then falls back to ``system``.
sys_text = base_instructions if base_instructions is not None else system
return _Thread(model=model, system=sys_text)
def install_as_openai_codex() -> None:
"""Insert this module into ``sys.modules`` under the names the real
SDK would use, so ``importlib.util.find_spec`` succeeds and the
provider's existing import path picks it up unchanged.
Idempotent: a second call is a no-op. Called from ``codex_provider``
inside ``_import_codex`` when the env flag is set.
"""
import sys
for name in ("openai_codex", "codex_app_server"):
if name in sys.modules:
continue
sys.modules[name] = _build_module_alias(name)
def _build_module_alias(name: str) -> Any:
"""Build a module-like object exposing the same public symbols as
this file, under the requested import name. Using a fresh module
object (rather than aliasing ``codex_spoof`` directly) means the
SDK's ``__name__`` lookups (e.g. for ``ImportError`` messages) get
the real upstream-style name.
"""
import types
import importlib.machinery
mod = types.ModuleType(name)
# ``importlib.util.find_spec(name)`` walks ``sys.modules[name].__spec__``
# first, so an empty spec is required for the provider's existing
# availability probe to recognise the spoof.
mod.__spec__ = importlib.machinery.ModuleSpec(name, loader=None)
mod.AsyncCodex = AsyncCodex # type: ignore[attr-defined]
mod.AppServerConfig = AppServerConfig # type: ignore[attr-defined]
mod.ApprovalMode = ApprovalMode # type: ignore[attr-defined]
mod.SandboxMode = SandboxMode # type: ignore[attr-defined]
mod.__spoof__ = True # marker -- tests can assert this
return mod

View file

@ -2353,3 +2353,118 @@ class TestCodexDoneSentinelExactMatch:
assert 'if "[DONE]" in line' not in stripped, (
"Codex SSE wrapper still uses substring [DONE] check: " + stripped
)
class TestCodexSpoofModule:
"""Ensure the in-process Codex SDK spoof installs cleanly under the
env flag, satisfies _import_codex, exposes the SDK surface the
provider uses, and streams deterministic per-tab text. The spoof
is the credit-free path that the rest of the test suite (and the
UI demo) rides on, so a regression here would silently break
every downstream consumer.
"""
def test_install_swaps_in_module_when_flag_set(self, monkeypatch):
# Ensure clean import state.
import sys
for name in ("openai_codex", "codex_app_server"):
sys.modules.pop(name, None)
from core.inference import codex_spoof
monkeypatch.setenv(codex_spoof.SPOOF_ENV_VAR, "1")
assert codex_spoof.is_spoof_enabled()
codex_spoof.install_as_openai_codex()
import openai_codex # type: ignore[import-not-found]
assert getattr(openai_codex, "__spoof__", False) is True
assert hasattr(openai_codex, "AsyncCodex")
assert hasattr(openai_codex, "AppServerConfig")
assert openai_codex.ApprovalMode.deny_all
assert openai_codex.SandboxMode.read_only
def test_flag_disabled_does_not_install(self, monkeypatch):
import sys
for name in ("openai_codex", "codex_app_server"):
sys.modules.pop(name, None)
from core.inference import codex_spoof
monkeypatch.delenv(codex_spoof.SPOOF_ENV_VAR, raising=False)
assert not codex_spoof.is_spoof_enabled()
def test_spoof_stream_emits_deltas_and_completion(self):
import asyncio
from core.inference import codex_spoof
async def run():
codex = codex_spoof.AsyncCodex(
config=codex_spoof.AppServerConfig(env={})
)
thread = await codex.thread_start(
model="gpt-5.4-mini",
base_instructions=None,
approval_mode=codex_spoof.ApprovalMode.deny_all,
sandbox=codex_spoof.SandboxMode.read_only,
)
events = []
async for ev in thread.turn("hello").stream():
events.append(ev)
return events
events = asyncio.run(run())
deltas = [e for e in events if isinstance(e, dict) and e.get("type") == "message.delta"]
assert len(deltas) >= 3, "spoof should stream multiple deltas"
last = events[-1]
assert type(last).__name__ == "_ItemCompletedNotification"
# Must carry the worker tag when no [tab N] marker is in the system prompt.
full_text = last.item.root.text
assert "spoof reply from gpt-5.4-mini" in full_text
assert "no upstream tokens" in full_text
def test_spoof_tags_per_tab(self):
import asyncio
from core.inference import codex_spoof
async def reply_for_tab(idx: int) -> str:
codex = codex_spoof.AsyncCodex()
thread = await codex.thread_start(
model="gpt-5.4-mini",
base_instructions=f"[tab {idx}/3]",
)
result = await thread.run("explain LoRA")
return result.text
async def _gather():
return await asyncio.gather(
reply_for_tab(1),
reply_for_tab(2),
reply_for_tab(3),
)
tab_replies = asyncio.run(_gather())
# Each tab must mention its own worker index, so the UI tabs
# show visibly distinct text when clicked.
for i, reply in enumerate(tab_replies, start=1):
assert f"worker {i}" in reply, f"tab {i} missing its tag: {reply}"
def test_provider_picks_up_spoof_via_import(self, monkeypatch):
import sys
for name in ("openai_codex", "codex_app_server"):
sys.modules.pop(name, None)
from core.inference import codex_provider, codex_spoof
monkeypatch.setenv(codex_spoof.SPOOF_ENV_VAR, "1")
mod = codex_provider._import_codex()
assert getattr(mod, "__spoof__", False) is True
def test_safety_kwargs_resolve_against_spoof(self, monkeypatch):
import sys
for name in ("openai_codex", "codex_app_server"):
sys.modules.pop(name, None)
from core.inference import codex_provider, codex_spoof
monkeypatch.setenv(codex_spoof.SPOOF_ENV_VAR, "1")
codex_provider._import_codex() # ensures install
kwargs = codex_provider._safe_thread_safety_kwargs()
# Spoof exports ApprovalMode.deny_all + SandboxMode.read_only,
# so the provider must be able to pin both without falling
# through to the unsafe-defaults gate.
assert kwargs, "safety kwargs missing -- provider would fail closed"
assert str(kwargs["approval_mode"]).endswith("deny_all")
assert str(kwargs["sandbox"]).endswith("read_only")

View file

@ -23,6 +23,7 @@ import {
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { ToolGroup } from "@/components/assistant-ui/tool-group";
import { CodeExecutionToolUI } from "@/components/assistant-ui/tool-ui-code-execution";
import { CodexParallelToolUI } from "@/components/assistant-ui/tool-ui-codex-parallel";
import { ImageGenerationToolUI } from "@/components/assistant-ui/tool-ui-image-generation";
import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python";
import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal";
@ -1315,6 +1316,7 @@ const AssistantMessage: FC = () => {
python: PythonToolUI,
terminal: TerminalToolUI,
code_execution: CodeExecutionToolUI,
codex_parallel: CodexParallelToolUI,
image_generation: ImageGenerationToolUI,
},
Fallback: ToolFallback,

View file

@ -0,0 +1,33 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"use client";
/**
* Tool-call renderer for Codex parallel-calls fan-out.
*
* Driven by the chat-adapter pushing a tool-call part with
* ``toolName === "codex_parallel"`` whose ``args.state`` is a
* ``CodexParallelState`` value. We just unpack the state and hand it
* to the existing ``CodexParallelTabs`` component. Mounted via the
* ``tools.by_name`` map on ``MessagePrimitive.Parts`` in
* ``thread.tsx`` so it renders inline above the assistant's prose.
*/
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { memo } from "react";
import {
CodexParallelTabs,
EMPTY_CODEX_PARALLEL_STATE,
type CodexParallelState,
} from "@/features/chat/components/codex-parallel-tabs";
const CodexParallelToolUIImpl: ToolCallMessagePartComponent = ({ args }) => {
const state = (args as { state?: CodexParallelState } | undefined)?.state;
return <CodexParallelTabs state={state ?? EMPTY_CODEX_PARALLEL_STATE} />;
};
export const CodexParallelToolUI = memo(
CodexParallelToolUIImpl,
) as ToolCallMessagePartComponent;
CodexParallelToolUI.displayName = "CodexParallelToolUI";

View file

@ -58,6 +58,13 @@ import {
hasClosedThinkTag,
parseAssistantContent,
} from "../utils/parse-assistant-content";
import {
EMPTY_CODEX_PARALLEL_STATE,
hasCodexParallelContent,
reduceCodexParallelState,
type CodexParallelEvent,
type CodexParallelState,
} from "../components/codex-parallel-tabs";
import {
generateAudio,
listCachedGguf,
@ -1714,29 +1721,47 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
// dict keyed by tab_id and re-assembling cumulativeText from
// scratch on every codex event puts each tab's text under its
// own header regardless of arrival interleaving.
const codexTabBuffers = new Map<number, string>();
const codexTabClosed = new Set<number>();
const codexTabError = new Map<number, string>();
let codexTotalTabs = 0;
// Per-tab Codex fan-out state. Each codex_* SSE event is folded
// into ``codexParallelState`` via the pure reducer in
// ``components/codex-parallel-tabs``. The state is re-published
// on every yield as the ``args`` of a single tool-call part with
// ``toolName === "codex_parallel"`` so the assistant-ui surface
// can render real clickable tabs (one per worker plus a
// Synthesis tab) instead of inline ``[Codex tab N]`` headings.
// The stable toolCallId keeps assistant-ui updating the same
// part across stream yields rather than spawning new cards.
let codexParallelState: CodexParallelState = EMPTY_CODEX_PARALLEL_STATE;
let codexGatherEmitted = false;
const CODEX_PARALLEL_TOOL_ID = "codex_parallel_main";
function renderCodexTabsBlock(): string {
if (codexTabBuffers.size === 0) return "";
const lines: string[] = [];
const ids = [...codexTabBuffers.keys()].sort((a, b) => a - b);
for (const id of ids) {
const header = codexTotalTabs
? `[Codex tab ${id}/${codexTotalTabs}]`
: `[Codex tab ${id}]`;
lines.push(`\n\n${header}\n${codexTabBuffers.get(id) ?? ""}`);
if (codexTabError.has(id)) {
lines.push(`\n[Codex tab ${id} error: ${codexTabError.get(id)}]\n`);
}
if (codexTabClosed.has(id)) {
lines.push("\n");
}
function upsertCodexParallelToolPart(): void {
if (!hasCodexParallelContent(codexParallelState)) return;
const args = { state: codexParallelState };
const argsText = "";
const idx = toolCallParts.findIndex(
(p) => p.toolCallId === CODEX_PARALLEL_TOOL_ID,
);
const part: ToolCallMessagePart = {
type: "tool-call" as const,
toolCallId: CODEX_PARALLEL_TOOL_ID,
toolName: "codex_parallel",
argsText,
args: args as unknown as ToolCallMessagePart["args"],
};
if (idx === -1) {
toolCallParts.push(part);
} else {
toolCallParts[idx] = part;
}
return lines.join("");
}
// No inline `[Codex tab N]` block in the message body any more --
// the tab UI is mounted as a tool-call part above. The function
// is kept (returning the empty string) so the rest of the
// adapter's renderFullContent() / pin signature paths are
// unchanged across the file.
function renderCodexTabsBlock(): string {
return "";
}
// Codex parallel-calls fan-out renders the labeled tab outputs
@ -2222,50 +2247,70 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
chunk as unknown as { _toolEvent?: Record<string, unknown> }
)._toolEvent;
if (toolEvent !== undefined) {
// Codex parallel-calls fan-out events: route chunks
// into per-tab buffers keyed by tab_id, then render
// the whole codex block from scratch each event so
// concurrent tabs cannot interleave under the wrong
// header. `codex_gather` carries the synthesis
// payload which the backend also emits as a normal
// content delta later in the same SSE stream, so we
// only render a divider here to avoid duplicating
// the synthesis text.
// Codex parallel-calls fan-out events. Each event is
// folded into ``codexParallelState`` and re-published
// as the ``args.state`` of the ``codex_parallel`` tool-
// call part, so the assistant-ui surface renders one
// tab per worker plus a Synthesis tab the user can
// click between. ``codex_gather`` flips the flag so
// ``renderFullContent`` knows the synthesis stream is
// about to arrive on the regular content-delta path.
if (typeof toolEvent.type === "string" && toolEvent.type.startsWith("codex_")) {
if (toolEvent.type === "codex_tab_open") {
const tabId = Number(toolEvent.tab_id);
const evType = toolEvent.type;
const tabId = Number(toolEvent.tab_id);
let reduced: CodexParallelEvent | null = null;
if (evType === "codex_tab_open" && Number.isFinite(tabId)) {
const total = Number(toolEvent.total_tabs);
if (Number.isFinite(tabId)) {
if (!codexTabBuffers.has(tabId)) {
codexTabBuffers.set(tabId, "");
}
if (Number.isFinite(total) && total > codexTotalTabs) {
codexTotalTabs = total;
}
reduced = {
type: "codex_tab_open",
tab_id: tabId,
query:
typeof toolEvent.query === "string"
? toolEvent.query
: undefined,
total_tabs: Number.isFinite(total) ? total : undefined,
};
} else if (evType === "codex_tab_chunk" && Number.isFinite(tabId)) {
const text =
typeof toolEvent.text === "string" ? toolEvent.text : "";
if (text) {
reduced = {
type: "codex_tab_chunk",
tab_id: tabId,
text,
};
}
} else if (toolEvent.type === "codex_tab_chunk") {
const tabId = Number(toolEvent.tab_id);
const text = typeof toolEvent.text === "string" ? toolEvent.text : "";
if (Number.isFinite(tabId) && text) {
const prev = codexTabBuffers.get(tabId) ?? "";
codexTabBuffers.set(tabId, prev + text);
}
} else if (toolEvent.type === "codex_tab_error") {
const tabId = Number(toolEvent.tab_id);
const err = typeof toolEvent.error === "string" ? toolEvent.error : "error";
if (Number.isFinite(tabId)) {
codexTabError.set(tabId, err);
if (!codexTabBuffers.has(tabId)) {
codexTabBuffers.set(tabId, "");
}
}
} else if (toolEvent.type === "codex_tab_close") {
const tabId = Number(toolEvent.tab_id);
if (Number.isFinite(tabId)) {
codexTabClosed.add(tabId);
}
} else if (toolEvent.type === "codex_gather") {
} else if (evType === "codex_tab_error" && Number.isFinite(tabId)) {
reduced = {
type: "codex_tab_error",
tab_id: tabId,
error:
typeof toolEvent.error === "string"
? toolEvent.error
: "error",
};
} else if (evType === "codex_tab_close" && Number.isFinite(tabId)) {
reduced = { type: "codex_tab_close", tab_id: tabId };
} else if (evType === "codex_gather") {
codexGatherEmitted = true;
reduced = {
type: "codex_gather",
summary:
typeof toolEvent.summary === "string"
? toolEvent.summary
: undefined,
tab_count:
typeof toolEvent.tab_count === "number"
? toolEvent.tab_count
: undefined,
};
}
if (reduced) {
codexParallelState = reduceCodexParallelState(
codexParallelState,
reduced,
);
upsertCodexParallelToolPart();
}
const codexParts = parseAssistantContent(renderFullContent());
yield {