feat: 子任务默认模型/强度(child_model/child_effort)

主任务可显式声明派生默认,或在 prompt 里写“子任务用 gpt-6-luna max”
由服务端解析填入;marker task-create 与协调员分工未显式写模型/强度时
按 块/分工 > 派生默认 > 父任务模型/强度 继承,fork 复制派生默认,
continue 可改派生默认;console 新建/继续表单与任务详情同步展示。
This commit is contained in:
host committed 2026-10-29 05:27:22 +08:00
1 parent f99e667222
commit 14757f15c4
7 files changed
+588 -25

No files matched your search

+292
View File
@@ -0,0 +1,292 @@
"""子任务默认模型/强度(child_model / child_effort)单测。
不跑真实模型、不联网:解析是纯函数;服务端用本地临时端口
+ 临时 SQLite(与 test_server_endpoints 同款骨架);worker
派生用临时 SQLite + _deliver_marker_tools(与
test_marker_tools 同款骨架,不走 run_one 全量流程)。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.marker_tools import parse_spawn_defaults
from cloud.server import create_server, init_db
from cloud.worker import Worker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.marker_tools import parse_spawn_defaults
from cloud.server import create_server, init_db
from cloud.worker import Worker
FAKE_ENV = {"XCM_API_KEY": "test-placeholder"}
class ParseSpawnDefaultsTestCase(unittest.TestCase):
def test_user_example(self):
self.assertEqual(
parse_spawn_defaults("子代理用 gpt-6-luna max"),
{"model": "gpt-6-luna", "effort": "max"})
def test_labelled_forms(self):
self.assertEqual(
parse_spawn_defaults("子任务模型:gpt-6-luna,强度:max"),
{"model": "gpt-6-luna", "effort": "max"})
self.assertEqual(
parse_spawn_defaults("子任务用 gpt-6.1-sol,effort high"),
{"model": "gpt-6.1-sol", "effort": "high"})
def test_english_and_reversed(self):
self.assertEqual(
parse_spawn_defaults("subtasks use gpt-6-luna with effort max"),
{"model": "gpt-6-luna", "effort": "max"})
self.assertEqual(
parse_spawn_defaults("用 gpt-6-luna max 跑子任务"),
{"model": "gpt-6-luna", "effort": "max"})
def test_no_false_positives(self):
self.assertEqual(
parse_spawn_defaults("子任务要写报告,不要用默认模型"), {})
self.assertEqual(
parse_spawn_defaults("模型选择很重要,子任务用默认配置"), {})
# 裸说主线用 X 是本任务自己的模型,不算派生默认。
self.assertEqual(parse_spawn_defaults("主线用 gpt-6-luna max"), {})
# 跨子句的强度词不算。
self.assertEqual(
parse_spawn_defaults("子任务用 gpt-6-luna。下一句强度是 high。"),
{"model": "gpt-6-luna"})
class ServerChildModelTestCase(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)
hdrs = {}
payload = None
if body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs["Content-Type"] = "application/json"
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
def _create(self, prompt, **extra):
body = {"project_id": self.project["id"], "prompt": prompt}
body.update(extra)
status, task = self._req("POST", "/tasks", body, self.owner_key)
self.assertEqual(status, 201, task)
return task
def _set_terminal(self, task_id):
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET status = 'completed' WHERE id = ?",
(task_id,))
conn.commit()
finally:
conn.close()
def test_create_explicit_child_fields(self):
task = self._create(
"做事", model="gpt-5.6-terra", effort="low",
child_model="gpt-6-luna", child_effort="max")
self.assertEqual(task["model"], "gpt-5.6-terra")
self.assertEqual(task["child_model"], "gpt-6-luna")
self.assertEqual(task["child_effort"], "max")
def test_create_prompt_directive_fills_child_fields(self):
task = self._create("子代理用 gpt-6-luna max,做三件事")
self.assertEqual(task["child_model"], "gpt-6-luna")
self.assertEqual(task["child_effort"], "max")
# prompt 指令只填派生默认,不改本任务自己的模型。
self.assertIsNone(task["model"])
def test_create_explicit_wins_over_prompt(self):
task = self._create(
"子代理用 gpt-6-luna max", child_model="gpt-6-sol",
child_effort="high")
self.assertEqual(task["child_model"], "gpt-6-sol")
self.assertEqual(task["child_effort"], "high")
def test_create_invalid_child_effort_400(self):
status, body = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "做事",
"child_effort": "huge"}, self.owner_key)
self.assertEqual(status, 400)
def test_fork_copies_child_fields(self):
parent = self._create(
"做事", model="gpt-5.6-terra", child_model="gpt-6-luna",
child_effort="max")
self._set_terminal(parent["id"])
status, forked = self._req(
"POST", "/tasks/%s/fork" % parent["id"],
{"message": "继续"}, self.owner_key)
self.assertEqual(status, 201, forked)
self.assertEqual(forked["model"], "gpt-5.6-terra")
self.assertEqual(forked["child_model"], "gpt-6-luna")
self.assertEqual(forked["child_effort"], "max")
def test_continue_updates_child_fields(self):
task = self._create("做事")
self._set_terminal(task["id"])
status, continued = self._req(
"POST", "/tasks/%s/continue" % task["id"],
{"message": "再跑一轮", "child_model": "gpt-6-luna",
"child_effort": "max"}, self.owner_key)
self.assertEqual(status, 200, continued)
self.assertEqual(continued["child_model"], "gpt-6-luna")
self.assertEqual(continued["child_effort"], "max")
class WorkerChildModelTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.auth = AuthStore(self.db_path)
self.user_id, _key = self.auth.create_user("tester")
self._insert("PM", status="running", model="gpt-5.6-terra",
effort="low", child_model="gpt-6-luna",
child_effort="max")
def tearDown(self):
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _insert(self, task_id, status="queued", **cols):
now = time.time()
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat)"
" VALUES(?, 'p1', 'do work', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL)",
(task_id, status, now, now))
for key, value in cols.items():
conn.execute(
"UPDATE tasks SET %s = ? WHERE id = ?" % key,
(value, task_id))
conn.commit()
finally:
conn.close()
def _child_row(self):
conn = sqlite3.connect(self.db_path)
try:
return conn.execute(
"SELECT model, effort, child_model, child_effort"
" FROM tasks WHERE parent_task_id = 'PM'").fetchone()
finally:
conn.close()
def _deliver(self, creates):
worker = Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
worker._deliver_marker_tools(worker.get_task("PM"), {
"status": "completed", "task_creates": creates,
"task_polls": [], "wait_threads": [], "task_cancels": [],
"task_retries": []})
def test_marker_inherits_child_defaults_when_block_blank(self):
self._deliver([{"name": "E1", "prompt": "do e1",
"model": None, "effort": None, "executor": None}])
row = self._child_row()
self.assertIsNotNone(row)
self.assertEqual(row[0], "gpt-6-luna")
self.assertEqual(row[1], "max")
# 派生默认本身也传给子任务,隔代派生不丢。
self.assertEqual(row[2], "gpt-6-luna")
self.assertEqual(row[3], "max")
def test_marker_explicit_block_wins(self):
self._deliver([{"name": "E1", "prompt": "do e1",
"model": "gpt-6-sol", "effort": "high",
"executor": None}])
row = self._child_row()
self.assertEqual(row[0], "gpt-6-sol")
self.assertEqual(row[1], "high")
def test_marker_falls_back_to_parent_model_without_child_defaults(self):
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET child_model = NULL,"
" child_effort = NULL WHERE id = 'PM'")
conn.commit()
finally:
conn.close()
self._deliver([{"name": "E1", "prompt": "do e1",
"model": None, "effort": None, "executor": None}])
row = self._child_row()
self.assertEqual(row[0], "gpt-5.6-terra")
self.assertEqual(row[1], "low")
def test_coordinator_inherits_child_defaults(self):
worker = Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
parent = worker.get_task("PM")
children = worker._coord_sync_children(parent, [
{"name": "后端", "prompt": "做后端", "model": None,
"effort": None, "executor": None}])
self.assertEqual(len(children), 1)
conn = sqlite3.connect(self.db_path)
try:
row = conn.execute(
"SELECT model, effort FROM tasks WHERE id = ?",
(children[0]["id"],)).fetchone()
finally:
conn.close()
self.assertEqual(row[0], "gpt-6-luna")
self.assertEqual(row[1], "max")
if __name__ == "__main__":
unittest.main()