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,161 @@
1
+ """Orchestrator: split tasks and dispatch to workers (parallel or sequential)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ from .task_split import auto_split_enabled, split_user_tasks
11
+ from .worker import Worker
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class Orchestrator:
18
+ """
19
+ Dispatch user work to one or more workers.
20
+
21
+ - Default: first worker, single task.
22
+ - ``metadata["subtasks"]``: explicit list of prompts.
23
+ - ``metadata["parallel"]=true`` or multiple workers + auto-split: parallel run.
24
+ """
25
+
26
+ workers: list[Worker] = field(default_factory=list)
27
+ parallel: bool = False
28
+
29
+ def run(
30
+ self,
31
+ *,
32
+ session_id: str,
33
+ user_text: str,
34
+ max_tool_rounds: int = 16,
35
+ metadata: dict[str, Any] | None = None,
36
+ ) -> tuple[str, dict[str, Any]]:
37
+ if not self.workers:
38
+ raise RuntimeError("Orchestrator has no workers configured")
39
+
40
+ meta = dict(metadata or {})
41
+ subtasks = meta.get("subtasks")
42
+ if isinstance(subtasks, list):
43
+ tasks = [str(t).strip() for t in subtasks if str(t).strip()]
44
+ else:
45
+ tasks = []
46
+
47
+ if not tasks and auto_split_enabled(meta.get("auto_split")):
48
+ tasks = split_user_tasks(user_text)
49
+
50
+ if not tasks:
51
+ tasks = [user_text]
52
+
53
+ if "parallel" in meta:
54
+ use_parallel = bool(meta.get("parallel"))
55
+ else:
56
+ use_parallel = bool(
57
+ self.parallel and len(tasks) > 1 and len(self.workers) > 1
58
+ )
59
+
60
+ if len(tasks) == 1:
61
+ reply, wmeta = self.workers[0].run(
62
+ session_id=session_id,
63
+ user_text=tasks[0],
64
+ max_tool_rounds=max_tool_rounds,
65
+ )
66
+ out_meta = dict(wmeta or {})
67
+ out_meta["orchestrator"] = {"mode": "single", "tasks": 1}
68
+ return reply, out_meta
69
+
70
+ if use_parallel and len(self.workers) > 1:
71
+ return self._run_parallel(
72
+ tasks,
73
+ session_id=session_id,
74
+ max_tool_rounds=max_tool_rounds,
75
+ meta=meta,
76
+ )
77
+ return self._run_sequential(
78
+ tasks,
79
+ session_id=session_id,
80
+ max_tool_rounds=max_tool_rounds,
81
+ meta=meta,
82
+ )
83
+
84
+ def _run_sequential(
85
+ self,
86
+ tasks: list[str],
87
+ *,
88
+ session_id: str,
89
+ max_tool_rounds: int,
90
+ meta: dict[str, Any],
91
+ ) -> tuple[str, dict[str, Any]]:
92
+ parts: list[str] = []
93
+ metas: list[dict[str, Any]] = []
94
+ for i, task in enumerate(tasks):
95
+ worker = self.workers[i % len(self.workers)]
96
+ sub_sid = f"{session_id}::sub{i}"
97
+ reply, wmeta = worker.run(
98
+ session_id=sub_sid,
99
+ user_text=task,
100
+ max_tool_rounds=max_tool_rounds,
101
+ )
102
+ parts.append(f"### Subtask {i + 1}\n\n{reply}")
103
+ metas.append(wmeta or {})
104
+ merged = "\n\n".join(parts)
105
+ return merged, {
106
+ "orchestrator": {
107
+ "mode": "sequential",
108
+ "tasks": len(tasks),
109
+ "workers_used": len(self.workers),
110
+ },
111
+ "subtask_meta": metas,
112
+ **meta,
113
+ }
114
+
115
+ def _run_parallel(
116
+ self,
117
+ tasks: list[str],
118
+ *,
119
+ session_id: str,
120
+ max_tool_rounds: int,
121
+ meta: dict[str, Any],
122
+ ) -> tuple[str, dict[str, Any]]:
123
+ results: dict[int, tuple[str, dict[str, Any]]] = {}
124
+ max_workers = min(len(tasks), len(self.workers), 8)
125
+
126
+ def _job(i: int, task: str) -> tuple[int, str, dict[str, Any]]:
127
+ worker = self.workers[i % len(self.workers)]
128
+ sub_sid = f"{session_id}::sub{i}"
129
+ reply, wmeta = worker.run(
130
+ session_id=sub_sid,
131
+ user_text=task,
132
+ max_tool_rounds=max_tool_rounds,
133
+ )
134
+ return i, reply, wmeta or {}
135
+
136
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
137
+ futures = [pool.submit(_job, i, t) for i, t in enumerate(tasks)]
138
+ for fut in as_completed(futures):
139
+ try:
140
+ i, reply, wmeta = fut.result()
141
+ results[i] = (reply, wmeta)
142
+ except Exception as e:
143
+ logger.exception("orchestrator subtask failed")
144
+ results[len(results)] = (f"[subtask error] {e}", {})
145
+
146
+ parts = []
147
+ metas = []
148
+ for i in sorted(results.keys()):
149
+ reply, wmeta = results[i]
150
+ parts.append(f"### Subtask {i + 1}\n\n{reply}")
151
+ metas.append(wmeta)
152
+ merged = "\n\n".join(parts)
153
+ return merged, {
154
+ "orchestrator": {
155
+ "mode": "parallel",
156
+ "tasks": len(tasks),
157
+ "workers_used": max_workers,
158
+ },
159
+ "subtask_meta": metas,
160
+ **meta,
161
+ }
@@ -0,0 +1,324 @@
1
+ """Runtime prompt enrichment: dynamic skills, fresh system prompt, diary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional # noqa: UP045
6
+
7
+ import seed.core.env_access as _ea
8
+ from codeagent.core import env as ca_env
9
+ from seed.core.config_plane import build_system_prompt, project_root
10
+ from seed.core.env_access import pick_nonempty
11
+
12
+
13
+ def _compute_persona_hash(agent_id: str) -> str:
14
+ """Hash of all persona files + memory.md + project rules that compose the system prompt.
15
+
16
+ Used to detect file changes mid-session so the cache is invalidated.
17
+ """
18
+ import hashlib
19
+
20
+ from codeagent.core.settings import get_system_prompt_filenames
21
+ from seed.core.paths import agent_id_default, agent_persona_dir, agent_persona_memory_path
22
+
23
+ aid = (agent_id or "").strip() or agent_id_default()
24
+ persona_dir = agent_persona_dir(aid)
25
+ h = hashlib.sha256()
26
+ for fname in sorted(get_system_prompt_filenames()):
27
+ p = persona_dir / fname
28
+ if p.is_file():
29
+ h.update(p.read_bytes())
30
+ p_mem = agent_persona_memory_path(aid)
31
+ if p_mem.is_file():
32
+ h.update(p_mem.read_bytes())
33
+
34
+ # Also hash project-level rules files so edits invalidate the cache
35
+ try:
36
+ from codeagent.core.paths import read_global_rules, read_agent_rules
37
+ gr = read_global_rules()
38
+ if gr:
39
+ h.update(gr.encode("utf-8"))
40
+ ar = read_agent_rules(aid)
41
+ if ar:
42
+ h.update(ar.encode("utf-8"))
43
+ except Exception:
44
+ pass
45
+
46
+ return h.hexdigest()
47
+
48
+
49
+ def get_cached_system_prompt(session, *, agent_id: str | None = None) -> str:
50
+ """Return system prompt, building fresh only on first turn or if files changed.
51
+
52
+ Caches the result in ``session.metadata["base_system"]`` with a hash
53
+ in ``session.metadata["base_system_hash"]``.
54
+ """
55
+ md = session.metadata
56
+ if not isinstance(md, dict):
57
+ md = {}
58
+ aid = (agent_id or "").strip() or "default"
59
+ current_hash = _compute_persona_hash(aid)
60
+ cached = md.get("base_system", "")
61
+ cached_hash = md.get("base_system_hash", "")
62
+
63
+ if cached and cached_hash == current_hash:
64
+ return cached
65
+
66
+ fresh = fresh_system_prompt(agent_id=aid)
67
+ if isinstance(session.metadata, dict):
68
+ session.metadata["base_system"] = fresh
69
+ session.metadata["base_system_hash"] = current_hash
70
+ return fresh
71
+
72
+
73
+ def _codeagent_vars_dict(agent_id: str) -> dict[str, str]:
74
+ """CodeAgent-specific variables extending Seed's base vars.
75
+
76
+ All project working paths are under ``.codeagent/{agent_id}/``.
77
+ The ``SESSIONS_ARTIFACTS`` remains under the system-level agent home
78
+ (shared across all projects for cross-session artifact access).
79
+ """
80
+ from seed.core.paths import agent_home
81
+ from codeagent.core.paths import _agent_work_dir
82
+ from pathlib import Path
83
+
84
+ root = Path.cwd().resolve()
85
+ wd = _agent_work_dir(agent_id, root)
86
+ return {
87
+ "SESSIONS_ARTIFACTS": str(agent_home(agent_id) / "sessions" / "_artifacts"),
88
+ "PLANS": str(wd / "plans"),
89
+ "SCRIPTS": str(wd / "scripts"),
90
+ "DOCS": str(wd / "docs"),
91
+ "TMP": str(wd / "tmp"),
92
+ "SESSION_LOG": str(wd / "sessions"),
93
+ "AGENT_STATE": str(wd / "state.md"),
94
+ }
95
+
96
+
97
+ def _codeagent_path_registry(vars_dict: dict[str, str]) -> str:
98
+ """Build the CodeAgent extension of the path registry table."""
99
+ rows: list[str] = [
100
+ "\n\n## 路径基准(CodeAgent 扩展)\n",
101
+ "| 变量 | 当前值 |",
102
+ "|------|--------|",
103
+ ]
104
+ for key in sorted(vars_dict):
105
+ rows.append(f"| `${key}` | `{vars_dict[key]}` |")
106
+ return "\n".join(rows)
107
+
108
+
109
+ def _runtime_info_block() -> str:
110
+ """Build a block with current system environment and datetime.
111
+
112
+ Added to the system prompt so the agent knows its OS and the real current
113
+ time, avoiding cross-platform command errors and stale LLM-training dates.
114
+ """
115
+ import datetime
116
+ import platform
117
+ import os
118
+ from pathlib import Path
119
+
120
+ now = datetime.datetime.now(datetime.timezone.utc).astimezone()
121
+ os_name = platform.system() # Windows / Linux / Darwin
122
+ os_release = platform.release()
123
+
124
+ lines = [
125
+ "\n\n## 当前环境信息\n",
126
+ f"- **当前时间**: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}",
127
+ f"- **操作系统**: {os_name} {os_release}",
128
+ f"- **主机名**: {platform.node()}",
129
+ f"- **Shell**: {os.environ.get('SHELL', '') or os.environ.get('ComSpec', '')}",
130
+ f"- **工作目录**: {Path.cwd().resolve()}",
131
+ ]
132
+ if os_name == "Windows":
133
+ lines.append("\n⚠️ 当前运行在 **Windows** 环境。执行命令时请使用 Windows 兼容的指令")
134
+ lines.append("(如 `dir` 而非 `ls`,`type` 而非 `cat`),路径使用反斜杠或正斜杠均可。")
135
+ elif os_name == "Linux":
136
+ lines.append("\n⚠️ 当前运行在 **Linux** 环境。执行命令时请使用 Linux/POSIX 指令。")
137
+ elif os_name == "Darwin":
138
+ lines.append("\n⚠️ 当前运行在 **macOS** 环境。执行命令时请使用 macOS/POSIX 指令。")
139
+
140
+ return "\n".join(lines)
141
+
142
+
143
+ def fresh_system_prompt(*, agent_id: str | None = None) -> str:
144
+ """
145
+ System prompt for chat: respects Web UI ``config_md_enabled``, plugin skills
146
+ (via ``build_system_prompt``), and optional persona ``memory.md``.
147
+ """
148
+ explicit = pick_nonempty(*_ea.SYSTEM_PROMPT)
149
+ if explicit.strip():
150
+ return explicit.strip()
151
+
152
+ from seed.core.paths import agent_id_default
153
+
154
+ aid = (agent_id or "").strip() or agent_id_default()
155
+
156
+ from codeagent.core.settings import get_system_prompt_filenames
157
+
158
+ base = build_system_prompt(
159
+ base=project_root(),
160
+ filenames=get_system_prompt_filenames(),
161
+ agent_id=aid,
162
+ )
163
+
164
+ # ── Append project-level constraints (rules.md) ──
165
+ from codeagent.core.paths import read_global_rules, read_agent_rules
166
+
167
+ rules_parts: list[str] = []
168
+ gr = read_global_rules()
169
+ if gr:
170
+ rules_parts.append(f"## Project Rules (.codeagent/rules.md)\n\n{gr}")
171
+ ar = read_agent_rules(aid)
172
+ if ar:
173
+ rules_parts.append(f"## Agent Rules (.codeagent/{aid}/rules.md)\n\n{ar}")
174
+ if rules_parts:
175
+ base += "\n\n---\n" + "\n\n".join(rules_parts)
176
+
177
+ # Merge Seed vars + CodeAgent vars for memory.md rendering
178
+ from seed.core.config_plane import render_persona, _build_seed_vars_dict
179
+
180
+ seed_vars = _build_seed_vars_dict(aid, project_root())
181
+ ca_vars = _codeagent_vars_dict(aid)
182
+ all_vars: dict[str, str] = {**seed_vars, **ca_vars}
183
+
184
+ try:
185
+ from seed.core.paths import agent_persona_memory_path
186
+
187
+ p = agent_persona_memory_path(aid)
188
+ if p.is_file():
189
+ text = p.read_text(encoding="utf-8").strip()
190
+ if text:
191
+ # Render memory.md through render_persona (expands $VAR in memory content)
192
+ text = render_persona(text, all_vars)
193
+ max_chars = int(pick_nonempty(*_ea.PERSONA_MEMORY_MAX_CHARS) or "4000")
194
+ max_chars = max(200, min(max_chars, 50_000))
195
+ if len(text) > max_chars:
196
+ text = text[: max_chars - 20].rstrip() + "\n…[已截断]"
197
+ header_path = f"agents/{aid}/persona/memory.md"
198
+ base = (
199
+ base.rstrip()
200
+ + "\n\n---\n"
201
+ + f"## Persona core memory (`{header_path}`)\n\n"
202
+ + text
203
+ + "\n"
204
+ )
205
+ except Exception:
206
+ pass
207
+
208
+ # Append CodeAgent extension path registry
209
+ base += _codeagent_path_registry(ca_vars)
210
+
211
+ # Append session-stable runtime info (time, OS, workdir)
212
+ # — cached once per session so LLM provider prefix-cache stays effective
213
+ base += _runtime_info_block()
214
+
215
+ base = base.rstrip() + vision_multimodal_appendix()
216
+ return base
217
+
218
+
219
+ def build_skills_suffix(
220
+ agent_id: str,
221
+ *,
222
+ user_text: str,
223
+ workspace_suffix: str = "",
224
+ ) -> Optional[str]:
225
+ """Combine workspace hint + dynamically selected agent skills for ``skills_suffix``."""
226
+ parts: list[str] = []
227
+ if workspace_suffix and workspace_suffix.strip():
228
+ parts.append(workspace_suffix.strip())
229
+
230
+ if ca_env.env_truthy(ca_env.SKILLS_AUTO, default="1"):
231
+ try:
232
+ from codeagent.core.attachments import content_text_for_skills
233
+ from codeagent.skills.select import build_selected_skills_appendix
234
+
235
+ appendix = build_selected_skills_appendix(
236
+ agent_id,
237
+ user_text=content_text_for_skills(user_text),
238
+ )
239
+ if appendix:
240
+ parts.append(appendix)
241
+ except Exception:
242
+ pass
243
+
244
+ if not parts:
245
+ return None
246
+ return "\n".join(parts)
247
+
248
+
249
+ def vision_multimodal_appendix() -> str:
250
+ from codeagent.core.image_understanding import (
251
+ MCP_QUALIFIED_UNDERSTAND_IMAGE,
252
+ minimax_mcp_configured,
253
+ )
254
+ from codeagent.core.vision_models import list_vision_presets
255
+
256
+ mcp = minimax_mcp_configured()
257
+ has_vision = bool(list_vision_presets())
258
+ parts = ["\n\n## Multimodal (images)\n\n"]
259
+
260
+ if mcp:
261
+ parts.append(
262
+ "When the user message contains `[attachment:<id> ...]` and you need to **understand** "
263
+ "image content, call `attachment_resolve_path` with the attachment id, then "
264
+ f"`{MCP_QUALIFIED_UNDERSTAND_IMAGE}` with `image_url` set to the returned absolute "
265
+ "`path` and a clear `prompt` describing what to extract. "
266
+ "Do not claim to see images without tool results.\n\n"
267
+ )
268
+ if has_vision:
269
+ parts.append(
270
+ "When `[attachment:...]` or `[image_dir:...]` appears, you may also use "
271
+ "`vision_analyze` or `vision_analyze_directory` (requires an active vision LLM preset). "
272
+ "Prefer the tool that matches your available backends.\n\n"
273
+ )
274
+ elif not mcp:
275
+ parts.append(
276
+ "Image understanding requires a vision LLM preset or MiniMax MCP; "
277
+ "do not guess image content without tools.\n\n"
278
+ )
279
+
280
+ parts.append(
281
+ "When the user asks to **create, draw, or generate** an image, call `image_generate` "
282
+ "with a detailed prompt. For image-to-image (keep subject/style from a photo), pass "
283
+ "`attachment_ids` or `reference_image_urls`. Share returned `attachment_id` / URL in your reply.\n\n"
284
+ "When the user asks to **compose, write, or generate music or a song**, call `music_generate` "
285
+ "with a style `prompt` and `lyrics` (use [Verse]/[Chorus] tags). For instrumental-only, "
286
+ "set `is_instrumental=true`. If lyrics are missing, use `lyrics_optimizer=true` or ask the user. "
287
+ "Share the returned `attachment_id` / URL for playback.\n\n"
288
+ "When the user asks to **create or generate a video** (text-to-video, animate an image, or "
289
+ "keyframe transition), call `video_generate` with a detailed cinematic `prompt`. For image-to-video, "
290
+ "pass `attachment_ids` or `image_url`. Share the returned `attachment_id` / URL for playback.\n\n"
291
+ "For **audio** attachments, call `audio_transcribe` before answering about spoken content.\n"
292
+ "For **video** attachments, call `video_analyze` (requires server ffmpeg for frame sampling).\n"
293
+ "Camera captures appear as `[attachment:...]` — analyze them like uploaded photos.\n"
294
+ )
295
+ return "".join(parts)
296
+
297
+
298
+ def record_chat_turn_diary(
299
+ agent_id: str,
300
+ *,
301
+ user_text: str,
302
+ reply: str,
303
+ tools_used: list[str] | None = None,
304
+ project_id: str | None = None,
305
+ ) -> None:
306
+ """Append a short turn summary to the agent daily diary (best-effort)."""
307
+ if ca_env.env_falsy(ca_env.DIARY, default="1"):
308
+ return
309
+ try:
310
+ from codeagent.core.attachments import content_text_for_skills
311
+ from codeagent.memory.diary import append_diary_entry, archive_old_diaries
312
+
313
+ u = content_text_for_skills(user_text or "")
314
+ if not u and (user_text or "").strip():
315
+ u = "[用户附加了图片或文档]"
316
+ r = (reply or "").strip()
317
+ block = f"**用户**: {u[:500]}\n\n**助手**: {r[:800]}"
318
+ if tools_used:
319
+ block += f"\n\n**工具**: {', '.join(tools_used[:16])}"
320
+ append_diary_entry(agent_id, text=block, project_id=project_id or None)
321
+ keep = ca_env.pick_int(7, ca_env.DIARY_KEEP_DAYS)
322
+ archive_old_diaries(agent_id, keep_days=max(1, min(keep, 60)))
323
+ except Exception:
324
+ pass
@@ -0,0 +1,30 @@
1
+ """Session manager with per-session agent binding and locks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class SessionManager:
11
+ """Track active agent per session; optional per-session locks."""
12
+
13
+ active_agent_by_session: dict[str, str] = field(default_factory=dict)
14
+ _lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
15
+ _session_locks: dict[str, threading.Lock] = field(default_factory=dict, repr=False)
16
+
17
+ def set_active_agent(self, session_id: str, agent_id: str) -> None:
18
+ with self._lock:
19
+ self.active_agent_by_session[str(session_id)] = str(agent_id)
20
+
21
+ def get_active_agent(self, session_id: str) -> str | None:
22
+ with self._lock:
23
+ return self.active_agent_by_session.get(str(session_id))
24
+
25
+ def session_lock(self, session_id: str) -> threading.Lock:
26
+ with self._lock:
27
+ sid = str(session_id)
28
+ if sid not in self._session_locks:
29
+ self._session_locks[sid] = threading.Lock()
30
+ return self._session_locks[sid]
@@ -0,0 +1,38 @@
1
+ """Heuristic task splitting for multi-worker orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from codeagent.core import env as ca_env
8
+
9
+
10
+ def auto_split_enabled(explicit: bool | None = None) -> bool:
11
+ if explicit is not None:
12
+ return bool(explicit)
13
+ return ca_env.env_truthy(ca_env.ORCHESTRATOR_AUTO_SPLIT, default="0")
14
+
15
+
16
+ def split_user_tasks(user_text: str) -> list[str]:
17
+ """
18
+ Split a user message into subtasks.
19
+
20
+ Rules (first match wins):
21
+ 1. ``---`` on its own line
22
+ 2. Numbered lines ``1.`` ``2.`` (at least 2 items)
23
+ 3. Otherwise single task
24
+ """
25
+ text = (user_text or "").strip()
26
+ if not text:
27
+ return []
28
+
29
+ if re.search(r"(?m)^---\s*$", text):
30
+ parts = [p.strip() for p in re.split(r"(?m)^---\s*$", text) if p.strip()]
31
+ if len(parts) > 1:
32
+ return parts
33
+
34
+ numbered = re.findall(r"(?m)^\s*\d+[.)]\s+(.+)$", text)
35
+ if len(numbered) >= 2:
36
+ return [p.strip() for p in numbered if p.strip()]
37
+
38
+ return [text]
@@ -0,0 +1,13 @@
1
+ """Turn loop engine (compat wrapper)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from seed.core.turn_loop import ( # noqa: F401
6
+ AutonomousAgent,
7
+ AutonomousTurnLoop,
8
+ TurnLoopConfig,
9
+ TurnLoopEngine,
10
+ estimate_tokens,
11
+ execute_command_as_tool,
12
+ )
13
+
@@ -0,0 +1,48 @@
1
+ """Worker abstraction (v0 skeleton for multi-agent)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class WorkerProtocol(Protocol):
10
+ def run(
11
+ self,
12
+ *,
13
+ session_id: str,
14
+ user_text: str,
15
+ tools: list[dict[str, Any]] | None = None,
16
+ max_tool_rounds: int = 16,
17
+ ) -> tuple[str, dict[str, Any]]:
18
+ """Run a single user turn, returning assistant text + metadata."""
19
+
20
+
21
+ @dataclass
22
+ class Worker:
23
+ """
24
+ Minimal worker wrapper.
25
+
26
+ Phase 1: delegates to the existing single-agent runtime (HTTP API uses seed.server).
27
+ Phase 2+: will host an isolated tool-loop with its own tool registry, memory view, etc.
28
+ """
29
+
30
+ impl: WorkerProtocol | None = None
31
+
32
+ def run(
33
+ self,
34
+ *,
35
+ session_id: str,
36
+ user_text: str,
37
+ tools: list[dict[str, Any]] | None = None,
38
+ max_tool_rounds: int = 16,
39
+ ) -> tuple[str, dict[str, Any]]:
40
+ if self.impl is None:
41
+ raise RuntimeError("Worker.impl is not configured")
42
+ return self.impl.run(
43
+ session_id=session_id,
44
+ user_text=user_text,
45
+ tools=tools,
46
+ max_tool_rounds=max_tool_rounds,
47
+ )
48
+