Swarm I/O:接收用户 prompt(追加输入)+ 返回结果(/result + swarm.completed 带答案)(Refs #40)
补齐「客户端如何把 prompt 给我们 + 如何拿到结果」的端到端路径。
输入:
- POST /api/swarms/{id}/input(+别名):接收用户后续 prompt,注入 source=user_append 任务进共享池
(终态 run 自动 reopen 为 running;stopped 拒绝 RUN_STOPPED)。指令原文作任务描述下发给 agent,
**不回显进事件流**——仅产一条 task.created(user_append) 类别 message。(初始 prompt 仍走 create
的 requirement.objective)
输出:
- GET /api/swarms/{id}/result(+别名):返回 {summary, deliverable, artifacts[], termination_reason,
status}(build_run_result)。产物内容在 artifact.uri(git/runtime),result 给摘要+定位。
- swarm.completed 事件 payload 增带 summary + deliverable,客户端看一条终态事件即得答案。
文档:runtime-contract §3 增 input/result 行;event-schema swarm.completed 标注带 summary/deliverable;
CLIENT_GUIDE §3.5/§3.6(input/result)+ §3.2 表 + 修正 §5「Agent 平台拉起」为「Swarm 拉起 + 从
secret_ref 解析 key」(对齐 team 决议)+ 新增 §9 完整工作流地图(client→HM→Swarm→output→client)。
测试 scripts/test-swarm-io.py(TestClient:input 注入 + 原文不入事件流 + 终态 reopen + stopped 拒绝 +
result 形状)接入 CI。e2e/contract 回归通过。
影响范围:仅 agent_swarm(新增 2 只读/写端点 + 终态事件增字段 + 文档 + 测试 + CI)。
追加输入原文不入事件流/回调/日志;不改鉴权/计费账本/审批链。SSE 实时流仍归 HM Phase2(#46)。
Refs #40
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5138d9a370
commit
dd96b73b2d
@@ -0,0 +1,98 @@
|
||||
"""Swarm I/O tests (agent_swarm#40 + result delivery): receive user prompts + return result.
|
||||
|
||||
Exercises the real HTTP endpoints via FastAPI TestClient (hermetic: REDIS_FAKE, planner offline,
|
||||
no connected agents, launch backend = none):
|
||||
* POST /api/swarms/{id}/input — injects a `source="user_append"` task; the raw instruction is
|
||||
delivered to the agent (task description) but **redacted from the event stream** (task.created
|
||||
carries only a category message); rejects a stopped run.
|
||||
* GET /api/swarms/{id}/result — returns {summary, deliverable, artifacts, termination_reason}.
|
||||
|
||||
Run from agent_swarm_v6 (install deps first):
|
||||
pip install -r orchestrator/requirements.txt
|
||||
REDIS_FAKE=1 python scripts/test-swarm-io.py
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["REDIS_FAKE"] = "1"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from orchestrator import main as orch
|
||||
|
||||
orch.planner.client = None # force planner offline (no model calls)
|
||||
logging.getLogger("orchestrator.swarm_runtime").setLevel(logging.ERROR)
|
||||
|
||||
failures = []
|
||||
SECRET_PROMPT = "SUPER-SECRET-USER-FOLLOWUP-PROMPT-9173-do-the-thing"
|
||||
|
||||
|
||||
def check(name, cond):
|
||||
print(("PASS" if cond else "FAIL"), "-", name)
|
||||
if not cond:
|
||||
failures.append(name)
|
||||
|
||||
|
||||
def create_run(client):
|
||||
body = {"mode": "swarm", "requirement": {"objective": "build add(a,b)"},
|
||||
"callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []},
|
||||
"metadata": {"manager_deployment_id": "io-1"}}
|
||||
r = client.post("/api/swarms", json=body)
|
||||
return r.json()["data"]["deployment_id"]
|
||||
|
||||
|
||||
def main():
|
||||
with TestClient(orch.app) as client:
|
||||
dep = create_run(client)
|
||||
check("run created", bool(dep))
|
||||
|
||||
# --- receive user prompt (append) ---
|
||||
ri = client.post(f"/api/swarms/{dep}/input", json={"instruction": SECRET_PROMPT})
|
||||
check("input accepted (200/success)", ri.status_code == 200 and ri.json().get("success"))
|
||||
idata = ri.json().get("data", {})
|
||||
check("input created a user_append task", bool(idata.get("task_id")))
|
||||
|
||||
# the appended task carries the instruction (agent-facing) ...
|
||||
tasks = client.get(f"/api/swarms/{dep}/tasks").json()["data"]["tasks"]
|
||||
appended = [t for t in tasks if t.get("source") == "user_append"]
|
||||
check("appended task present with source=user_append", len(appended) == 1)
|
||||
check("appended task description = the instruction (agent-facing)",
|
||||
appended and appended[0].get("description") == SECRET_PROMPT)
|
||||
|
||||
# ... but the instruction is NOT echoed into the event stream (redaction).
|
||||
events = client.get(f"/api/swarms/{dep}/logs").json()["data"]["events"]
|
||||
blob = json.dumps(events, ensure_ascii=False)
|
||||
check("instruction NOT in any event payload (redacted)", SECRET_PROMPT not in blob)
|
||||
tc = [e for e in events if e["event_type"] == "task.created"
|
||||
and (e.get("payload") or {}).get("source") == "user_append"]
|
||||
check("a redacted task.created(user_append) event was emitted", len(tc) == 1)
|
||||
check("that event carries a category message, not the prompt",
|
||||
tc and "message" in tc[0]["payload"] and SECRET_PROMPT not in json.dumps(tc[0]["payload"], ensure_ascii=False))
|
||||
|
||||
# --- result endpoint ---
|
||||
res = client.get(f"/api/swarms/{dep}/result")
|
||||
check("result endpoint 200/success", res.status_code == 200 and res.json().get("success"))
|
||||
rdata = res.json().get("data", {})
|
||||
check("result has summary/deliverable/artifacts keys",
|
||||
{"summary", "deliverable", "artifacts", "status"}.issubset(rdata.keys()))
|
||||
check("result deliverable is a dict", isinstance(rdata.get("deliverable"), dict))
|
||||
|
||||
# --- stopped run rejects append ---
|
||||
client.post(f"/api/swarms/{dep}/stop", json={"reason": "test stop"})
|
||||
rstop = client.post(f"/api/swarms/{dep}/input", json={"instruction": "more"})
|
||||
body = rstop.json()
|
||||
check("append to stopped run rejected (RUN_STOPPED)",
|
||||
body.get("success") is False and (body.get("error") or {}).get("code") == "RUN_STOPPED")
|
||||
|
||||
print()
|
||||
if failures:
|
||||
print(f"{len(failures)} swarm-io check(s) FAILED: {failures}")
|
||||
sys.exit(1)
|
||||
print("all swarm-io checks passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user