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:
@@ -25,6 +25,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"
|
||||
|
||||
"git.hangman-lab.top/hzhang/Plexum-gemini-provider/internal/runner"
|
||||
@@ -125,31 +126,29 @@ func (p *geminiPlugin) StreamWithAgent(ctx context.Context, modelID string, agen
|
||||
return runner.Run(ctx, p.host, agent, cliModel, p.cfg.Binary, p.cfg.ExtraArgs, req)
|
||||
}
|
||||
|
||||
// MutateSession is the session-mirror hook (decision #29x). gemini-cli
|
||||
// stores transcripts under ~/.gemini/tmp/<workspace-name>/chats/
|
||||
// session-<ISO>-<session_id_prefix>.jsonl with a `$set`-style mutation
|
||||
// log (each line either a session-meta header or `{"$set":{...}}`
|
||||
// applied in order). Tool-call records use gemini-cli's own id
|
||||
// namespace (no shape doc as of this writing) which doesn't match
|
||||
// Plexum's toolu_*-style block ids.
|
||||
//
|
||||
// v1 ships as a logged no-op: we resolve the file path so operator
|
||||
// telemetry confirms wiring, but we don't attempt to rewrite. When
|
||||
// Plexum starts routing tool-call ids through gemini's native surface
|
||||
// (preserving the round-trip), the rewriter plugs in here.
|
||||
// MutateSession mirrors host dynamic-* consume mutations onto the
|
||||
// gemini chat JSONL at ~/.gemini/tmp/<ws>/chats/session-*.jsonl so
|
||||
// the next `gemini --resume` sees the consumed view. Tool-call
|
||||
// round-trip is captured by runner.ParseGeminiToolCalls +
|
||||
// EmitGeminiToolCalls (post-turn), so the BlockMutations we receive
|
||||
// here resolve to ids actually in gemini's session.
|
||||
func (p *geminiPlugin) MutateSession(ctx context.Context, req plugin.SessionMutateRequest) error {
|
||||
path, err := runner.FindGeminiSessionFile(req.Workspace)
|
||||
if err != nil {
|
||||
p.host.Log("debug", "gemini session mutate: find failed", map[string]any{
|
||||
"agent": req.AgentID, "err": err.Error(),
|
||||
})
|
||||
if len(req.Mutations) == 0 {
|
||||
return nil
|
||||
}
|
||||
p.host.Log("info", "gemini session mutate (no-op v1)", map[string]any{
|
||||
"agent": req.AgentID,
|
||||
"session_path": path,
|
||||
"requested": len(req.Mutations),
|
||||
"reason": "gemini tool-call id round-trip not yet wired",
|
||||
mu := make([]runner.GeminiMutation, 0, len(req.Mutations))
|
||||
for _, m := range req.Mutations {
|
||||
mu = append(mu, runner.GeminiMutation{BlockID: m.BlockID, Op: m.Op})
|
||||
}
|
||||
touched, err := runner.MutateGeminiSession(req.Workspace, mu)
|
||||
if err != nil {
|
||||
p.host.Log("warn", "gemini session mutate failed", map[string]any{
|
||||
"agent": req.AgentID, "err": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
p.host.Log("info", "gemini session mutate complete", map[string]any{
|
||||
"agent": req.AgentID, "touched": touched, "requested": len(req.Mutations),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -169,8 +168,41 @@ func mapModel(plexumModel string) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "mcp-host" {
|
||||
if err := runMCPHost(os.Args[2:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mcp-host: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := plugin.Serve(&geminiPlugin{}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "plexum-gemini-provider-plugin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runMCPHost(argv []string) error {
|
||||
opts := mcpbridge.Opts{
|
||||
SocketPath: os.Getenv("PLEXUM_MCP_SOCKET"),
|
||||
AgentID: os.Getenv("PLEXUM_MCP_AGENT_ID"),
|
||||
}
|
||||
for i := 0; i < len(argv); i++ {
|
||||
switch argv[i] {
|
||||
case "--socket":
|
||||
if i+1 >= len(argv) {
|
||||
return errors.New("--socket needs value")
|
||||
}
|
||||
opts.SocketPath = argv[i+1]
|
||||
i++
|
||||
case "--agent-id":
|
||||
if i+1 >= len(argv) {
|
||||
return errors.New("--agent-id needs value")
|
||||
}
|
||||
opts.AgentID = argv[i+1]
|
||||
i++
|
||||
default:
|
||||
return fmt.Errorf("unknown mcp-host flag: %s", argv[i])
|
||||
}
|
||||
}
|
||||
return mcpbridge.Run(context.Background(), opts)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user