@evandu/super-pi 0.1.164 → 0.1.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/coder.md +13 -1
- package/agents/critic.md +6 -2
- package/agents/patcher.md +6 -2
- package/agents/planner.md +14 -2
- package/agents/project-leader.md +25 -0
- package/agents/reviewer.md +13 -1
- package/agents/scout.md +6 -2
- package/agents/super-pi-task/core.ts +11 -7
- package/agents/verifier.md +6 -2
- package/dist/client/.super-pi-client-sources.json +10 -8
- package/dist/client/assets/{CodeMirrorEditor-B7hhyG33.js → CodeMirrorEditor-q5ckKYpW.js} +2 -2
- package/dist/client/assets/{dist-CdpuBRoy.js → dist-3Qp_-Z3t.js} +1 -1
- package/dist/client/assets/{dist-B5xG-jTK.js → dist-B61EUjUz.js} +1 -1
- package/dist/client/assets/{dist-BLhPsVFt.js → dist-BN9HZG4O.js} +1 -1
- package/dist/client/assets/{dist-CxZb6ATc.js → dist-BOU0i9JW.js} +1 -1
- package/dist/client/assets/{dist-BBfDBMAk.js → dist-BT3fZ0wT.js} +1 -1
- package/dist/client/assets/{dist-BDy5Nw4_.js → dist-B_l0KBjV.js} +1 -1
- package/dist/client/assets/{dist-Foaz_oK2.js → dist-Bb-gT7aj.js} +1 -1
- package/dist/client/assets/{dist-D607J8oC.js → dist-CBqBGuBl.js} +1 -1
- package/dist/client/assets/{dist-CGO5Bn1x.js → dist-CXXAxAE4.js} +1 -1
- package/dist/client/assets/{dist-DJ4sWDSy.js → dist-Crew6BC1.js} +1 -1
- package/dist/client/assets/{dist-BjlmPyRl.js → dist-Cs1P7JrO.js} +1 -1
- package/dist/client/assets/{dist-CsBiEr54.js → dist-CyTiEqGG.js} +1 -1
- package/dist/client/assets/{dist-7Nz91m3a.js → dist-D10kcMrw.js} +1 -1
- package/dist/client/assets/{dist-D6nsZZd3.js → dist-DMynScoj.js} +1 -1
- package/dist/client/assets/{dist-CrJV_eES.js → dist-DRHWMjx_.js} +1 -1
- package/dist/client/assets/{dist-qqDS87aN.js → dist-DSIy9Ers.js} +1 -1
- package/dist/client/assets/{dist-BmeBkDNB.js → dist-DdfZJR8e.js} +1 -1
- package/dist/client/assets/{dist-CF9zuubZ.js → dist-DiqGp2ie.js} +1 -1
- package/dist/client/assets/{dist-ByEzPyEi.js → dist-YFQ3ixZU.js} +1 -1
- package/dist/client/assets/{dist-DaUyp_NN.js → dist-_pAt4bRw.js} +1 -1
- package/dist/client/assets/{dist-BhouAfNI.js → dist-bLHDT2q5.js} +1 -1
- package/dist/client/assets/{dist-C4ql8ak8.js → dist-dbLsO_41.js} +1 -1
- package/dist/client/assets/{index-Wtlj0s4R.css → index-C6YWfUu2.css} +1 -1
- package/dist/client/assets/{index-1hS3wVqF.js → index-dARLbQ-8.js} +19 -19
- package/dist/client/index.html +2 -2
- package/dist/server/agents.d.ts +11 -0
- package/dist/server/agents.js +50 -6
- package/dist/server/application/project-service.d.ts +95 -4
- package/dist/server/application/project-service.js +328 -72
- package/dist/server/http-server.js +7 -0
- package/dist/server/store.d.ts +17 -0
- package/dist/server/store.js +97 -1
- package/dist/server/subagent-runs.js +19 -1
- package/dist/server/task-tool.d.ts +1 -1
- package/dist/server/task-tool.js +4 -3
- package/dist/server/types.d.ts +44 -1
- package/package.json +11 -2
|
@@ -4,13 +4,22 @@ const stamp = () => new Date().toISOString();
|
|
|
4
4
|
* WHAT THE PROJECT LEADER IS TOLD WHEN IT STARTS.
|
|
5
5
|
*
|
|
6
6
|
* The two prompts are kept apart on purpose. The standard one is the shape the project flow has
|
|
7
|
-
* always had - decompose, delegate, review
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* always had - decompose, delegate, review. The lightweight one is a different set of
|
|
8
|
+
* instructions about a different shape: one session, no decomposition, no reviewer. Writing one
|
|
9
|
+
* prompt with holes in it would mean every sentence had to carry a condition, and the model would
|
|
10
|
+
* have to work out which shape it was in.
|
|
11
|
+
*
|
|
12
|
+
* THE ESCALATION RULE IS SAID ONCE, IN ONE VOICE. The standard leader used to be told two
|
|
13
|
+
* opposite things about the same event - "your subordinates' escalations come to you first, you
|
|
14
|
+
* decide whether to handle them yourself or report them up" AND "you must not resolve escalations
|
|
15
|
+
* yourself" - and a model reading both has no way to pick, so it hesitates between them. The
|
|
16
|
+
* rule is now one sentence with three parts: handle it at your own level, escalate only what
|
|
17
|
+
* really needs the Boss, and never decide for the Boss or drop an escalation. The lightweight
|
|
18
|
+
* leader has no subordinates at all, so it is told the second half only - it must not decide
|
|
19
|
+
* alone either - and is never told about subordinates it does not have.
|
|
11
20
|
*/
|
|
12
|
-
const STANDARD_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,兼任秘书。先调用 project(action=get, projectId) 了解最新状态。小项目只拆最少必要任务,大项目按交付边界分工。用 add_task 登记任务(明确目标、交付物、验收标准、依赖);add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认;需要继续拆解的工作设 role=leader,叶子执行任务设 role=executor,子 Leader 用 parentTaskId 登记自己的子任务,dispatch 启动执行子会话;不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。收到交付后用 request_review 启动独立复核。子会话结束会自动回报给你,届时继续派发 ready 任务、复核待验收任务或按 review.summary 返工。不要轮询等待,不要反复无进展重试,并发满时结束当前回复等待回报。用 report
|
|
13
|
-
const LIGHTWEIGHT_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,这是一个轻量项目:这个项目由你亲自完成,不要拆子任务、不要派发给别人、也没有独立复核。先调用 project(action=get, projectId) 了解最新状态。用 add_task 登记任务(明确目标、交付物、验收标准、依赖),任务一律登记为执行任务:role=leader 或带 parentTaskId 都会被拒绝。dispatch 会把任务交回给你自己,你亲自做完;做完后调用 project(action=submit, projectId, taskId, output, evidenceRefs) 提交交付,提交即完成,不会有人回来复核,证据必须附上。不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。不要轮询等待,不要反复无进展重试。用 report 登记关键进展,用 escalate 将重大偏差、权限或预算问题交给 Boss,不得自行解决升级事项。所有任务交付后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
|
|
21
|
+
export const STANDARD_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,兼任秘书。先调用 project(action=get, projectId) 了解最新状态。小项目只拆最少必要任务,大项目按交付边界分工。用 add_task 登记任务(明确目标、交付物、验收标准、依赖);add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认;需要继续拆解的工作设 role=leader,叶子执行任务设 role=executor,子 Leader 用 parentTaskId 登记自己的子任务,dispatch 启动执行子会话;不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。收到交付后用 request_review 启动独立复核。子会话结束会自动回报给你,届时继续派发 ready 任务、复核待验收任务或按 review.summary 返工。不要轮询等待,不要反复无进展重试,并发满时结束当前回复等待回报。用 report 登记关键进展。你的下属的升级先到你这里:你在自己这一层处理,能自己解决的自己解决;只有确实需要 Boss 决策的(重大偏差、权限或预算问题)才用 escalate 交给 Boss。你不能替 Boss 做决定,也不要把没处理的升级压下去不理。所有任务验收后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
|
|
22
|
+
export const LIGHTWEIGHT_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,这是一个轻量项目:这个项目由你亲自完成,不要拆子任务、不要派发给别人、也没有独立复核。先调用 project(action=get, projectId) 了解最新状态。用 add_task 登记任务(明确目标、交付物、验收标准、依赖),任务一律登记为执行任务:role=leader 或带 parentTaskId 都会被拒绝。dispatch 会把任务交回给你自己,你亲自做完;做完后调用 project(action=submit, projectId, taskId, output, evidenceRefs) 提交交付,提交即完成,不会有人回来复核,证据必须附上。不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。不要轮询等待,不要反复无进展重试。用 report 登记关键进展,用 escalate 将重大偏差、权限或预算问题交给 Boss,不得自行解决升级事项。所有任务交付后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
|
|
14
23
|
const text = (value, label) => {
|
|
15
24
|
if (typeof value !== "string" || !value.trim())
|
|
16
25
|
throw new Error(`${label} 不能为空`);
|
|
@@ -122,6 +131,16 @@ const CACHE_WRITE_WEIGHT = 1.25;
|
|
|
122
131
|
const tokens = (usage) => usage ? Math.round(usage.input + usage.output + usage.cacheWrite * CACHE_WRITE_WEIGHT + usage.cacheRead * CACHE_READ_WEIGHT) : 0;
|
|
123
132
|
/** The budget a project starts with, and the floor the one-click raise falls back to. */
|
|
124
133
|
const DEFAULT_MAX_TOKENS = 100_000;
|
|
134
|
+
/**
|
|
135
|
+
* HOW MANY PROJECTS ONE LIST CALL MAY CARRY.
|
|
136
|
+
*
|
|
137
|
+
* The list goes into an agent's context, and a workspace that has been alive for a year has
|
|
138
|
+
* hundreds of finished projects. Twenty is enough to answer "how are my projects doing" - the
|
|
139
|
+
* running ones and the ones that just finished are all in there - and small enough that the
|
|
140
|
+
* answer does not arrive wrapped in a year of history. The count that was cut is NOT hidden:
|
|
141
|
+
* the list carries the real total next to the truncated one.
|
|
142
|
+
*/
|
|
143
|
+
const PROJECT_LIST_LIMIT = 20;
|
|
125
144
|
/**
|
|
126
145
|
* HOW MANY EVENTS THE REPORT'S TIMELINE PRINTS.
|
|
127
146
|
*
|
|
@@ -150,7 +169,7 @@ const EVENT_TEXT = {
|
|
|
150
169
|
created: "创建项目", started: "启动执行", task_created: "登记任务", dispatched: "派发执行",
|
|
151
170
|
submitted: "提交交付", reviewed: "复核", review_requested: "启动复核", task_updated: "任务更新", status: "状态变更",
|
|
152
171
|
budget: "预算变更", report: "进展汇报", escalated: "例外升级", decision: "决定",
|
|
153
|
-
completed: "项目完成", ready: "依赖就绪",
|
|
172
|
+
completed: "项目完成", ready: "依赖就绪", unreachable: "转述对象不可达",
|
|
154
173
|
};
|
|
155
174
|
const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略" };
|
|
156
175
|
const SEVERITY_TEXT = { low: "低", medium: "中", high: "高" };
|
|
@@ -202,13 +221,24 @@ export class ProjectService {
|
|
|
202
221
|
delete t.effectiveReviewerProfile;
|
|
203
222
|
return t;
|
|
204
223
|
}
|
|
205
|
-
|
|
224
|
+
filterEscalations(escalations, actor) {
|
|
225
|
+
return escalations.filter((e) => {
|
|
226
|
+
const audience = e.audience ?? "boss";
|
|
227
|
+
if (audience === "boss")
|
|
228
|
+
return true;
|
|
229
|
+
if (!actor)
|
|
230
|
+
return false;
|
|
231
|
+
return actor === e.targetConversationId || actor === e.requestedByConversationId;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
projectView(p, actor) {
|
|
206
235
|
const resources = this.resources(p);
|
|
207
236
|
return {
|
|
208
237
|
...p,
|
|
209
238
|
resources,
|
|
210
239
|
suggestedMaxTokens: this.suggestedMaxTokens(p, resources.tokens),
|
|
211
240
|
tasks: p.tasks.map((t) => ProjectService.taskView(t, p)),
|
|
241
|
+
escalations: this.filterEscalations(p.escalations ?? [], actor),
|
|
212
242
|
};
|
|
213
243
|
}
|
|
214
244
|
/**
|
|
@@ -241,7 +271,7 @@ export class ProjectService {
|
|
|
241
271
|
p.updatedAt = stamp();
|
|
242
272
|
(p.events ??= []).push({ id: randomUUID(), kind, summary, taskId, actorConversationId, createdAt: p.updatedAt });
|
|
243
273
|
}
|
|
244
|
-
async change(id, workspaceId, work) {
|
|
274
|
+
async change(id, workspaceId, work, actor) {
|
|
245
275
|
this.workspace(workspaceId);
|
|
246
276
|
const previous = this.queues.get(id) ?? Promise.resolve();
|
|
247
277
|
const pending = previous.catch(() => { }).then(async () => {
|
|
@@ -253,7 +283,7 @@ export class ProjectService {
|
|
|
253
283
|
}
|
|
254
284
|
await this.store.setProject(p);
|
|
255
285
|
if (result && typeof result === "object" && "tasks" in result && Array.isArray(result.tasks)) {
|
|
256
|
-
return structuredClone(this.projectView(result));
|
|
286
|
+
return structuredClone(this.projectView(result, actor));
|
|
257
287
|
}
|
|
258
288
|
return structuredClone(result);
|
|
259
289
|
});
|
|
@@ -268,6 +298,45 @@ export class ProjectService {
|
|
|
268
298
|
}
|
|
269
299
|
live(p) { if (closed(p))
|
|
270
300
|
throw new Error("项目已结束,不能继续修改或派发"); }
|
|
301
|
+
/**
|
|
302
|
+
* WAKING THE OTHER SIDE MUST NOT TAKE THE REPORT DOWN WITH IT.
|
|
303
|
+
*
|
|
304
|
+
* The report, the escalation or the decision is already written to the store by the time the
|
|
305
|
+
* other side is woken, so a dispatch that fails - a session that was archived, stopped or never
|
|
306
|
+
* existed - must NOT fail the request. It would: the caller would be told the whole thing
|
|
307
|
+
* failed although it succeeded, it would try again, and the same report would be written twice.
|
|
308
|
+
*
|
|
309
|
+
* So the wake is wrapped, the failure is written down, and the record is returned as the
|
|
310
|
+
* success it is.
|
|
311
|
+
*
|
|
312
|
+
* AND THE FAILURE IS NOT SILENT. A report nobody was told about is a report nobody will ever
|
|
313
|
+
* handle, so the project's own timeline gets an event saying exactly that, and the log line
|
|
314
|
+
* names the project and the escalation so the case can be found again later.
|
|
315
|
+
*/
|
|
316
|
+
async wake(target, prompt, what, projectId, workspaceId, actor) {
|
|
317
|
+
if (!this.orchestrator)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
await this.orchestrator.dispatch(target, prompt, { wait: false });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
console.warn(`Super Pi: 项目 ${projectId} 的${what}已保存,但叫不醒会话 ${target}:${message}`);
|
|
325
|
+
await this.noteUnreachable(projectId, workspaceId, `${what}已保存,但转述对象会话不可达(${target}):${message}`, actor);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** THE TIMELINE SAYS THAT NOBODY COULD BE TOLD. Written after the fact, so it never loses the
|
|
329
|
+
* record that was already saved. */
|
|
330
|
+
async noteUnreachable(projectId, workspaceId, summary, actor) {
|
|
331
|
+
if (workspaceId === undefined)
|
|
332
|
+
return;
|
|
333
|
+
try {
|
|
334
|
+
await this.change(projectId, workspaceId, (p) => { this.event(p, "unreachable", summary, undefined, actor); });
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
console.warn(`Super Pi: 项目 ${projectId} 记录「转述对象不可达」事件失败:${error instanceof Error ? error.message : String(error)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
271
340
|
leader(p, actor) { if (actor && actor !== p.ownerConversationId)
|
|
272
341
|
throw new Error("仅项目负责人可以执行此操作"); }
|
|
273
342
|
topLevelAuthor(p, actor) {
|
|
@@ -320,6 +389,78 @@ export class ProjectService {
|
|
|
320
389
|
if (actor && actor !== p.ownerConversationId && actor !== p.initiatorConversationId && !p.tasks.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
|
|
321
390
|
throw new Error("调用会话不属于项目执行团队");
|
|
322
391
|
}
|
|
392
|
+
isMember(p, actor) {
|
|
393
|
+
if (!actor)
|
|
394
|
+
return false;
|
|
395
|
+
if (actor === p.initiatorConversationId)
|
|
396
|
+
return true;
|
|
397
|
+
if (actor === p.ownerConversationId)
|
|
398
|
+
return true;
|
|
399
|
+
if (p.tasks?.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
|
|
400
|
+
return true;
|
|
401
|
+
if (this.projectForSession(actor)?.id === p.id)
|
|
402
|
+
return true;
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
directSupervisor(p, actor, taskId) {
|
|
406
|
+
if (!actor || actor === p.ownerConversationId || actor === p.initiatorConversationId)
|
|
407
|
+
return undefined;
|
|
408
|
+
const session = this.store.conversation(actor);
|
|
409
|
+
const parentId = session?.parentConversationId;
|
|
410
|
+
if (parentId && this.isMember(p, parentId))
|
|
411
|
+
return parentId;
|
|
412
|
+
const t = taskId
|
|
413
|
+
? p.tasks.find((item) => item.id === taskId)
|
|
414
|
+
: p.tasks.find((item) => item.ownerConversationId === actor || item.reviewerConversationId === actor);
|
|
415
|
+
const manager = t ? this.manager(p, t) : undefined;
|
|
416
|
+
if (manager && this.isMember(p, manager))
|
|
417
|
+
return manager;
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* THE PROJECT A SESSION BELONGS TO RIGHT NOW.
|
|
422
|
+
*
|
|
423
|
+
* `projectForSession` answers a different question - "which project is this session part of" -
|
|
424
|
+
* and it counts the finished projects too, because the history of a project has to stay visible
|
|
425
|
+
* to the people who worked on it. This method answers the narrower question the QUESTION path
|
|
426
|
+
* needs: "is this session inside a project that is still running".
|
|
427
|
+
*
|
|
428
|
+
* WHY THAT DISTINCTION MATTERS. A conversation that took part in a project which has since been
|
|
429
|
+
* completed is not a project member any more - it is a conversation that happens to have a
|
|
430
|
+
* finished project in its history. The old lookup mixed the two, so asking the Boss a question
|
|
431
|
+
* from such a conversation was refused with "a project member may not ask the Boss directly",
|
|
432
|
+
* which is not what was true: the project was over, and nobody was being bypassed. The finished
|
|
433
|
+
* project is still found and still visible in the history; it just does not block the question.
|
|
434
|
+
*
|
|
435
|
+
* A session that is in a running project AND in a finished one belongs to the running one - it
|
|
436
|
+
* is the one that is still in charge of it.
|
|
437
|
+
*/
|
|
438
|
+
liveProjectForSession(conversationId) {
|
|
439
|
+
let c = this.store.conversation(conversationId);
|
|
440
|
+
const seen = new Set();
|
|
441
|
+
while (c && !seen.has(c.id)) {
|
|
442
|
+
seen.add(c.id);
|
|
443
|
+
const projects = this.store.projects().filter((p) => p.workspaceId === c.workspaceId
|
|
444
|
+
&& (p.ownerConversationId === c.id || p.tasks.some((t) => t.ownerConversationId === c.id || t.reviewerConversationId === c.id)));
|
|
445
|
+
const live = projects.find((p) => !closed(p));
|
|
446
|
+
if (live)
|
|
447
|
+
return live;
|
|
448
|
+
c = c.parentConversationId ? this.store.conversation(c.parentConversationId) : undefined;
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
canAskQuestion(conversationId) {
|
|
453
|
+
const project = this.liveProjectForSession(conversationId);
|
|
454
|
+
if (!project)
|
|
455
|
+
return { ok: true };
|
|
456
|
+
if (conversationId === project.ownerConversationId || conversationId === project.initiatorConversationId) {
|
|
457
|
+
return { ok: true };
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
reason: "项目成员不能直接向 Boss 提问:请通过直接上级上报,由负责人代为转述",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
323
464
|
reconcile(p) {
|
|
324
465
|
if (closed(p))
|
|
325
466
|
return;
|
|
@@ -329,9 +470,15 @@ export class ProjectService {
|
|
|
329
470
|
t.updatedAt = stamp();
|
|
330
471
|
this.event(p, "ready", `前置交付已验收:${t.title}`, t.id);
|
|
331
472
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
473
|
+
const isLeader = (convId) => !convId || convId === p.ownerConversationId || convId === p.initiatorConversationId;
|
|
474
|
+
const openBoss = p.escalations.filter((e) => e.status === "open" && (e.audience === "boss" || !e.audience));
|
|
475
|
+
const hasLeaderBlockedTask = p.tasks.some((t) => t.status === "blocked" && isLeader(t.blockedReportedByConversationId));
|
|
476
|
+
const lastLeaderProjectReport = [...p.reports].reverse().find((r) => !r.taskId && isLeader(r.authorConversationId));
|
|
477
|
+
const leaderProjectBlocked = lastLeaderProjectReport?.kind === "blocked";
|
|
478
|
+
p.status = openBoss.some((e) => e.severity === "high") ? "awaiting_approval"
|
|
479
|
+
: openBoss.length || hasLeaderBlockedTask || leaderProjectBlocked ? "blocked"
|
|
480
|
+
: p.tasks.some((t) => t.status === "awaiting_review") ? "awaiting_review"
|
|
481
|
+
: p.tasks.length ? "active" : "planning";
|
|
335
482
|
}
|
|
336
483
|
resources(p) {
|
|
337
484
|
const state = this.store.snapshot();
|
|
@@ -374,7 +521,15 @@ export class ProjectService {
|
|
|
374
521
|
report zero running agents. The concurrency guard is not touched by this: the in-flight task
|
|
375
522
|
it counts (`reserved`) already covers the busy leader, so the cap behaves exactly as before. */
|
|
376
523
|
const leaderIsWorker = lightweight(p) && p.ownerConversationId !== undefined;
|
|
377
|
-
|
|
524
|
+
/* ONE ACCOUNTING PATH, AND IT IS THE SESSION TREE.
|
|
525
|
+
The number below is the sum of the member sessions' usage, and NOTHING ELSE. A one-shot call
|
|
526
|
+
(a sub-agent) used to be added here from the run records on top of that sum, which was a
|
|
527
|
+
SECOND path to the same money: the run's usage was already inside the conversation's usage
|
|
528
|
+
(the store folds it in when the run settles), so the same tokens were counted twice.
|
|
529
|
+
Now it is counted once - through the session - and a project's budget therefore includes what
|
|
530
|
+
its one-shot calls cost, which is the whole point of a budget a person can predict.
|
|
531
|
+
See `Store.accountSubagentUsage`. */
|
|
532
|
+
return { tokens: sessions.reduce((sum, s) => sum + s.tokens, 0),
|
|
378
533
|
usageKnown: sessions.length > 0 && sessions.every((s) => Boolean(this.store.conversation(s.id)?.usage)) && runs.every((r) => Boolean(r.usage)),
|
|
379
534
|
activeWorkers: sessions.filter((s) => (!coordinatorIds.has(s.id) || (leaderIsWorker && s.id === p.ownerConversationId)) && busy(s.status)).length + runs.filter((r) => r.status === "running").length,
|
|
380
535
|
// THE PROJECT'S OWN MEMORY TREE, named once so the client does not have to infer it from
|
|
@@ -385,7 +540,7 @@ export class ProjectService {
|
|
|
385
540
|
const scope = this.workspace(workspaceId);
|
|
386
541
|
return this.store.projects().filter((p) => p.workspaceId === scope).map((p) => this.projectView(p));
|
|
387
542
|
}
|
|
388
|
-
headline(p) {
|
|
543
|
+
headline(p, actor) {
|
|
389
544
|
const total = p.tasks.length;
|
|
390
545
|
const completed = p.tasks.filter((t) => t.status === "completed").length;
|
|
391
546
|
if (p.status === "completed") {
|
|
@@ -395,9 +550,10 @@ export class ProjectService {
|
|
|
395
550
|
return "项目已取消";
|
|
396
551
|
if (p.status === "failed")
|
|
397
552
|
return "项目失败";
|
|
398
|
-
const
|
|
553
|
+
const visibleEscalations = this.filterEscalations(p.escalations ?? [], actor);
|
|
554
|
+
const openEscalation = visibleEscalations.find((e) => e.status === "open");
|
|
399
555
|
if (openEscalation) {
|
|
400
|
-
return p.status === "awaiting_approval"
|
|
556
|
+
return (openEscalation.audience === "boss" || !openEscalation.audience) && p.status === "awaiting_approval"
|
|
401
557
|
? `等待审批:${openEscalation.reason}`
|
|
402
558
|
: `项目已阻塞:${openEscalation.reason}`;
|
|
403
559
|
}
|
|
@@ -424,26 +580,16 @@ export class ProjectService {
|
|
|
424
580
|
return "项目规划中,尚未拆解任务";
|
|
425
581
|
}
|
|
426
582
|
isActorRelated(p, actor) {
|
|
427
|
-
|
|
428
|
-
return false;
|
|
429
|
-
if (actor === p.initiatorConversationId)
|
|
430
|
-
return true;
|
|
431
|
-
if (actor === p.ownerConversationId)
|
|
432
|
-
return true;
|
|
433
|
-
if (p.tasks?.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
|
|
434
|
-
return true;
|
|
435
|
-
if (this.projectForSession(actor)?.id === p.id)
|
|
436
|
-
return true;
|
|
437
|
-
return false;
|
|
583
|
+
return this.isMember(p, actor);
|
|
438
584
|
}
|
|
439
|
-
projectSummary(p) {
|
|
585
|
+
projectSummary(p, actor) {
|
|
440
586
|
const resources = this.resources(p);
|
|
441
587
|
return {
|
|
442
588
|
id: p.id,
|
|
443
589
|
name: p.title,
|
|
444
590
|
status: p.status,
|
|
445
591
|
mode: p.mode ?? "standard",
|
|
446
|
-
headline: this.headline(p),
|
|
592
|
+
headline: this.headline(p, actor),
|
|
447
593
|
tasks: {
|
|
448
594
|
total: p.tasks.length,
|
|
449
595
|
completed: p.tasks.filter((t) => t.status === "completed").length,
|
|
@@ -455,15 +601,32 @@ export class ProjectService {
|
|
|
455
601
|
updatedAt: p.updatedAt,
|
|
456
602
|
};
|
|
457
603
|
}
|
|
458
|
-
|
|
604
|
+
/**
|
|
605
|
+
* THE PROJECTS AN ACTOR IS PART OF, AND HOW MANY OF THEM THERE ARE.
|
|
606
|
+
*
|
|
607
|
+
* The list is capped, because the list is what goes into an agent's context: a workspace that
|
|
608
|
+
* has been alive for a year has hundreds of finished projects, and handing all of them over
|
|
609
|
+
* answers the question "how are my projects doing" with a wall of history. The cap is on the
|
|
610
|
+
* RECENT ones - the same order the list has always had, newest update first - so what the
|
|
611
|
+
* agent reads first is what actually changed.
|
|
612
|
+
*
|
|
613
|
+
* AND THE COUNT IS TOLD, NOT IMPLIED. `total` is how many projects the session is really
|
|
614
|
+
* related to, counted BEFORE the cut, and `returned` is how many came back. An agent that sees
|
|
615
|
+
* total 57 and returned 20 knows the workspace has more history than it was shown, and says so
|
|
616
|
+
* instead of telling the Boss the workspace only has 20 projects. A silently truncated list
|
|
617
|
+
* would be a lie the agent could not see through.
|
|
618
|
+
*/
|
|
619
|
+
listForActor(workspaceId, actor, limit = PROJECT_LIST_LIMIT) {
|
|
459
620
|
const scope = this.workspace(workspaceId);
|
|
460
|
-
|
|
621
|
+
const related = this.store.projects()
|
|
461
622
|
.filter((p) => p.workspaceId === scope && this.isActorRelated(p, actor))
|
|
462
|
-
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
463
|
-
|
|
623
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
624
|
+
const total = related.length;
|
|
625
|
+
const projects = related.slice(0, limit).map((p) => this.projectSummary(p, actor));
|
|
626
|
+
return { projects, total, returned: projects.length };
|
|
464
627
|
}
|
|
465
|
-
get(id, workspaceId) {
|
|
466
|
-
return this.projectView(this.raw(id, workspaceId));
|
|
628
|
+
get(id, workspaceId, actor) {
|
|
629
|
+
return this.projectView(this.raw(id, workspaceId), actor);
|
|
467
630
|
}
|
|
468
631
|
/**
|
|
469
632
|
* THE PROJECT AS A REPORT A HUMAN CAN READ.
|
|
@@ -524,7 +687,9 @@ export class ProjectService {
|
|
|
524
687
|
lines.push(`- 预算上限:${p.budget?.maxTokens ?? DEFAULT_MAX_TOKENS} Token`);
|
|
525
688
|
// THE USAGE IS REPORTED IN THE SAME BILLING-EQUIVALENT UNITS THE BUDGET IS SPENT IN, and the
|
|
526
689
|
// formula is printed next to the number: a budget nobody can predict is a budget nobody trusts.
|
|
527
|
-
|
|
690
|
+
// The sentence after the formula says what the number INCLUDES - a one-shot call (a sub-agent)
|
|
691
|
+
// is part of it, so a reader who dispatches children can see what that cost them.
|
|
692
|
+
lines.push(`- 已用等效 Token:${this.resources(p).tokens}(口径:输入 + 输出 + 缓存写入×${CACHE_WRITE_WEIGHT} + 缓存读取×${CACHE_READ_WEIGHT},取整;含项目内的一次性调用(subagent))`);
|
|
528
693
|
lines.push(`- 任务:共 ${p.tasks.length} 个,已完成 ${p.tasks.filter((t) => t.status === "completed").length} 个`);
|
|
529
694
|
lines.push("", "## 交付与验收", "");
|
|
530
695
|
if (!p.tasks.length)
|
|
@@ -634,7 +799,7 @@ export class ProjectService {
|
|
|
634
799
|
}
|
|
635
800
|
admission(p, checkConcurrency = true, coordinator = false) {
|
|
636
801
|
this.live(p);
|
|
637
|
-
if (p.escalations.some((e) => e.status === "open" && e.severity === "high"))
|
|
802
|
+
if (p.escalations.some((e) => e.status === "open" && e.severity === "high" && (e.audience === "boss" || !e.audience)))
|
|
638
803
|
throw new Error("等待 Boss 处理升级决策后才能继续派发");
|
|
639
804
|
const r = this.resources(p);
|
|
640
805
|
if (p.budget?.maxTokens && r.tokens >= p.budget.maxTokens)
|
|
@@ -810,15 +975,16 @@ export class ProjectService {
|
|
|
810
975
|
t.status = "in_progress";
|
|
811
976
|
t.updatedAt = stamp();
|
|
812
977
|
delete t.blockedReason;
|
|
978
|
+
delete t.blockedReportedByConversationId;
|
|
813
979
|
this.event(p, "dispatched", `派发执行:${t.title}`, t.id, actor);
|
|
814
980
|
this.reconcile(p);
|
|
815
981
|
await this.store.setProject(p);
|
|
816
|
-
const delegation = t.role === "leader" ? `你是分支 Leader,负责这项任务的拆解、委派、集成交付。用 project add_task 时必须带 parentTaskId="${t.id}";可按必要性指定 role="leader" 或 "executor";add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认。用 project dispatch / request_review 管理直属子任务,子会话结束自动回报给你,不要轮询;并发满时结束本轮等待回报。所有必需子任务独立验收后才能 submit 聚合交付,上级还会独立复核你的集成结果。最多 3 层 Leader、8 个分支 Leader
|
|
982
|
+
const delegation = t.role === "leader" ? `你是分支 Leader,负责这项任务的拆解、委派、集成交付。用 project add_task 时必须带 parentTaskId="${t.id}";可按必要性指定 role="leader" 或 "executor";add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认。用 project dispatch / request_review 管理直属子任务,子会话结束自动回报给你,不要轮询;并发满时结束本轮等待回报。所有必需子任务独立验收后才能 submit 聚合交付,上级还会独立复核你的集成结果。最多 3 层 Leader、8 个分支 Leader;小任务不要继续拆层。你的下属的升级先到你这里;你决定是自己解决还是再向上级转述。` : "你是执行者,不得自行拆解子任务。";
|
|
817
983
|
/* THE LIGHTWEIGHT TASK SAYS WHAT IS DIFFERENT, because the difference is the point: there is
|
|
818
984
|
no reviewer coming back, so the evidence that the reviewer would have demanded is the only
|
|
819
985
|
thing that stands behind the delivery. */
|
|
820
986
|
const modeNote = lightweight(p) ? "本项目是轻量项目:你就是唯一的执行会话,不会有独立复核人回来,完成后调用 project(action=submit, …) 提交即完成,交付证据必须附上。" : "";
|
|
821
|
-
await this.orchestrator.dispatch(t.ownerConversationId, `${this.context(p)}\n${delegation}\n你负责执行任务 ${t.id}:${t.title}\n目标:${t.objective}\n交付物:${t.deliverables.join(";")}\n验收:${t.acceptanceCriteria.join(";")}\n上次复核意见与验证证据:${JSON.stringify(t.review ?? null)}\n已验收前置产物:${JSON.stringify(p.tasks.filter((d) => t.dependencies.includes(d.id)).map((d) => ({ title: d.title, output: d.output, evidenceRefs: d.evidenceRefs })))}\n你不是唯一执行者,遵守分工边界。${modeNote}完成后必须调用 project(action=submit, projectId="${p.id}", taskId="${t.id}", output="交付总结", evidenceRefs=["产物路径或验证记录"]),不能自行标记已完成。复核退回时修复后重新 submit。遇到阻塞用 report 或 escalate,保留证据、假设和未解决问题。不要自行创建其他执行会话或 task/workflow。`, { wait: false });
|
|
987
|
+
await this.orchestrator.dispatch(t.ownerConversationId, `${this.context(p)}\n${delegation}\n你负责执行任务 ${t.id}:${t.title}\n目标:${t.objective}\n交付物:${t.deliverables.join(";")}\n验收:${t.acceptanceCriteria.join(";")}\n上次复核意见与验证证据:${JSON.stringify(t.review ?? null)}\n已验收前置产物:${JSON.stringify(p.tasks.filter((d) => t.dependencies.includes(d.id)).map((d) => ({ title: d.title, output: d.output, evidenceRefs: d.evidenceRefs })))}\n你不是唯一执行者,遵守分工边界。${modeNote}完成后必须调用 project(action=submit, projectId="${p.id}", taskId="${t.id}", output="交付总结", evidenceRefs=["产物路径或验证记录"]),不能自行标记已完成。复核退回时修复后重新 submit。遇到阻塞用 report(阻塞) 或 escalate 上报给你的直接上级,不要直接找 Boss,保留证据、假设和未解决问题。不要自行创建其他执行会话或 task/workflow。`, { wait: false });
|
|
822
988
|
return ProjectService.taskView(t, p);
|
|
823
989
|
});
|
|
824
990
|
}
|
|
@@ -938,6 +1104,12 @@ export class ProjectService {
|
|
|
938
1104
|
if (patch.status === "ready" && actor && actor !== manager)
|
|
939
1105
|
throw new Error("仅直属 Leader 可以重新派发任务");
|
|
940
1106
|
t.status = patch.status;
|
|
1107
|
+
if (t.status === "blocked") {
|
|
1108
|
+
t.blockedReportedByConversationId = actor;
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
delete t.blockedReportedByConversationId;
|
|
1112
|
+
}
|
|
941
1113
|
}
|
|
942
1114
|
if (patch.executorProfile !== undefined) {
|
|
943
1115
|
if (t.ownerConversationId)
|
|
@@ -1137,7 +1309,10 @@ export class ProjectService {
|
|
|
1137
1309
|
return { ...raised, project };
|
|
1138
1310
|
}
|
|
1139
1311
|
async addReport(id, input, workspaceId, actor) {
|
|
1140
|
-
|
|
1312
|
+
let supervisorToWake;
|
|
1313
|
+
let taskForWake;
|
|
1314
|
+
let memberBlockedReport = false;
|
|
1315
|
+
const report = await this.change(id, workspaceId, (p) => {
|
|
1141
1316
|
this.live(p);
|
|
1142
1317
|
this.member(p, actor);
|
|
1143
1318
|
if (!["progress", "blocked", "delivery", "decision_request", "review"].includes(input.kind))
|
|
@@ -1151,31 +1326,85 @@ export class ProjectService {
|
|
|
1151
1326
|
progress: text(input.progress, "汇报进展"), variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
|
|
1152
1327
|
decisionNeeded: optional(input.decisionNeeded, "所需决策"), evidenceRefs: strings(input.evidenceRefs, "证据"), createdAt: stamp() };
|
|
1153
1328
|
p.reports.push(report);
|
|
1154
|
-
if (report.kind === "blocked"
|
|
1155
|
-
t
|
|
1156
|
-
|
|
1157
|
-
|
|
1329
|
+
if (report.kind === "blocked") {
|
|
1330
|
+
if (t) {
|
|
1331
|
+
t.status = "blocked";
|
|
1332
|
+
t.blockedReason = report.progress;
|
|
1333
|
+
t.blockedReportedByConversationId = actor;
|
|
1334
|
+
t.updatedAt = stamp();
|
|
1335
|
+
}
|
|
1336
|
+
if (actor && actor !== p.ownerConversationId && actor !== p.initiatorConversationId) {
|
|
1337
|
+
memberBlockedReport = true;
|
|
1338
|
+
supervisorToWake = this.directSupervisor(p, actor, t?.id);
|
|
1339
|
+
taskForWake = t;
|
|
1340
|
+
}
|
|
1158
1341
|
}
|
|
1159
1342
|
this.event(p, "report", report.progress, t?.id, actor);
|
|
1160
1343
|
this.reconcile(p);
|
|
1161
1344
|
return report;
|
|
1162
|
-
});
|
|
1345
|
+
}, actor);
|
|
1346
|
+
if (supervisorToWake) {
|
|
1347
|
+
const taskInfo = taskForWake ? `任务 ${taskForWake.id}(${taskForWake.title})` : "所属任务";
|
|
1348
|
+
await this.wake(supervisorToWake, `下级成员(会话:${actor})汇报阻塞:${taskInfo}\n原因:${report.progress}\n请评估阻塞并跟进解决,必要时向 Boss 转述或升级。`, "阻塞上报", id, workspaceId, actor);
|
|
1349
|
+
}
|
|
1350
|
+
else if (memberBlockedReport) {
|
|
1351
|
+
/* A MEMBER REPORTED A BLOCK AND NO SUPERVISOR COULD BE FOUND. The report is saved either
|
|
1352
|
+
* way, but nobody has been told, so the case is written down instead of vanishing. */
|
|
1353
|
+
const summary = "阻塞上报已保存,但找不到直接上级会话,上报没有被转述";
|
|
1354
|
+
console.warn(`Super Pi: 项目 ${id} 的${summary}`);
|
|
1355
|
+
await this.noteUnreachable(id, workspaceId, summary, actor);
|
|
1356
|
+
}
|
|
1357
|
+
return report;
|
|
1163
1358
|
}
|
|
1164
1359
|
async addEscalation(id, input, workspaceId, actor) {
|
|
1165
|
-
|
|
1360
|
+
let supervisorToWake;
|
|
1361
|
+
let wakeMemberRole;
|
|
1362
|
+
const result = await this.change(id, workspaceId, (p) => {
|
|
1166
1363
|
this.live(p);
|
|
1167
1364
|
this.member(p, actor);
|
|
1168
1365
|
if (!["low", "medium", "high"].includes(input.severity))
|
|
1169
1366
|
throw new Error("升级风险等级不合法");
|
|
1170
|
-
const t = input.taskId
|
|
1171
|
-
|
|
1367
|
+
const t = input.taskId
|
|
1368
|
+
? this.task(p, text(input.taskId, "任务 id"))
|
|
1369
|
+
: p.tasks.find((task) => task.ownerConversationId === actor || task.reviewerConversationId === actor);
|
|
1370
|
+
const isBossAudience = !actor || actor === p.ownerConversationId || actor === p.initiatorConversationId;
|
|
1371
|
+
const audience = isBossAudience ? "boss" : "supervisor";
|
|
1372
|
+
let targetConversationId;
|
|
1373
|
+
if (!isBossAudience) {
|
|
1374
|
+
targetConversationId = this.directSupervisor(p, actor, t?.id);
|
|
1375
|
+
if (!targetConversationId) {
|
|
1376
|
+
throw new Error("无法确定直接上级会话:上级不在项目执行团队内且无法定位所属任务 Leader,升级已拒绝。");
|
|
1377
|
+
}
|
|
1378
|
+
supervisorToWake = targetConversationId;
|
|
1379
|
+
wakeMemberRole = p.tasks.some((task) => task.role === "leader" && task.ownerConversationId === actor)
|
|
1380
|
+
? "分支 Leader"
|
|
1381
|
+
: p.tasks.some((task) => task.reviewerConversationId === actor)
|
|
1382
|
+
? "独立复核"
|
|
1383
|
+
: "执行者";
|
|
1384
|
+
}
|
|
1385
|
+
const e = {
|
|
1386
|
+
id: randomUUID(),
|
|
1387
|
+
projectId: id,
|
|
1388
|
+
taskId: t?.id,
|
|
1389
|
+
reason: text(input.reason, "升级原因"),
|
|
1390
|
+
severity: input.severity,
|
|
1391
|
+
status: "open",
|
|
1392
|
+
requestedByConversationId: actor,
|
|
1393
|
+
audience,
|
|
1394
|
+
...(targetConversationId ? { targetConversationId } : {}),
|
|
1395
|
+
createdAt: stamp(),
|
|
1396
|
+
};
|
|
1172
1397
|
p.escalations.push(e);
|
|
1173
1398
|
this.event(p, "escalated", e.reason, t?.id, actor);
|
|
1174
1399
|
this.reconcile(p);
|
|
1175
1400
|
return e;
|
|
1176
|
-
});
|
|
1401
|
+
}, actor);
|
|
1402
|
+
if (supervisorToWake) {
|
|
1403
|
+
await this.wake(supervisorToWake, `下级成员(会话:${actor},身份:${wakeMemberRole ?? "执行者"})发起例外升级(ID:${result.id}):\n原因:${result.reason}\n严重级别:${result.severity}\n请及时处理该例外(调用 project action="resolve_escalation" 记录处理意见),或按需向上级转述升级。`, `例外升级(ID:${result.id})`, id, workspaceId, actor);
|
|
1404
|
+
}
|
|
1405
|
+
return result;
|
|
1177
1406
|
}
|
|
1178
|
-
async resolveEscalation(id, escalationId, decision, status, workspaceId) {
|
|
1407
|
+
async resolveEscalation(id, escalationId, decision, status, workspaceId, actor) {
|
|
1179
1408
|
const result = await this.change(id, workspaceId, (p) => {
|
|
1180
1409
|
this.live(p);
|
|
1181
1410
|
if (status !== "resolved" && status !== "dismissed")
|
|
@@ -1185,33 +1414,54 @@ export class ProjectService {
|
|
|
1185
1414
|
throw new Error("升级事项不存在");
|
|
1186
1415
|
if (e.status !== "open")
|
|
1187
1416
|
throw new Error("升级事项已处理");
|
|
1417
|
+
const audience = e.audience ?? "boss";
|
|
1418
|
+
if (audience === "boss") {
|
|
1419
|
+
if (actor)
|
|
1420
|
+
throw new Error("此例外需由 Boss 决策,团队成员无权处理");
|
|
1421
|
+
}
|
|
1422
|
+
else if (audience === "supervisor") {
|
|
1423
|
+
if (!actor)
|
|
1424
|
+
throw new Error("该例外属于直接上级处理范围,Boss 不能直接处理 supervisor 级别的例外");
|
|
1425
|
+
if (actor !== e.targetConversationId)
|
|
1426
|
+
throw new Error("只有指定的直接上级可以处理此例外");
|
|
1427
|
+
e.resolvedByConversationId = actor;
|
|
1428
|
+
}
|
|
1188
1429
|
e.status = status;
|
|
1189
1430
|
e.decision = text(decision, "处理意见");
|
|
1190
1431
|
e.resolvedAt = stamp();
|
|
1191
|
-
|
|
1432
|
+
const decider = audience === "boss" ? "Boss" : "直接上级";
|
|
1433
|
+
this.event(p, "decision", `${decider}决策:${e.decision}`, e.taskId, actor);
|
|
1192
1434
|
this.reconcile(p);
|
|
1193
1435
|
return e;
|
|
1194
|
-
});
|
|
1195
|
-
const p = this.
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
let directLeaderId;
|
|
1200
|
-
if (targetTask) {
|
|
1201
|
-
if (targetTask.role === "leader" && targetTask.ownerConversationId) {
|
|
1202
|
-
directLeaderId = targetTask.ownerConversationId;
|
|
1436
|
+
}, actor);
|
|
1437
|
+
const p = this.raw(id, workspaceId);
|
|
1438
|
+
if (result.audience === "supervisor") {
|
|
1439
|
+
if (result.requestedByConversationId) {
|
|
1440
|
+
await this.wake(result.requestedByConversationId, `直接上级已处理例外(ID:${result.id}):${result.reason}\n处理意见:${result.decision}\n请按处理意见继续工作。`, `例外处理结果(ID:${result.id})`, id, workspaceId, actor);
|
|
1203
1441
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
const targetTask = result.taskId
|
|
1445
|
+
? p.tasks.find((t) => t.id === result.taskId)
|
|
1446
|
+
: p.tasks.find((t) => t.ownerConversationId === result.requestedByConversationId || t.reviewerConversationId === result.requestedByConversationId);
|
|
1447
|
+
let directLeaderId;
|
|
1448
|
+
if (targetTask) {
|
|
1449
|
+
if (targetTask.role === "leader" && targetTask.ownerConversationId) {
|
|
1450
|
+
directLeaderId = targetTask.ownerConversationId;
|
|
1451
|
+
}
|
|
1452
|
+
else if (targetTask.parentTaskId) {
|
|
1453
|
+
const parent = p.tasks.find((t) => t.id === targetTask.parentTaskId);
|
|
1454
|
+
directLeaderId = parent?.ownerConversationId ?? targetTask.accountableConversationId;
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
directLeaderId = targetTask.accountableConversationId ?? p.ownerConversationId;
|
|
1458
|
+
}
|
|
1207
1459
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1460
|
+
const recipientId = directLeaderId || p.ownerConversationId;
|
|
1461
|
+
if (recipientId) {
|
|
1462
|
+
await this.wake(recipientId, `Boss 已处理项目 ${p.id} 的例外:${result.reason}\n决策:${result.decision}\n先读取 project 最新状态,按决策继续范围内的工作。`, `例外处理结果(ID:${result.id})`, id, workspaceId, actor);
|
|
1210
1463
|
}
|
|
1211
1464
|
}
|
|
1212
|
-
const recipientId = directLeaderId || p.ownerConversationId;
|
|
1213
|
-
if (recipientId && this.orchestrator)
|
|
1214
|
-
await this.orchestrator.dispatch(recipientId, `Boss 已处理项目 ${p.id} 的例外:${result.reason}\n决策:${result.decision}\n先读取 project 最新状态,按决策继续范围内的工作。`, { wait: false });
|
|
1215
1465
|
return result;
|
|
1216
1466
|
}
|
|
1217
1467
|
async complete(id, summary, evidenceRefs, workspaceId, actor) {
|
|
@@ -1337,13 +1587,16 @@ export class ProjectService {
|
|
|
1337
1587
|
return this.get(project.id, workspaceId);
|
|
1338
1588
|
}
|
|
1339
1589
|
if (body.action === "list") {
|
|
1590
|
+
// THE LIST IS CAPPED, AND IT SAYS SO. `total` and `returned` travel with it, so an agent
|
|
1591
|
+
// that sees a short list can tell the difference between "the workspace has three projects"
|
|
1592
|
+
// and "the workspace has fifty and I was shown twenty".
|
|
1340
1593
|
return this.listForActor(workspaceId, actor);
|
|
1341
1594
|
}
|
|
1342
1595
|
const id = text(body.projectId, "projectId");
|
|
1343
|
-
const
|
|
1344
|
-
this.member(
|
|
1596
|
+
const rawProject = this.raw(id, workspaceId);
|
|
1597
|
+
this.member(rawProject, actor);
|
|
1345
1598
|
switch (body.action) {
|
|
1346
|
-
case "get": return
|
|
1599
|
+
case "get": return this.get(id, workspaceId, actor);
|
|
1347
1600
|
case "export": return this.exportReport(id, workspaceId);
|
|
1348
1601
|
case "start": return this.start(id, workspaceId, actor);
|
|
1349
1602
|
case "add_task": return this.addTask(id, body, workspaceId, actor);
|
|
@@ -1354,6 +1607,9 @@ export class ProjectService {
|
|
|
1354
1607
|
case "update_task": return this.updateTask(id, text(body.taskId, "taskId"), { ...body, workspaceId }, actor);
|
|
1355
1608
|
case "report": return this.addReport(id, body, workspaceId, actor);
|
|
1356
1609
|
case "escalate": return this.addEscalation(id, body, workspaceId, actor);
|
|
1610
|
+
case "resolve_escalation":
|
|
1611
|
+
case "resolveEscalation":
|
|
1612
|
+
return this.resolveEscalation(id, text(body.escalationId, "escalationId"), text(body.decision, "decision"), body.status ?? "resolved", workspaceId, actor);
|
|
1357
1613
|
case "complete": return this.complete(id, body.summary, body.evidenceRefs, workspaceId, actor);
|
|
1358
1614
|
case "cancel": return this.cancel(id, body, workspaceId, actor);
|
|
1359
1615
|
default: throw new Error("未知的项目工具操作");
|
|
@@ -2099,6 +2099,13 @@ export function createHttpServer(options) {
|
|
|
2099
2099
|
response.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({ error: verdict.message }));
|
|
2100
2100
|
return;
|
|
2101
2101
|
}
|
|
2102
|
+
if (context.projectService) {
|
|
2103
|
+
const check = context.projectService.canAskQuestion(body.conversationId);
|
|
2104
|
+
if (!check.ok) {
|
|
2105
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: check.reason }));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2102
2109
|
const result = await runService.askQuestion(body.conversationId);
|
|
2103
2110
|
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
|
|
2104
2111
|
}
|