mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
Team wake plumbing: trait-gated verbs, durable queues, staffing gate, digests (OPE-97)
team: manifest trait gates lead/worker toolsets; propose_team pre-spawns worker sessions on approval (fail closed on solo personas). Deliveries + lead subscriptions are cursor-consumed projections; turns end with a queue kick, ticks replay; timer wakes carry the code-computed staleness digest; hourly wake cap is the budget gate.
This commit is contained in:
@@ -1835,6 +1835,43 @@ def create_app(manager: SessionManager) -> FastAPI:
|
||||
}
|
||||
return {"approved": True, "mode": resp.get("mode") or "interactive"}
|
||||
|
||||
async def team_approver(_args: dict, tool_call_id=None) -> dict:
|
||||
# The staffing gate. The engine already emitted TEAM_PROPOSED; park an
|
||||
# Inbox item as the durable resolution vehicle, wait for the verdict, and
|
||||
# on approval PRE-SPAWN the team (create_team fails closed on non-worker
|
||||
# personas, so a bad roster reads as a rejection with the reason).
|
||||
members = _args.get("members") or []
|
||||
roster = "\n".join(
|
||||
f"- {m.get('persona', '?')}"
|
||||
+ (f" · {m['model']}" if m.get("model") else "")
|
||||
+ (f" — {m['reason']}" if m.get("reason") else "")
|
||||
for m in members
|
||||
if isinstance(m, dict)
|
||||
)
|
||||
item = manager.inbox.add_plan(
|
||||
session_id,
|
||||
"Create this team?",
|
||||
body=roster,
|
||||
inbox=_route(),
|
||||
visibility=_visibility(),
|
||||
tool_call_id=tool_call_id,
|
||||
)
|
||||
if item.state == "pending":
|
||||
manager.persist_session(session_id)
|
||||
if item.visibility == VIS_INBOX:
|
||||
await _mirror(item)
|
||||
resp = _parse_json(await manager.inbox.wait(item.id))
|
||||
if not resp.get("approved"):
|
||||
return {
|
||||
"approved": False,
|
||||
"feedback": resp.get("feedback") or "the user declined this roster",
|
||||
}
|
||||
return manager.create_team(
|
||||
session_id,
|
||||
[m for m in members if isinstance(m, dict)],
|
||||
enable_chat=bool(_args.get("enable_chat", False)),
|
||||
)
|
||||
|
||||
async def _apply_model(model: Optional[str]) -> None:
|
||||
# Mid-session rebind is allowed (roadmap item 3, supersedes the 2026-07-04
|
||||
# lock): history is canonical and providers convert per call. A real switch
|
||||
@@ -1874,6 +1911,7 @@ def create_app(manager: SessionManager) -> FastAPI:
|
||||
plan_approver=plan_approver,
|
||||
question_asker=question_asker,
|
||||
tool_requester=tool_requester,
|
||||
team_approver=team_approver,
|
||||
)
|
||||
if engine is None:
|
||||
await ws.send_json(
|
||||
@@ -2024,6 +2062,15 @@ def create_app(manager: SessionManager) -> FastAPI:
|
||||
}
|
||||
)
|
||||
)
|
||||
elif kind == "team_response":
|
||||
_resolve_pending(
|
||||
json.dumps(
|
||||
{
|
||||
"approved": bool(message.get("approved")),
|
||||
"feedback": message.get("feedback", ""),
|
||||
}
|
||||
)
|
||||
)
|
||||
elif kind == "question_response":
|
||||
_resolve_pending(str(message.get("answer", "")))
|
||||
elif kind == "interrupt":
|
||||
|
||||
Reference in New Issue
Block a user