Skip to content

ACP server ignores the configured provider, hardcodes tool kind, and its unknown-session fallback always raises #120

Description

@LivXue

Testing Mini-Agent's ACP server against a third-party ACP client turned up three
defects in mini_agent/acp/. All are reproduced below on main (d76a4f638968),
Python 3.12, agent-client-protocol 0.12.1.

Note: reaching any of this first requires pinning mcp==1.9.4, per #119 -- with the
resolved mcp 2.x the package does not import at all. That one is already tracked,
so it is not repeated here.


1. The ACP server ignores the configured provider, so any OpenAI-wire endpoint 404s

mini_agent/acp/__init__.py:186 constructs the client without provider:

llm = LLMClient(api_key=config.llm.api_key, api_base=config.llm.api_base,
                model=config.llm.model, retry_config=...)

LLMClient.provider defaults to LLMProvider.ANTHROPIC, and the module never imports
LLMProvider at all. The CLI does derive it (mini_agent/cli.py:559):

provider = LLMProvider.ANTHROPIC if config.llm.provider.lower() == "anthropic" else LLMProvider.OPENAI

So provider: "openai" is honoured by mini-agent and silently ignored by
mini-agent-acp: the ACP server posts Anthropic-shaped requests at an
OpenAI-compatible endpoint and every turn fails.

Reproduction -- one config.yaml, two entry points:

api_key: "<key>"
api_base: "https://api.deepseek.com/v1"
model: "deepseek-v4-pro"
provider: "openai"
$ mini-agent -w /tmp/demo -t "Read hello.txt and reply with its exact contents."
  -> works: 1 tool call, correct answer

$ mini-agent-acp        # same config, driven over ACP
  -> Error: Retry failed after 4 attempts. Last error: Error code: 404

I verified this is not the /v1 suffix question from #37: the CLI succeeds with
api_base both with and without /v1, and the ACP server fails with both.

Fix (verified locally -- the same prompt then returns the correct answer over ACP):

-from mini_agent.llm import LLMClient
+from mini_agent.llm import LLMClient
+from mini_agent.schema.schema import LLMProvider

 llm = LLMClient(api_key=config.llm.api_key, api_base=config.llm.api_base,
                 model=config.llm.model,
+                provider=(LLMProvider.ANTHROPIC if config.llm.provider.lower() == "anthropic"
+                          else LLMProvider.OPENAI),
                 retry_config=...)

2. Every tool call is reported to the client as kind="execute"

mini_agent/acp/__init__.py:152:

await self._send(session_id, start_tool_call(call.id, label, kind="execute", raw_input=args))

The kind is a literal, so a file read, an edit, a search and a shell command are all
announced identically. The ACP schema defines ten kinds
(read, edit, delete, move, search, execute, think, fetch,
switch_mode, other), and clients group, label and icon tool calls from this field.

Observed frame for a plain file read -- the title already knows the right answer
while kind does not:

{"sessionUpdate": "tool_call", "kind": "execute",
 "title": "read_file(path='hello.txt')", "rawInput": {"path": "hello.txt"}}

A client that renders kind (the spec field) therefore shows every read as an
execution. Mapping the tool name onto the closer kind, with other as the fallback,
would fix it.


3. The unknown-session fallback in prompt() always raises

prompt() handles a missing session id like this:

# Auto-create session if not found (compatibility with clients that skip newSession)
new_session = await self.newSession(NewSessionRequest(cwd=None))

NewSessionRequest requires cwd: str and mcpServers, so this call fails validation
on every invocation and the recovery path has never run.

Reproduction -- initialize, then session/prompt with an id that was never created:

{"code": -32602, "message": "Invalid params", "data": {"errors": [
  {"loc": ["cwd"], "msg": "Input should be a valid string", "input": null},
  {"loc": ["mcpServers"], "msg": "Field required"}]}}

So a client that loses its session gets a confusing complaint about cwd rather than
"unknown session". Worth noting the intended behaviour is also questionable: had the
call succeeded, cwd=None would fall back to config.agent.workspace_dir rather than
the directory the client asked for, so the agent would silently operate somewhere
else. Returning a clear error for an unknown session seems better than reviving the
fallback.


Smaller things noticed while reading

  • self._sessions is never evicted, so an Agent (with its full message history) is
    retained per session for the process lifetime.
  • initialize ignores params entirely, so clientCapabilities are never read (the
    client's fs/terminal cannot be used) and the protocol version is echoed rather
    than negotiated.
  • AgentCapabilities(loadSession=False) is hardcoded and no loadSession /
    authenticate / setSessionMode methods exist. That is a fair description of what
    is implemented; flagging it only because clients use loadSession to decide whether
    a session handle can be reused, so Mini-Agent is treated as single-turn by clients
    that gate on it -- even though prompting one sessionId twice does hold context
    correctly, which I confirmed.
  • README.md links "ACP" to modelcontextprotocol/protocol; the code implements
    Zed's Agent Client Protocol (agent-client-protocol), which is a different spec.
  • pytest, pip and pipx are listed in [project.dependencies] rather than as dev
    extras.

Happy to open a PR for 1 and 2 if that is useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions