feat(codex): plexum-host MCP exposure + real consume mirror
Three-part wiring so codex agents get full dynamic-tool consume
support:
1. mcp-host subcommand in the plugin binary (routes argv[1] to
mcpbridge.Run). Reads PLEXUM_MCP_SOCKET / PLEXUM_MCP_AGENT_ID
from env, baked into codex's `mcp add --env` registration.
2. EnsureCodexMCPRegistered registers a per-agent stable server
name (plexum-host-<sanitised>) lazily on first turn. Stable
per-agent socket path via StableSocketPath so codex's
registration entry and the per-turn bridge listener agree.
3. Post-turn ParseRolloutToolCalls + EmitCodexToolCalls walks the
matching rollout-*-<thread_id>.jsonl and emits canonical events
for every function_call + function_call_output pair using
codex's REAL call_X ids. dynamic.jsonl now has block_ids that
match codex's session, so consume mirror has real targets.
4. MutateCodexSession rewrites those response_item entries on
consume: function_call.arguments → "{}" (heavy), output → marker.
E2E verified: codex resume sees "...(tool called)" instead of
the original output.
This commit is contained in:
@@ -32,6 +32,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical"
|
||||
"git.hangman-lab.top/hzhang/Plexum-sdk-go/mcpbridge"
|
||||
plugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
|
||||
)
|
||||
|
||||
@@ -62,6 +63,28 @@ func Run(
|
||||
return nil, errors.New("codex: no user text in request messages")
|
||||
}
|
||||
|
||||
// Per-turn MCP bridge so codex can call Plexum host tools the
|
||||
// host advertised in req.Tools. Best-effort: setup failure logs
|
||||
// + continues; codex still runs with its built-in tools.
|
||||
var br *mcpbridge.Bridge
|
||||
if len(req.Tools) > 0 {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
if _, regErr := EnsureCodexMCPRegistered(ctx, binary, exe, agent.AgentID); regErr != nil {
|
||||
host.Log("warn", "codex: mcp registration failed", map[string]any{"err": regErr.Error()})
|
||||
} else {
|
||||
// Open the listener on the stable per-agent socket
|
||||
// path; codex's mcp-host subprocess will dial it.
|
||||
b, err := mcpbridge.SetupOnPath(ctx, host, agent.AgentID, req.Tools, StableSocketPath(agent.AgentID))
|
||||
if err != nil {
|
||||
host.Log("warn", "codex: bridge setup failed", map[string]any{"err": err.Error()})
|
||||
} else {
|
||||
br = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeID := loadSessionID(workspace)
|
||||
|
||||
// Build the argv. `codex exec [resume <id>]` then flags then prompt.
|
||||
@@ -105,7 +128,10 @@ func Run(
|
||||
}
|
||||
|
||||
out := make(chan canonical.TurnEvent, 16)
|
||||
go pumpEvents(ctx, host, cmd, stdout, stderr, workspace, out)
|
||||
go func() {
|
||||
pumpEvents(ctx, host, cmd, stdout, stderr, workspace, out)
|
||||
br.Close() // nil-safe
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -249,6 +275,23 @@ func pumpEvents(
|
||||
if hadError || werr != nil {
|
||||
stopReason = canonical.StopReason("error")
|
||||
}
|
||||
// Read the session rollout once codex has finished and replay
|
||||
// every recorded function_call / function_call_output pair as
|
||||
// tool_call_start/end + EventToolResult — with codex's REAL
|
||||
// call_X ids. The agentic loop attaches these to the iteration
|
||||
// so dynamic-* consume can target the same ids SessionMutator
|
||||
// later mirrors into the session JSONL.
|
||||
if sessionPath, perr := FindCodexSessionFile(workspace); perr == nil {
|
||||
calls, err := ParseRolloutToolCalls(sessionPath)
|
||||
if err != nil {
|
||||
host.Log("warn", "codex: parse rollout failed",
|
||||
map[string]any{"err": err.Error(), "path": sessionPath})
|
||||
} else {
|
||||
EmitCodexToolCalls(calls, emit)
|
||||
host.Log("debug", "codex: emitted tool calls from rollout",
|
||||
map[string]any{"count": len(calls), "path": sessionPath})
|
||||
}
|
||||
}
|
||||
emit(canonical.TurnEvent{
|
||||
Type: canonical.EventMessageEnd, StopReason: stopReason, Usage: &usage,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user