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,131 @@
|
|
|
1
|
+
# pyright: reportUnusedCallResult=false, reportAttributeAccessIssue=false
|
|
2
|
+
"""会话仓库:root 目录下 create / list / open / delete(pig-mono SessionManager 的裁剪版)。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from .session import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionMeta(BaseModel):
|
|
17
|
+
"""会话元信息(list() 用)。"""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
path: Path
|
|
21
|
+
created_at: str
|
|
22
|
+
entries: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionStore:
|
|
26
|
+
"""会话仓库:一个会话一个 <workspace/root>/<id>.jsonl 文件(pig-mono 式 workspace 隔离)。"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
root: str | Path = ".my_agent_core/sessions",
|
|
31
|
+
workspace: str | Path | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""workspace 默认 Path.cwd()。会话目录 = workspace/root(root 为绝对路径则直接用)。
|
|
34
|
+
|
|
35
|
+
每个项目在 <workspace>/.my_agent_core/sessions/ 下建自己的会话目录,
|
|
36
|
+
create/list/open/delete/fork 全部限定在该目录内,跨项目天然隔离。
|
|
37
|
+
"""
|
|
38
|
+
self.workspace = Path(workspace) if workspace else Path.cwd()
|
|
39
|
+
root_path = Path(root)
|
|
40
|
+
self.root = root_path if root_path.is_absolute() else self.workspace / root_path
|
|
41
|
+
|
|
42
|
+
def create(self) -> Session:
|
|
43
|
+
"""新会话:写 <root>/<id>.jsonl(不含 system)。Session.cwd = workspace。"""
|
|
44
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
while True:
|
|
46
|
+
sid = f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{uuid4().hex[:8]}"
|
|
47
|
+
path = self.root / f"{sid}.jsonl"
|
|
48
|
+
if not path.exists():
|
|
49
|
+
session = Session(path=path, cwd=str(self.workspace))
|
|
50
|
+
session.id = sid
|
|
51
|
+
session.save()
|
|
52
|
+
return session
|
|
53
|
+
|
|
54
|
+
def create_session(self) -> Session:
|
|
55
|
+
"""创建新会话(等同于 self.create())。"""
|
|
56
|
+
return self.create()
|
|
57
|
+
|
|
58
|
+
def list(self) -> list[SessionMeta]:
|
|
59
|
+
"""全部会话,按 created_at 倒序(新→旧)。损坏/缺字段文件跳过。"""
|
|
60
|
+
metas: list[SessionMeta] = []
|
|
61
|
+
for f in self.root.glob("*.jsonl"):
|
|
62
|
+
try:
|
|
63
|
+
with open(f, encoding="utf-8") as fh:
|
|
64
|
+
first_line = fh.readline()
|
|
65
|
+
if not first_line:
|
|
66
|
+
continue
|
|
67
|
+
header = json.loads(first_line)
|
|
68
|
+
entries = sum(1 for _ in fh)
|
|
69
|
+
sid = header.get("id")
|
|
70
|
+
created_at = header.get("createdAt") or header.get("created_at")
|
|
71
|
+
if not sid or created_at is None:
|
|
72
|
+
continue
|
|
73
|
+
metas.append(
|
|
74
|
+
SessionMeta(
|
|
75
|
+
id=sid,
|
|
76
|
+
path=f,
|
|
77
|
+
created_at=str(created_at),
|
|
78
|
+
entries=entries,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
except (json.JSONDecodeError, KeyError, OSError):
|
|
82
|
+
continue
|
|
83
|
+
metas.sort(key=lambda m: m.created_at, reverse=True)
|
|
84
|
+
return metas
|
|
85
|
+
|
|
86
|
+
def _resolve(self, id_or_prefix: str) -> Path:
|
|
87
|
+
"""全 id 或唯一前缀 → 文件路径;未找到/歧义 → ValueError。"""
|
|
88
|
+
matches: list[Path] = []
|
|
89
|
+
for f in self.root.glob("*.jsonl"):
|
|
90
|
+
try:
|
|
91
|
+
with open(f, encoding="utf-8") as fh:
|
|
92
|
+
header = json.loads(fh.readline())
|
|
93
|
+
if header.get("id", "").startswith(id_or_prefix):
|
|
94
|
+
matches.append(f)
|
|
95
|
+
except (json.JSONDecodeError, KeyError):
|
|
96
|
+
continue
|
|
97
|
+
if not matches:
|
|
98
|
+
raise ValueError(f"Session not found: {id_or_prefix}")
|
|
99
|
+
if len(matches) > 1:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Ambiguous session prefix {id_or_prefix!r}: "
|
|
102
|
+
f"candidates {[m.stem for m in matches]}"
|
|
103
|
+
)
|
|
104
|
+
return matches[0]
|
|
105
|
+
|
|
106
|
+
def open(self, id_or_prefix: str) -> Session:
|
|
107
|
+
"""按 id 或唯一前缀打开会话(恢复整棵树)。"""
|
|
108
|
+
return Session.load(self._resolve(id_or_prefix))
|
|
109
|
+
|
|
110
|
+
def open_session(self, id_or_prefix: str) -> Session:
|
|
111
|
+
"""打开会话(等同于 self.open(id_or_prefix))。"""
|
|
112
|
+
return self.open(id_or_prefix)
|
|
113
|
+
|
|
114
|
+
def delete(self, id_or_prefix: str) -> None:
|
|
115
|
+
"""删除会话文件。未找到 → ValueError。"""
|
|
116
|
+
self._resolve(id_or_prefix).unlink()
|
|
117
|
+
|
|
118
|
+
def fork(self, id_or_prefix: str, entry_id: str) -> Session:
|
|
119
|
+
"""从某会话 entry 分叉:复制根→entry 路径为新会话(新 id/路径,独立演化)。"""
|
|
120
|
+
src = self.open(id_or_prefix)
|
|
121
|
+
new = self.create()
|
|
122
|
+
for entry in src.tree.get_path_to_entry(entry_id):
|
|
123
|
+
if hasattr(entry, "message"):
|
|
124
|
+
_ = new.add_message(entry.role, entry.content, **entry.metadata) # pyright: ignore[reportAttributeAccessIssue]
|
|
125
|
+
return new
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"SessionMeta",
|
|
130
|
+
"SessionStore",
|
|
131
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Pure in-memory DAG tree algorithms for session entries.
|
|
2
|
+
|
|
3
|
+
Provides entry mapping indexing with duplicate ID protection, cycle detection,
|
|
4
|
+
and path extraction from root to leaf, operating with zero I/O side-effects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
10
|
+
|
|
11
|
+
from .entries import SessionEntry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SessionTreeError(ValueError):
|
|
15
|
+
"""Raised when an illegal operation or cycle occurs in the session DAG tree."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def entries_by_id(entries: Sequence[SessionEntry]) -> dict[str, SessionEntry]:
|
|
19
|
+
"""建立 entry_id 到 SessionEntry 的索引字典。遇到重复 ID 抛出 SessionTreeError。"""
|
|
20
|
+
by_id: dict[str, SessionEntry] = {}
|
|
21
|
+
for entry in entries:
|
|
22
|
+
if entry.id in by_id:
|
|
23
|
+
raise SessionTreeError(f"Duplicate entry id: {entry.id}")
|
|
24
|
+
by_id[entry.id] = entry
|
|
25
|
+
return by_id
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def path_to_entry(
|
|
29
|
+
entries: Sequence[SessionEntry] | Mapping[str, SessionEntry],
|
|
30
|
+
leaf_id: str,
|
|
31
|
+
) -> list[SessionEntry]:
|
|
32
|
+
"""从根节点回溯到 leaf_id 的有序路径条目列表(根节点在前,叶节点在后)。
|
|
33
|
+
|
|
34
|
+
遇到缺失父节点报错,遇到循环引用抛出带 'Cycle detected' 的 SessionTreeError。
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(entries, Mapping):
|
|
37
|
+
by_id = entries
|
|
38
|
+
else:
|
|
39
|
+
by_id = entries_by_id(entries)
|
|
40
|
+
|
|
41
|
+
if leaf_id not in by_id:
|
|
42
|
+
raise SessionTreeError(f"Entry {leaf_id} not found")
|
|
43
|
+
|
|
44
|
+
path: list[SessionEntry] = []
|
|
45
|
+
seen: set[str] = set()
|
|
46
|
+
curr_id: str | None = leaf_id
|
|
47
|
+
|
|
48
|
+
while curr_id is not None:
|
|
49
|
+
if curr_id in seen:
|
|
50
|
+
raise SessionTreeError(f"Cycle detected at entry {curr_id}")
|
|
51
|
+
seen.add(curr_id)
|
|
52
|
+
|
|
53
|
+
entry = by_id.get(curr_id)
|
|
54
|
+
if entry is None:
|
|
55
|
+
raise SessionTreeError(f"Missing parent entry: {curr_id}")
|
|
56
|
+
|
|
57
|
+
path.append(entry)
|
|
58
|
+
curr_id = entry.parent_id
|
|
59
|
+
|
|
60
|
+
path.reverse()
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def lowest_common_ancestor(
|
|
65
|
+
entries: Sequence[SessionEntry] | Mapping[str, SessionEntry],
|
|
66
|
+
id1: str,
|
|
67
|
+
id2: str,
|
|
68
|
+
) -> str | None:
|
|
69
|
+
"""计算两个节点的最近公共祖先 (LCA) ID。若无公共祖先则返回 None。"""
|
|
70
|
+
path1 = [e.id for e in path_to_entry(entries, id1)]
|
|
71
|
+
path2 = [e.id for e in path_to_entry(entries, id2)]
|
|
72
|
+
ancestor: str | None = None
|
|
73
|
+
for a, b in zip(path1, path2, strict=False):
|
|
74
|
+
if a == b:
|
|
75
|
+
ancestor = a
|
|
76
|
+
else:
|
|
77
|
+
break
|
|
78
|
+
return ancestor
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"SessionTreeError",
|
|
83
|
+
"entries_by_id",
|
|
84
|
+
"path_to_entry",
|
|
85
|
+
"lowest_common_ancestor",
|
|
86
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""skill 机制:数据模型 + 仓储发现 + 格式化(pig-mono 式三件套,模型侧无 read 工具)。
|
|
2
|
+
|
|
3
|
+
SkillManager 为 Repository 形态:持有 dict 状态,构造即发现,查询/格式化全部
|
|
4
|
+
收编为实例方法(对应 pig-mono 的 SkillManager 类;模块级函数版本见历史)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Skill:
|
|
18
|
+
"""一个技能:name 来自目录名,description 来自 frontmatter,content 是正文。"""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
content: str # frontmatter 之下的正文
|
|
23
|
+
file_path: Path # 调试用;不暴露给模型
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
27
|
+
"""首部 --- 换行 YAML 换行 --- → (字段 dict, body)。无 frontmatter → ({}, 全文)。
|
|
28
|
+
用 yaml.safe_load;坏 YAML → 降级 ({}, 全文),不抛不告警。"""
|
|
29
|
+
header = "---\n"
|
|
30
|
+
if not text.startswith(header):
|
|
31
|
+
return {}, text
|
|
32
|
+
end = text.find("\n---\n", len(header))
|
|
33
|
+
if end == -1:
|
|
34
|
+
return {}, text
|
|
35
|
+
block = text[len(header) : end]
|
|
36
|
+
body = text[end + len("\n---\n") :].strip()
|
|
37
|
+
try:
|
|
38
|
+
fields = yaml.safe_load(block)
|
|
39
|
+
except yaml.YAMLError:
|
|
40
|
+
return {}, text
|
|
41
|
+
if not isinstance(fields, dict):
|
|
42
|
+
return {}, text
|
|
43
|
+
return {str(k): str(v) for k, v in fields.items()}, body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SkillManager:
|
|
47
|
+
"""skill 仓储(Repository):发现 + 按名索引 + 清单/调用文本生成。
|
|
48
|
+
|
|
49
|
+
发现规则(pig-mono 同款):只扫每个来源目录的一层子目录,认 <name>/SKILL.md;
|
|
50
|
+
name = 目录名(不读 frontmatter name)。缺 description → 跳过该 skill。
|
|
51
|
+
隐藏目录(. 开头)跳过。目录不存在 → 静默跳过。容错全静默,不抛不告警。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
dirs: Sequence[str | Path] | None = None,
|
|
57
|
+
extra_dirs: Sequence[str | Path] | None = None,
|
|
58
|
+
):
|
|
59
|
+
"""构造即发现:None → 探测 <cwd>/.agents/skills(不存在 → 空,静默);
|
|
60
|
+
[] → 显式禁用;非空 list → 只扫这些目录。extra_dirs 追加额外发现目录(如 Plugin 解构技能目录)。"""
|
|
61
|
+
self.skills: dict[str, Skill] = {}
|
|
62
|
+
if dirs is None:
|
|
63
|
+
dirs = [Path.cwd() / ".agents" / "skills"]
|
|
64
|
+
for root in dirs:
|
|
65
|
+
self._discover_dir(Path(root))
|
|
66
|
+
if extra_dirs:
|
|
67
|
+
for root in extra_dirs:
|
|
68
|
+
self._discover_dir(Path(root))
|
|
69
|
+
|
|
70
|
+
def _discover_dir(self, root: Path) -> None:
|
|
71
|
+
"""扫单一来源目录的一层子目录(认 <name>/SKILL.md,或根级 SKILL.md 单技能简写)。"""
|
|
72
|
+
if not root.is_dir():
|
|
73
|
+
return
|
|
74
|
+
# 1. 根级单 SKILL.md 简写支持
|
|
75
|
+
root_skill = root / "SKILL.md"
|
|
76
|
+
if root_skill.is_file():
|
|
77
|
+
skill = self._load_one(root_skill)
|
|
78
|
+
if skill is not None:
|
|
79
|
+
self.skills[skill.name] = skill
|
|
80
|
+
|
|
81
|
+
# 2. 一层子目录 <child>/SKILL.md 扫描
|
|
82
|
+
for child in sorted(root.iterdir()):
|
|
83
|
+
if not child.is_dir() or child.name.startswith("."):
|
|
84
|
+
continue
|
|
85
|
+
skill_file = child / "SKILL.md"
|
|
86
|
+
if not skill_file.is_file():
|
|
87
|
+
continue
|
|
88
|
+
skill = self._load_one(skill_file)
|
|
89
|
+
if skill is not None:
|
|
90
|
+
self.skills[skill.name] = skill
|
|
91
|
+
|
|
92
|
+
def _load_one(self, path: Path) -> Skill | None:
|
|
93
|
+
"""读单个 SKILL.md → Skill;任何失败/缺 description → None(静默)。"""
|
|
94
|
+
try:
|
|
95
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
96
|
+
except OSError:
|
|
97
|
+
return None
|
|
98
|
+
meta, body = parse_frontmatter(text)
|
|
99
|
+
description = meta.get("description")
|
|
100
|
+
if not description:
|
|
101
|
+
return None
|
|
102
|
+
name = (meta.get("name") or "").strip() or path.parent.name
|
|
103
|
+
return Skill(name=name, description=description, content=body, file_path=path)
|
|
104
|
+
|
|
105
|
+
def get(self, name: str) -> Skill | None:
|
|
106
|
+
"""按名查询(Repository 主查询)。"""
|
|
107
|
+
return self.skills.get(name)
|
|
108
|
+
|
|
109
|
+
def list(self) -> list[Skill]:
|
|
110
|
+
"""全部 skills,发现顺序。"""
|
|
111
|
+
return list(self.skills.values())
|
|
112
|
+
|
|
113
|
+
def __len__(self) -> int:
|
|
114
|
+
return len(self.skills)
|
|
115
|
+
|
|
116
|
+
def __contains__(self, name: str) -> bool:
|
|
117
|
+
return name in self.skills
|
|
118
|
+
|
|
119
|
+
def format_prompt(self, names: Sequence[str] | None = None) -> str:
|
|
120
|
+
"""全部(或指定名字子集)skills → XML 清单块;空 → 空串(进 system)。
|
|
121
|
+
names 给定只格式化这些名字(供 subagent skills 字段取子集);未知名忽略。"""
|
|
122
|
+
skills = (
|
|
123
|
+
(self.skills[n] for n in names if n in self.skills)
|
|
124
|
+
if names is not None
|
|
125
|
+
else self.skills.values()
|
|
126
|
+
)
|
|
127
|
+
parts = ["<available_skills>"]
|
|
128
|
+
for s in skills:
|
|
129
|
+
parts.append(" <skill>")
|
|
130
|
+
parts.append(f" <name>{s.name}</name>")
|
|
131
|
+
parts.append(f" <description>{s.description}</description>")
|
|
132
|
+
parts.append(" </skill>")
|
|
133
|
+
if len(parts) == 1:
|
|
134
|
+
return ""
|
|
135
|
+
parts.append("</available_skills>")
|
|
136
|
+
return "\n".join(parts)
|
|
137
|
+
|
|
138
|
+
def format_invocation(self, name: str, instructions: str = "") -> str:
|
|
139
|
+
"""按名取正文并包装 '<skill name="…" location="…">\\n{content}\\n</skill>'
|
|
140
|
+
+ 可选附言(\\n\\n 衔接)。未知名 → ValueError(列可用名字)。"""
|
|
141
|
+
skill = self.get(name)
|
|
142
|
+
if skill is None:
|
|
143
|
+
available = ", ".join(sorted(self.skills)) or "(none)"
|
|
144
|
+
raise ValueError(f"Unknown skill '{name}'. Available: {available}")
|
|
145
|
+
block = (
|
|
146
|
+
f'<skill name="{skill.name}" location="{skill.file_path}">\n'
|
|
147
|
+
f"{skill.content}\n</skill>"
|
|
148
|
+
)
|
|
149
|
+
return f"{block}\n\n{instructions}" if instructions else block
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""subagent 委派的任务生命周期:SubagentTask 模型 + SubagentTaskStatus + SubagentTaskManager。
|
|
2
|
+
|
|
3
|
+
委派逻辑(_filter_tools/_system_for)从 subagents.py 移入;make_task_tool 位于
|
|
4
|
+
tools/builtin/task.py(工具桥,真实逻辑在 SubagentTaskManager)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from my_agent_core.session import Session
|
|
14
|
+
from my_agent_core.subagents import DEFAULT_SUBAGENT, Subagent, SubagentManager
|
|
15
|
+
from my_agent_core.tools import Tool
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from my_agent_core.agent import Agent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SubagentTaskStatus(StrEnum):
|
|
22
|
+
"""子代理委派任务三态。"""
|
|
23
|
+
|
|
24
|
+
RUNNING = "running"
|
|
25
|
+
COMPLETED = "completed"
|
|
26
|
+
ERROR = "error"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SubagentTask:
|
|
31
|
+
"""一次子代理委派执行句柄:有 id/状态/结果,可查询。"""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
status: SubagentTaskStatus
|
|
35
|
+
result: str | None = None
|
|
36
|
+
error: str | None = None
|
|
37
|
+
|
|
38
|
+
def set_result(self, result: str) -> None:
|
|
39
|
+
"""标记成功。"""
|
|
40
|
+
self.result = result
|
|
41
|
+
self.error = None
|
|
42
|
+
self.status = SubagentTaskStatus.COMPLETED
|
|
43
|
+
|
|
44
|
+
def set_error(self, error: str) -> None:
|
|
45
|
+
"""标记失败。"""
|
|
46
|
+
self.error = error
|
|
47
|
+
self.result = None
|
|
48
|
+
self.status = SubagentTaskStatus.ERROR
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _system_for(sub: Subagent, parent: Agent) -> str:
|
|
52
|
+
"""子代理 system = 正文 + (若有 skills)子集清单;不收父 system prompt(Claude 官方语义)。"""
|
|
53
|
+
parts = [sub.content]
|
|
54
|
+
if sub.skills:
|
|
55
|
+
block = parent.skill_manager.format_prompt(sub.skills)
|
|
56
|
+
if block:
|
|
57
|
+
parts.append(block)
|
|
58
|
+
return "\n\n".join(p for p in parts if p)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _filter_tools(parent: Agent, sub: Subagent) -> list[Tool]: # pyright: ignore[reportUndefinedVariable]
|
|
62
|
+
"""父工具集按白/黑名单过滤;task、memory 与 task_* 永不出现(防递归与隔离)。"""
|
|
63
|
+
builtins = (
|
|
64
|
+
"task",
|
|
65
|
+
"memory",
|
|
66
|
+
"todo",
|
|
67
|
+
"task_create",
|
|
68
|
+
"task_update",
|
|
69
|
+
"task_get",
|
|
70
|
+
"task_list",
|
|
71
|
+
"todo_write",
|
|
72
|
+
)
|
|
73
|
+
tools = [t for t in parent.registry.list() if t.name not in builtins]
|
|
74
|
+
if sub.tools is not None:
|
|
75
|
+
allowed = set(sub.tools)
|
|
76
|
+
tools = [t for t in tools if t.name in allowed]
|
|
77
|
+
black = set(sub.disallowed_tools)
|
|
78
|
+
tools = [t for t in tools if t.name not in black]
|
|
79
|
+
return tools
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SubagentTaskManager:
|
|
83
|
+
"""子代理委派任务的生命周期管理器(对标 OpenHands TaskManager)。"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, manager: SubagentManager, parent: Agent) -> None:
|
|
86
|
+
self._manager = manager # 查 agent 定义
|
|
87
|
+
self._parent = parent # 供 llm/工具集/skill_manager/max_iterations
|
|
88
|
+
self._counter = 0
|
|
89
|
+
self._active_agents: dict[
|
|
90
|
+
str, Agent
|
|
91
|
+
] = {} # 追踪运行中的子代理实例 (task_id -> Agent)
|
|
92
|
+
|
|
93
|
+
def steer_task(self, task_id: str, message: str) -> bool:
|
|
94
|
+
"""向指定运行中的子代理发送 Steer 转向指令。"""
|
|
95
|
+
agent = self._active_agents.get(task_id)
|
|
96
|
+
if agent is not None:
|
|
97
|
+
agent.steer(message)
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def follow_up_task(self, task_id: str, message: str) -> bool:
|
|
102
|
+
"""向指定运行中的子代理发送 Follow-up 追问指令。"""
|
|
103
|
+
agent = self._active_agents.get(task_id)
|
|
104
|
+
if agent is not None:
|
|
105
|
+
agent.follow_up(message)
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
async def start_task(
|
|
110
|
+
self, prompt: str, subagent_type: str = "default"
|
|
111
|
+
) -> SubagentTask:
|
|
112
|
+
"""异步:建 SubagentTask(RUNNING) → spawn → run → 更新状态 → 返回。"""
|
|
113
|
+
task = self._create_task(subagent_type)
|
|
114
|
+
try:
|
|
115
|
+
task.set_result(await self._run(prompt, subagent_type, task.id))
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
task.set_error(str(exc))
|
|
118
|
+
return task
|
|
119
|
+
|
|
120
|
+
def _create_task(self, _subagent_type: str) -> SubagentTask:
|
|
121
|
+
self._counter += 1
|
|
122
|
+
return SubagentTask(
|
|
123
|
+
id=f"task_{self._counter:08x}", status=SubagentTaskStatus.RUNNING
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def _run(self, prompt: str, subagent_type: str, task_id: str) -> str:
|
|
127
|
+
"""查定义 → 建独立 session → 过滤工具 → spawn 子 Agent → run → 返回最终文本。"""
|
|
128
|
+
from my_agent_core.agent import Agent # 延迟 import 避循环
|
|
129
|
+
|
|
130
|
+
sub = self._manager.get(subagent_type)
|
|
131
|
+
if sub is None and subagent_type == "default":
|
|
132
|
+
sub = DEFAULT_SUBAGENT
|
|
133
|
+
if sub is None:
|
|
134
|
+
available = ", ".join(sorted(self._manager.subagents)) or "(none)"
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Unknown subagent '{subagent_type}'. Available: {available}"
|
|
137
|
+
)
|
|
138
|
+
child_session = Session(
|
|
139
|
+
path=self._parent.session.path.parent
|
|
140
|
+
/ "subagents"
|
|
141
|
+
/ f"agent-{task_id}.jsonl",
|
|
142
|
+
cwd=self._parent.session.cwd,
|
|
143
|
+
metadata={
|
|
144
|
+
"agent_type": subagent_type,
|
|
145
|
+
"parent_session_id": self._parent.session.id,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
child_session.save()
|
|
149
|
+
child = Agent(
|
|
150
|
+
llm=self._parent.llm,
|
|
151
|
+
tools=_filter_tools(self._parent, sub),
|
|
152
|
+
session=child_session,
|
|
153
|
+
system_prompt=_system_for(sub, self._parent),
|
|
154
|
+
model=sub.model,
|
|
155
|
+
max_iterations=sub.max_turns
|
|
156
|
+
if sub.max_turns is not None
|
|
157
|
+
else self._parent.max_iterations,
|
|
158
|
+
skill_dirs=[], # skill 清单已由 _system_for 拼入
|
|
159
|
+
subagent_dirs=[], # 防递归:禁用子代理再探测
|
|
160
|
+
memory_dir=False, # 隔离:子代理禁用长期记忆探测与维护
|
|
161
|
+
task_store=False, # 隔离:子代理禁用任务看板探测与维护
|
|
162
|
+
plugin_dirs=[], # 隔离:子代理禁用插件再探测(防递归注册 task 工具)
|
|
163
|
+
)
|
|
164
|
+
self._active_agents[task_id] = child
|
|
165
|
+
try:
|
|
166
|
+
return (await child.run(prompt)) or "(no summary)"
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
raise RuntimeError(f"Subagent '{subagent_type}' failed: {exc}") from exc
|
|
169
|
+
finally:
|
|
170
|
+
_ = self._active_agents.pop(task_id, None)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""subagent 机制:数据模型 + 仓储发现 + 清单格式化(对标 Claude Code 生态 agent 定义)。
|
|
2
|
+
|
|
3
|
+
SubagentManager 为 Repository 形态(对标 SkillManager):构造即发现 agents/*.md,
|
|
4
|
+
按名索引,清单格式化进 system prompt。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from my_agent_core.skills import parse_frontmatter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Subagent:
|
|
18
|
+
"""一个子代理定义:name 来自 frontmatter(缺省文件名),description 供主模型
|
|
19
|
+
触发选择,content 是正文(= 子代理 system prompt,不收父 system prompt)。"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
content: str
|
|
24
|
+
file_path: Path
|
|
25
|
+
model: str | None = None # 缺省 inherit = 继承父模型
|
|
26
|
+
effort: str | None = (
|
|
27
|
+
None # v1 解析但不消费(SDK 无统一 effort 参数,端到端映射留 LLM 层演进)
|
|
28
|
+
)
|
|
29
|
+
max_turns: int | None = None # maxTurns(缺省继承父 max_iterations)
|
|
30
|
+
tools: tuple[str, ...] | None = None # 白名单;None=继承父全部
|
|
31
|
+
disallowed_tools: tuple[str, ...] = () # 黑名单
|
|
32
|
+
skills: tuple[str, ...] | None = None # 子代理 skill 名(清单拼 system)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEFAULT_SUBAGENT_SYSTEM = (
|
|
36
|
+
"You are a subagent. Complete the given task independently, "
|
|
37
|
+
"then summarize your findings."
|
|
38
|
+
)
|
|
39
|
+
DEFAULT_SUBAGENT = Subagent(
|
|
40
|
+
name="default",
|
|
41
|
+
description="A general-purpose subagent for standalone tasks.",
|
|
42
|
+
content=DEFAULT_SUBAGENT_SYSTEM,
|
|
43
|
+
file_path=Path("<builtin>"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _split_csv(value: object) -> tuple[str, ...] | None:
|
|
48
|
+
"""frontmatter 逗号分隔字符串 → tuple;非字符串/空 → None(保留缺省)。"""
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
return None
|
|
51
|
+
parts = tuple(p.strip() for p in value.split(",") if p.strip())
|
|
52
|
+
return parts if parts else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_max_turns(value: object) -> int | None:
|
|
56
|
+
"""maxTurns:str 或 int → int;坏值/其他 → None。"""
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return None
|
|
59
|
+
if isinstance(value, int):
|
|
60
|
+
return value
|
|
61
|
+
if isinstance(value, str):
|
|
62
|
+
try:
|
|
63
|
+
return int(value.strip())
|
|
64
|
+
except ValueError:
|
|
65
|
+
return None
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SubagentManager:
|
|
70
|
+
"""subagent 仓储:发现 agents/*.md + 按名索引 + 清单格式化。"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
dirs: Sequence[str | Path] | None = None,
|
|
75
|
+
extra_dirs: Sequence[str | Path] | None = None,
|
|
76
|
+
):
|
|
77
|
+
"""构造即发现:None → 探测 <cwd>/.agents/agents(不存在 → 空,静默);
|
|
78
|
+
[] → 显式禁用;非空 → 只扫这些目录。extra_dirs 追加额外发现目录(如 Plugin 解构子代理目录)。"""
|
|
79
|
+
self.subagents: dict[str, Subagent] = {}
|
|
80
|
+
if dirs is None:
|
|
81
|
+
dirs = [Path.cwd() / ".agents" / "agents"]
|
|
82
|
+
for root in dirs:
|
|
83
|
+
self._discover_dir(Path(root))
|
|
84
|
+
if extra_dirs:
|
|
85
|
+
for root in extra_dirs:
|
|
86
|
+
self._discover_dir(Path(root))
|
|
87
|
+
|
|
88
|
+
def _discover_dir(self, root: Path) -> None:
|
|
89
|
+
"""扫一层 *.md(不递归、不认子目录、跳过 README/隐藏文件)。同名后覆盖。"""
|
|
90
|
+
if not root.is_dir():
|
|
91
|
+
return
|
|
92
|
+
for child in sorted(root.iterdir()):
|
|
93
|
+
if child.is_dir() or child.suffix.lower() != ".md":
|
|
94
|
+
continue
|
|
95
|
+
if child.name.startswith(".") or child.name.lower() == "readme.md":
|
|
96
|
+
continue
|
|
97
|
+
sub = self._load_one(child)
|
|
98
|
+
if sub is not None:
|
|
99
|
+
self.subagents[sub.name] = sub
|
|
100
|
+
|
|
101
|
+
def _load_one(self, path: Path) -> Subagent | None:
|
|
102
|
+
"""读单文件 → Subagent;读失败/坏 YAML/缺 description → None(静默)。"""
|
|
103
|
+
try:
|
|
104
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
105
|
+
except OSError:
|
|
106
|
+
return None
|
|
107
|
+
meta, body = parse_frontmatter(text)
|
|
108
|
+
description = (meta.get("description") or "").strip()
|
|
109
|
+
if not description:
|
|
110
|
+
return None
|
|
111
|
+
name = (meta.get("name") or "").strip() or path.stem
|
|
112
|
+
return Subagent(
|
|
113
|
+
name=name,
|
|
114
|
+
description=description,
|
|
115
|
+
content=body,
|
|
116
|
+
file_path=path,
|
|
117
|
+
model=meta.get("model") or None,
|
|
118
|
+
effort=meta.get("effort") or None,
|
|
119
|
+
max_turns=_parse_max_turns(meta.get("maxTurns")),
|
|
120
|
+
tools=_split_csv(meta.get("tools")),
|
|
121
|
+
disallowed_tools=_split_csv(meta.get("disallowedTools")) or (),
|
|
122
|
+
skills=_split_csv(meta.get("skills")),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def get(self, name: str) -> Subagent | None:
|
|
126
|
+
return self.subagents.get(name)
|
|
127
|
+
|
|
128
|
+
def list(self) -> list[Subagent]:
|
|
129
|
+
return list(self.subagents.values())
|
|
130
|
+
|
|
131
|
+
def __len__(self) -> int:
|
|
132
|
+
return len(self.subagents)
|
|
133
|
+
|
|
134
|
+
def __contains__(self, name: str) -> bool:
|
|
135
|
+
return name in self.subagents
|
|
136
|
+
|
|
137
|
+
def format_prompt(self) -> str:
|
|
138
|
+
"""全部 agents → XML 清单块(名字 + description);空 → 空串(进 system)。"""
|
|
139
|
+
if not self.subagents:
|
|
140
|
+
return ""
|
|
141
|
+
parts = ["<available_agents>"]
|
|
142
|
+
for s in self.subagents.values():
|
|
143
|
+
parts.append(" <agent>")
|
|
144
|
+
parts.append(f" <name>{s.name}</name>")
|
|
145
|
+
parts.append(f" <description>{s.description}</description>")
|
|
146
|
+
parts.append(" </agent>")
|
|
147
|
+
parts.append("</available_agents>")
|
|
148
|
+
return "\n".join(parts)
|