feat: agent definition v2 (registry, persistence, developerInstructions channel)
Registry: POST /tasks accepts agent_name, loading <name>.json from repo-root agents/ (traversal-guarded, skill-pack re-validated); agent and agent_name together is a 400. Persistence: tasks gains agent_name/agent_version/agent_instructions (migrate + PG schema), returned by GET /tasks; stored prompt stays clean. Channel: local and docker dispatch pass --developer-instructions to host.py dispatch -> create_thread(developerInstructions=...); run/fork do not re-send. Cloud executor has no such channel, so the worker falls back to the v1 marked header in the in-memory dispatch prompt only (DB prompt stays clean). Goal/coordinator tasks still reject instructions. Tests: test_agent_def_v2.py (7 tests) + updated v1 suite.
This commit is contained in:
1 parent
7bb3c37a10
commit
1672ab9ff2
8 files changed
+361
-35
No files matched your search
+74
-17
@@ -57,8 +57,9 @@ from decimal import Decimal
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
from cloud.agent_def import format_instructions_header as _agent_format_header
|
||||
from cloud.agent_def import default_agents_dir as _agent_default_dir
|
||||
from cloud.agent_def import list_known_skills as _agent_known_skills
|
||||
from cloud.agent_def import load_agent_definition as _load_agent_definition
|
||||
from cloud.agent_def import parse_agent_definition as _parse_agent_definition
|
||||
from cloud.auth import ROLES as _MEMBER_ROLES
|
||||
from cloud.auth import AuthStore
|
||||
@@ -1236,6 +1237,12 @@ def migrate_tasks_table(conn):
|
||||
"auto_resume_stall_count": "INTEGER DEFAULT 0",
|
||||
"checkpoint_round_at_last_resume": "INTEGER",
|
||||
"last_error_signature": "TEXT",
|
||||
# agent 定义 v2:定义身份与指令落库(NULL = 未用定义)。
|
||||
# 老库行缺列时按 NULL 计,worker/server 读时用 _col /
|
||||
# _row_get 容错,不炸。
|
||||
"agent_name": "TEXT",
|
||||
"agent_version": "TEXT",
|
||||
"agent_instructions": "TEXT",
|
||||
}
|
||||
for name, coltype in new_cols.items():
|
||||
if name not in cols:
|
||||
@@ -1833,6 +1840,11 @@ def _task_to_dict(row, attachments=None, total_tokens=None):
|
||||
"parent_task_id": _row_get(row, "parent_task_id"),
|
||||
"coordinator_part": _row_get(row, "coordinator_part"),
|
||||
"goal_resume_pending": _row_get(row, "goal_resume_pending") or 0,
|
||||
# agent 定义 v2:身份与指令落库后直接回读(老行缺列
|
||||
# 时为 None;指令是定义文本、非密钥,任务详情里可见)。
|
||||
"agent_name": _row_get(row, "agent_name"),
|
||||
"agent_version": _row_get(row, "agent_version"),
|
||||
"agent_instructions": _row_get(row, "agent_instructions"),
|
||||
"paused_by_user": _row_get(row, "paused_by_user") or 0,
|
||||
"interrupted_reason": _row_get(row, "interrupted_reason"),
|
||||
"auto_resume_count": _row_get(row, "auto_resume_count") or 0,
|
||||
@@ -4301,12 +4313,18 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
if prompt_goal_target:
|
||||
prompt = prompt_goal_target
|
||||
# 声明式 agent 定义 v1(见 cloud/agent_def.py):inline
|
||||
# agent 对象,逐字段填充任务选项;显式 body 字段优先
|
||||
# 于定义,定义本身不新开任务表列,解析出的字段落进
|
||||
# 已有列,instructions 以带标记的头拼进 prompt(见后),
|
||||
# 身份在创建响应里回显。
|
||||
# 声明式 agent 定义 v1/v2(见 cloud/agent_def.py):
|
||||
# inline agent 对象,或 agent_name(注册表 agents/ 下
|
||||
# 的 <name>.json);两者不能同时给,歧义不猜。显式
|
||||
# body 字段逐项优先于定义;解析出的字段落进已有列,
|
||||
# 定义身份与指令落进 agent_name/agent_version/
|
||||
# agent_instructions 列(v2),prompt 不再带头。
|
||||
definition = None
|
||||
if body.get("agent") is not None \
|
||||
and body.get("agent_name") is not None:
|
||||
self._send_json(400, {
|
||||
"error": "agent and agent_name cannot both be used"})
|
||||
return
|
||||
if body.get("agent") is not None:
|
||||
if not isinstance(body.get("agent"), dict):
|
||||
self._send_json(400, {"error": "agent must be an object"})
|
||||
@@ -4318,6 +4336,36 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
except ValueError as exc:
|
||||
self._send_json(400, {"error": str(exc)})
|
||||
return
|
||||
if body.get("agent_name") is not None:
|
||||
agent_name = body.get("agent_name")
|
||||
if not isinstance(agent_name, str) or not agent_name.strip():
|
||||
self._send_json(400, {
|
||||
"error": "agent_name must be a non-empty string"})
|
||||
return
|
||||
try:
|
||||
definition = _load_agent_definition(
|
||||
agent_name.strip() + ".json",
|
||||
agents_dir=_agent_default_dir())
|
||||
except ValueError as exc:
|
||||
self._send_json(400, {"error": str(exc)})
|
||||
return
|
||||
# 注册表定义同样须过技能包校验:load 只校验了
|
||||
# 格式(known_skills=None 时不查存在性),这里
|
||||
# 用同一解析器补存在性校验,校验不过即 400。
|
||||
try:
|
||||
definition = _parse_agent_definition(
|
||||
{
|
||||
"name": definition.name,
|
||||
"version": definition.version,
|
||||
"description": definition.description,
|
||||
"instructions": definition.instructions,
|
||||
"skills": definition.skills,
|
||||
"defaults": definition.defaults,
|
||||
},
|
||||
known_skills=_agent_known_skills())
|
||||
except ValueError as exc:
|
||||
self._send_json(400, {"error": str(exc)})
|
||||
return
|
||||
depends_on = body.get("depends_on") or []
|
||||
if isinstance(depends_on, str):
|
||||
depends_on = [depends_on]
|
||||
@@ -4615,18 +4663,22 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
if not bundle_files:
|
||||
self._send_json(400, {"error": "附件包为空"})
|
||||
return
|
||||
# agent instructions:goal 与协调员任务不接受(见
|
||||
# agent_def.py 的边界说明)。非这两种时以带标记的头
|
||||
# 拼进 prompt,存库的 prompt 即带头的 prompt,worker
|
||||
# 派发、回读看到的都是同一份;子任务派生默认值解析
|
||||
# 在前面已用原始 prompt 跑过,不受头影响。
|
||||
# agent instructions v2:goal 与协调员任务仍不接受
|
||||
# (goal 循环与协调员父任务不传 instructions,见
|
||||
# agent_def.py 的 v2 边界)。非这两种时指令不拼进
|
||||
# prompt,存进 agent_instructions 列,由 worker 按
|
||||
# executor 选通道派发(local/docker 走
|
||||
# --developer-instructions,cloud 走内存里的头回落)。
|
||||
if definition is not None and definition.instructions:
|
||||
if goal_mode or verify_command or coord_parts:
|
||||
self._send_json(400, {
|
||||
"error": "agent instructions cannot be used with"
|
||||
" goal/coordinator tasks"})
|
||||
return
|
||||
prompt = _agent_format_header(definition, prompt)
|
||||
agent_name_val = definition.name if definition else None
|
||||
agent_version_val = definition.version if definition else None
|
||||
agent_instructions_val = (
|
||||
definition.instructions if definition else None)
|
||||
now = time.time()
|
||||
conn = self._db()
|
||||
try:
|
||||
@@ -4651,16 +4703,19 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
" model, effort, approval, plan, goal_mode,"
|
||||
" environment_id, coordinator_parts,"
|
||||
" parent_task_id, coordinator_part,"
|
||||
" child_model, child_effort)"
|
||||
" child_model, child_effort,"
|
||||
" agent_name, agent_version, agent_instructions)"
|
||||
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, idempotency_key,
|
||||
json.dumps(depends_on), priority, user_id, now, now,
|
||||
executor, goal, verify_command, goal_max_rounds,
|
||||
model, effort, approval, plan, goal_mode,
|
||||
environment_id,
|
||||
json.dumps(coord_parts) if coord_parts else None,
|
||||
None, None, child_model, child_effort),
|
||||
None, None, child_model, child_effort,
|
||||
agent_name_val, agent_version_val,
|
||||
agent_instructions_val),
|
||||
)
|
||||
# 长任务不限时:API 字段 no_time_limit=1 时写标记,
|
||||
# worker 认领后按不限时跑(单轮 host 600 秒仍在);
|
||||
@@ -4714,8 +4769,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
len(coord_parts)
|
||||
* COORDINATOR_RESERVE_PER_PART_USD_MICROS)
|
||||
if definition is not None:
|
||||
# 仅新建任务的 201 响应回显定义身份;幂等返回的
|
||||
# 已有任务不补这个字段(那是另一条创建记录)。
|
||||
# 201 响应回显定义身份与技能(v1 兼容);幂等返回
|
||||
# 的已有任务靠 _task_to_dict 里的 agent_name /
|
||||
# agent_version / agent_instructions(v2 落库后
|
||||
# 回读即有),不补 agent 汇总字段。
|
||||
task["agent"] = definition.to_summary()
|
||||
self._send_json(201, task)
|
||||
|
||||
|
||||
Reference in new issue
Block a user