Commit Graph
6 Commits
Author SHA1 Message Date
9914d51040 MUL-5846 fix(agent): stop launching CodeBuddy with MCP disabled (#6556)
* fix(agent): stop launching CodeBuddy with MCP disabled (MUL-5846)

`--strict-mcp-config` means "only use MCP servers from --mcp-config".
buildCodebuddyArgs passed it unconditionally, so every CodeBuddy agent
without a Multica-managed mcp_config was launched in strict mode with no
config at all — zero MCP servers, and the user's own ~/.codebuddy.json
silently ignored. claude.go already gates the flag on hasManagedMcpConfig;
do the same here, and gate the --mcp-config temp file on the same
three-state check so a JSON `null` no longer counts as a managed set.

Also split CodeBuddy off Claude in runtime_mcp.go. CodeBuddy is a Claude
Code fork but keeps its own config file and plugin root (~/.codebuddy.json,
~/.workbuddy/), so reading ~/.claude.json leaked Claude's servers into
CodeBuddy launches while dropping CodeBuddy's own.

Add kimi to the runtime MCP inventory (<KIMI_CODE_HOME>/mcp.json), which
previously reported this runtime as not supporting MCP. Inventory only:
`kimi acp` already merges that file with the ephemeral `mcpServers` array
the ACP backend sends in session/new, and a duplicate name spawns the
server twice, so the task-local merge stays a passthrough for kimi.

Verified against CodeBuddy 2.x and kimi-code 0.33.0 with a local stdio MCP
probe server: before, the strict launch line answered NO_MCP_TOOL and never
spawned the server; after, it returns the probe's payload.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): resolve CodeBuddy's real MCP config path (MUL-5846)

Review follow-up. The previous commit pointed CodeBuddy at ~/.codebuddy.json,
which is only the last fallback in its chain. Verified against the installed
CodeBuddy CLI:

  - the config dir is $CODEBUDDY_CONFIG_DIR, defaulting to ~/.codebuddy
  - user-scope MCP is the FIRST existing of <configDir>/.mcp.json ->
    <configDir>/mcp.json -> ~/.codebuddy.json (a fallback chain, not a merge);
    `codebuddy mcp add --scope user` writes the first
  - those files are JSONC: comments and trailing commas parse fine
  - plugins live under <configDir>/plugins, not ~/.workbuddy

This is not just a missing inventory row. When an agent has a managed
mcp_config, codebuddy.go turns on --strict-mcp-config, so the merged document
the daemon writes is the entire world CodeBuddy sees. Reading the wrong file
meant "add one cloud MCP server" silently dropped every server the user had
configured locally, which is exactly what mergeRuntimeAndAgentMcpConfig exists
to prevent.

stripJSONC rewrites comments to spaces and drops trailing commas without
touching string literals, so a `//` or a comma inside a command argument
survives.

Also adds the session/new counterpart to TestKimiResumeIncludesMcpServers:
Kimi takes MCP over ACP rather than as a CLI flag, so a bare `kimi acp`
launch line is not evidence the servers were dropped.

Regressions added: path precedence, CODEBUDDY_CONFIG_DIR, JSONC (including
comment-like text inside strings), managed + local merge, same-name override,
explicit empty set, JSON null passthrough.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): let CodeBuddy merge its own MCP scopes natively (MUL-5846)

Round-2 review follow-up. Pointing the daemon at CodeBuddy's user-scope file
only fixed one of three scopes: --strict-mcp-config drops user, project AND
local config, so an agent with a managed mcp_config still lost every server
the user had configured locally.

Measured against CodeBuddy 2.x with one real MCP server registered per scope,
using process spawns as the oracle (the model's own tool list is not
trustworthy here — it happily names servers that failed to connect):

  --mcp-config only ....... managed + user + local
  --mcp-config + strict ... managed only
  strict only ............. nothing at all

So stop passing --strict-mcp-config entirely and let CodeBuddy build the
union itself. A managed entry already wins a same-name collision natively
(verified), which is exactly what mergeRuntimeAndAgentMcpConfig promises, so
codebuddy also drops out of the daemon-side merge and becomes a passthrough
like kimi.

This removes the class of bugs the previous approach kept generating: the
daemon no longer has to reproduce CodeBuddy's config-root resolution, scope
precedence, or JSONC parsing on the launch path, and the review's second
must-fix goes away with it — agent custom_env CODEBUDDY_CONFIG_DIR is now
read by the CLI itself, so the daemon resolving a different directory can no
longer desync the launch. Project-scope servers also keep their native
approval gate instead of being rewritten into daemon-generated CLI config.
(Measured: a project-scope server sits at "Needs approval" and never starts
headless, with or without our flags.)

codebuddyUserMcpConfigPath and the JSONC reader stay, now used only by the
read-only runtime inventory the Agent > MCP tab renders.

stripJSONC also tightened per the review: only the last comma before a closer
is dropped, so `[1,,,]` stays invalid instead of being silently repaired, and
output is now exactly the same length as input so parse-error offsets line up.

