tagword-codeagent 1.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2 @@
1
+ from .models import Session # noqa: F401
2
+
@@ -0,0 +1,342 @@
1
+ """Attachment subsystem — file storage, user message building, directory scan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import mimetypes
7
+ import os
8
+ import re
9
+ from dataclasses import asdict, dataclass, field
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ from codeagent.core import env as ca_env
15
+
16
+ ATTACHMENT_TAG_RE = re.compile(
17
+ r"\[attachment:([^\s\]]+)(?:\s+([^\]]+))?\]",
18
+ re.IGNORECASE,
19
+ )
20
+ IMAGE_DIR_TAG_RE = re.compile(
21
+ r"\[image_dir:([^\s\]]+)(?:\s+max=(\d+))?\]",
22
+ re.IGNORECASE,
23
+ )
24
+
25
+ _DEFAULT_IMAGE_GLOBS = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.gif", "*.bmp")
26
+
27
+
28
+ @dataclass
29
+ class AttachmentMeta:
30
+ id: str
31
+ kind: str
32
+ mime: str
33
+ filename: str
34
+ size_bytes: int
35
+ path: str = ""
36
+ extracted_text: Optional[str] = None
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ return {k: v for k, v in asdict(self).items() if v is not None}
40
+
41
+
42
+ def _max_count() -> int:
43
+ return ca_env.pick_int(8, "CODEAGENT_ATTACHMENTS_MAX_COUNT")
44
+
45
+
46
+ def _max_bytes() -> int:
47
+ return ca_env.pick_int(20 * 1024 * 1024, "CODEAGENT_ATTACHMENTS_MAX_BYTES")
48
+
49
+
50
+ def _dir_max_files() -> int:
51
+ return ca_env.pick_int(32, "CODEAGENT_ATTACHMENTS_DIR_MAX_FILES")
52
+
53
+
54
+ def _allowed_mime_prefixes() -> tuple[str, ...]:
55
+ raw = ca_env.pick_default(
56
+ "image/,video/,audio/,application/pdf,text/",
57
+ "CODEAGENT_ATTACHMENTS_ALLOWED_MIME",
58
+ )
59
+ return tuple(p.strip() for p in raw.split(",") if p.strip())
60
+
61
+
62
+ def mime_allowed(mime: str, filename: str = "") -> bool:
63
+ m = (mime or "").lower().split(";")[0].strip()
64
+ if not m and filename:
65
+ guessed, _ = mimetypes.guess_type(filename)
66
+ m = (guessed or "").lower()
67
+ if m in ("image/svg+xml", "text/html"):
68
+ return False
69
+ for prefix in _allowed_mime_prefixes():
70
+ if prefix.endswith("/") and m.startswith(prefix):
71
+ return True
72
+ if m == prefix:
73
+ return True
74
+ return False
75
+
76
+
77
+ def _max_bytes_for_kind(kind: str) -> int:
78
+ k = (kind or "").lower()
79
+ if k in ("video", "audio"):
80
+ return ca_env.pick_int(100 * 1024 * 1024, "CODEAGENT_MEDIA_MAX_BYTES")
81
+ return _max_bytes()
82
+
83
+
84
+ def _kind_for_mime(mime: str, filename: str) -> str:
85
+ m = (mime or "").lower()
86
+ if m.startswith("image/"):
87
+ return "image"
88
+ if m.startswith("video/"):
89
+ return "video"
90
+ if m.startswith("audio/"):
91
+ return "audio"
92
+ if m == "application/pdf" or filename.lower().endswith(".pdf"):
93
+ return "document"
94
+ return "document"
95
+
96
+
97
+ def extract_document_text(raw: bytes, mime: str, filename: str = "") -> str:
98
+ m = (mime or "").lower()
99
+ name = (filename or "").lower()
100
+ if m.startswith("text/") or name.endswith((".txt", ".md", ".markdown")):
101
+ return raw.decode("utf-8", errors="replace").strip()
102
+ if m == "application/pdf" or name.endswith(".pdf"):
103
+ try:
104
+ from pypdf import PdfReader
105
+ import io
106
+
107
+ reader = PdfReader(io.BytesIO(raw))
108
+ parts = []
109
+ for page in reader.pages:
110
+ t = page.extract_text() or ""
111
+ if t.strip():
112
+ parts.append(t.strip())
113
+ text = "\n\n".join(parts).strip()
114
+ if not text:
115
+ return "[PDF 无文本层;请转为图片后使用 vision_analyze]"
116
+ return text
117
+ except ImportError:
118
+ return "[PDF 解析需要 pip install pypdf]"
119
+ except Exception as e:
120
+ return f"[PDF 提取失败: {e}]"
121
+ return ""
122
+
123
+
124
+ def save_attachment(
125
+ *,
126
+ agent_id: str,
127
+ session_id: str,
128
+ raw_bytes: bytes,
129
+ filename: str,
130
+ mime: str = "",
131
+ ) -> AttachmentMeta:
132
+ kind_guess = _kind_for_mime(mime, filename)
133
+ limit = _max_bytes_for_kind(kind_guess)
134
+ if len(raw_bytes) > limit:
135
+ raise ValueError(f"attachment exceeds {limit} bytes")
136
+ if not mime_allowed(mime, filename):
137
+ raise ValueError(f"mime not allowed: {mime or filename}")
138
+ from seed.core.media_store import save_session_media
139
+
140
+ aid, path = save_session_media(
141
+ agent_id=agent_id,
142
+ session_id=session_id,
143
+ raw_bytes=raw_bytes,
144
+ filename=filename,
145
+ mime=mime,
146
+ )
147
+ kind = _kind_for_mime(mime, filename)
148
+ extracted = None
149
+ if kind == "document":
150
+ extracted = extract_document_text(raw_bytes, mime, filename) or None
151
+ return AttachmentMeta(
152
+ id=aid,
153
+ kind=kind,
154
+ mime=(mime or mimetypes.guess_type(filename)[0] or "application/octet-stream"),
155
+ filename=filename or aid,
156
+ size_bytes=len(raw_bytes),
157
+ path=str(path),
158
+ extracted_text=extracted,
159
+ )
160
+
161
+
162
+ def resolve_attachment_path(agent_id: str, session_id: str, attachment_id: str) -> Optional[Path]:
163
+ from seed.core.media_store import resolve_session_media_path
164
+
165
+ p = resolve_session_media_path(agent_id, session_id, attachment_id)
166
+ return p
167
+
168
+
169
+ def load_attachment_meta(agent_id: str, session_id: str, attachment_id: str) -> Optional[AttachmentMeta]:
170
+ p = resolve_attachment_path(agent_id, session_id, attachment_id)
171
+ if not p or not p.is_file():
172
+ return None
173
+ mime, _ = mimetypes.guess_type(str(p))
174
+ kind = _kind_for_mime(mime or "", p.name)
175
+ return AttachmentMeta(
176
+ id=attachment_id,
177
+ kind=kind,
178
+ mime=mime or "application/octet-stream",
179
+ filename=p.name.split("_", 1)[-1] if "_" in p.name else p.name,
180
+ size_bytes=p.stat().st_size,
181
+ path=str(p.resolve()),
182
+ )
183
+
184
+
185
+ def build_user_message(
186
+ text: str,
187
+ attachments: list[AttachmentMeta],
188
+ *,
189
+ image_dir_tag: str = "",
190
+ ) -> dict[str, Any]:
191
+ lines: list[str] = []
192
+ t = (text or "").strip()
193
+ if t:
194
+ lines.append(t)
195
+ elif attachments:
196
+ lines.append("[附件]")
197
+ for a in attachments:
198
+ if a.kind == "document" and a.extracted_text:
199
+ lines.append(f"[文档: {a.filename}]\n{a.extracted_text}")
200
+ else:
201
+ lines.append(f"[attachment:{a.id} {a.filename}]")
202
+ if image_dir_tag.strip():
203
+ lines.append(image_dir_tag.strip())
204
+ msg: dict[str, Any] = {
205
+ "role": "user",
206
+ "content": "\n".join(lines),
207
+ "ts": datetime.now(timezone.utc).isoformat(),
208
+ }
209
+ if attachments:
210
+ msg["attachments"] = [a.to_dict() for a in attachments]
211
+ return msg
212
+
213
+
214
+ def content_text_for_skills(content: Any) -> str:
215
+ if content is None:
216
+ return ""
217
+ if isinstance(content, str):
218
+ text = content
219
+ elif isinstance(content, list):
220
+ parts = []
221
+ for item in content:
222
+ if isinstance(item, dict) and item.get("type") == "text":
223
+ parts.append(str(item.get("text") or ""))
224
+ text = "".join(parts)
225
+ else:
226
+ text = str(content)
227
+ text = ATTACHMENT_TAG_RE.sub("", text)
228
+ text = IMAGE_DIR_TAG_RE.sub("", text)
229
+ return text.strip()
230
+
231
+
232
+ def parse_image_dir_tags(text: str) -> list[tuple[str, int]]:
233
+ out: list[tuple[str, int]] = []
234
+ for m in IMAGE_DIR_TAG_RE.finditer(text or ""):
235
+ rel = m.group(1).strip()
236
+ try:
237
+ mx = int(m.group(2)) if m.group(2) else _dir_max_files()
238
+ except ValueError:
239
+ mx = _dir_max_files()
240
+ out.append((rel, min(mx, _dir_max_files())))
241
+ return out
242
+
243
+
244
+ def scan_image_directory(
245
+ workspace_root: Path,
246
+ rel_path: str,
247
+ *,
248
+ pattern: Optional[str] = None,
249
+ max_files: Optional[int] = None,
250
+ ) -> tuple[list[Path], bool]:
251
+ """Return (image paths, truncated). Paths resolved under workspace_root."""
252
+ root = workspace_root.resolve()
253
+ rel = (rel_path or "").strip().replace("\\", "/").lstrip("/")
254
+ if ".." in rel.split("/"):
255
+ raise ValueError("path traversal not allowed")
256
+ target = (root / rel).resolve()
257
+ if not str(target).startswith(str(root)):
258
+ raise ValueError("path outside workspace")
259
+ if not target.is_dir():
260
+ raise ValueError(f"not a directory: {rel_path}")
261
+
262
+ globs = _parse_globs(pattern)
263
+ limit = max_files if max_files is not None else _dir_max_files()
264
+ found: list[Path] = []
265
+ truncated = False
266
+ for dirpath, _, filenames in os.walk(target):
267
+ for name in sorted(filenames):
268
+ if not _matches_globs(name, globs):
269
+ continue
270
+ p = Path(dirpath) / name
271
+ if not p.is_file():
272
+ continue
273
+ found.append(p.resolve())
274
+ if len(found) >= limit:
275
+ truncated = True
276
+ return found, truncated
277
+ return found, truncated
278
+
279
+
280
+ def _parse_globs(pattern: Optional[str]) -> tuple[str, ...]:
281
+ if pattern and pattern.strip():
282
+ return tuple(g.strip() for g in pattern.split(",") if g.strip())
283
+ raw = ca_env.pick_default(
284
+ ",".join(_DEFAULT_IMAGE_GLOBS),
285
+ "CODEAGENT_IMAGE_DIR_ALLOWED_GLOB",
286
+ )
287
+ return tuple(g.strip() for g in raw.split(",") if g.strip())
288
+
289
+
290
+ def _matches_globs(name: str, globs: tuple[str, ...]) -> bool:
291
+ low = name.lower()
292
+ for g in globs:
293
+ if fnmatch.fnmatch(low, g.lower()):
294
+ return True
295
+ return False
296
+
297
+
298
+ def message_has_image_attachments(msg: dict[str, Any]) -> bool:
299
+ atts = msg.get("attachments")
300
+ if isinstance(atts, list):
301
+ for a in atts:
302
+ if isinstance(a, dict) and a.get("kind") == "image":
303
+ return True
304
+ if IMAGE_DIR_TAG_RE.search(str(msg.get("content") or "")):
305
+ return True
306
+ return False
307
+
308
+
309
+ def message_has_video_attachments(msg: dict[str, Any]) -> bool:
310
+ atts = msg.get("attachments")
311
+ if isinstance(atts, list):
312
+ for a in atts:
313
+ if isinstance(a, dict) and a.get("kind") == "video":
314
+ return True
315
+ return False
316
+
317
+
318
+ def message_has_audio_attachments(msg: dict[str, Any]) -> bool:
319
+ atts = msg.get("attachments")
320
+ if isinstance(atts, list):
321
+ for a in atts:
322
+ if isinstance(a, dict) and a.get("kind") == "audio":
323
+ return True
324
+ return False
325
+
326
+
327
+ def collect_attachment_ids_from_message(msg: dict[str, Any]) -> list[str]:
328
+ ids: list[str] = []
329
+ atts = msg.get("attachments")
330
+ if isinstance(atts, list):
331
+ for a in atts:
332
+ if isinstance(a, dict) and a.get("id"):
333
+ ids.append(str(a["id"]))
334
+ for m in ATTACHMENT_TAG_RE.finditer(str(msg.get("content") or "")):
335
+ ids.append(m.group(1))
336
+ seen: set[str] = set()
337
+ out: list[str] = []
338
+ for i in ids:
339
+ if i not in seen:
340
+ seen.add(i)
341
+ out.append(i)
342
+ return out
@@ -0,0 +1,50 @@
1
+ """Audio transcription preset resolution for audio_transcribe tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from codeagent.core import env as ca_env
8
+
9
+
10
+ def list_audio_presets() -> list[dict[str, Any]]:
11
+ from seed.core.llm_presets import load_presets
12
+
13
+ return [p for p in load_presets() if preset_supports_audio(p)]
14
+
15
+
16
+ def preset_supports_audio(preset: dict[str, Any] | None) -> bool:
17
+ if not isinstance(preset, dict):
18
+ return False
19
+ return preset.get("supports_audio") is True
20
+
21
+
22
+ def preset_supports_audio_id(preset_id: str) -> bool:
23
+ from seed.core.llm_presets import load_presets
24
+
25
+ pid = (preset_id or "").strip()
26
+ for p in load_presets():
27
+ if str(p.get("id") or "").strip() == pid:
28
+ return preset_supports_audio(p)
29
+ return False
30
+
31
+
32
+ def resolve_audio_preset(preset_id: Optional[str] = None) -> dict[str, Any]:
33
+ from seed.core.llm_presets import load_presets
34
+
35
+ pid = (preset_id or "").strip()
36
+ if not pid:
37
+ pid = ca_env.pick_default("", "CODEAGENT_AUDIO_PRESET_ID").strip()
38
+ if not pid:
39
+ presets = list_audio_presets()
40
+ if len(presets) == 1:
41
+ return dict(presets[0])
42
+ raise ValueError(
43
+ "audio preset required: set CODEAGENT_AUDIO_PRESET_ID or Web UI audio_llm_id"
44
+ )
45
+ for p in load_presets():
46
+ if str(p.get("id") or "").strip() == pid:
47
+ if preset_supports_audio(p):
48
+ return dict(p)
49
+ raise ValueError(f"preset {pid} does not support audio transcription")
50
+ raise ValueError(f"audio preset not found: {pid}")
@@ -0,0 +1,178 @@
1
+ """Code Agent process bootstrap (product home → config → Seed bridge)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from codeagent.core.env import apply_default_product_home
11
+ from codeagent.core.seed_bridge import bridge_codeagent_env_to_seed
12
+ from seed.integrations.env_config import apply_seed_env_from_config
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _CODEAGENT_BOOTSTRAP_MD = """\
17
+ # CodeAgent 首次启动引导
18
+
19
+ 首次使用请按以下顺序完成初始化。
20
+
21
+ ---
22
+
23
+ ## 1. 环境变量
24
+
25
+ 复制模板并填写 LLM 信息:
26
+
27
+ ```bash
28
+ cp config/env.example config/env
29
+ ```
30
+
31
+ 最低必填项(编辑 `config/env`):
32
+
33
+ | 变量 | 说明 |
34
+ |------|------|
35
+ | `SEED_LLM_BASEURL` | LLM API 地址(OpenAI 格式) |
36
+ | `SEED_LLM_MODEL` | 模型名称 |
37
+ | `SEED_LLM_API_KEY` | API 密钥 |
38
+
39
+ 系统环境变量优先级 > env 文件。
40
+
41
+ ---
42
+
43
+ ## 2. Agent 人格配置
44
+
45
+ Agent 行为由 `agents/default/persona/` 下的 Markdown 文件定义:
46
+
47
+ | 文件 | 作用 |
48
+ |------|------|
49
+ | `identity.md` | 角色定位 |
50
+ | `soul.md` | 核心价值观与行为准则 |
51
+ | `agent.md` | 工作流程与项目管理规范 |
52
+ | `skills.md` | 核心工作流 |
53
+ | `tools.md` | 工具选用规则 |
54
+ | `user.md` | 用户偏好 |
55
+
56
+ 按需修改即可调整 Agent 风格。
57
+
58
+ ---
59
+
60
+ ## 3. 首次验证
61
+
62
+ ```bash
63
+ # Web UI(推荐)
64
+ codeagent web
65
+
66
+ # 或 CLI 模式
67
+ codeagent chat
68
+ ```
69
+
70
+ 确认 LLM 能正常响应、会话日志写入正常。
71
+
72
+ ---
73
+
74
+ ## 4. (可选)Git 版本管理
75
+
76
+ 对配置进行版本控制,方便回溯:
77
+
78
+ ```bash
79
+ cd ~/.codeagent
80
+ git init
81
+ ```
82
+
83
+ 创建 `.gitignore` 忽略运行数据:
84
+
85
+ ```
86
+ agents/default/sessions/
87
+ agents/default/projects-data/
88
+ agents/default/memory/
89
+ mcp-minimax-out/
90
+ config/env
91
+ config/seed.env
92
+ config/webui.token
93
+ config/codeagent.webui.token
94
+ ```
95
+
96
+ 首次提交:
97
+
98
+ ```bash
99
+ git add .gitignore agents/default/persona/ agents/default/skills/ config/
100
+ git commit -m "chore: init codeagent config"
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 目录结构
106
+
107
+ ```
108
+ ~/.codeagent/
109
+ ├── config/ # 全局配置(env、cron、MCP、LSP、hooks)
110
+ ├── agents/default/
111
+ │ ├── persona/ # Agent 人格定义(修改这里)
112
+ │ ├── skills/ # 自定义技能
113
+ │ ├── sessions/ # 对话历史(自动生成)
114
+ │ ├── memory/ # 长期记忆(自动生成)
115
+ │ └── projects-data/ # 项目记忆(自动生成)
116
+ ├── mcp-minimax-out/ # MCP 工具缓存
117
+ └── bootstrap.md # ← 本文件
118
+ ```
119
+ """
120
+
121
+
122
+ def _ensure_codeagent_bootstrap_md(home: Path) -> None:
123
+ """Overwrite Seed's generic bootstrap.md with CodeAgent's own version."""
124
+ dest = home / "config" / "bootstrap.md"
125
+ dest.parent.mkdir(parents=True, exist_ok=True)
126
+ # Always overwrite: CodeAgent's bootstrap is the authoritative one.
127
+ dest.write_text(_CODEAGENT_BOOTSTRAP_MD, encoding="utf-8")
128
+
129
+
130
+ def bootstrap_codeagent_runtime(base: Optional[Path] = None) -> Path:
131
+ """
132
+ 1. Default data root ``~/.codeagent`` (or ``CODEAGENT_HOME``)
133
+ 2. Set ``SEED_PROJECT_ROOT`` so Seed / seed-tools use the same tree
134
+ 3. Load ``config/env`` (or legacy ``config/seed.env``) + ``config/codeagent.env``
135
+ 4. Bridge ``CODEAGENT_*`` → ``SEED_*`` for kernel env keys
136
+ 5. Write CodeAgent's bootstrap.md (overrides Seed's generic version)
137
+ """
138
+ if base is not None:
139
+ home = base.resolve()
140
+ os.environ["CODEAGENT_HOME"] = str(home)
141
+ os.environ["SEED_PROJECT_ROOT"] = str(home)
142
+ home.mkdir(parents=True, exist_ok=True)
143
+ (home / "config").mkdir(parents=True, exist_ok=True)
144
+ else:
145
+ home = apply_default_product_home()
146
+
147
+ apply_seed_env_from_config(None)
148
+ bridge_codeagent_env_to_seed()
149
+
150
+ from codeagent.runtime.compact_prompt import default_summarizer_prompt_path
151
+
152
+ if default_summarizer_prompt_path().is_file():
153
+ os.environ.setdefault(
154
+ "CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_PROMPT_FILE",
155
+ str(default_summarizer_prompt_path()),
156
+ )
157
+ os.environ.setdefault("CODEAGENT_CONTEXT_COMPACT_MID_LOOP", "1")
158
+ os.environ.setdefault("CODEAGENT_CONTEXT_COMPACT_KEEP_USER_ROUNDS", "2")
159
+ bridge_codeagent_env_to_seed()
160
+
161
+ _ensure_codeagent_bootstrap_md(home)
162
+
163
+ # ── Team config: load if exists ──
164
+ try:
165
+ team_cfg_path = home / "config" / "team.json"
166
+ if team_cfg_path.is_file():
167
+ from codeagent.core.team_manager import TeamManager
168
+ tm = TeamManager()
169
+ if tm.init_from_config(team_cfg_path):
170
+ logger.info(f"Team mode enabled: {len(tm.config.members)} member agents configured")
171
+ else:
172
+ logger.info("Single-agent mode (team config invalid or empty)")
173
+ else:
174
+ logger.info("Single-agent mode (no team.json found)")
175
+ except Exception:
176
+ logger.exception("Team config init failed — continuing in single-agent mode")
177
+
178
+ return home