feat(gemini): plexum-host MCP exposure + real consume mirror
Same architecture as the codex sister change:
1. mcp-host subcommand routes argv[1] to mcpbridge.Run, reading
PLEXUM_MCP_SOCKET / PLEXUM_MCP_AGENT_ID from gemini-cli's
`gemini mcp add --env` env baking.
2. EnsureGeminiMCPRegistered handles per-agent stable registration
under `-s user --trust` so gemini auto-approves the tool surface.
3. Post-turn ParseGeminiToolCalls + EmitGeminiToolCalls scans the
chat JSONL at ~/.gemini/tmp/<ws>/chats/session-*.jsonl. Pairs
gemini's nested `toolCalls[].id` with the matching
`functionResponse.id` from later user lines.
4. MutateGeminiSession rewrites the chat JSONL: toolCalls[].args
→ {} (heavy), functionResponse.response.output → marker.
E2E verified: gemini call exec via plexum-host → dynamic.jsonl
records the call → dynamic-tool-clear consumes → gemini session
mirrored → resume gemini sees consumed marker not the original.
This commit is contained in:
@@ -26,6 +26,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"
|
||||
)
|
||||
|
||||
@@ -58,6 +59,25 @@ func Run(
|
||||
return nil, errors.New("gemini: no user text in request messages")
|
||||
}
|
||||
|
||||
// Per-turn MCP bridge so gemini can call Plexum host tools the
|
||||
// host advertised in req.Tools. Best-effort.
|
||||
var br *mcpbridge.Bridge
|
||||
if len(req.Tools) > 0 {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
if _, regErr := EnsureGeminiMCPRegistered(ctx, binary, exe, agent.AgentID); regErr != nil {
|
||||
host.Log("warn", "gemini: mcp registration failed", map[string]any{"err": regErr.Error()})
|
||||
} else {
|
||||
b, err := mcpbridge.SetupOnPath(ctx, host, agent.AgentID, req.Tools, StableSocketPath(agent.AgentID))
|
||||
if err != nil {
|
||||
host.Log("warn", "gemini: bridge setup failed", map[string]any{"err": err.Error()})
|
||||
} else {
|
||||
br = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeID := loadSessionID(workspace)
|
||||
|
||||
args := []string{"--skip-trust", "--output-format", "json"}
|
||||
@@ -156,6 +176,21 @@ func Run(
|
||||
})
|
||||
}
|
||||
|
||||
// Post-turn: scan gemini's chat JSONL for tool calls and emit
|
||||
// them as canonical events so dynamic.jsonl captures the real
|
||||
// ids (same ones SessionMutator will rewrite on consume).
|
||||
if sessionPath, perr := FindGeminiSessionFile(workspace); perr == nil {
|
||||
calls, perr := ParseGeminiToolCalls(sessionPath)
|
||||
if perr != nil {
|
||||
host.Log("warn", "gemini: parse session failed",
|
||||
map[string]any{"err": perr.Error(), "path": sessionPath})
|
||||
} else {
|
||||
EmitGeminiToolCalls(calls, emit)
|
||||
host.Log("debug", "gemini: emitted tool calls from session",
|
||||
map[string]any{"count": len(calls), "path": sessionPath})
|
||||
}
|
||||
}
|
||||
|
||||
var usage canonical.Usage
|
||||
for _, m := range resp.Stats.Models {
|
||||
usage.InputTokens += m.Tokens.Input
|
||||
@@ -172,6 +207,7 @@ func Run(
|
||||
StopReason: stopReason,
|
||||
Usage: &usage,
|
||||
})
|
||||
br.Close() // nil-safe
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user