feat: declarative agent definition v1 (inline agent on task creation)

Agent definition is a versioned JSON document bundling the task
fields this codebase already honours (model/effort/approval/
executor/plan), plus instructions and required skills. At task
creation, explicit body fields win per field over the definition;
unresolved fields stay NULL and follow the worker-default chain
(and rule routing v0 when model is left empty).

Instructions reach the model only as a marked header prepended
to the stored prompt, because the dispatch path has no
developerInstructions channel; goal and coordinator tasks reject
instructions. Skills are validated against the cloud/skills pack
at parse time; seeding itself stays worker-global, documented
as the v1 boundary. Routing policy is not part of a definition
in v1 (worker-global), and unknown fields are rejected so typos
cannot be silently dropped. No new tasks-table column in v1;
definition identity is echoed in the creation response and via
the prompt header.

Tests: test_agent_def.py (11 tests).
This commit is contained in:
host committed 2026-11-04 18:01:44 +00:00
1 parent e364970b46
commit 7bb3c37a10
4 files changed
+552 -1

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
"""声明式 agent 定义 v1 单测:解析、校验、解析到任务字段、建任务入口。
运行(在仓库根目录下):
python3 -m unittest discover -s cloud/tests -p "test_agent_def.py" -v
说明:服务端部分用 create_server 本地 HTTP(test_task_options
同款),不跑真实模型、不联网、不登录任何服务;skill 名单
以仓库 cloud/skills/ 实际包为准。
"""
import http.client
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.agent_def import (
format_instructions_header,
list_known_skills,
load_agent_definition,
parse_agent_definition,
resolve_task_fields,
)
from cloud.auth import AuthStore
from cloud.server import create_server
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.agent_def import (
format_instructions_header,
list_known_skills,
load_agent_definition,
parse_agent_definition,
resolve_task_fields,
)
from cloud.auth import AuthStore
from cloud.server import create_server
AGENT = {
"name": "research-brief",
"version": "v1",
"description": "Evidence-backed brief.",
"instructions": "Cite sources and dates.",
"skills": ["live-info"],
"defaults": {
"model": "gpt-6.1-sol",
"effort": "medium",
"approval": "ask",
"executor": "docker",
},
}
def _agent(**overrides):
agent = json.loads(json.dumps(AGENT))
agent.update(overrides)
return agent
class ParseTestCase(unittest.TestCase):
def test_valid_and_known_skills(self):
definition = parse_agent_definition(
_agent(), known_skills=list_known_skills())
self.assertEqual(definition.name, "research-brief")
self.assertEqual(definition.defaults["effort"], "medium")
self.assertEqual(definition.to_summary()["skills"], ["live-info"])
def test_name_and_unknown_fields_rejected(self):
for bad in (
_agent(name="Bad Name"),
_agent(name=""),
_agent(routing={"mode": "active"}),
_agent(defaults={"efforts": "high"}),
_agent(defaults={"effort": "ultra"}),
_agent(defaults={"plan": True, "approval": "full"}),
_agent(skills=["no-such-skill"]),
_agent(instructions="x" * 8001),
):
with self.assertRaises(ValueError, msg=bad):
parse_agent_definition(
bad, known_skills=list_known_skills())
def test_unknown_skill_skipped_when_no_known_list(self):
# known_skills=None 只跳过存在性检查,绝不发明技能。
definition = parse_agent_definition(_agent(skills=["x"]))
self.assertEqual(definition.skills, ["x"])
def test_resolve_explicit_body_wins_per_field(self):
definition = parse_agent_definition(_agent())
resolved = resolve_task_fields(
definition, {"effort": "low", "plan": True})
self.assertEqual(resolved["model"], "gpt-6.1-sol")
self.assertEqual(resolved["effort"], "low")
self.assertEqual(resolved["approval"], "ask")
self.assertEqual(resolved["executor"], "docker")
self.assertEqual(resolved["plan"], 1)
# 不改输入。
self.assertEqual(definition.defaults["effort"], "medium")
def test_header_and_no_header(self):
definition = parse_agent_definition(_agent())
headed = format_instructions_header(definition, "do research")
self.assertTrue(headed.startswith(
"[Agent: research-brief v1]\nCite sources and dates.\n\n"))
self.assertTrue(headed.endswith("do research"))
self.assertEqual(
format_instructions_header(None, "do research"), "do research")
def test_load_example_and_traversal_guard(self):
repo_root = Path(__file__).resolve().parents[2]
definition = load_agent_definition(
"research-brief.json", agents_dir=repo_root / "agents")
self.assertEqual(definition.name, "research-brief")
with self.assertRaises(ValueError):
load_agent_definition(
"../cloud/agent_def.py", agents_dir=repo_root / "agents")
class ServerAgentTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=10)
headers = {"Content-Type": "application/json"}
if key:
headers["X-API-Key"] = key
conn.request(method, path,
json.dumps(body) if body is not None else None, headers)
resp = conn.getresponse()
raw = resp.read()
conn.close()
try:
data = json.loads(raw)
except ValueError:
data = None
return resp.status, data
def _list_tasks(self):
status, data = self._req(
"GET", "/tasks?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(status, 200)
return data["tasks"]
def test_agent_fills_fields_and_header(self):
status, task = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"agent": _agent()}, self.owner_key)
self.assertEqual(status, 201, task)
self.assertEqual(task["model"], "gpt-6.1-sol")
self.assertEqual(task["effort"], "medium")
self.assertEqual(task["approval"], "ask")
self.assertEqual(task["executor"], "docker")
self.assertEqual(task["agent"]["name"], "research-brief")
self.assertEqual(task["agent"]["skills"], ["live-info"])
stored = self._list_tasks()[0]
self.assertTrue(stored["prompt"].startswith(
"[Agent: research-brief v1]\nCite sources and dates.\n\n"))
self.assertTrue(stored["prompt"].endswith("do research"))
def test_explicit_body_wins_over_agent(self):
status, task = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"effort": "low", "agent": _agent()}, self.owner_key)
self.assertEqual(status, 201, task)
self.assertEqual(task["effort"], "low")
self.assertEqual(task["model"], "gpt-6.1-sol")
def test_invalid_agent_rejected_and_no_task_created(self):
for bad_agent in (
_agent(skills=["no-such-skill"]),
_agent(defaults={"effort": "ultra"}),
"not-an-object",
):
status, data = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"agent": bad_agent}, self.owner_key)
self.assertEqual(status, 400, bad_agent)
self.assertEqual(self._list_tasks(), [])
def test_agent_instructions_rejected_with_goal(self):
status, data = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"goal_mode": 1, "agent": _agent()}, self.owner_key)
self.assertEqual(status, 400, data)
self.assertEqual(self._list_tasks(), [])
def test_no_agent_unchanged(self):
status, task = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "hi"},
self.owner_key)
self.assertEqual(status, 201, task)
self.assertIsNone(task["model"])
self.assertEqual(task["prompt"], "hi")
self.assertNotIn("agent", task)