yycode 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent/__init__.py +33 -0
- agent/acp/__init__.py +2 -0
- agent/acp/approval_adapter.py +134 -0
- agent/acp/content_adapter.py +45 -0
- agent/acp/jsonrpc.py +92 -0
- agent/acp/server.py +197 -0
- agent/acp/session_manager.py +193 -0
- agent/acp/update_adapter.py +192 -0
- agent/app_paths.py +25 -0
- agent/approval.py +169 -0
- agent/cancellation.py +52 -0
- agent/change_snapshot.py +186 -0
- agent/context_compressor.py +116 -0
- agent/graph.py +137 -0
- agent/llm_retry.py +434 -0
- agent/logger.py +97 -0
- agent/lsp/__init__.py +13 -0
- agent/lsp/client.py +151 -0
- agent/lsp/manager.py +234 -0
- agent/lsp/types.py +119 -0
- agent/message_context_manager.py +322 -0
- agent/message_format.py +105 -0
- agent/nodes/llm_node.py +58 -0
- agent/nodes/state.py +12 -0
- agent/nodes/task_guard_node.py +50 -0
- agent/nodes/tools_node.py +70 -0
- agent/plan_snapshot.py +70 -0
- agent/providers/__init__.py +13 -0
- agent/providers/anthropic_provider.py +268 -0
- agent/providers/base.py +52 -0
- agent/providers/openai_provider.py +279 -0
- agent/providers/text_tool_calls.py +118 -0
- agent/runtime/approval_service.py +184 -0
- agent/runtime/context.py +43 -0
- agent/runtime/tool_events.py +368 -0
- agent/runtime/tool_executor.py +208 -0
- agent/runtime/tool_output.py +261 -0
- agent/runtime/tool_registry.py +91 -0
- agent/runtime/tool_scheduler.py +35 -0
- agent/runtime/workflow_guard.py +217 -0
- agent/runtime/workspace.py +5 -0
- agent/runtime/workspace_tools.py +22 -0
- agent/session.py +787 -0
- agent/session_replay.py +95 -0
- agent/session_store.py +186 -0
- agent/skills.py +254 -0
- agent/streaming.py +248 -0
- agent/subagent.py +634 -0
- agent/task_memory.py +340 -0
- agent/todo_manager.py +304 -0
- agent/tool_retry.py +106 -0
- agent/tui/__init__.py +14 -0
- agent/tui/app.py +1325 -0
- agent/tui/approval.py +53 -0
- agent/tui/commands/__init__.py +6 -0
- agent/tui/commands/base.py +48 -0
- agent/tui/commands/clear.py +37 -0
- agent/tui/commands/help.py +27 -0
- agent/tui/commands/registry.py +94 -0
- agent/tui/help_content.py +108 -0
- agent/tui/renderers.py +1961 -0
- agent/tui/runner.py +439 -0
- agent/tui/state.py +653 -0
- main.py +465 -0
- tools/__init__.py +50 -0
- tools/apply_patch.py +305 -0
- tools/bash.py +76 -0
- tools/diff_utils.py +139 -0
- tools/edit_file.py +40 -0
- tools/git_diff.py +72 -0
- tools/git_show.py +65 -0
- tools/grep.py +149 -0
- tools/list_files.py +90 -0
- tools/list_skills.py +24 -0
- tools/load_skill.py +30 -0
- tools/lsp_definition.py +27 -0
- tools/lsp_diagnostics.py +32 -0
- tools/lsp_document_symbols.py +23 -0
- tools/lsp_hover.py +29 -0
- tools/lsp_references.py +37 -0
- tools/lsp_utils.py +38 -0
- tools/lsp_workspace_symbols.py +23 -0
- tools/read_file.py +61 -0
- tools/read_many_files.py +50 -0
- tools/safety.py +50 -0
- tools/subagent.py +57 -0
- tools/todo.py +89 -0
- tools/verify.py +107 -0
- tools/web_search.py +250 -0
- tools/workspace.py +36 -0
- tools/workspace_state.py +60 -0
- tools/write_file.py +88 -0
- utils/__init__.py +5 -0
- utils/retry.py +13 -0
- yycode-0.3.2.data/data/skills/code_review.md +61 -0
- yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
- yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
- yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
- yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
- yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
- yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
- yycode-0.3.2.data/data/skills/plan.md +115 -0
- yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
- yycode-0.3.2.dist-info/METADATA +12 -0
- yycode-0.3.2.dist-info/RECORD +131 -0
- yycode-0.3.2.dist-info/WHEEL +5 -0
- yycode-0.3.2.dist-info/entry_points.txt +2 -0
- yycode-0.3.2.dist-info/top_level.txt +4 -0
agent/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Agent package."""
|
|
2
|
+
|
|
3
|
+
from .graph import build_graph
|
|
4
|
+
from .session import Session
|
|
5
|
+
from .skills import LoadedSkill, SkillRegistry, discover_skills, load_skills, parse_skill_paths
|
|
6
|
+
from .streaming import ConsoleStreamRenderer, StreamEvent, StreamPrinter
|
|
7
|
+
from .todo_manager import TodoManager
|
|
8
|
+
from .providers import (
|
|
9
|
+
LLMProvider,
|
|
10
|
+
ChatResponse,
|
|
11
|
+
ToolCall,
|
|
12
|
+
AnthropicProvider,
|
|
13
|
+
OpenAIProvider,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"build_graph",
|
|
18
|
+
"Session",
|
|
19
|
+
"LoadedSkill",
|
|
20
|
+
"SkillRegistry",
|
|
21
|
+
"discover_skills",
|
|
22
|
+
"load_skills",
|
|
23
|
+
"parse_skill_paths",
|
|
24
|
+
"ConsoleStreamRenderer",
|
|
25
|
+
"StreamEvent",
|
|
26
|
+
"StreamPrinter",
|
|
27
|
+
"TodoManager",
|
|
28
|
+
"LLMProvider",
|
|
29
|
+
"ChatResponse",
|
|
30
|
+
"ToolCall",
|
|
31
|
+
"AnthropicProvider",
|
|
32
|
+
"OpenAIProvider",
|
|
33
|
+
]
|
agent/acp/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""ACP permission request adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from agent.approval import ApprovalDecision, ApprovalRequest
|
|
10
|
+
from agent.acp.update_adapter import _tool_kind
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PermissionRequester = Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AcpApprovalAdapter:
|
|
17
|
+
"""Convert runtime approval callbacks into ACP permission requests."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
session_id: str,
|
|
22
|
+
requester: PermissionRequester,
|
|
23
|
+
*,
|
|
24
|
+
workdir: Path | None = None,
|
|
25
|
+
timeout_seconds: float | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.session_id = session_id
|
|
28
|
+
self.requester = requester
|
|
29
|
+
self.workdir = workdir
|
|
30
|
+
self.timeout_seconds = timeout_seconds
|
|
31
|
+
self._pending: set[asyncio.Task] = set()
|
|
32
|
+
|
|
33
|
+
async def callback(self, request: ApprovalRequest) -> bool:
|
|
34
|
+
"""Return True when the ACP client approves the requested action."""
|
|
35
|
+
return (await self.decide(request)).approved
|
|
36
|
+
|
|
37
|
+
async def decide(self, request: ApprovalRequest) -> ApprovalDecision:
|
|
38
|
+
"""Request a permission decision from the ACP client."""
|
|
39
|
+
task = asyncio.create_task(self.requester("session/request_permission", self.payload(request)))
|
|
40
|
+
self._pending.add(task)
|
|
41
|
+
try:
|
|
42
|
+
if self.timeout_seconds is None:
|
|
43
|
+
response = await task
|
|
44
|
+
else:
|
|
45
|
+
response = await asyncio.wait_for(task, timeout=self.timeout_seconds)
|
|
46
|
+
except asyncio.CancelledError:
|
|
47
|
+
return ApprovalDecision("cancelled")
|
|
48
|
+
except TimeoutError:
|
|
49
|
+
return ApprovalDecision("denied")
|
|
50
|
+
finally:
|
|
51
|
+
self._pending.discard(task)
|
|
52
|
+
option_id = _response_option_id(response)
|
|
53
|
+
if option_id in {"approve", "allow", "approved"}:
|
|
54
|
+
return ApprovalDecision("approved")
|
|
55
|
+
if option_id in {"cancel", "cancelled"}:
|
|
56
|
+
return ApprovalDecision("cancelled")
|
|
57
|
+
return ApprovalDecision("denied")
|
|
58
|
+
|
|
59
|
+
def cancel_pending(self) -> int:
|
|
60
|
+
"""Cancel pending permission requests."""
|
|
61
|
+
count = 0
|
|
62
|
+
for task in list(self._pending):
|
|
63
|
+
if not task.done():
|
|
64
|
+
task.cancel()
|
|
65
|
+
count += 1
|
|
66
|
+
self._pending.clear()
|
|
67
|
+
return count
|
|
68
|
+
|
|
69
|
+
def payload(self, request: ApprovalRequest) -> dict[str, Any]:
|
|
70
|
+
"""Build the ACP permission request payload."""
|
|
71
|
+
locations = []
|
|
72
|
+
for path in _split_paths(request.path):
|
|
73
|
+
location_path = str(path)
|
|
74
|
+
if self.workdir is not None and path and not path.startswith("/"):
|
|
75
|
+
location_path = str((self.workdir / path).resolve())
|
|
76
|
+
locations.append({"path": location_path})
|
|
77
|
+
return {
|
|
78
|
+
"sessionId": self.session_id,
|
|
79
|
+
"toolCall": {
|
|
80
|
+
"title": _permission_title(request),
|
|
81
|
+
"kind": _tool_kind(request.tool_name),
|
|
82
|
+
"status": "waiting_for_user",
|
|
83
|
+
"locations": locations,
|
|
84
|
+
"rawInput": {
|
|
85
|
+
"action": request.action,
|
|
86
|
+
"toolName": request.tool_name,
|
|
87
|
+
"path": request.path,
|
|
88
|
+
"command": request.command,
|
|
89
|
+
"reason": request.reason,
|
|
90
|
+
"risk": request.risk,
|
|
91
|
+
"diffPreview": request.diff_preview,
|
|
92
|
+
},
|
|
93
|
+
"content": [
|
|
94
|
+
{
|
|
95
|
+
"type": "text",
|
|
96
|
+
"text": request.format(include_diff=bool(request.diff_preview)),
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
"options": [
|
|
101
|
+
{"optionId": "approve", "name": "Approve", "kind": "allow"},
|
|
102
|
+
{"optionId": "deny", "name": "Deny", "kind": "reject"},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _permission_title(request: ApprovalRequest) -> str:
|
|
108
|
+
if request.action == "edit_file":
|
|
109
|
+
return "Approve file edit"
|
|
110
|
+
if request.action == "create_file":
|
|
111
|
+
return "Approve file creation"
|
|
112
|
+
if request.action == "run_command":
|
|
113
|
+
return "Approve command"
|
|
114
|
+
return "Approve action"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _split_paths(path: str) -> list[str]:
|
|
118
|
+
return [item.strip() for item in path.split(",") if item.strip()]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _response_option_id(response: Any) -> str:
|
|
122
|
+
if isinstance(response, str):
|
|
123
|
+
return response
|
|
124
|
+
if not isinstance(response, dict):
|
|
125
|
+
return ""
|
|
126
|
+
for key in ("optionId", "option_id", "decision", "status"):
|
|
127
|
+
if response.get(key):
|
|
128
|
+
return str(response[key])
|
|
129
|
+
if response.get("approved") is True:
|
|
130
|
+
return "approve"
|
|
131
|
+
if response.get("approved") is False:
|
|
132
|
+
return "deny"
|
|
133
|
+
return ""
|
|
134
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""ACP prompt content conversion helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
MAX_EMBEDDED_TEXT_CHARS = 20_000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def content_blocks_to_text(content: Any) -> str:
|
|
12
|
+
"""Convert ACP content blocks or plain text into a yoyoagent prompt string."""
|
|
13
|
+
if isinstance(content, str):
|
|
14
|
+
return content
|
|
15
|
+
if isinstance(content, dict):
|
|
16
|
+
return _block_to_text(content)
|
|
17
|
+
if not isinstance(content, list):
|
|
18
|
+
return str(content or "")
|
|
19
|
+
parts = [_block_to_text(block) for block in content]
|
|
20
|
+
return "\n\n".join(part for part in parts if part).strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _block_to_text(block: Any) -> str:
|
|
24
|
+
if isinstance(block, str):
|
|
25
|
+
return block
|
|
26
|
+
if not isinstance(block, dict):
|
|
27
|
+
return str(block)
|
|
28
|
+
block_type = str(block.get("type") or block.get("kind") or "text")
|
|
29
|
+
if block_type in {"text", "markdown"}:
|
|
30
|
+
return str(block.get("text") or block.get("content") or "")
|
|
31
|
+
if block_type in {"resource_link", "resource", "uri"}:
|
|
32
|
+
uri = block.get("uri") or block.get("url") or block.get("path") or ""
|
|
33
|
+
name = block.get("name") or block.get("title") or "resource"
|
|
34
|
+
return f"Context resource: {name}\n{uri}".strip()
|
|
35
|
+
if block_type in {"embedded_resource", "embedded"}:
|
|
36
|
+
uri = block.get("uri") or block.get("url") or block.get("path") or ""
|
|
37
|
+
text = str(block.get("text") or block.get("content") or block.get("data") or "")
|
|
38
|
+
if len(text) > MAX_EMBEDDED_TEXT_CHARS:
|
|
39
|
+
text = text[:MAX_EMBEDDED_TEXT_CHARS] + "\n... embedded resource truncated"
|
|
40
|
+
header = f"Embedded context resource: {uri}" if uri else "Embedded context resource:"
|
|
41
|
+
return f"{header}\n```\n{text}\n```"
|
|
42
|
+
if block_type in {"image", "audio"}:
|
|
43
|
+
return f"[Unsupported ACP {block_type} content omitted]"
|
|
44
|
+
return str(block.get("text") or block.get("content") or block)
|
|
45
|
+
|
agent/acp/jsonrpc.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Small JSON-RPC 2.0 helpers for ACP stdio transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PARSE_ERROR = -32700
|
|
11
|
+
INVALID_REQUEST = -32600
|
|
12
|
+
METHOD_NOT_FOUND = -32601
|
|
13
|
+
INVALID_PARAMS = -32602
|
|
14
|
+
INTERNAL_ERROR = -32603
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class JsonRpcMessage:
|
|
19
|
+
"""One decoded JSON-RPC message."""
|
|
20
|
+
|
|
21
|
+
method: str | None = None
|
|
22
|
+
params: Any = None
|
|
23
|
+
id: str | int | None = None
|
|
24
|
+
is_request: bool = False
|
|
25
|
+
is_notification: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JsonRpcError(Exception):
|
|
29
|
+
"""JSON-RPC error with a stable code."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, code: int, message: str, data: Any = None):
|
|
32
|
+
self.code = code
|
|
33
|
+
self.message = message
|
|
34
|
+
self.data = data
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def decode_message(line: str) -> JsonRpcMessage:
|
|
39
|
+
"""Decode one newline-delimited JSON-RPC message."""
|
|
40
|
+
try:
|
|
41
|
+
payload = json.loads(line)
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise JsonRpcError(PARSE_ERROR, "Parse error") from exc
|
|
44
|
+
if not isinstance(payload, dict) or payload.get("jsonrpc") != "2.0":
|
|
45
|
+
raise JsonRpcError(INVALID_REQUEST, "Invalid Request")
|
|
46
|
+
method = payload.get("method")
|
|
47
|
+
if method is None:
|
|
48
|
+
return JsonRpcMessage(id=payload.get("id"))
|
|
49
|
+
if not isinstance(method, str):
|
|
50
|
+
raise JsonRpcError(INVALID_REQUEST, "Invalid Request")
|
|
51
|
+
has_id = "id" in payload
|
|
52
|
+
return JsonRpcMessage(
|
|
53
|
+
method=method,
|
|
54
|
+
params=payload.get("params"),
|
|
55
|
+
id=payload.get("id"),
|
|
56
|
+
is_request=has_id,
|
|
57
|
+
is_notification=not has_id,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def response(result: Any, request_id: str | int | None) -> dict[str, Any]:
|
|
62
|
+
"""Build a JSON-RPC success response."""
|
|
63
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def error_response(
|
|
67
|
+
code: int,
|
|
68
|
+
message: str,
|
|
69
|
+
request_id: str | int | None = None,
|
|
70
|
+
data: Any = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""Build a JSON-RPC error response."""
|
|
73
|
+
error: dict[str, Any] = {"code": code, "message": message}
|
|
74
|
+
if data is not None:
|
|
75
|
+
error["data"] = data
|
|
76
|
+
return {"jsonrpc": "2.0", "id": request_id, "error": error}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def request(method: str, params: Any, request_id: str | int) -> dict[str, Any]:
|
|
80
|
+
"""Build a JSON-RPC request."""
|
|
81
|
+
return {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def notification(method: str, params: Any) -> dict[str, Any]:
|
|
85
|
+
"""Build a JSON-RPC notification."""
|
|
86
|
+
return {"jsonrpc": "2.0", "method": method, "params": params}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def encode(payload: dict[str, Any]) -> str:
|
|
90
|
+
"""Encode one JSON-RPC payload as a stdio line."""
|
|
91
|
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
92
|
+
|
agent/acp/server.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""ACP stdio server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, TextIO
|
|
9
|
+
|
|
10
|
+
from agent.acp.jsonrpc import (
|
|
11
|
+
INTERNAL_ERROR,
|
|
12
|
+
INVALID_PARAMS,
|
|
13
|
+
METHOD_NOT_FOUND,
|
|
14
|
+
JsonRpcError,
|
|
15
|
+
decode_message,
|
|
16
|
+
encode,
|
|
17
|
+
error_response,
|
|
18
|
+
notification,
|
|
19
|
+
request,
|
|
20
|
+
response,
|
|
21
|
+
)
|
|
22
|
+
from agent.acp.session_manager import AcpSessionManager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ACP_PROTOCOL_VERSION = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AcpServer:
|
|
29
|
+
"""Minimal newline-delimited JSON-RPC server for ACP stdio."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
stdin: TextIO | None = None,
|
|
35
|
+
stdout: TextIO | None = None,
|
|
36
|
+
stderr: TextIO | None = None,
|
|
37
|
+
auto_approve: bool = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.stdin = stdin or sys.stdin
|
|
40
|
+
self.stdout = stdout or sys.stdout
|
|
41
|
+
self.stderr = stderr or sys.stderr
|
|
42
|
+
self._write_lock = asyncio.Lock()
|
|
43
|
+
self._next_id = 1
|
|
44
|
+
self._pending: dict[str | int, asyncio.Future] = {}
|
|
45
|
+
self.sessions = AcpSessionManager(self.notify, self.request_client, auto_approve=auto_approve)
|
|
46
|
+
|
|
47
|
+
async def serve(self) -> None:
|
|
48
|
+
"""Run the stdio read loop until EOF."""
|
|
49
|
+
try:
|
|
50
|
+
while True:
|
|
51
|
+
line = await asyncio.to_thread(self.stdin.readline)
|
|
52
|
+
if not line:
|
|
53
|
+
break
|
|
54
|
+
await self.handle_line(line)
|
|
55
|
+
finally:
|
|
56
|
+
await self.sessions.close()
|
|
57
|
+
|
|
58
|
+
async def handle_line(self, line: str) -> None:
|
|
59
|
+
"""Handle one JSON-RPC line."""
|
|
60
|
+
try:
|
|
61
|
+
message = decode_message(line)
|
|
62
|
+
except JsonRpcError as exc:
|
|
63
|
+
await self._write(error_response(exc.code, exc.message))
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if not message.method:
|
|
67
|
+
self._resolve_response(line)
|
|
68
|
+
return
|
|
69
|
+
if message.is_notification:
|
|
70
|
+
await self._handle_notification(message.method, message.params)
|
|
71
|
+
return
|
|
72
|
+
try:
|
|
73
|
+
result = await self._dispatch(message.method, _params_dict(message.params))
|
|
74
|
+
except JsonRpcError as exc:
|
|
75
|
+
await self._write(error_response(exc.code, exc.message, message.id, exc.data))
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
print(f"ACP method failed: {message.method}: {exc}", file=self.stderr)
|
|
78
|
+
await self._write(error_response(INTERNAL_ERROR, str(exc), message.id))
|
|
79
|
+
else:
|
|
80
|
+
await self._write(response(result, message.id))
|
|
81
|
+
|
|
82
|
+
async def notify(self, method: str, params: dict[str, Any]) -> None:
|
|
83
|
+
"""Send a JSON-RPC notification to the ACP client."""
|
|
84
|
+
await self._write(notification(method, params))
|
|
85
|
+
|
|
86
|
+
async def request_client(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
87
|
+
"""Send a JSON-RPC request to the ACP client and await its response."""
|
|
88
|
+
request_id = self._next_request_id()
|
|
89
|
+
loop = asyncio.get_running_loop()
|
|
90
|
+
future = loop.create_future()
|
|
91
|
+
self._pending[request_id] = future
|
|
92
|
+
await self._write(request(method, params, request_id))
|
|
93
|
+
result = await future
|
|
94
|
+
return result if isinstance(result, dict) else {"result": result}
|
|
95
|
+
|
|
96
|
+
async def _dispatch(self, method: str, params: dict[str, Any]) -> Any:
|
|
97
|
+
if method == "initialize":
|
|
98
|
+
return _initialize_result()
|
|
99
|
+
if method == "session/new":
|
|
100
|
+
return await self.sessions.new_session(params)
|
|
101
|
+
if method == "session/load":
|
|
102
|
+
return await self.sessions.load_session(params)
|
|
103
|
+
if method == "session/prompt":
|
|
104
|
+
return await self.sessions.prompt(params)
|
|
105
|
+
if method == "session/cancel":
|
|
106
|
+
return await self.sessions.cancel(params)
|
|
107
|
+
raise JsonRpcError(METHOD_NOT_FOUND, f"Method not found: {method}")
|
|
108
|
+
|
|
109
|
+
async def _handle_notification(self, method: str, params: Any) -> None:
|
|
110
|
+
if method in {"initialized", "$/cancelRequest"}:
|
|
111
|
+
return
|
|
112
|
+
print(f"Ignoring ACP notification: {method} {params!r}", file=self.stderr)
|
|
113
|
+
|
|
114
|
+
def _resolve_response(self, line: str) -> None:
|
|
115
|
+
import json
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
payload = json.loads(line)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
return
|
|
121
|
+
request_id = payload.get("id")
|
|
122
|
+
future = self._pending.pop(request_id, None)
|
|
123
|
+
if future is None or future.done():
|
|
124
|
+
return
|
|
125
|
+
if "error" in payload:
|
|
126
|
+
error = payload.get("error") or {}
|
|
127
|
+
future.set_exception(RuntimeError(str(error.get("message") or error)))
|
|
128
|
+
else:
|
|
129
|
+
future.set_result(payload.get("result"))
|
|
130
|
+
|
|
131
|
+
async def _write(self, payload: dict[str, Any]) -> None:
|
|
132
|
+
async with self._write_lock:
|
|
133
|
+
self.stdout.write(encode(payload))
|
|
134
|
+
self.stdout.write("\n")
|
|
135
|
+
self.stdout.flush()
|
|
136
|
+
|
|
137
|
+
def _next_request_id(self) -> str:
|
|
138
|
+
value = f"yoyo-acp-{self._next_id}"
|
|
139
|
+
self._next_id += 1
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _params_dict(params: Any) -> dict[str, Any]:
|
|
144
|
+
if params is None:
|
|
145
|
+
return {}
|
|
146
|
+
if not isinstance(params, dict):
|
|
147
|
+
raise JsonRpcError(INVALID_PARAMS, "params must be an object")
|
|
148
|
+
return params
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _initialize_result() -> dict[str, Any]:
|
|
152
|
+
return {
|
|
153
|
+
"protocolVersion": ACP_PROTOCOL_VERSION,
|
|
154
|
+
"agentCapabilities": {
|
|
155
|
+
"loadSession": True,
|
|
156
|
+
"promptCapabilities": {
|
|
157
|
+
"image": False,
|
|
158
|
+
"audio": False,
|
|
159
|
+
"embeddedContext": True,
|
|
160
|
+
},
|
|
161
|
+
"mcpCapabilities": {
|
|
162
|
+
"http": False,
|
|
163
|
+
"sse": False,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
"agentInfo": {
|
|
167
|
+
"name": "yycode",
|
|
168
|
+
"title": "yycode",
|
|
169
|
+
"version": _project_version(),
|
|
170
|
+
},
|
|
171
|
+
"authMethods": [],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _project_version() -> str:
|
|
176
|
+
try:
|
|
177
|
+
import tomllib
|
|
178
|
+
|
|
179
|
+
root = Path(__file__).resolve().parents[2]
|
|
180
|
+
payload = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
|
|
181
|
+
return str(payload.get("project", {}).get("version") or "")
|
|
182
|
+
except Exception:
|
|
183
|
+
return ""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def run_stdio_server(*, auto_approve: bool = False) -> None:
|
|
187
|
+
"""Run the ACP stdio server."""
|
|
188
|
+
await AcpServer(auto_approve=auto_approve).serve()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main(*, auto_approve: bool = False) -> None:
|
|
192
|
+
"""Synchronous entrypoint for python -m agent.acp.server."""
|
|
193
|
+
asyncio.run(run_stdio_server(auto_approve=auto_approve))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
main()
|