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.
- codeagent/__init__.py +10 -0
- codeagent/__main__.py +13 -0
- codeagent/cli/__init__.py +13 -0
- codeagent/cli/entrypoint.py +161 -0
- codeagent/cli/main.py +979 -0
- codeagent/core/__init__.py +2 -0
- codeagent/core/attachments.py +342 -0
- codeagent/core/audio_models.py +50 -0
- codeagent/core/bootstrap.py +178 -0
- codeagent/core/default_presets.py +159 -0
- codeagent/core/env.py +106 -0
- codeagent/core/folder_picker.py +151 -0
- codeagent/core/image_understanding.py +90 -0
- codeagent/core/models.py +6 -0
- codeagent/core/paths.py +238 -0
- codeagent/core/pricing.py +107 -0
- codeagent/core/process_ports.py +258 -0
- codeagent/core/seed_bridge.py +21 -0
- codeagent/core/settings.py +98 -0
- codeagent/core/speech_synth.py +330 -0
- codeagent/core/team_config.py +247 -0
- codeagent/core/team_manager.py +245 -0
- codeagent/core/token_counter.py +41 -0
- codeagent/core/tts_voices.py +103 -0
- codeagent/core/usage_billing.py +72 -0
- codeagent/core/vision_models.py +93 -0
- codeagent/memory/diary.py +144 -0
- codeagent/persona_defaults/README.md +12 -0
- codeagent/persona_defaults/agent.md +352 -0
- codeagent/persona_defaults/identity.md +29 -0
- codeagent/persona_defaults/memory.md +4 -0
- codeagent/persona_defaults/skills.md +325 -0
- codeagent/persona_defaults/soul.md +38 -0
- codeagent/persona_defaults/tools.md +41 -0
- codeagent/persona_defaults/user.md +18 -0
- codeagent/runtime/__init__.py +11 -0
- codeagent/runtime/agent_context.py +6 -0
- codeagent/runtime/agent_runtime.py +21 -0
- codeagent/runtime/compact_prompt.py +9 -0
- codeagent/runtime/compact_state.py +40 -0
- codeagent/runtime/llm_worker.py +132 -0
- codeagent/runtime/orchestrator.py +161 -0
- codeagent/runtime/prompt_enrichment.py +324 -0
- codeagent/runtime/session_manager.py +30 -0
- codeagent/runtime/task_split.py +38 -0
- codeagent/runtime/turn_loop.py +13 -0
- codeagent/runtime/worker.py +48 -0
- codeagent/server/__init__.py +505 -0
- codeagent/server/app_factory.py +1292 -0
- codeagent/server/attachment_api.py +261 -0
- codeagent/server/hub.py +175 -0
- codeagent/server/icon.png +0 -0
- codeagent/server/self_healing.py +143 -0
- codeagent/server/team_engine.py +243 -0
- codeagent/server/tts_api.py +51 -0
- codeagent/server/webui_api_app.py +3180 -0
- codeagent/skills/__init__.py +2 -0
- codeagent/skills/select.py +144 -0
- codeagent/tools/__init__.py +6 -0
- codeagent/tools/agent_tools.py +10 -0
- codeagent/web/__init__.py +1 -0
- codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
- codeagent/web/auth_impl.py +326 -0
- codeagent/web/static/00-marked.min.js +6 -0
- codeagent/web/static/00-mermaid.min.js +2314 -0
- codeagent/web/static/00-purify.min.js +3 -0
- codeagent/web/static/00-storage.js +83 -0
- codeagent/web/static/00-utils.js +51 -0
- codeagent/web/static/01-theme.css +96 -0
- codeagent/web/static/01a-scroll.js +86 -0
- codeagent/web/static/01b-think.js +62 -0
- codeagent/web/static/01c-session-identity.js +81 -0
- codeagent/web/static/01c-session-thinking.js +61 -0
- codeagent/web/static/01c-session-tree-menus.js +349 -0
- codeagent/web/static/01c-session-tree.js +531 -0
- codeagent/web/static/01c-session-utilities.js +83 -0
- codeagent/web/static/01c-vision-model.js +132 -0
- codeagent/web/static/01d-image-gen-model.js +84 -0
- codeagent/web/static/01e-audio-model.js +97 -0
- codeagent/web/static/01f-compose-ui.js +88 -0
- codeagent/web/static/01g-tts-settings.js +128 -0
- codeagent/web/static/01h-music-model.js +85 -0
- codeagent/web/static/01i-model-stack.js +291 -0
- codeagent/web/static/01j-video-gen-model.js +85 -0
- codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
- codeagent/web/static/01k-model-stack-state.js +209 -0
- codeagent/web/static/01q-inflight.js +195 -0
- codeagent/web/static/02-reset.css +22 -0
- codeagent/web/static/02-session-ui.js +201 -0
- codeagent/web/static/03-layout-p1.css +2 -0
- codeagent/web/static/03-layout-p2.css +2 -0
- codeagent/web/static/03a-links-stream.js +88 -0
- codeagent/web/static/03b-stream-text.js +119 -0
- codeagent/web/static/03c-render-bubble.js +520 -0
- codeagent/web/static/03d-agent-bubble.js +126 -0
- codeagent/web/static/03e-tool-trace.js +182 -0
- codeagent/web/static/03f-timeline.js +54 -0
- codeagent/web/static/03g-bubble-tts.js +292 -0
- codeagent/web/static/04-components-p1.css +1 -0
- codeagent/web/static/04-components-p2b.css +1 -0
- codeagent/web/static/04-components-p3.css +2 -0
- codeagent/web/static/04-components-p4.css +1 -0
- codeagent/web/static/04-components-p5.css +1 -0
- codeagent/web/static/04-components-p6.css +58 -0
- codeagent/web/static/04-components-p7.css +4 -0
- codeagent/web/static/04-components-p8.css +1 -0
- codeagent/web/static/04-components-p9.css +1 -0
- codeagent/web/static/04-ws-connect.js +351 -0
- codeagent/web/static/04-ws-helpers.js +116 -0
- codeagent/web/static/05-chat-p1.css +1058 -0
- codeagent/web/static/05-chat-p2.css +1 -0
- codeagent/web/static/05-chat-p3.css +295 -0
- codeagent/web/static/05a-session-history.js +176 -0
- codeagent/web/static/05b-session-list.js +114 -0
- codeagent/web/static/05c-dialog.js +123 -0
- codeagent/web/static/05d-project-create.js +172 -0
- codeagent/web/static/05e-project-remote.js +143 -0
- codeagent/web/static/06-chat.js +552 -0
- codeagent/web/static/06-utilities.css +181 -0
- codeagent/web/static/06b-attachments.js +304 -0
- codeagent/web/static/06c-camera.js +488 -0
- codeagent/web/static/07-activity-bar.css +127 -0
- codeagent/web/static/09-git-p1.css +1 -0
- codeagent/web/static/09-git-p2.css +1 -0
- codeagent/web/static/09a-init-bind.js +110 -0
- codeagent/web/static/09b-panel-toggle.js +80 -0
- codeagent/web/static/09c-api-refresh.js +100 -0
- codeagent/web/static/09d-status-log.js +80 -0
- codeagent/web/static/09e-commit.js +80 -0
- codeagent/web/static/09f-remote.js +93 -0
- codeagent/web/static/11a-cron-parse.js +117 -0
- codeagent/web/static/11b-cron-panel.js +100 -0
- codeagent/web/static/11c-cron-card.js +133 -0
- codeagent/web/static/11d-cron-form.js +105 -0
- codeagent/web/static/11e-env-config.js +168 -0
- codeagent/web/static/11f-env-llm-a-state.js +136 -0
- codeagent/web/static/11f-env-llm-b-form.js +409 -0
- codeagent/web/static/11f-env-llm-c-board.js +322 -0
- codeagent/web/static/11g-env-paths-git.js +973 -0
- codeagent/web/static/11h-env-mcp-a-state.js +161 -0
- codeagent/web/static/11h-env-mcp-b-form.js +229 -0
- codeagent/web/static/11h-env-mcp-c-board.js +565 -0
- codeagent/web/static/12-plugins.js +127 -0
- codeagent/web/static/13a-skills.js +156 -0
- codeagent/web/static/13a-todo-utils.js +140 -0
- codeagent/web/static/13b-todo-fetch.js +106 -0
- codeagent/web/static/13b-tools.js +127 -0
- codeagent/web/static/13c-todo-crud.js +92 -0
- codeagent/web/static/14-activity-bar.js +177 -0
- codeagent/web/static/15-plans.css +194 -0
- codeagent/web/static/15-plans.js +219 -0
- codeagent/web/static/16-mobile.css +612 -0
- codeagent/web/static/16-mobile.js +178 -0
- codeagent/web/static/17-preset-ux.css +287 -0
- codeagent/web/static/18-agent-mgr.css +616 -0
- codeagent/web/static/18-agent-mgr.js +204 -0
- codeagent/web/static/24-health-dashboard.js +68 -0
- codeagent/web/static/99-brand-polish.css +152 -0
- codeagent/web/static/body.html +824 -0
- codeagent/web/static/setup/setup_form.html +171 -0
- codeagent/web/static/setup/setup_head.html +100 -0
- codeagent/web/static/setup/setup_tail.html +264 -0
- codeagent/web/static/setup/setup_test.html +38 -0
- codeagent/web/web_login.html +265 -0
- codeagent/web/webui.html +35 -0
- tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
- tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
- tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
- tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
- tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|