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.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- 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}")
|