feat(tasks): 适配 DSH 0.1.7 的 Agent Teams(读 projection + 写路由对接真实服务)
团队功能在 0.1.7 上整条失效:team-routes.ts 当初镜像的是 0.1.6 的 Remote 词汇
(remoteView / remoteCreateTask / remoteUpdateTask),而 0.1.7 把这三个方法删掉了,
读路径改到 Lead Session 的 `agentTeam` Session projection。真机 3080 复现:
teams.view 对带团队的会话返回 400 "membership.svc.remoteView is not a function",
于是 TeamBoard 从不渲染、节点富化与任务窗连带失效。
- 读:删掉 teams.view 与 5s 轮询,改读 SessionListState.projectionsBySession[root]
.values.agentTeam(与 subagentCatalog 同源,推送式);新增 client/team-projection.ts
做纯适配器,成员运行状态由 subagents.live 的 running 叠加、Lead 回退 session list
(官方 README 的 "overlays activity from Session status")。projection 缺席 =
这里没有团队,与旧的 {available:false}/{team:null} 合并成同一静默降级。
- 写:taskCreate/taskUpdate 改调 0.1.7 的 createTask/updateTask(返回已提交的 view,
拒绝改为抛 TeamError),TEAM_TASK_STALE_REVISION → 409 team-conflict,其余 → 400
team-error(wire 新增 team-conflict)。api.ts 的 writeTask 把失败信封还原成
{ok:false,code,message} —— 原先窗口永远走不到该分支(readEnvelope 是抛)。
- 任务窗:外壳换成上一轮的 FloatingWindow(可拖拽/可拉伸/只在关闭或 Esc 时消失),
动作行下沉到 shell 的 footer,长描述不再把动作推出视野。
- 真机抓到的两个单测抓不到的问题:
1. 队列中任务的「完成」被宿主状态机拒绝(pending → claim → in_progress →
complete → completed → reopen)→ 动作行跟随状态,pending 出「认领」;
2. 认领后 Tag 变「阻塞」—— ready 的语义只是「pending 且无未完成 blocker」,
对所有非 pending 任务都是 false → 抽出唯一规则 taskBlocked/taskStatusLabel/
taskTone/taskDotState,四处展示点全部改走它。
- i18n:新增 teamTaskClaim / taskWindowCreate / taskWindowDetail /
tasksStateProvisioning 词条 ×20 词典。
验证:typecheck / lint 通过;pnpm test 1276 passed / 9 skipped;
pnpm test:mount 7/7(真实 DSH 0.1.7-rc.2 挂载 + tab 全扫无崩溃)。
真机 3080(0.1.7-rc.1 + Agent Teams 实验层):看板从 projection 渲染、
队列任务「认领」→「完成」全链路写通(负责人 lead / 进行中 / 已完成)、
外部点击不关窗、Escape 关窗,截图见 docs/plans/2026-09-27-agent-teams-dsh-0.1.7-adaptation.md。
(cherry picked from commit dbc524e0b350c5d3564843f4776cd15d797506d8)
This commit is contained in:
+38
-19
@@ -12,8 +12,6 @@ import type {
|
||||
SidebarCreateTeamTaskRequest,
|
||||
SidebarChildLiveView,
|
||||
SidebarSessionEvent,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamTaskMutationResult,
|
||||
SidebarTeamTaskView,
|
||||
SidebarUpdateTeamTaskRequest,
|
||||
} from '../context-types.ts'
|
||||
@@ -109,19 +107,24 @@ export type SubagentLiveResult = { live: Record<string, SidebarChildLiveView> }
|
||||
/** The `workflows.list` response: the tree's folded workflow runs. */
|
||||
export type WorkflowsListResult = { runs: WorkflowRunView[] }
|
||||
|
||||
/** The `teams.view` response (structural degradation contract). */
|
||||
export type TeamsViewResult =
|
||||
| { available: false }
|
||||
| { available: true; team: { members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] } | null }
|
||||
|
||||
/** The `teams.taskCreate` request payload (minus the rootSessionId). */
|
||||
export type TeamsTaskCreateRequest = SidebarCreateTeamTaskRequest
|
||||
|
||||
/** The `teams.taskUpdate` request payload (minus the rootSessionId). */
|
||||
export type TeamsTaskUpdateRequest = SidebarUpdateTeamTaskRequest
|
||||
|
||||
/** The task-mutation result union (CAS conflict stays distinct). */
|
||||
export type TeamsTaskMutationResult = SidebarTeamTaskMutationResult
|
||||
/**
|
||||
* One team-task write as CALLERS consume it. The board's READ path has no
|
||||
* route at all (it rides the Lead Session's `agentTeam` projection, see
|
||||
* team-projection.ts), and a rejected write is a RESULT rather than an
|
||||
* exception: the route answers 409 `team-conflict` for a stale revision and
|
||||
* 400 `team-error` otherwise, {@link writeTask} turns that failed envelope
|
||||
* back into `{ok:false, code, message}`, and the task window routes on the
|
||||
* code (a stale revision deserves its own wording, not "operation failed").
|
||||
*/
|
||||
export type TeamsTaskMutationResult =
|
||||
| { ok: true; value: SidebarTeamTaskView }
|
||||
| { ok: false; code?: string; message: string }
|
||||
|
||||
/**
|
||||
* Parse one `/sidebar` JSON response envelope into its value. A non-ok
|
||||
@@ -157,6 +160,29 @@ async function call<T>(method: string, payload: Record<string, unknown>, signal?
|
||||
return readEnvelope<T>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* One team-task write with the rejection turned back into a RESULT. Every
|
||||
* other `/sidebar/api` caller lets a failed envelope throw (that is what
|
||||
* {@link readEnvelope} is for); here the route's error CODE is data the task
|
||||
* window needs — `team-conflict` means "someone changed this task, refresh",
|
||||
* which is a different sentence from every other failure.
|
||||
* @param method - `teams.taskCreate` or `teams.taskUpdate`.
|
||||
* @param payload - the request plus its `rootSessionId`.
|
||||
*/
|
||||
async function writeTask(
|
||||
method: string,
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<TeamsTaskMutationResult> {
|
||||
try {
|
||||
return { ok: true, value: await call<SidebarTeamTaskView>(method, payload) }
|
||||
} catch (error) {
|
||||
if (error instanceof SidebarApiError) {
|
||||
return { ok: false, code: error.code, message: error.message }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload one file to the sidebar's raw upload route: the File goes straight
|
||||
* into the POST body (no JSON/base64 re-encoding — the host streams it into
|
||||
@@ -348,19 +374,12 @@ export const api = {
|
||||
*/
|
||||
workflowsList: (rootSessionId: string, signal?: AbortSignal) =>
|
||||
call<WorkflowsListResult>('workflows.list', { rootSessionId }, signal),
|
||||
/**
|
||||
* The team led by the tree's root agent (experimental Agent Teams layer):
|
||||
* `{available:false}` hides the whole block; `{team:null}` means the root
|
||||
* leads no team. Poll-driven (the official panel is manual-refresh too).
|
||||
*/
|
||||
teamsView: (rootSessionId: string, signal?: AbortSignal) =>
|
||||
call<TeamsViewResult>('teams.view', { rootSessionId }, signal),
|
||||
/** Create one shared task on the root-led team. */
|
||||
teamsTaskCreate: (rootSessionId: string, req: TeamsTaskCreateRequest) =>
|
||||
call<TeamsTaskMutationResult>('teams.taskCreate', { rootSessionId, ...req }),
|
||||
/** CAS-mutate one shared task; conflicts ride the returned union. */
|
||||
writeTask('teams.taskCreate', { rootSessionId, ...req }),
|
||||
/** CAS-mutate one shared task; a stale revision yields `team-conflict`. */
|
||||
teamsTaskUpdate: (rootSessionId: string, req: TeamsTaskUpdateRequest) =>
|
||||
call<TeamsTaskMutationResult>('teams.taskUpdate', { rootSessionId, ...req }),
|
||||
writeTask('teams.taskUpdate', { rootSessionId, ...req }),
|
||||
/** Create a Side Chat thread: a child session seeded with the parent's
|
||||
* full log up to now. Empty question = immediate create (Codex-style):
|
||||
* the thread opens empty, the first prompt carries the boundary. */
|
||||
|
||||
Reference in New Issue
Block a user