Co-authored-by: multica-agent <[email protected]>

* test(agent): pin kimi MCP delivery against the real CLI (MUL-5846)

Users reported that MCP configured in Multica never reaches kimi, citing the
bare `kimi acp` launch line as evidence. That line carries no MCP flags
because the CLI has none — `kimi acp --help` lists only --login and --help —
so kimi takes MCP over ACP session/new instead, and only an end-to-end run
against the real binary can settle the claim.

Drives this package's own kimi backend with a Multica-shaped agent.mcp_config.
The oracle is the MCP server process itself: a stdio fixture that appends to a
log when spawned and returns a sentinel from its one tool, so neither a
hand-written ACP fixture nor the model's own account of its tools can fake a
pass. Verified against kimi-code 0.33.0: server spawned, tools/list served,
tools/call answered, sentinel in the task output.

Behind the agentintegration tag and MULTICA_RUN_REAL_AGENT_SMOKE, so the
default suites and CI are unaffected.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): reject unterminated JSONC block comments (MUL-5846)

Review nits.

stripJSONC blanked an unterminated `/*` all the way to EOF, so
`{"mcpServers":{}} /* oops` became valid JSON plus whitespace, and the `/*/`
near-miss let the opener's own `*` double as the closer's. Either could make
the Agent > MCP tab list servers out of a file CodeBuddy itself rejects —
verified: the CLI answers "No MCP servers configured" for both. It now
consumes the opener before scanning and returns an error when no closer is
found. Only the inventory is affected; the launch path stopped depending on
this reader last commit.

