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.
Files changed (131) hide show
  1. agent/__init__.py +33 -0
  2. agent/acp/__init__.py +2 -0
  3. agent/acp/approval_adapter.py +134 -0
  4. agent/acp/content_adapter.py +45 -0
  5. agent/acp/jsonrpc.py +92 -0
  6. agent/acp/server.py +197 -0
  7. agent/acp/session_manager.py +193 -0
  8. agent/acp/update_adapter.py +192 -0
  9. agent/app_paths.py +25 -0
  10. agent/approval.py +169 -0
  11. agent/cancellation.py +52 -0
  12. agent/change_snapshot.py +186 -0
  13. agent/context_compressor.py +116 -0
  14. agent/graph.py +137 -0
  15. agent/llm_retry.py +434 -0
  16. agent/logger.py +97 -0
  17. agent/lsp/__init__.py +13 -0
  18. agent/lsp/client.py +151 -0
  19. agent/lsp/manager.py +234 -0
  20. agent/lsp/types.py +119 -0
  21. agent/message_context_manager.py +322 -0
  22. agent/message_format.py +105 -0
  23. agent/nodes/llm_node.py +58 -0
  24. agent/nodes/state.py +12 -0
  25. agent/nodes/task_guard_node.py +50 -0
  26. agent/nodes/tools_node.py +70 -0
  27. agent/plan_snapshot.py +70 -0
  28. agent/providers/__init__.py +13 -0
  29. agent/providers/anthropic_provider.py +268 -0
  30. agent/providers/base.py +52 -0
  31. agent/providers/openai_provider.py +279 -0
  32. agent/providers/text_tool_calls.py +118 -0
  33. agent/runtime/approval_service.py +184 -0
  34. agent/runtime/context.py +43 -0
  35. agent/runtime/tool_events.py +368 -0
  36. agent/runtime/tool_executor.py +208 -0
  37. agent/runtime/tool_output.py +261 -0
  38. agent/runtime/tool_registry.py +91 -0
  39. agent/runtime/tool_scheduler.py +35 -0
  40. agent/runtime/workflow_guard.py +217 -0
  41. agent/runtime/workspace.py +5 -0
  42. agent/runtime/workspace_tools.py +22 -0
  43. agent/session.py +787 -0
  44. agent/session_replay.py +95 -0
  45. agent/session_store.py +186 -0
  46. agent/skills.py +254 -0
  47. agent/streaming.py +248 -0
  48. agent/subagent.py +634 -0
  49. agent/task_memory.py +340 -0
  50. agent/todo_manager.py +304 -0
  51. agent/tool_retry.py +106 -0
  52. agent/tui/__init__.py +14 -0
  53. agent/tui/app.py +1325 -0
  54. agent/tui/approval.py +53 -0
  55. agent/tui/commands/__init__.py +6 -0
  56. agent/tui/commands/base.py +48 -0
  57. agent/tui/commands/clear.py +37 -0
  58. agent/tui/commands/help.py +27 -0
  59. agent/tui/commands/registry.py +94 -0
  60. agent/tui/help_content.py +108 -0
  61. agent/tui/renderers.py +1961 -0
  62. agent/tui/runner.py +439 -0
  63. agent/tui/state.py +653 -0
  64. main.py +465 -0
  65. tools/__init__.py +50 -0
  66. tools/apply_patch.py +305 -0
  67. tools/bash.py +76 -0
  68. tools/diff_utils.py +139 -0
  69. tools/edit_file.py +40 -0
  70. tools/git_diff.py +72 -0
  71. tools/git_show.py +65 -0
  72. tools/grep.py +149 -0
  73. tools/list_files.py +90 -0
  74. tools/list_skills.py +24 -0
  75. tools/load_skill.py +30 -0
  76. tools/lsp_definition.py +27 -0
  77. tools/lsp_diagnostics.py +32 -0
  78. tools/lsp_document_symbols.py +23 -0
  79. tools/lsp_hover.py +29 -0
  80. tools/lsp_references.py +37 -0
  81. tools/lsp_utils.py +38 -0
  82. tools/lsp_workspace_symbols.py +23 -0
  83. tools/read_file.py +61 -0
  84. tools/read_many_files.py +50 -0
  85. tools/safety.py +50 -0
  86. tools/subagent.py +57 -0
  87. tools/todo.py +89 -0
  88. tools/verify.py +107 -0
  89. tools/web_search.py +250 -0
  90. tools/workspace.py +36 -0
  91. tools/workspace_state.py +60 -0
  92. tools/write_file.py +88 -0
  93. utils/__init__.py +5 -0
  94. utils/retry.py +13 -0
  95. yycode-0.3.2.data/data/skills/code_review.md +61 -0
  96. yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
  97. yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
  98. yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
  99. yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
  100. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
  101. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
  102. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
  103. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
  104. yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
  105. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
  106. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
  107. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
  108. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
  109. yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
  110. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
  111. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
  112. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
  113. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
  114. yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
  115. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
  116. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
  117. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
  118. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
  119. yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
  120. yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
  121. yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
  122. yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
  123. yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
  124. yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
  125. yycode-0.3.2.data/data/skills/plan.md +115 -0
  126. yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
  127. yycode-0.3.2.dist-info/METADATA +12 -0
  128. yycode-0.3.2.dist-info/RECORD +131 -0
  129. yycode-0.3.2.dist-info/WHEEL +5 -0
  130. yycode-0.3.2.dist-info/entry_points.txt +2 -0
  131. 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,2 @@
1
+ """ACP server support for yoyoagent."""
2
+
@@ -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()