feat: support for codex as external CLI
fix: improved handling of MCP token limits when handling CLI output
This commit is contained in:
@@ -5,10 +5,12 @@ from __future__ import annotations
|
||||
from clink.models import ResolvedCLIClient
|
||||
|
||||
from .base import AgentOutput, BaseCLIAgent, CLIAgentError
|
||||
from .codex import CodexAgent
|
||||
from .gemini import GeminiAgent
|
||||
|
||||
_AGENTS: dict[str, type[BaseCLIAgent]] = {
|
||||
"gemini": GeminiAgent,
|
||||
"codex": CodexAgent,
|
||||
}
|
||||
|
||||
|
||||
|
||||
41
clink/agents/codex.py
Normal file
41
clink/agents/codex.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Codex-specific CLI agent hooks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from clink.models import ResolvedCLIClient
|
||||
from clink.parsers.base import ParserError
|
||||
|
||||
from .base import AgentOutput, BaseCLIAgent
|
||||
|
||||
|
||||
class CodexAgent(BaseCLIAgent):
|
||||
"""Codex CLI agent with JSONL recovery support."""
|
||||
|
||||
def __init__(self, client: ResolvedCLIClient):
|
||||
super().__init__(client)
|
||||
|
||||
def _recover_from_error(
|
||||
self,
|
||||
*,
|
||||
returncode: int,
|
||||
stdout: str,
|
||||
stderr: str,
|
||||
sanitized_command: list[str],
|
||||
duration_seconds: float,
|
||||
output_file_content: str | None,
|
||||
) -> AgentOutput | None:
|
||||
try:
|
||||
parsed = self._parser.parse(stdout, stderr)
|
||||
except ParserError:
|
||||
return None
|
||||
|
||||
return AgentOutput(
|
||||
parsed=parsed,
|
||||
sanitized_command=sanitized_command,
|
||||
returncode=returncode,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
duration_seconds=duration_seconds,
|
||||
parser_name=self._parser.name,
|
||||
output_file_content=output_file_content,
|
||||
)
|
||||
@@ -33,4 +33,10 @@ INTERNAL_DEFAULTS: dict[str, CLIInternalDefaults] = {
|
||||
default_role_prompt="systemprompts/clink/gemini_default.txt",
|
||||
runner="gemini",
|
||||
),
|
||||
"codex": CLIInternalDefaults(
|
||||
parser="codex_jsonl",
|
||||
additional_args=["exec"],
|
||||
default_role_prompt="systemprompts/clink/codex_default.txt",
|
||||
runner="codex",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import BaseParser, ParsedCLIResponse, ParserError
|
||||
from .codex import CodexJSONLParser
|
||||
from .gemini import GeminiJSONParser
|
||||
|
||||
_PARSER_CLASSES: dict[str, type[BaseParser]] = {
|
||||
CodexJSONLParser.name: CodexJSONLParser,
|
||||
GeminiJSONParser.name: GeminiJSONParser,
|
||||
}
|
||||
|
||||
|
||||
63
clink/parsers/codex.py
Normal file
63
clink/parsers/codex.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Parser for Codex CLI JSONL output."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from .base import BaseParser, ParsedCLIResponse, ParserError
|
||||
|
||||
|
||||
class CodexJSONLParser(BaseParser):
|
||||
"""Parse stdout emitted by `codex exec --json`."""
|
||||
|
||||
name = "codex_jsonl"
|
||||
|
||||
def parse(self, stdout: str, stderr: str) -> ParsedCLIResponse:
|
||||
lines = [line.strip() for line in (stdout or "").splitlines() if line.strip()]
|
||||
events: list[dict[str, Any]] = []
|
||||
agent_messages: list[str] = []
|
||||
errors: list[str] = []
|
||||
usage: dict[str, Any] | None = None
|
||||
|
||||
for line in lines:
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
events.append(event)
|
||||
event_type = event.get("type")
|
||||
if event_type == "item.completed":
|
||||
item = event.get("item") or {}
|
||||
if item.get("type") == "agent_message":
|
||||
text = item.get("text")
|
||||
if isinstance(text, str) and text.strip():
|
||||
agent_messages.append(text.strip())
|
||||
elif event_type == "error":
|
||||
message = event.get("message")
|
||||
if isinstance(message, str) and message.strip():
|
||||
errors.append(message.strip())
|
||||
elif event_type == "turn.completed":
|
||||
turn_usage = event.get("usage")
|
||||
if isinstance(turn_usage, dict):
|
||||
usage = turn_usage
|
||||
|
||||
if not agent_messages and errors:
|
||||
agent_messages.extend(errors)
|
||||
|
||||
if not agent_messages:
|
||||
raise ParserError("Codex CLI JSONL output did not include an agent_message item")
|
||||
|
||||
content = "\n\n".join(agent_messages).strip()
|
||||
metadata: dict[str, Any] = {"events": events}
|
||||
if errors:
|
||||
metadata["errors"] = errors
|
||||
if usage:
|
||||
metadata["usage"] = usage
|
||||
if stderr and stderr.strip():
|
||||
metadata["stderr"] = stderr.strip()
|
||||
|
||||
return ParsedCLIResponse(content=content, metadata=metadata)
|
||||
Reference in New Issue
Block a user