my-pi-agent 0.1.0

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 (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. package/tui/package.json +25 -0
@@ -0,0 +1,364 @@
1
+ # pyright: reportArgumentType=false, reportCallIssue=false
2
+ """OpenAI provider:基准实现,deepseek 以此为模板。"""
3
+
4
+ import json
5
+ from collections.abc import AsyncIterator, Iterator
6
+
7
+ import openai
8
+
9
+ from ..config import Config
10
+ from ..models import Message, Response, StreamChunk, ToolCall
11
+ from ._base import Provider
12
+
13
+
14
+ class _ToolCallAccumulator:
15
+ """聚合流式 tool_calls 增量片段,产出统一形状。
16
+
17
+ OpenAI 兼容流式把 tool_call 分片送达:id/name 只出现一次,
18
+ arguments 是碎片 JSON 字符串,按 index 键控拼接。
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ self._by_index: dict[int, dict[str, str]] = {}
23
+
24
+ def add(self, delta) -> None:
25
+ """消费一个 delta 的 tool_calls 片段。"""
26
+ for tc in getattr(delta, "tool_calls", None) or []:
27
+ index = getattr(tc, "index", 0) or 0
28
+ slot = self._by_index.setdefault(index, {"id": "", "name": "", "arguments": ""})
29
+ if getattr(tc, "id", None):
30
+ slot["id"] = tc.id
31
+ fn = getattr(tc, "function", None)
32
+ if fn is not None:
33
+ if getattr(fn, "name", None):
34
+ slot["name"] = fn.name
35
+ if getattr(fn, "arguments", None):
36
+ slot["arguments"] += fn.arguments
37
+
38
+ def finish(self) -> list[ToolCall] | None:
39
+ """流式结束:产出完整 tool_calls(无则 None)。"""
40
+ if not self._by_index:
41
+ return None
42
+ out: list[ToolCall] = []
43
+ for _, slot in sorted(self._by_index.items()):
44
+ raw_args = slot["arguments"]
45
+ args = {}
46
+ error = None
47
+ try:
48
+ if raw_args.strip():
49
+ parsed = json.loads(raw_args)
50
+ if isinstance(parsed, dict):
51
+ args = parsed
52
+ else:
53
+ error = f"Tool arguments must be a dict, got {type(parsed).__name__}"
54
+ except Exception as exc:
55
+ error = f"Malformed JSON arguments: {exc}"
56
+ out.append(
57
+ ToolCall(
58
+ id=slot["id"],
59
+ name=slot["name"],
60
+ args=args,
61
+ error=error,
62
+ )
63
+ )
64
+ return out
65
+
66
+
67
+ class OpenAIProvider(Provider):
68
+ """OpenAI provider 实现。"""
69
+
70
+ def __init__(self, config: Config, client=None, async_client=None):
71
+ """初始化。client/async_client 可注入(测试缝隙)。"""
72
+ self.config = config
73
+ if client is not None or async_client is not None:
74
+ self.client = client
75
+ self.async_client = async_client
76
+ return
77
+ kwargs = {
78
+ "api_key": config.api_key,
79
+ "timeout": config.timeout,
80
+ "max_retries": config.max_retries,
81
+ }
82
+ if config.base_url:
83
+ kwargs["base_url"] = config.base_url
84
+ self.client = openai.OpenAI(**kwargs)
85
+ self.async_client = openai.AsyncOpenAI(**kwargs)
86
+
87
+ def _convert_messages(self, messages: list[Message]) -> list[dict]:
88
+ """Message → OpenAI wire dict。"""
89
+ result = []
90
+ for msg in messages:
91
+ if msg.role == "assistant" and msg.metadata and "tool_calls" in msg.metadata:
92
+ wire_calls = []
93
+ for tc in msg.metadata["tool_calls"]:
94
+ if isinstance(tc, ToolCall):
95
+ wire_calls.append(tc.to_wire_dict())
96
+ elif isinstance(tc, dict):
97
+ if "function" in tc:
98
+ wire_calls.append(tc)
99
+ else:
100
+ wire_calls.append(ToolCall.model_validate(tc).to_wire_dict())
101
+ if wire_calls:
102
+ result.append(
103
+ {
104
+ "role": "assistant",
105
+ "content": msg.content or None,
106
+ "tool_calls": wire_calls,
107
+ }
108
+ )
109
+ else:
110
+ result.append({"role": "assistant", "content": msg.content or ""})
111
+ elif msg.role == "tool" and msg.metadata:
112
+ result.append(
113
+ {
114
+ "role": "tool",
115
+ "content": msg.content,
116
+ "tool_call_id": msg.metadata.get("tool_call_id", ""),
117
+ }
118
+ )
119
+ else:
120
+ result.append({"role": msg.role, "content": msg.content})
121
+ return result
122
+
123
+ @staticmethod
124
+ def _extract_tool_calls(message) -> list[ToolCall] | None:
125
+ """从 OpenAI 响应 message 提取 tool_calls(统一形状)。"""
126
+ if not getattr(message, "tool_calls", None):
127
+ return None
128
+ out: list[ToolCall] = []
129
+ for tc in message.tool_calls:
130
+ fn = getattr(tc, "function", None)
131
+ name = getattr(fn, "name", "") if fn else ""
132
+ raw_args = getattr(fn, "arguments", "{}") if fn else "{}"
133
+ args = {}
134
+ error = None
135
+ try:
136
+ if isinstance(raw_args, str) and raw_args.strip():
137
+ parsed = json.loads(raw_args)
138
+ if isinstance(parsed, dict):
139
+ args = parsed
140
+ else:
141
+ error = f"Tool arguments must be a dict, got {type(parsed).__name__}"
142
+ elif isinstance(raw_args, dict):
143
+ args = raw_args
144
+ except Exception as exc:
145
+ error = f"Malformed JSON arguments: {exc}"
146
+ out.append(
147
+ ToolCall(
148
+ id=getattr(tc, "id", ""),
149
+ name=name,
150
+ args=args,
151
+ error=error,
152
+ )
153
+ )
154
+ return out
155
+
156
+ @staticmethod
157
+ def _extract_usage(response) -> dict[str, int] | None:
158
+ """从响应或流式 chunk 提取完整 usage(含 OpenAI & DeepSeek 真实 Prompt Cache)。"""
159
+ u = getattr(response, "usage", None)
160
+ if u is None:
161
+ return None
162
+
163
+ prompt_tokens = getattr(u, "prompt_tokens", 0) or 0
164
+ completion_tokens = getattr(u, "completion_tokens", 0) or 0
165
+ total_tokens = getattr(u, "total_tokens", 0) or (prompt_tokens + completion_tokens)
166
+
167
+ # 1. 提取 OpenAI 官方 Prompt Cache (prompt_tokens_details.cached_tokens) 与 DeepSeek 专用 Cache
168
+ cache_read = 0
169
+ try:
170
+ details = getattr(u, "prompt_tokens_details", None)
171
+ if details is not None:
172
+ if isinstance(details, dict):
173
+ cache_read = int(details.get("cached_tokens", 0) or 0)
174
+ else:
175
+ cache_read = int(getattr(details, "cached_tokens", 0) or 0)
176
+
177
+ if not cache_read:
178
+ hit = getattr(u, "prompt_cache_hit_tokens", None)
179
+ if hit is not None:
180
+ cache_read = int(hit or 0)
181
+ elif isinstance(u, dict):
182
+ cache_read = int(u.get("prompt_cache_hit_tokens", 0) or 0)
183
+ except (TypeError, ValueError):
184
+ cache_read = 0
185
+
186
+ # 计算净非缓存输入量
187
+ net_prompt = max(0, prompt_tokens - cache_read) if prompt_tokens >= cache_read else prompt_tokens
188
+
189
+ out = {
190
+ "prompt_tokens": net_prompt,
191
+ "completion_tokens": completion_tokens,
192
+ "total_tokens": total_tokens,
193
+ }
194
+ if cache_read > 0:
195
+ out["cache_read_tokens"] = cache_read
196
+ return out
197
+
198
+ def chat(
199
+ self,
200
+ messages: list[Message],
201
+ *,
202
+ model: str,
203
+ tools: list[dict] | None = None,
204
+ **kwargs,
205
+ ) -> Response:
206
+ """同步对话。"""
207
+ if self.client is None:
208
+ raise RuntimeError("client not provided; cannot run sync methods")
209
+ response = self.client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
210
+ model=model,
211
+ messages=self._convert_messages(messages),
212
+ tools=tools,
213
+ **kwargs,
214
+ )
215
+ choice = response.choices[0]
216
+ return Response(
217
+ content=choice.message.content or "",
218
+ model=response.model,
219
+ usage=self._extract_usage(response),
220
+ finish_reason=choice.finish_reason,
221
+ tool_calls=self._extract_tool_calls(choice.message),
222
+ )
223
+
224
+ def stream(
225
+ self,
226
+ messages: list[Message],
227
+ *,
228
+ model: str,
229
+ tools: list[dict] | None = None,
230
+ **kwargs,
231
+ ) -> Iterator[StreamChunk]:
232
+ """同步流式:逐 delta 产文本块;流式结束补发末块(完整 tool_calls + usage)。"""
233
+ if self.client is None:
234
+ raise RuntimeError("client not provided; cannot run sync methods")
235
+ stream = self.client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
236
+ model=model,
237
+ messages=self._convert_messages(messages),
238
+ tools=tools,
239
+ stream=True,
240
+ **kwargs,
241
+ )
242
+ accumulator = _ToolCallAccumulator()
243
+ text_acc = ""
244
+ usage = None
245
+ final_finish_reason: str | None = None
246
+ for chunk in stream:
247
+ chunk_usage = self._extract_usage(chunk)
248
+ if chunk_usage:
249
+ usage = chunk_usage
250
+ if not chunk.choices:
251
+ continue # usage-only 末块(choices 为空)——usage 已捕获,流式结束
252
+ choice = chunk.choices[0]
253
+ if choice.finish_reason:
254
+ final_finish_reason = choice.finish_reason
255
+ delta = choice.delta
256
+ accumulator.add(delta)
257
+ if reasoning_delta := getattr(delta, "reasoning_content", None):
258
+ yield StreamChunk(
259
+ content="",
260
+ metadata={"reasoning_content": reasoning_delta},
261
+ finish_reason=choice.finish_reason,
262
+ )
263
+ if getattr(delta, "content", None):
264
+ text_acc += delta.content
265
+ yield StreamChunk(content=delta.content, finish_reason=choice.finish_reason)
266
+ tool_calls = accumulator.finish()
267
+ final_response = Response(
268
+ content=text_acc,
269
+ model=model,
270
+ tool_calls=tool_calls,
271
+ usage=usage,
272
+ finish_reason=final_finish_reason,
273
+ )
274
+ yield StreamChunk(
275
+ content="",
276
+ tool_calls=tool_calls,
277
+ usage=usage,
278
+ finish_reason=final_finish_reason,
279
+ response=final_response,
280
+ )
281
+
282
+ async def achat(
283
+ self,
284
+ messages: list[Message],
285
+ *,
286
+ model: str,
287
+ tools: list[dict] | None = None,
288
+ **kwargs,
289
+ ) -> Response:
290
+ """异步对话。"""
291
+ if self.async_client is None:
292
+ raise RuntimeError("async_client not provided; cannot run async methods")
293
+ response = await self.async_client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
294
+ model=model,
295
+ messages=self._convert_messages(messages),
296
+ tools=tools,
297
+ **kwargs,
298
+ )
299
+ choice = response.choices[0]
300
+ return Response(
301
+ content=choice.message.content or "",
302
+ model=response.model,
303
+ usage=self._extract_usage(response),
304
+ finish_reason=choice.finish_reason,
305
+ tool_calls=self._extract_tool_calls(choice.message),
306
+ )
307
+
308
+ async def achat_stream(
309
+ self,
310
+ messages: list[Message],
311
+ *,
312
+ model: str,
313
+ tools: list[dict] | None = None,
314
+ **kwargs,
315
+ ) -> AsyncIterator[StreamChunk]:
316
+ """异步流式:逐 delta 产文本块;流式结束补发末块(完整 tool_calls + usage)。"""
317
+ if self.async_client is None:
318
+ raise RuntimeError("async_client not provided; cannot run async methods")
319
+ stream = await self.async_client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
320
+ model=model,
321
+ messages=self._convert_messages(messages),
322
+ tools=tools,
323
+ stream=True,
324
+ **kwargs,
325
+ )
326
+ accumulator = _ToolCallAccumulator()
327
+ text_acc = ""
328
+ usage = None
329
+ final_finish_reason: str | None = None
330
+ async for chunk in stream:
331
+ chunk_usage = self._extract_usage(chunk)
332
+ if chunk_usage:
333
+ usage = chunk_usage
334
+ if not chunk.choices:
335
+ continue # usage-only 末块(choices 为空)——usage 已捕获,流式结束
336
+ choice = chunk.choices[0]
337
+ if choice.finish_reason:
338
+ final_finish_reason = choice.finish_reason
339
+ delta = choice.delta
340
+ accumulator.add(delta)
341
+ if reasoning_delta := getattr(delta, "reasoning_content", None):
342
+ yield StreamChunk(
343
+ content="",
344
+ metadata={"reasoning_content": reasoning_delta},
345
+ finish_reason=choice.finish_reason,
346
+ )
347
+ if getattr(delta, "content", None):
348
+ text_acc += delta.content
349
+ yield StreamChunk(content=delta.content, finish_reason=choice.finish_reason)
350
+ tool_calls = accumulator.finish()
351
+ final_response = Response(
352
+ content=text_acc,
353
+ model=model,
354
+ tool_calls=tool_calls,
355
+ usage=usage,
356
+ finish_reason=final_finish_reason,
357
+ )
358
+ yield StreamChunk(
359
+ content="",
360
+ tool_calls=tool_calls,
361
+ usage=usage,
362
+ finish_reason=final_finish_reason,
363
+ response=final_response,
364
+ )
@@ -0,0 +1,16 @@
1
+ """Provider 注册表:provider 名 → 实现类,门面据此路由。"""
2
+
3
+ from ._base import Provider
4
+ from .anthropic import AnthropicProvider
5
+ from .antigravity import ( # pyright: ignore[reportMissingImports]
6
+ AntigravityProvider,
7
+ )
8
+ from .deepseek import DeepSeekProvider
9
+ from .openai import OpenAIProvider
10
+
11
+ PROVIDER_REGISTRY: dict[str, type[Provider]] = {
12
+ "openai": OpenAIProvider,
13
+ "deepseek": DeepSeekProvider,
14
+ "anthropic": AnthropicProvider,
15
+ "antigravity": AntigravityProvider,
16
+ }
@@ -0,0 +1,218 @@
1
+ """通用流式累加与规范化器(对标 Tau stream.py 与 canonicalize_provider_stream)。
2
+
3
+ 统一在模型层维护 partial: Message,消化 Token 累加、首字启动、工具拼装、取消与异常包装。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import AsyncIterator
9
+ from typing import Any
10
+
11
+ from .events import (
12
+ StreamDoneEvent,
13
+ StreamErrorEvent,
14
+ StreamEvent,
15
+ StreamStartEvent,
16
+ TextDeltaEvent,
17
+ ThinkingDeltaEvent,
18
+ ToolCallDoneEvent,
19
+ )
20
+ from .models import Message, Response, StreamChunk, ToolCall, TurnOutcome
21
+
22
+ __all__ = ["StreamAccumulator"]
23
+
24
+
25
+ class StreamAccumulator:
26
+ """流式累加器:维护单次请求的流式累加状态并输出高阶事件。"""
27
+
28
+ def __init__(self, role: str = "assistant") -> None:
29
+ self.role = role
30
+ self.content: str = ""
31
+ self.metadata: dict[str, Any] = {}
32
+ self.started: bool = False
33
+ self.tool_calls: list[ToolCall] = []
34
+ self.last_usage: dict[str, int] | None = None
35
+ self.final_response: Response | None = None
36
+
37
+ def _snapshot(self) -> Message:
38
+ meta = dict(self.metadata) if self.metadata else {}
39
+ if self.tool_calls:
40
+ meta["tool_calls"] = [
41
+ tc.model_dump() if hasattr(tc, "model_dump") else tc
42
+ for tc in self.tool_calls
43
+ ]
44
+ return Message(
45
+ role=self.role,
46
+ content=self.content,
47
+ metadata=meta if meta else None,
48
+ )
49
+
50
+ def feed(self, chunk: Any) -> list[StreamEvent]:
51
+ """消化单个底层 StreamChunk / Response 并产出对应的上层高阶事件。"""
52
+ events: list[StreamEvent] = []
53
+
54
+ if not self.started:
55
+ self.started = True
56
+ events.append(StreamStartEvent(partial=self._snapshot()))
57
+
58
+ # 1. 思考链增量 (Reasoning / Thinking)
59
+ meta = getattr(chunk, "metadata", None)
60
+ reasoning_delta: str | None = None
61
+ if isinstance(meta, dict) and "reasoning_content" in meta:
62
+ reasoning_delta = meta["reasoning_content"]
63
+ elif getattr(chunk, "reasoning_content", None):
64
+ reasoning_delta = chunk.reasoning_content
65
+
66
+ if reasoning_delta:
67
+ prev_reasoning = str(self.metadata.get("reasoning_content", ""))
68
+ self.metadata["reasoning_content"] = prev_reasoning + reasoning_delta
69
+ events.append(
70
+ ThinkingDeltaEvent(delta=reasoning_delta, partial=self._snapshot())
71
+ )
72
+
73
+ # 2. 正文文本增量 (Text Delta)
74
+ content = getattr(chunk, "content", "") or ""
75
+ if content:
76
+ self.content += content
77
+ events.append(TextDeltaEvent(delta=content, partial=self._snapshot()))
78
+
79
+ # 3. 工具调用增量 (Tool Calls)
80
+ tool_calls = getattr(chunk, "tool_calls", None)
81
+ if tool_calls:
82
+ self.tool_calls = []
83
+ for idx, tc in enumerate(tool_calls):
84
+ if isinstance(tc, dict):
85
+ tc_obj = ToolCall.model_validate(tc)
86
+ else:
87
+ tc_obj = tc
88
+ self.tool_calls.append(tc_obj)
89
+ events.append(
90
+ ToolCallDoneEvent(
91
+ index=idx,
92
+ tool_call=tc_obj,
93
+ partial=self._snapshot(),
94
+ )
95
+ )
96
+
97
+ # 4. Usage 统计
98
+ usage = getattr(chunk, "usage", None)
99
+ if isinstance(usage, dict):
100
+ self.last_usage = usage
101
+ self.metadata["usage"] = usage
102
+
103
+ # 5. 终态已拼装好的 Response 实体
104
+ resp = getattr(chunk, "response", None)
105
+ if resp is not None and isinstance(resp, Response):
106
+ self.final_response = resp
107
+ elif isinstance(chunk, Response):
108
+ self.final_response = chunk
109
+
110
+ return events
111
+
112
+ def finish(self, stop_reason: str | None = None) -> StreamDoneEvent:
113
+ """完成流式会话,交付最终完型的 Message 与 Usage。"""
114
+ if not self.started:
115
+ self.started = True
116
+
117
+ if self.final_response is not None and hasattr(
118
+ self.final_response, "to_message"
119
+ ):
120
+ msg = self.final_response.to_message(
121
+ role=self.role, stop_reason=stop_reason
122
+ )
123
+ usage = self.last_usage or getattr(self.final_response, "usage", None)
124
+ else:
125
+ meta = dict(self.metadata) if self.metadata else {}
126
+ if self.tool_calls:
127
+ meta["tool_calls"] = [
128
+ tc.model_dump() if hasattr(tc, "model_dump") else tc
129
+ for tc in self.tool_calls
130
+ ]
131
+ effective_stop = stop_reason or (
132
+ TurnOutcome.TOOL_CALLS.value
133
+ if self.tool_calls
134
+ else TurnOutcome.COMPLETED.value
135
+ )
136
+ meta["stop_reason"] = effective_stop
137
+ msg = Message(
138
+ role=self.role,
139
+ content=self.content,
140
+ metadata=meta if meta else None,
141
+ )
142
+ usage = self.last_usage
143
+
144
+ return StreamDoneEvent(message=msg, usage=usage)
145
+
146
+ def fail(
147
+ self,
148
+ exc: Exception | None = None,
149
+ cancelled: bool = False,
150
+ ) -> StreamErrorEvent:
151
+ """遇到取消或异常时,构建安全的终态错误 Message(Never-Throw 保证)。"""
152
+ stop_reason = "cancelled" if cancelled else "error"
153
+ content = self.content
154
+ if exc is not None:
155
+ err_str = str(exc)
156
+ content = (
157
+ f"{content} (Error during model stream: {err_str})"
158
+ if content
159
+ else err_str
160
+ )
161
+
162
+ meta = dict(self.metadata) if self.metadata else {}
163
+ if self.tool_calls:
164
+ meta["tool_calls"] = [
165
+ tc.model_dump() if hasattr(tc, "model_dump") else tc
166
+ for tc in self.tool_calls
167
+ ]
168
+ meta["stop_reason"] = stop_reason
169
+
170
+ error_msg = Message(
171
+ role=self.role,
172
+ content=content,
173
+ metadata=meta if meta else None,
174
+ )
175
+ return StreamErrorEvent(error=error_msg, stop_reason=stop_reason, exc=exc)
176
+
177
+ async def stream(
178
+ self,
179
+ source: AsyncIterator[StreamChunk],
180
+ signal: Any | None = None,
181
+ ) -> AsyncIterator[StreamEvent]:
182
+ """全生命周期异步生成器:将底层 StreamChunk 转换为高阶 StreamEvent。"""
183
+ cancelled = False
184
+ error_exc: Exception | None = None
185
+
186
+ def is_cancelled() -> bool:
187
+ if signal is None:
188
+ return False
189
+ check = getattr(signal, "is_cancelled", None)
190
+ if callable(check):
191
+ return bool(check())
192
+ return bool(getattr(signal, "cancelled", False))
193
+
194
+ try:
195
+ if is_cancelled():
196
+ cancelled = True
197
+ else:
198
+ async for chunk in source:
199
+ for ev in self.feed(chunk):
200
+ yield ev
201
+
202
+ if is_cancelled():
203
+ cancelled = True
204
+ break
205
+ except Exception as exc:
206
+ if is_cancelled():
207
+ cancelled = True
208
+ else:
209
+ error_exc = exc
210
+
211
+ if not self.started:
212
+ self.started = True
213
+ yield StreamStartEvent(partial=self._snapshot())
214
+
215
+ if cancelled or error_exc is not None:
216
+ yield self.fail(exc=error_exc, cancelled=cancelled)
217
+ else:
218
+ yield self.finish()
@@ -0,0 +1,66 @@
1
+ """my_coding_agent 公共 API(产品层:文件工具 + MCP + 业务装配)。"""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from my_coding_agent.agent import CodingAgent
6
+ from my_coding_agent.file_reference import FileReferenceParser
7
+ from my_coding_agent.macro import MacroEngine
8
+ from my_coding_agent.mcp import MCPClientManager, MCPConnection, MCPServerConfig
9
+ from my_coding_agent.mutation_queue import FileMutationQueue
10
+ from my_coding_agent.paths import AgentPaths
11
+ from my_coding_agent.permissions import PermissionGate, PermissionMode, PermissionRequest
12
+ from my_coding_agent.prompt import build_default_coding_prompt
13
+ from my_coding_agent.settings import CompactionSettings, Settings, load_settings, save_settings
14
+ from my_coding_agent.tools import (
15
+ EditBlock,
16
+ build_coding_tools,
17
+ make_bash_tool,
18
+ make_edit_tool,
19
+ make_find_tool,
20
+ make_grep_tool,
21
+ make_ls_tool,
22
+ make_read_tool,
23
+ make_write_tool,
24
+ resolve_path,
25
+ )
26
+
27
+ __all__ = [
28
+ "AgentPaths",
29
+ "CodingAgent",
30
+ "CompactionSettings",
31
+ "FileReferenceParser",
32
+ "MacroEngine",
33
+ "Settings",
34
+ "load_settings",
35
+ "save_settings",
36
+ "build_coding_tools",
37
+ "build_default_coding_prompt",
38
+ "FileMutationQueue",
39
+ "resolve_path",
40
+ "RpcServer",
41
+ "EditBlock",
42
+ "make_read_tool",
43
+ "make_write_tool",
44
+ "make_edit_tool",
45
+ "make_bash_tool",
46
+ "make_grep_tool",
47
+ "make_find_tool",
48
+ "make_ls_tool",
49
+ "MCPServerConfig",
50
+ "MCPConnection",
51
+ "MCPClientManager",
52
+ "PermissionGate",
53
+ "PermissionRequest",
54
+ "PermissionMode",
55
+ ]
56
+
57
+ if TYPE_CHECKING:
58
+ from my_coding_agent.rpc_server import RpcServer
59
+
60
+
61
+ def __getattr__(name: str):
62
+ if name == "RpcServer":
63
+ from my_coding_agent.rpc_server import RpcServer
64
+
65
+ return RpcServer
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")