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,248 @@
1
+ """TaskItem 数据模型与 TaskStore DAG 依赖状态机。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from dataclasses import asdict, dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+ TaskStatus = Literal["pending", "in_progress", "completed", "deleted"]
13
+
14
+
15
+ @dataclass
16
+ class TaskItem:
17
+ """单个结构化工单项(对标 OpenHands TaskItem 与 trpc-agent TaskRecord)。"""
18
+
19
+ id: str
20
+ subject: str
21
+ description: str = ""
22
+ status: TaskStatus = "pending"
23
+ owner: str | None = None
24
+ active_form: str | None = None
25
+ blocked_by: list[str] = field(default_factory=list)
26
+ metadata: dict[str, Any] = field(default_factory=dict)
27
+
28
+
29
+ class TaskStore:
30
+ """基于 DAG 依赖图与原子持久化的项目任务仓库。"""
31
+
32
+ def __init__(
33
+ self,
34
+ workspace: Path | str,
35
+ enforce_single_in_progress: bool = True,
36
+ allow_parallel: bool = False,
37
+ ) -> None:
38
+ self.workspace = Path(workspace).resolve()
39
+ self.store_dir = self.workspace / ".my_agent_core"
40
+ self.file_path = self.store_dir / "tasks.json"
41
+ self.enforce_single_in_progress = (
42
+ False if allow_parallel else enforce_single_in_progress
43
+ )
44
+ self.tasks: dict[str, TaskItem] = {}
45
+ self._next_id = 1
46
+ self._load_from_disk()
47
+
48
+ def _load_from_disk(self) -> None:
49
+ if not self.file_path.exists():
50
+ return
51
+ try:
52
+ data = json.loads(self.file_path.read_text(encoding="utf-8"))
53
+ self._next_id = data.get("next_id", 1)
54
+ for item in data.get("tasks", []):
55
+ task = TaskItem(**item)
56
+ self.tasks[task.id] = task
57
+ except (json.JSONDecodeError, OSError, TypeError, ValueError):
58
+ # 损坏文件或空文件容错,保留初始空状态
59
+ pass
60
+
61
+ def _save_to_disk(self) -> None:
62
+ self.store_dir.mkdir(parents=True, exist_ok=True)
63
+ payload = {
64
+ "next_id": self._next_id,
65
+ "tasks": [asdict(t) for t in self.tasks.values()],
66
+ }
67
+ content = json.dumps(payload, ensure_ascii=False, indent=2)
68
+ fd, tmp_path = tempfile.mkstemp(
69
+ dir=self.store_dir, prefix="tasks_", suffix=".tmp"
70
+ )
71
+ try:
72
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
73
+ f.write(content)
74
+ f.flush()
75
+ os.fsync(f.fileno())
76
+ os.replace(tmp_path, self.file_path)
77
+ except Exception:
78
+ if os.path.exists(tmp_path):
79
+ os.remove(tmp_path)
80
+ raise
81
+
82
+ def _depends_on(self, task_id: str, target_id: str) -> bool:
83
+ """检查 task_id 是否传递性依赖于 target_id。"""
84
+ visited = set()
85
+ queue = [task_id]
86
+ while queue:
87
+ curr = queue.pop(0)
88
+ if curr == target_id:
89
+ return True
90
+ if curr in visited:
91
+ continue
92
+ visited.add(curr)
93
+ if curr in self.tasks:
94
+ queue.extend(self.tasks[curr].blocked_by)
95
+ return False
96
+
97
+ async def create(
98
+ self,
99
+ subject: str,
100
+ description: str = "",
101
+ active_form: str | None = None,
102
+ metadata: dict | None = None,
103
+ ) -> TaskItem:
104
+ """创建新任务,自动分配递增 ID。"""
105
+ sub = subject.strip()
106
+ if not sub:
107
+ raise ValueError("Task subject cannot be empty")
108
+ task_id = f"task_{self._next_id}"
109
+ self._next_id += 1
110
+ task = TaskItem(
111
+ id=task_id,
112
+ subject=sub,
113
+ description=description,
114
+ status="pending",
115
+ active_form=active_form,
116
+ metadata=metadata or {},
117
+ )
118
+ self.tasks[task_id] = task
119
+ self._save_to_disk()
120
+ return task
121
+
122
+ async def update(
123
+ self,
124
+ task_id: str,
125
+ status: TaskStatus | None = None,
126
+ subject: str | None = None,
127
+ description: str | None = None,
128
+ active_form: str | None = None,
129
+ owner: str | None = None,
130
+ metadata: dict | None = None,
131
+ add_blocked_by: list[str] | None = None,
132
+ remove_blocked_by: list[str] | None = None,
133
+ ) -> tuple[TaskItem, list[str]]:
134
+ """局部增量更新任务字段与 DAG 依赖,并计算自动解锁列表。"""
135
+ if task_id not in self.tasks:
136
+ raise KeyError(f"Task '{task_id}' not found")
137
+ task = self.tasks[task_id]
138
+
139
+ if status == "in_progress" and self.enforce_single_in_progress:
140
+ for other_id, other in self.tasks.items():
141
+ if other_id != task_id and other.status == "in_progress":
142
+ raise ValueError(f"Task '{other_id}' is already in progress")
143
+
144
+ if add_blocked_by:
145
+ for dep in add_blocked_by:
146
+ if dep == task_id:
147
+ raise ValueError("Task cannot depend on itself")
148
+ if dep not in self.tasks:
149
+ raise KeyError(f"Dependency task '{dep}' not found")
150
+ if self._depends_on(dep, task_id):
151
+ raise ValueError(f"Cycle detected: {task_id} -> {dep} -> {task_id}")
152
+ if dep not in task.blocked_by:
153
+ task.blocked_by.append(dep)
154
+
155
+ if remove_blocked_by:
156
+ task.blocked_by = [d for d in task.blocked_by if d not in remove_blocked_by]
157
+
158
+ if status is not None:
159
+ task.status = status
160
+ if subject is not None:
161
+ task.subject = subject.strip()
162
+ if description is not None:
163
+ task.description = description
164
+ if active_form is not None:
165
+ task.active_form = active_form
166
+ if owner is not None:
167
+ task.owner = owner
168
+ if metadata is not None:
169
+ task.metadata.update(metadata)
170
+
171
+ unblocked: list[str] = []
172
+ if status == "completed":
173
+ for other_id, other in self.tasks.items():
174
+ if other.status == "pending" and task_id in other.blocked_by:
175
+ other.blocked_by.remove(task_id)
176
+ if len(other.blocked_by) == 0:
177
+ unblocked.append(other_id)
178
+
179
+ self._save_to_disk()
180
+ return task, unblocked
181
+
182
+ def get(self, task_id: str) -> TaskItem:
183
+ """获取单个任务详情。"""
184
+ if task_id not in self.tasks:
185
+ raise KeyError(f"Task '{task_id}' not found")
186
+ return self.tasks[task_id]
187
+
188
+ def list(self, include_deleted: bool = False) -> list[TaskItem]:
189
+ """列出所有活跃任务。"""
190
+ return [
191
+ t for t in self.tasks.values() if include_deleted or t.status != "deleted"
192
+ ]
193
+
194
+ async def batch_write(self, todos: list[dict[str, Any]]) -> list[TaskItem]:
195
+ """批量/便签覆盖写入。"""
196
+ for item in todos:
197
+ t_id = item.get("id")
198
+ if t_id and t_id in self.tasks:
199
+ t = self.tasks[t_id]
200
+ if "subject" in item:
201
+ t.subject = str(item["subject"]).strip()
202
+ if "status" in item and item["status"] in (
203
+ "pending",
204
+ "in_progress",
205
+ "completed",
206
+ "deleted",
207
+ ):
208
+ t.status = item["status"]
209
+ else:
210
+ new_id = f"task_{self._next_id}"
211
+ self._next_id += 1
212
+ self.tasks[new_id] = TaskItem(
213
+ id=new_id,
214
+ subject=str(item.get("subject", "Untitled")).strip(),
215
+ description=str(item.get("description", "")),
216
+ status=item.get("status", "pending"),
217
+ )
218
+ self._save_to_disk()
219
+ return self.list()
220
+
221
+ def clear(self) -> None:
222
+ """清空所有工单并落盘。"""
223
+ self.tasks.clear()
224
+ self._next_id = 1
225
+ self._save_to_disk()
226
+
227
+ def render_board(self) -> str:
228
+ """渲染紧凑 Markdown 看板。"""
229
+ tasks = self.list()
230
+ if not tasks:
231
+ return "(No active tasks)"
232
+ lines: list[str] = []
233
+ for t in tasks:
234
+ if t.status == "completed":
235
+ icon = "[x]"
236
+ elif t.status == "in_progress":
237
+ icon = "[>]"
238
+ else:
239
+ icon = "[ ]"
240
+
241
+ status_desc = f"{t.status}"
242
+ if t.active_form and t.status == "in_progress":
243
+ status_desc += f" - {t.active_form}"
244
+ if t.blocked_by:
245
+ status_desc += f", blocked by: {t.blocked_by}"
246
+
247
+ lines.append(f"{icon} {t.id}: {t.subject} ({status_desc})")
248
+ return "\n".join(lines)
@@ -0,0 +1,189 @@
1
+ """对话转录本自愈与断头保护引擎:保证每一条 Assistant 工具调用均有且仅有一条紧邻的工具结果。
2
+
3
+ 对齐 Tau (tau_agent.tool_history) 与 Pi 架构,采用三阶段状态机:
4
+ 1. Phase 1: 预留就近配对(防止同名 ID 误抢夺);
5
+ 2. Phase 2: 贪心匹配或合成中断结果("Tool call interrupted by user");
6
+ 3. Phase 2.5: 真实结果反超合成中断;
7
+ 4. Phase 3: 重构转录本、孤儿结果丢弃与保序重排。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import defaultdict
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass
15
+ from typing import Any
16
+
17
+ from my_agent_llm.models import Message
18
+
19
+ _INTERRUPTED_TOOL_RESULT = "Tool call interrupted by user"
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ToolHistoryRepair:
24
+ """修复后的合法转录本以及结构化诊断计数。"""
25
+
26
+ messages: tuple[Message, ...]
27
+ changed: bool = False
28
+ synthesized_results: int = 0
29
+ dropped_orphan_results: int = 0
30
+ dropped_duplicate_results: int = 0
31
+ reordered_results: int = 0
32
+
33
+ def diagnostic_data(self) -> dict[str, int]:
34
+ """返回 JSON 友好的诊断计数字典。"""
35
+ return {
36
+ "synthesizedResults": self.synthesized_results,
37
+ "droppedOrphanResults": self.dropped_orphan_results,
38
+ "droppedDuplicateResults": self.dropped_duplicate_results,
39
+ "reorderedResults": self.reordered_results,
40
+ }
41
+
42
+
43
+ def _get_tool_calls(msg: Message) -> list[dict[str, Any]]:
44
+ """提取 assistant 消息中包含的 tool_calls 列表。"""
45
+ if msg.role == "assistant" and msg.metadata:
46
+ calls = msg.metadata.get("tool_calls")
47
+ if isinstance(calls, list):
48
+ out: list[dict[str, Any]] = []
49
+ for c in calls:
50
+ if isinstance(c, dict):
51
+ out.append(c)
52
+ elif hasattr(c, "model_dump"):
53
+ out.append(c.model_dump())
54
+ return out
55
+ return []
56
+
57
+
58
+ def _get_tool_call_id(msg: Message) -> str | None:
59
+ """提取 tool 消息绑定的 tool_call_id。"""
60
+ if msg.role == "tool" and msg.metadata:
61
+ tid = msg.metadata.get("tool_call_id")
62
+ if tid is not None:
63
+ return str(tid)
64
+ return None
65
+
66
+
67
+ def repair_tool_history(messages: Sequence[Message]) -> ToolHistoryRepair:
68
+ """对会话历史进行确定性拓扑自愈,确保所有工具调用均合法闭合。"""
69
+ call_occurrences: list[tuple[tuple[int, int], dict[str, Any], int]] = []
70
+ for msg_idx, message in enumerate(messages):
71
+ calls = _get_tool_calls(message)
72
+ for offset, call in enumerate(calls, start=1):
73
+ call_occurrences.append(((msg_idx, offset), call, msg_idx + offset))
74
+
75
+ results_by_id: dict[str, list[tuple[int, Message]]] = defaultdict(list)
76
+ for msg_idx, message in enumerate(messages):
77
+ tid = _get_tool_call_id(message)
78
+ if tid is not None:
79
+ results_by_id[tid].append((msg_idx, message))
80
+
81
+ selected_results: dict[tuple[int, int], tuple[int | None, Message]] = {}
82
+ used_result_positions: set[int] = set()
83
+ synthesized_results = 0
84
+
85
+ # ── Phase 1: 预留已就近配对的调用 (Reserve already-adjacent pairs) ──
86
+ for occurrence, call, expected_pos in call_occurrences:
87
+ if expected_pos >= len(messages):
88
+ continue
89
+ candidate = messages[expected_pos]
90
+ if _get_tool_call_id(candidate) == call.get("id"):
91
+ selected_results[occurrence] = (expected_pos, candidate)
92
+ used_result_positions.add(expected_pos)
93
+
94
+ # ── Phase 2: 剩余调用贪心匹配或补齐中断结果 (Match remaining or synthesize) ──
95
+ for occurrence, call, _ in call_occurrences:
96
+ if occurrence in selected_results:
97
+ continue
98
+ call_id = str(call.get("id", ""))
99
+ candidates = results_by_id.get(call_id, [])
100
+
101
+ matched_pos: int | None = None
102
+ matched_msg: Message | None = None
103
+
104
+ # 优先在调用之后寻找未被使用的真实结果
105
+ for cand_pos, cand_msg in candidates:
106
+ if cand_pos in used_result_positions:
107
+ continue
108
+ if (
109
+ cand_pos > occurrence[0]
110
+ and cand_msg.content != _INTERRUPTED_TOOL_RESULT
111
+ ):
112
+ matched_pos, matched_msg = cand_pos, cand_msg
113
+ break
114
+
115
+ # 次优:任意未使用的结果
116
+ if matched_msg is None:
117
+ for cand_pos, cand_msg in candidates:
118
+ if cand_pos not in used_result_positions:
119
+ matched_pos, matched_msg = cand_pos, cand_msg
120
+ break
121
+
122
+ if matched_msg is not None and matched_pos is not None:
123
+ selected_results[occurrence] = (matched_pos, matched_msg)
124
+ used_result_positions.add(matched_pos)
125
+ else:
126
+ # 补齐中断结果
127
+ synthetic = Message(
128
+ role="tool",
129
+ content=_INTERRUPTED_TOOL_RESULT,
130
+ metadata={"tool_call_id": call_id, "is_error": True},
131
+ )
132
+ selected_results[occurrence] = (None, synthetic)
133
+ synthesized_results += 1
134
+
135
+ # ── Phase 2.5: 真实结果反超合成中断 ──
136
+ for occurrence, call, _ in call_occurrences:
137
+ pos, _ = selected_results[occurrence]
138
+ if pos is not None:
139
+ continue
140
+ call_id = str(call.get("id", ""))
141
+ for cand_pos, cand_msg in results_by_id.get(call_id, []):
142
+ if cand_pos not in used_result_positions:
143
+ selected_results[occurrence] = (cand_pos, cand_msg)
144
+ used_result_positions.add(cand_pos)
145
+ synthesized_results -= 1
146
+ break
147
+
148
+ # ── Phase 3: 重建转录本、孤儿丢弃与保序重排 ──
149
+ repaired: list[Message] = []
150
+ all_called_ids = {str(call.get("id", "")) for _, call, _ in call_occurrences}
151
+ dropped_orphan_results = 0
152
+ dropped_duplicate_results = 0
153
+ reordered_results = 0
154
+
155
+ for msg_idx, message in enumerate(messages):
156
+ if message.role == "tool":
157
+ tid = _get_tool_call_id(message)
158
+ if msg_idx not in used_result_positions:
159
+ if tid not in all_called_ids:
160
+ dropped_orphan_results += 1
161
+ else:
162
+ dropped_duplicate_results += 1
163
+ continue
164
+ # 已使用的工具结果将在对应的 assistant 消息之后紧跟插入,此处跳过
165
+ continue
166
+
167
+ repaired.append(message)
168
+ if message.role == "assistant":
169
+ calls = _get_tool_calls(message)
170
+ for offset, _ in enumerate(calls, start=1):
171
+ occ = (msg_idx, offset)
172
+ orig_pos, tool_res = selected_results[occ]
173
+ expected_pos = msg_idx + offset
174
+ if orig_pos != expected_pos:
175
+ reordered_results += 1
176
+ repaired.append(tool_res)
177
+
178
+ changed = len(repaired) != len(messages) or any(
179
+ r != o for r, o in zip(repaired, messages, strict=False)
180
+ )
181
+
182
+ return ToolHistoryRepair(
183
+ messages=tuple(repaired),
184
+ changed=changed,
185
+ synthesized_results=synthesized_results,
186
+ dropped_orphan_results=dropped_orphan_results,
187
+ dropped_duplicate_results=dropped_duplicate_results,
188
+ reordered_results=reordered_results,
189
+ )
@@ -0,0 +1,5 @@
1
+ """工具声明与分发 —— 上行翻译层。"""
2
+
3
+ from .core import Tool, ToolResult, tool
4
+
5
+ __all__ = ["Tool", "ToolResult", "tool"]
@@ -0,0 +1,5 @@
1
+ """内置工具:框架层提供的默认工具工厂。"""
2
+ from .task import make_task_tool
3
+ from .task_tools import make_task_tools # pyright: ignore[reportMissingImports]
4
+
5
+ __all__ = ["make_task_tool", "make_task_tools"]
@@ -0,0 +1,30 @@
1
+ """内置工具:task(subagent 委派)工厂。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from my_agent_core.subagent_tasks import ( # pyright: ignore[reportMissingImports]
8
+ SubagentTaskManager,
9
+ SubagentTaskStatus,
10
+ )
11
+ from my_agent_core.subagents import SubagentManager
12
+ from my_agent_core.tools import Tool
13
+
14
+ if TYPE_CHECKING:
15
+ from my_agent_core.agent import Agent
16
+
17
+
18
+ def make_task_tool(manager: SubagentManager, parent: Agent) -> Tool:
19
+ """产出内置 `task` 委派工具(工具桥:调 SubagentTaskManager.start_task → 转字符串)。"""
20
+ task_manager = SubagentTaskManager(manager, parent)
21
+
22
+ async def task(prompt: str, agent_type: str = "default") -> str:
23
+ """Spawn a subagent with fresh context to complete the given prompt.
24
+ agent_type: name of the subagent definition (see available agents)."""
25
+ t = await task_manager.start_task(prompt, agent_type)
26
+ if t.status is SubagentTaskStatus.COMPLETED:
27
+ return str(t.result) if t.result is not None else "(no summary)"
28
+ return str(t.error) if t.error is not None else "(no summary)"
29
+
30
+ return Tool(func=task, name="task", is_parallel_safe=True)
@@ -0,0 +1,215 @@
1
+ """统一待办任务工具:基于 TaskStore 的单一标准 todo 工具与 discrete 工具族。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import asdict
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+
9
+ from my_agent_core.events import AgentStart, TurnEnd
10
+ from my_agent_core.tools.core import Tool, ToolResult, tool
11
+
12
+ if TYPE_CHECKING:
13
+ from my_agent_core.task_store import ( # pyright: ignore[reportMissingImports]
14
+ TaskStore,
15
+ )
16
+
17
+
18
+ TODO_GUIDELINES = """\
19
+ Manage a task list for tracking multi-step progress.
20
+ Actions:
21
+ - create: add a new task with subject (and optional description, active_form)
22
+ - update: change task status, fields, or add/remove dependencies
23
+ - list: review all active tasks and current board
24
+ - get: get full details of a specific task by task_id
25
+ - clear: clear all tasks from the board
26
+ - write: batch overwrite scratchpad todo items
27
+
28
+ Rules:
29
+ - Keep exactly one task in_progress at a time.
30
+ - Mark completed immediately when done.
31
+ - Upstream task must complete before blocked downstream tasks can start.
32
+ """
33
+
34
+
35
+ def make_todo_tool(store: TaskStore) -> Tool:
36
+ """生成单一统一的标准 todo 工具(对标 Pi & Hermes-Agent)。"""
37
+
38
+ @tool(
39
+ name="todo",
40
+ description=TODO_GUIDELINES,
41
+ is_parallel_safe=False,
42
+ )
43
+ async def todo(
44
+ action: Literal["create", "update", "list", "get", "clear", "write"],
45
+ subject: str | None = None,
46
+ task_id: str | None = None,
47
+ status: Literal["pending", "in_progress", "completed", "deleted"] | None = None,
48
+ description: str | None = None,
49
+ active_form: str | None = None,
50
+ owner: str | None = None,
51
+ add_blocked_by: list[str] | None = None,
52
+ remove_blocked_by: list[str] | None = None,
53
+ todos: list[dict[str, Any]] | None = None,
54
+ include_deleted: bool = False,
55
+ ) -> ToolResult:
56
+ try:
57
+ if action == "create":
58
+ if not subject:
59
+ return ToolResult(
60
+ ok=False,
61
+ error="Parameter 'subject' is required for action 'create'",
62
+ )
63
+ task = await store.create(
64
+ subject=subject,
65
+ description=description or "",
66
+ active_form=active_form,
67
+ )
68
+ return ToolResult(
69
+ ok=True,
70
+ data={
71
+ "action": "create",
72
+ "task": {
73
+ "id": task.id,
74
+ "subject": task.subject,
75
+ "status": task.status,
76
+ },
77
+ "board": store.render_board(),
78
+ "message": f"Created {task.id}",
79
+ },
80
+ )
81
+
82
+ elif action == "update":
83
+ if not task_id:
84
+ return ToolResult(
85
+ ok=False,
86
+ error="Parameter 'task_id' is required for action 'update'",
87
+ )
88
+ task, unblocked = await store.update(
89
+ task_id=task_id,
90
+ status=status,
91
+ subject=subject,
92
+ description=description,
93
+ active_form=active_form,
94
+ owner=owner,
95
+ add_blocked_by=add_blocked_by,
96
+ remove_blocked_by=remove_blocked_by,
97
+ )
98
+ return ToolResult(
99
+ ok=True,
100
+ data={
101
+ "action": "update",
102
+ "task": {
103
+ "id": task.id,
104
+ "subject": task.subject,
105
+ "status": task.status,
106
+ "blocked_by": task.blocked_by,
107
+ },
108
+ "unblocked": unblocked,
109
+ "board": store.render_board(),
110
+ "message": f"Updated {task.id}",
111
+ },
112
+ )
113
+
114
+ elif action == "list":
115
+ tasks = store.list(include_deleted=include_deleted)
116
+ return ToolResult(
117
+ ok=True,
118
+ data={
119
+ "action": "list",
120
+ "tasks": [
121
+ {
122
+ "id": t.id,
123
+ "subject": t.subject,
124
+ "status": t.status,
125
+ "owner": t.owner,
126
+ "active_form": t.active_form,
127
+ "blocked_by": t.blocked_by,
128
+ }
129
+ for t in tasks
130
+ ],
131
+ "board": store.render_board(),
132
+ },
133
+ )
134
+
135
+ elif action == "get":
136
+ if not task_id:
137
+ return ToolResult(
138
+ ok=False,
139
+ error="Parameter 'task_id' is required for action 'get'",
140
+ )
141
+ task = store.get(task_id)
142
+ return ToolResult(
143
+ ok=True,
144
+ data={"action": "get", "task": asdict(task)},
145
+ )
146
+
147
+ elif action == "clear":
148
+ store.clear()
149
+ return ToolResult(
150
+ ok=True,
151
+ data={
152
+ "action": "clear",
153
+ "message": "Cleared all tasks",
154
+ "board": "(No active tasks)",
155
+ },
156
+ )
157
+
158
+ elif action == "write":
159
+ if todos is None:
160
+ return ToolResult(
161
+ ok=False,
162
+ error="Parameter 'todos' is required for action 'write'",
163
+ )
164
+ items = await store.batch_write(todos)
165
+ return ToolResult(
166
+ ok=True,
167
+ data={
168
+ "action": "write",
169
+ "tasks": [
170
+ {"id": t.id, "subject": t.subject, "status": t.status}
171
+ for t in items
172
+ ],
173
+ "board": store.render_board(),
174
+ },
175
+ )
176
+
177
+ return ToolResult(ok=False, error=f"Unknown action: {action}")
178
+ except Exception as e:
179
+ return ToolResult(ok=False, error=str(e))
180
+
181
+ return todo
182
+
183
+
184
+ def make_task_tools(store: TaskStore) -> list[Tool]:
185
+ """导出单一 todo 标准工具(对标 Pi & Hermes)。"""
186
+ return [make_todo_tool(store)]
187
+
188
+
189
+ class TaskGuardHook:
190
+ """任务收尾早退守卫钩子(对标 Pi 扩展架构):在 TurnEnd 时检查未结清工单,通过 steer 提醒大模型。"""
191
+
192
+ def __init__(self, task_store: TaskStore, steer_fn: Callable[[str], None]) -> None:
193
+ self.task_store = task_store
194
+ self.steer_fn = steer_fn
195
+ self.nudged_ids: set[str] = set()
196
+
197
+ def on_agent_start(self, event: AgentStart) -> None:
198
+ """会话开始时重置已提醒集合。"""
199
+ self.nudged_ids.clear()
200
+
201
+ def on_turn_end(self, event: TurnEnd) -> None:
202
+ """Turn 结束时检查:若无工具调用且仍有 in_progress 任务,发起 steer 提醒。"""
203
+ if event.tool_results:
204
+ return
205
+
206
+ in_progress = [t for t in self.task_store.list() if t.status == "in_progress"]
207
+ for t in in_progress:
208
+ if t.id not in self.nudged_ids:
209
+ self.nudged_ids.add(t.id)
210
+ self.steer_fn(
211
+ f"Task '{t.id}' ({t.subject}) is still marked as 'in_progress'. "
212
+ f"If you have completed it, please call todo(action='update', task_id='{t.id}', status='completed') "
213
+ f"to update your progress before concluding."
214
+ )
215
+ break