Also refreshes the launch-path comment, which still described the managed file
as a "controlled set" gated by a strict-mode three-state check that
buildCodebuddyArgs no longer performs. The flag is gone; --mcp-config now adds
to CodeBuddy's own user/project/local scopes.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 15:22:01 +08:00
Bohan Jiang 3eff25401d MUL-5785 fix(agent): return an empty output when a stream-json run has no final text (#6557)
A successful stream-json run with no deliverable text returned the sentence "The agent completed without a final response." as its output. That string is a signal, not a message to a user, and shipping it as content defeated every no-response decision the delivery layer already owns — all three branch on the output being EMPTY: an issue task skips the synthesized fallback comment, a direct chat writes a localized no_response outcome, and a Slack/Lark channel drops the turn instead of pushing anything out.

Return the empty string instead, and classify each assistant event explicitly so the fallback answer only survives turns we positively understood: a tool-using turn clears it, an unparseable or unknown-shape event clears it (fail closed), and a genuinely silent thinking-only turn leaves it alone. The decision lives in one shared assistantTurn.resolveFallback across claude, codebuddy and qwen. Unreadable events are counted as unreadable_assistant_count so a CLI schema drift surfaces as a number rather than silently empty outputs.

Status stays completed: an empty output is a legitimate terminal result for a turn that ended on a tool call, and re-running it would repeat side effects that already happened.

Fixes #6462
2026-08-07 14:38:03 +08:00
75c11db048 MUL-5549: feat(agent): discover codebuddy models over ACP instead of scraping --help (#6203)
* feat(agent): discover codebuddy models over ACP instead of scraping --help (MUL-5549)

CodeBuddy speaks ACP, and `session/new` answers with a structured catalog under
models.availableModels plus a currentModelId — exactly the shape the shared
parseACPSessionNewModels already reads for Copilot / Kimi / Kiro / Qoder / Grok /
TRAE. Scraping the `--model` line out of `codebuddy --help` was never necessary.

The help text carried IDs and nothing else, which cost us three things:

- Labels were guessed from the ID and were simply wrong. `kimi-k3-1` rendered as
  "Kimi K3 1" where the CLI says Kimi-K3; `deepseek-v3-2-volc` as
  "Deepseek V3 2 Volc" where the CLI says DeepSeek-V3.2.
- The default model was a "first entry wins" guess rather than the advertised
  currentModelId.
- The effort catalog needed a second regex over the same output.

All three come from the handshake now. The effort catalog rides along in the
same session/new response as the `thought_level` config option, so it costs no
extra process — which also retires the "at most one --help per request"
constraint added in #6196, because --help is no longer run at all.

One trap worth naming: thought_level advertises `enabled` ("On (default)")
alongside the six real levels, but `--effort enabled` is not a valid command
line — the daemon passes the selected level straight to the flag. Advertised
levels are filtered against the flag's accepted set, and a currentValue outside
that set (the default `enabled`) becomes an empty DefaultLevel, which the UI
renders as a generic "Default" instead of a value we cannot pass through.

Two adjacent inaccuracies surfaced while confirming the real level set against
CodeBuddy 2.130.0, both fixed here: the static effort fallback omitted `minimal`
and `max`, and so did the server-side IsKnownThinkingValue gate — so the server
rejected two levels the CLI genuinely accepts.

Discovery keeps its fallback, still marked Fallback so it can never be cached as
authoritative (#6196). That covers the not-logged-in case, which is deliberately
NOT special-cased with an auth step: the catalog came back without calling
authenticate on a logged-in CLI, and inventing an auth branch we cannot exercise
would be speculation.

Removes codebuddyModelRe, parseCodebuddyModels, codebuddyModelLabel,
codebuddyModelProvider, codebuddyEffortRe, parseCodebuddyEffortHelp,
codebuddyEffortSuperset, codebuddyHelpOutput and its 60s help cache.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): keep codebuddy's vendor grouping after the ACP migration (MUL-5549)

Review nit, and a real regression in the previous commit. Dropping
codebuddyModelProvider looked like removing dead code, but it was the only thing
populating Model.Provider for CodeBuddy — and the picker groups on that field.

acpModelEntry can only recover a vendor from a `vendor:model` id. CodeBuddy's
are bare (`glm-5.2`, `kimi-k3-1`), so every model came back with an empty
Provider, and model-dropdown renders the empty group with no header at all: all
16 models would have collapsed into one unlabelled list where main shows Zhipu /
Kimi / MiniMax / DeepSeek / Hunyuan sections.

Restores the prefix inference as a post-pass over the ACP catalog, exactly the
shape discoverCopilotModels already uses for the same reason.

Verified against the real CLI: all 16 models land in five vendor groups with none
ungrouped. Tests assert the vendor for every id CodeBuddy 2.130.0 advertises plus
the static fallback ids, and that the fallback entries' hardcoded providers agree
with the inference. Removing the post-pass fails them.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-30 21:40:23 +08:00
280fa28e0d fix(agent): deny CodeBuddy's interactive plan-mode tools in headless runs (MUL-5383) (#6104)
CodeBuddy exempts AskUserQuestion and ExitPlanMode from permission-mode
finalization, so `--permission-mode bypassPermissions` never auto-approves
them. Once the model entered plan mode the session mode is Plan, not
BypassPermissions, so ExitPlanMode also missed the daemon's bypass
fast-path and went to the SDK permission bridge — which waits with no
timeout for a confirmation the headless runtime cannot render. The task
sat in-flight until the 2h tool watchdog, and users killed it by hand.

Deny EnterPlanMode/ExitPlanMode alongside the AskUserQuestion we already
deny. Each tool is passed as its own argv value because CodeBuddy matches
disallowedTools entries exactly and does not split on commas.

Also send `allowed: true` on control_response: CodeBuddy's
SdkPermissionClient reads `allowed`, not Claude Code's `behavior`, so the
daemon's "auto-approve" was being read as a denial.

Fixes #6012

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-29 15:55:02 +08:00
cf1fc64671 MUL-4824: fail closed on incomplete Claude-style result streams (#5522)
* fix(agent): fail closed on incomplete stream results

Co-authored-by: multica-agent <[email protected]>

* fix(agent): tighten empty result fallback

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-16 15:22:46 +08:00
Liu Guanzhongandkrislliu 4594c776e1 feat(agent): add CodeBuddy as first-class CLI backend (#3186)
* feat(agent): add codebuddyBackend struct and buildCodebuddyArgs

Introduces the codebuddy agent backend skeleton with args builder
that mirrors claudeBackend's protocol flags (stream-json, bypass
permissions, blocked args filtering) for the codebuddy CLI fork.

* feat(agent): implement codebuddyBackend.Execute with stream-json parsing

* feat(agent): wire codebuddy into New() factory and launchHeaders

* feat(agent): add codebuddy dynamic model discovery from --help

* feat(agent): add codebuddy thinking/effort discovery and providerThinkingEnums

* feat(daemon): add codebuddy CLI probe, env vars, and args support

* fix(agent): use len(models)==0 for default model instead of loop index

* fix(agent): increase codebuddy --help timeout to 35s for slow CLI startup

* fix(agent): address codebuddy PR review feedback

- Wire codebuddy into execenv: reuse claude's CLAUDE.md, .claude/skills,
  and ~/.claude/skills paths since CodeBuddy is a Claude Code fork
- Replace hardcoded 20-min timeout with runContext for zero-timeout =
  no-deadline semantics matching all other backends
- Restore runContext regression tests lost in rebase merge
- Mirror claude.go execution model: concurrent stdin write to prevent
  pipe deadlock, sync.Once for stdin closure, keep stdin open for
  control_request auto-approval mid-run
- Add control_request handling with auto-approve behavior
- Add RequestID/Request fields to codebuddySDKMessage
- Add codebuddy to metrics knownRuntimeProviders
- Add codebuddy to provider-logo.tsx (reuses ClaudeLogo)
- Consolidate --help discovery: shared codebuddyHelpOutput cache
  eliminates duplicate cold-start invocations

---------

Co-authored-by: krislliu <[email protected]>
2026-06-12 15:22:16 +08:00