|
|
@@ -39,6 +39,8 @@ function requestId(value: string): string {
|
|
|
}
|
|
|
|
|
|
export class WorkspaceTaskCoordinator {
|
|
|
+ private cleaning=new Set<string>()
|
|
|
+ private cleanupAfter=new Map<string,number>()
|
|
|
private pending = false
|
|
|
private closed = false
|
|
|
private unobserve: () => void
|
|
|
@@ -106,7 +108,7 @@ export class WorkspaceTaskCoordinator {
|
|
|
const team = this.store.require<Conversation>(owner, 'conversation', goal.conversationId)
|
|
|
const agent = this.store.require<Agent>(owner, 'agent', body.agentId)
|
|
|
this.runtime.nodes.requireSource(owner, agent)
|
|
|
- if (!team.memberIds.includes(agent.id) || agent.archived || agent.id === actorId)
|
|
|
+ if (!this.store.taskMemberIds(owner,team,goal.id).includes(agent.id) || agent.archived || agent.id === actorId)
|
|
|
throw new HttpError(400, '请选择其他有效团队成员;管理员可以直接处理自己的工作。', 'invalid_assignment_agent')
|
|
|
const previous = this.assignments(owner, goal.id)
|
|
|
if (body.dependsOn.some(id => !previous.some(a => a.id === id))) throw new HttpError(400, '依赖必须属于同一目标中的已有子任务', 'invalid_assignment_dependency')
|
|
|
@@ -115,7 +117,7 @@ export class WorkspaceTaskCoordinator {
|
|
|
const assignment: AgentAssignment = { ...body, id: randomUUID(), conversationId: goal.conversationId,
|
|
|
status: 'pending', attempt: 0, artifactIds: [], createdAt: Date.now(), updatedAt: Date.now() }
|
|
|
this.saveAssignment(owner, assignment)
|
|
|
- goal.status = 'running';goal.revision++;this.saveGoal(owner, goal)
|
|
|
+ if(goal.status!=='waiting')goal.status = 'running';goal.revision++;this.saveGoal(owner, goal)
|
|
|
return assignment
|
|
|
})
|
|
|
}
|
|
|
@@ -134,7 +136,7 @@ export class WorkspaceTaskCoordinator {
|
|
|
assignment.status = body.decision === 'accept' ? 'complete' : body.decision === 'retry' ? 'pending' : 'blocked'
|
|
|
if (body.decision === 'retry') { assignment.runId = undefined;assignment.result = undefined }
|
|
|
this.saveAssignment(owner, assignment)
|
|
|
- goal.status = 'running';goal.revision++;this.saveGoal(owner, goal)
|
|
|
+ if(goal.status!=='waiting')goal.status = 'running';goal.revision++;this.saveGoal(owner, goal)
|
|
|
return assignment
|
|
|
})
|
|
|
}
|
|
|
@@ -157,7 +159,7 @@ export class WorkspaceTaskCoordinator {
|
|
|
const target=this.store.require<Agent>(owner,'agent',patch.agentId)
|
|
|
const team=this.store.require<Conversation>(owner,'conversation',goal.conversationId)
|
|
|
this.runtime.nodes.requireSource(owner,target)
|
|
|
- if(target.archived||target.id===actorId||!team.memberIds.includes(target.id))throw new HttpError(400,'请选择有效团队成员','invalid_assignment_agent')
|
|
|
+ if(target.archived||target.id===actorId||!this.store.taskMemberIds(owner,team,goal.id).includes(target.id))throw new HttpError(400,'请选择有效团队成员','invalid_assignment_agent')
|
|
|
assignment.status='pending';assignment.runId=undefined;assignment.result=undefined
|
|
|
}
|
|
|
Object.assign(assignment,patch);this.saveAssignment(owner,assignment)
|
|
|
@@ -253,11 +255,32 @@ export class WorkspaceTaskCoordinator {
|
|
|
queueMicrotask(() => {
|
|
|
this.pending = false
|
|
|
if (this.closed) return
|
|
|
- for (const owner of this.store.owners()) for (const goal of this.store.list<AgentGoal>(owner, 'goal')) {
|
|
|
- try { this.reconcile(owner, goal) } catch { /* The next committed event retries durable state. */ }
|
|
|
+ for (const owner of this.store.owners()){
|
|
|
+ for (const goal of this.store.list<AgentGoal>(owner, 'goal')) {
|
|
|
+ try { this.reconcile(owner, goal);this.retireHelpers(owner,goal) } catch { /* The next committed event retries durable state. */ }
|
|
|
+ }
|
|
|
+ for(const goalId of new Set(this.store.list<Agent>(owner,'agent').flatMap(agent=>agent.temporaryGoalId?[agent.temporaryGoalId]:[])))if(!this.store.get(owner,'goal',goalId)||!this.store.get(owner,'conversation-task',goalId))this.retireHelpers(owner,{id:goalId,status:'cancelled',activation:0})
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
+ private retireHelpers(owner:string,goal:Pick<AgentGoal,'id'|'status'|'activation'>):void {
|
|
|
+ for(const helper of this.store.list<Agent>(owner,'agent').filter(agent=>agent.temporaryGoalId===goal.id)){
|
|
|
+ if(!helper.archived&&!terminalGoal(goal.status)&&helper.helperActivation===(goal.activation??1))continue
|
|
|
+ if(!helper.archived){helper.archived=true;helper.revision++;helper.updatedAt=Date.now();helper.retiredAt=Date.now();helper.cleanupState='pending';this.store.put(owner,'agent',helper.id,helper);this.store.event(owner,'agent.changed',helper)}
|
|
|
+ if(!helper.cleanupState){helper.cleanupState='pending';helper.retiredAt??=Date.now();this.store.put(owner,'agent',helper.id,helper)}
|
|
|
+ const works=this.store.list<{agentId:string;runId:string;status:string}>(owner,'turn').filter(work=>work.agentId===helper.id&&!terminalRun(work.status))
|
|
|
+ if(works.length){for(const runId of new Set(works.map(work=>work.runId)))void this.runtime.stop(owner,runId).catch(()=>{});continue}
|
|
|
+ if(helper.cleanupState==='complete')continue
|
|
|
+ const key=`${owner}:${helper.id}`
|
|
|
+ if(this.cleaning.has(key)||(this.cleanupAfter.get(key)??0)>Date.now())continue
|
|
|
+ this.cleaning.add(key)
|
|
|
+ void this.runtime.retireHelper(owner,helper).then(()=>{
|
|
|
+ if(this.closed)return
|
|
|
+ const current=this.store.require<Agent>(owner,'agent',helper.id)
|
|
|
+ current.cleanupState='complete';current.retiredAt??=Date.now();this.store.put(owner,'agent',current.id,current);this.store.event(owner,'agent.changed',current)
|
|
|
+ }).catch(()=>{this.cleanupAfter.set(key,Date.now()+5000)}).finally(()=>this.cleaning.delete(key))
|
|
|
+ }
|
|
|
+ }
|
|
|
private reconcile(owner: string, goal: AgentGoal): void {
|
|
|
if(goal.status==='cancelling') {
|
|
|
const all=this.store.list<AgentGoal>(owner,'goal'), owned=new Set([goal.id])
|
|
|
@@ -312,7 +335,7 @@ export class WorkspaceTaskCoordinator {
|
|
|
if (dependencies.some(a => a.status !== 'complete')) continue
|
|
|
const target = this.store.get<Agent>(owner, 'agent', assignment.agentId)
|
|
|
const team = this.store.require<Conversation>(owner, 'conversation', goal.conversationId)
|
|
|
- if (!target || target.archived || !team.memberIds.includes(target.id)) {
|
|
|
+ if (!target || target.archived || !this.store.taskMemberIds(owner,team,goal.id).includes(target.id)) {
|
|
|
assignment.status = 'blocked';assignment.result = '负责成员已不可用';this.saveAssignment(owner, assignment);continue
|
|
|
}
|
|
|
try { this.runtime.nodes.requireSource(owner, target) }
|