@evandu/super-pi 0.1.166 → 0.1.168
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/super-pi-task/core.ts +1 -1
- package/dist/client/.super-pi-client-sources.json +8 -9
- package/dist/client/assets/{CodeMirrorEditor-q5ckKYpW.js → CodeMirrorEditor-DWEFdLq1.js} +2 -2
- package/dist/client/assets/{dist-DMynScoj.js → dist-0cjcfZm8.js} +1 -1
- package/dist/client/assets/{dist-_pAt4bRw.js → dist-3mX68Ead.js} +1 -1
- package/dist/client/assets/{dist-CXXAxAE4.js → dist-84wmy0xb.js} +1 -1
- package/dist/client/assets/{dist-dbLsO_41.js → dist-B19er0Hr.js} +1 -1
- package/dist/client/assets/{dist-DSIy9Ers.js → dist-BWN1gFkd.js} +1 -1
- package/dist/client/assets/{dist-DRHWMjx_.js → dist-Bh9F9CwL.js} +1 -1
- package/dist/client/assets/{dist-DiqGp2ie.js → dist-BiVenLEE.js} +1 -1
- package/dist/client/assets/{dist-D10kcMrw.js → dist-Bp858rkg.js} +1 -1
- package/dist/client/assets/{dist-BT3fZ0wT.js → dist-BsaizV0U.js} +1 -1
- package/dist/client/assets/{dist-bLHDT2q5.js → dist-BygS3RZC.js} +1 -1
- package/dist/client/assets/{dist-CyTiEqGG.js → dist-CPZVRFqI.js} +1 -1
- package/dist/client/assets/{dist-3Qp_-Z3t.js → dist-CXIdy9EI.js} +1 -1
- package/dist/client/assets/{dist-BN9HZG4O.js → dist-Co6-VsRy.js} +1 -1
- package/dist/client/assets/{dist-B_l0KBjV.js → dist-D5SNpvpB.js} +1 -1
- package/dist/client/assets/{dist-B61EUjUz.js → dist-D9UIUBjg.js} +1 -1
- package/dist/client/assets/{dist-YFQ3ixZU.js → dist-DR3gLDod.js} +1 -1
- package/dist/client/assets/{dist-Cs1P7JrO.js → dist-DTYN3KZG.js} +1 -1
- package/dist/client/assets/{dist-DdfZJR8e.js → dist-ELtcJv97.js} +1 -1
- package/dist/client/assets/{dist-Crew6BC1.js → dist-H7kWZ7Cr.js} +1 -1
- package/dist/client/assets/{dist-Bb-gT7aj.js → dist-XSVtuaKi.js} +1 -1
- package/dist/client/assets/{dist-CBqBGuBl.js → dist-ei0l0QpD.js} +1 -1
- package/dist/client/assets/{dist-BOU0i9JW.js → dist-i063EgJc.js} +1 -1
- package/dist/client/assets/index-C3ETe3EL.css +1 -0
- package/dist/client/assets/{index-dARLbQ-8.js → index-C4d7l2t8.js} +10 -10
- package/dist/client/index.html +2 -2
- package/dist/server/agents.d.ts +18 -0
- package/dist/server/agents.js +38 -9
- package/dist/server/application/project-service.d.ts +47 -3
- package/dist/server/application/project-service.js +132 -15
- package/dist/server/index.js +13 -2
- package/dist/server/types.d.ts +14 -3
- package/dist/shared/agent-order.d.ts +44 -0
- package/dist/shared/agent-order.js +50 -0
- package/package.json +9 -2
- package/dist/client/assets/index-C6YWfUu2.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-C4d7l2t8.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C3ETe3EL.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/dist/server/agents.d.ts
CHANGED
|
@@ -199,6 +199,24 @@ export declare function readAgents(directory: string | readonly string[], option
|
|
|
199
199
|
mode?: AgentModeFilter;
|
|
200
200
|
builtinDir?: string;
|
|
201
201
|
}): Promise<AgentScan>;
|
|
202
|
+
/**
|
|
203
|
+
* WHAT KIND OF AGENT IS THIS ID?
|
|
204
|
+
*
|
|
205
|
+
* The catalogue is read in halves - the primaries and the subagents - and a question about ONE id
|
|
206
|
+
* has to be asked of both halves. The answer is the `mode:` frontmatter of the definition that
|
|
207
|
+
* WINS, which is the same override rule `readAgents` already applies: the first directory to offer
|
|
208
|
+
* an id owns it.
|
|
209
|
+
*
|
|
210
|
+
* `undefined` MEANS "NO SUCH AGENT IN THESE DIRECTORIES", and that is NOT the same as "not
|
|
211
|
+
* primary". An id nobody has heard of is a CUSTOM agent, and custom agents are legal - §3 keeps
|
|
212
|
+
* them legal on purpose, so that a project can name an agent the catalogue does not carry yet.
|
|
213
|
+
* Only an agent that EXISTS and is a subagent is a subagent.
|
|
214
|
+
*
|
|
215
|
+
* THE HIDDEN ONES COUNT. `hidden` decides whether a person is SHOWN an agent in a picker, not
|
|
216
|
+
* whether the agent exists, so a hidden subagent is still a subagent here. A guard that could be
|
|
217
|
+
* walked around by ticking one box in a frontmatter would not be a guard.
|
|
218
|
+
*/
|
|
219
|
+
export declare function agentModeOf(directories: string | readonly string[], agentId: string): Promise<"primary" | "subagent" | undefined>;
|
|
202
220
|
/** Parses one definition file, or returns the reason it is not a usable agent. */
|
|
203
221
|
export declare function parseDefinition(file: string, source: string): AgentDefinition | AgentProblem;
|
|
204
222
|
/**
|
package/dist/server/agents.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { orderAgentsForDisplay } from "../shared/agent-order.js";
|
|
4
5
|
/**
|
|
5
6
|
* A guard rather than a cast: it is the only thing that lets `tools:` be checked once and then used as
|
|
6
7
|
* a map everywhere without the compiler complaining. It narrows to the index-signature shape that
|
|
@@ -526,12 +527,13 @@ export async function readAgents(directory, options = {}) {
|
|
|
526
527
|
// invoked BY an agent and never chosen by a human, so listing one in the picker would promise a role
|
|
527
528
|
// the runtime cannot honour.
|
|
528
529
|
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
// package's own agents
|
|
533
|
-
//
|
|
534
|
-
// the
|
|
530
|
+
// THE ORDER IS NOT DECIDED HERE. It is `orderAgentsForDisplay` - the one rule in
|
|
531
|
+
// `src/shared/agent-order.ts`, which the client applies too. The server orders the whole catalogue it
|
|
532
|
+
// read and hands out the half that was asked for, so what leaves the server is ALREADY in display
|
|
533
|
+
// order: the package's own agents first, the conversation roles before the delegated ones, and
|
|
534
|
+
// alphabetical by id inside each. Every surface that draws a server catalogue therefore inherits that
|
|
535
|
+
// order instead of guessing it, and the one place that MERGES two catalogues re-applies the same
|
|
536
|
+
// rule rather than inventing a second one.
|
|
535
537
|
//
|
|
536
538
|
// WHY THE GROUP ORDER DOES NOT TOUCH THE OVERRIDE RULE. The rule that decides WHICH definition of
|
|
537
539
|
// an id survives was applied above, while reading: the first directory to offer an id wins, and the
|
|
@@ -539,12 +541,39 @@ export async function readAgents(directory, options = {}) {
|
|
|
539
541
|
// already survived that decision, so a user's copy of a built-in agent still wins - and it is then
|
|
540
542
|
// listed as the user's, because it IS the user's file.
|
|
541
543
|
return {
|
|
542
|
-
agents: agents
|
|
543
|
-
.filter((agent) => agent.mode === mode)
|
|
544
|
-
.sort((a, b) => (a.builtin === b.builtin ? a.id.localeCompare(b.id) : a.builtin ? -1 : 1)),
|
|
544
|
+
agents: orderAgentsForDisplay(agents).filter((agent) => agent.mode === mode),
|
|
545
545
|
problems,
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* WHAT KIND OF AGENT IS THIS ID?
|
|
550
|
+
*
|
|
551
|
+
* The catalogue is read in halves - the primaries and the subagents - and a question about ONE id
|
|
552
|
+
* has to be asked of both halves. The answer is the `mode:` frontmatter of the definition that
|
|
553
|
+
* WINS, which is the same override rule `readAgents` already applies: the first directory to offer
|
|
554
|
+
* an id owns it.
|
|
555
|
+
*
|
|
556
|
+
* `undefined` MEANS "NO SUCH AGENT IN THESE DIRECTORIES", and that is NOT the same as "not
|
|
557
|
+
* primary". An id nobody has heard of is a CUSTOM agent, and custom agents are legal - §3 keeps
|
|
558
|
+
* them legal on purpose, so that a project can name an agent the catalogue does not carry yet.
|
|
559
|
+
* Only an agent that EXISTS and is a subagent is a subagent.
|
|
560
|
+
*
|
|
561
|
+
* THE HIDDEN ONES COUNT. `hidden` decides whether a person is SHOWN an agent in a picker, not
|
|
562
|
+
* whether the agent exists, so a hidden subagent is still a subagent here. A guard that could be
|
|
563
|
+
* walked around by ticking one box in a frontmatter would not be a guard.
|
|
564
|
+
*/
|
|
565
|
+
export async function agentModeOf(directories, agentId) {
|
|
566
|
+
const id = agentId.trim();
|
|
567
|
+
if (!id)
|
|
568
|
+
return undefined;
|
|
569
|
+
const dirs = Array.isArray(directories) ? [...directories] : [directories];
|
|
570
|
+
for (const mode of ["subagent", "primary"]) {
|
|
571
|
+
const scan = await readAgents(dirs, { mode });
|
|
572
|
+
if (scan.agents.some((agent) => agent.id === id))
|
|
573
|
+
return mode;
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
548
577
|
/** Only `#rgb` and `#rrggbb`. A `color:` frontmatter value is rendered as a CSS background, so it has to
|
|
549
578
|
* be a colour and nothing else - accepting an arbitrary string would put author-controlled text into a
|
|
550
579
|
* style declaration. Anything else is dropped rather than sanitised into something misleading. */
|
|
@@ -80,12 +80,56 @@ export type BudgetRaiseResult = {
|
|
|
80
80
|
/** How many budget exceptions were closed by the raise. */
|
|
81
81
|
resolvedEscalations: number;
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* HOW THE SERVICE ASKS WHAT AN AGENT ID IS.
|
|
85
|
+
*
|
|
86
|
+
* The employee rule is a fact about the agent DIRECTORY, so the directory is read through the one
|
|
87
|
+
* reader the server already has (`readAgents`, through `agentModeOf`), and the question is asked of
|
|
88
|
+
* whoever owns those directories. The service deliberately does not know where the definitions
|
|
89
|
+
* live: the directories change when the user adds one, and a service that kept its own copy of the
|
|
90
|
+
* path would answer about a directory that is no longer there.
|
|
91
|
+
*
|
|
92
|
+
* `undefined` means "no such agent" - NOT "not primary". An id that is in no directory is a custom
|
|
93
|
+
* agent, and §3 keeps custom agents legal.
|
|
94
|
+
*/
|
|
95
|
+
export type AgentModeLookup = {
|
|
96
|
+
agentModeOf(agentId: string): Promise<"primary" | "subagent" | undefined>;
|
|
97
|
+
};
|
|
83
98
|
/** Persist explicit domain fields only; transport credentials never enter project records. */
|
|
84
99
|
export declare class ProjectService {
|
|
85
100
|
private readonly store;
|
|
86
101
|
private readonly orchestrator?;
|
|
102
|
+
/**
|
|
103
|
+
* Optional on purpose: a service built without an agent directory cannot tell a subagent from a
|
|
104
|
+
* custom agent, and it then behaves exactly as it did before this check existed. The real
|
|
105
|
+
* server always passes one.
|
|
106
|
+
*/
|
|
107
|
+
private readonly agents?;
|
|
87
108
|
private readonly queues;
|
|
88
|
-
constructor(store: Store, orchestrator?: (Pick<SessionOrchestratorService, "spawn" | "dispatch"> & Partial<Pick<SessionOrchestratorService, "stopSubtree">>) | undefined
|
|
109
|
+
constructor(store: Store, orchestrator?: (Pick<SessionOrchestratorService, "spawn" | "dispatch"> & Partial<Pick<SessionOrchestratorService, "stopSubtree">>) | undefined,
|
|
110
|
+
/**
|
|
111
|
+
* Optional on purpose: a service built without an agent directory cannot tell a subagent from a
|
|
112
|
+
* custom agent, and it then behaves exactly as it did before this check existed. The real
|
|
113
|
+
* server always passes one.
|
|
114
|
+
*/
|
|
115
|
+
agents?: AgentModeLookup | undefined);
|
|
116
|
+
/**
|
|
117
|
+
* A PROJECT EMPLOYEE MUST BE A PRIMARY AGENT.
|
|
118
|
+
*
|
|
119
|
+
* §1 says a subagent cannot be a project employee: a subagent is a tool-level, one-shot,
|
|
120
|
+
* invisible call, and a project that leans on one leans on a role the user can never see and
|
|
121
|
+
* never interrupt. The client only offers primary agents in the candidate list, but a candidate
|
|
122
|
+
* list is a SUGGESTION - the server is the constraint, and the refusal belongs at the moment the
|
|
123
|
+
* profile is written, while the reader is still looking at the form. Refusing later (when the
|
|
124
|
+
* task is dispatched) teaches the reader that the panel accepted something the project cannot
|
|
125
|
+
* use.
|
|
126
|
+
*
|
|
127
|
+
* WHAT IS REFUSED, AND WHAT IS NOT. An agent that EXISTS and is a subagent is refused, and the
|
|
128
|
+
* message names the slot and the agent, so the reader knows WHICH employee to change. An id that
|
|
129
|
+
* is in NO directory is a custom agent, and §3 keeps custom agents legal - so it is accepted here,
|
|
130
|
+
* exactly as it was before this check existed.
|
|
131
|
+
*/
|
|
132
|
+
private assertEmployee;
|
|
89
133
|
static taskView(task: ProjectTask, project: Pick<Project, "defaultExecutorProfile" | "defaultReviewerProfile">): ProjectTask;
|
|
90
134
|
private filterEscalations;
|
|
91
135
|
private projectView;
|
|
@@ -265,8 +309,8 @@ export declare class ProjectService {
|
|
|
265
309
|
*/
|
|
266
310
|
raiseBudgetAndContinue(id: string, workspaceId: string, callerConversationId?: string): Promise<BudgetRaiseResult>;
|
|
267
311
|
addReport(id: string, input: Omit<ProjectReport, "id" | "projectId" | "createdAt">, workspaceId?: string, actor?: string): Promise<ProjectReport>;
|
|
268
|
-
addEscalation(id: string, input: Pick<ProjectEscalation, "reason" | "severity" | "taskId">, workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
|
|
269
|
-
resolveEscalation(id: string, escalationId: string, decision: string, status: "resolved" | "dismissed", workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
|
|
312
|
+
addEscalation(id: string, input: Pick<ProjectEscalation, "reason" | "severity" | "taskId" | "title">, workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
|
|
313
|
+
resolveEscalation(id: string, escalationId: string, decision: string, status: "resolved" | "dismissed" | "returned", workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
|
|
270
314
|
complete(id: string, summary: string, evidenceRefs: unknown, workspaceId: string, actor?: string): Promise<Project>;
|
|
271
315
|
/**
|
|
272
316
|
* The tasks an initiator hands over with create. A dependency names the TITLE of another task in
|
|
@@ -158,7 +158,7 @@ const REPORT_EVENT_LIMIT = 200;
|
|
|
158
158
|
// THE WORDS THE REPORT SPEAKS IN. A reader of the report is a person, not a console, so every
|
|
159
159
|
// state and every role is written the way a person would say it out loud.
|
|
160
160
|
const PROJECT_STATUS_TEXT = {
|
|
161
|
-
planning: "草稿", active: "进行中", blocked: "已阻塞", awaiting_approval: "等待审批",
|
|
161
|
+
planning: "草稿", awaiting_start: "待启动", active: "进行中", blocked: "已阻塞", awaiting_approval: "等待审批",
|
|
162
162
|
awaiting_review: "等待复核", completed: "已完成", cancelled: "已取消", failed: "失败",
|
|
163
163
|
};
|
|
164
164
|
const TASK_STATUS_TEXT = {
|
|
@@ -171,7 +171,7 @@ const EVENT_TEXT = {
|
|
|
171
171
|
budget: "预算变更", report: "进展汇报", escalated: "例外升级", decision: "决定",
|
|
172
172
|
completed: "项目完成", ready: "依赖就绪", unreachable: "转述对象不可达",
|
|
173
173
|
};
|
|
174
|
-
const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略" };
|
|
174
|
+
const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略", returned: "已退回补充材料" };
|
|
175
175
|
const SEVERITY_TEXT = { low: "低", medium: "中", high: "高" };
|
|
176
176
|
const REPORT_KIND_TEXT = {
|
|
177
177
|
progress: "进展", blocked: "阻塞", delivery: "交付", decision_request: "决策请求", review: "复核",
|
|
@@ -202,10 +202,43 @@ const lightweight = (p) => p.mode === "lightweight";
|
|
|
202
202
|
export class ProjectService {
|
|
203
203
|
store;
|
|
204
204
|
orchestrator;
|
|
205
|
+
agents;
|
|
205
206
|
queues = new Map();
|
|
206
|
-
constructor(store, orchestrator
|
|
207
|
+
constructor(store, orchestrator,
|
|
208
|
+
/**
|
|
209
|
+
* Optional on purpose: a service built without an agent directory cannot tell a subagent from a
|
|
210
|
+
* custom agent, and it then behaves exactly as it did before this check existed. The real
|
|
211
|
+
* server always passes one.
|
|
212
|
+
*/
|
|
213
|
+
agents) {
|
|
207
214
|
this.store = store;
|
|
208
215
|
this.orchestrator = orchestrator;
|
|
216
|
+
this.agents = agents;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A PROJECT EMPLOYEE MUST BE A PRIMARY AGENT.
|
|
220
|
+
*
|
|
221
|
+
* §1 says a subagent cannot be a project employee: a subagent is a tool-level, one-shot,
|
|
222
|
+
* invisible call, and a project that leans on one leans on a role the user can never see and
|
|
223
|
+
* never interrupt. The client only offers primary agents in the candidate list, but a candidate
|
|
224
|
+
* list is a SUGGESTION - the server is the constraint, and the refusal belongs at the moment the
|
|
225
|
+
* profile is written, while the reader is still looking at the form. Refusing later (when the
|
|
226
|
+
* task is dispatched) teaches the reader that the panel accepted something the project cannot
|
|
227
|
+
* use.
|
|
228
|
+
*
|
|
229
|
+
* WHAT IS REFUSED, AND WHAT IS NOT. An agent that EXISTS and is a subagent is refused, and the
|
|
230
|
+
* message names the slot and the agent, so the reader knows WHICH employee to change. An id that
|
|
231
|
+
* is in NO directory is a custom agent, and §3 keeps custom agents legal - so it is accepted here,
|
|
232
|
+
* exactly as it was before this check existed.
|
|
233
|
+
*/
|
|
234
|
+
async assertEmployee(profile, label) {
|
|
235
|
+
const agentId = profile?.agentId;
|
|
236
|
+
if (!agentId || !this.agents)
|
|
237
|
+
return;
|
|
238
|
+
const mode = await this.agents.agentModeOf(agentId);
|
|
239
|
+
if (mode === "subagent") {
|
|
240
|
+
throw new Error(`${label}.agentId 指向的是 subagent(${agentId}),项目员工必须是 primary:subagent 是一次性的工具级调用,用户在侧栏看不到它的中间步骤,也插不上话。`);
|
|
241
|
+
}
|
|
209
242
|
}
|
|
210
243
|
static taskView(task, project) {
|
|
211
244
|
const t = { ...task };
|
|
@@ -475,10 +508,22 @@ export class ProjectService {
|
|
|
475
508
|
const hasLeaderBlockedTask = p.tasks.some((t) => t.status === "blocked" && isLeader(t.blockedReportedByConversationId));
|
|
476
509
|
const lastLeaderProjectReport = [...p.reports].reverse().find((r) => !r.taskId && isLeader(r.authorConversationId));
|
|
477
510
|
const leaderProjectBlocked = lastLeaderProjectReport?.kind === "blocked";
|
|
511
|
+
/* A PROJECT THAT HAS WORK REGISTERED BUT NO LEADER SESSION YET IS NOT RUNNING.
|
|
512
|
+
"active" used to mean "there is at least one task", so a project whose tasks were only
|
|
513
|
+
REGISTERED - nobody had started it, nobody was working on it - claimed on its card to be
|
|
514
|
+
running while the panel's own team section said there were no sessions at all. The card
|
|
515
|
+
and the team list contradicted each other, and the card was the one that lied: the
|
|
516
|
+
project's own status has to say what is true about the project, not what the client
|
|
517
|
+
happens to translate it into. So the state is split:
|
|
518
|
+
planning - nothing has been broken down yet,
|
|
519
|
+
awaiting_start - the work is registered and the project has NOT been started,
|
|
520
|
+
active - the project is started and a leader session exists.
|
|
521
|
+
A project has a leader session only after `start`, so the leader is the signal - not the
|
|
522
|
+
task count, and not anything the client sends. */
|
|
478
523
|
p.status = openBoss.some((e) => e.severity === "high") ? "awaiting_approval"
|
|
479
524
|
: openBoss.length || hasLeaderBlockedTask || leaderProjectBlocked ? "blocked"
|
|
480
525
|
: p.tasks.some((t) => t.status === "awaiting_review") ? "awaiting_review"
|
|
481
|
-
: p.tasks.length ? "active" : "planning";
|
|
526
|
+
: p.tasks.length ? (p.ownerConversationId ? "active" : "awaiting_start") : "planning";
|
|
482
527
|
}
|
|
483
528
|
resources(p) {
|
|
484
529
|
const state = this.store.snapshot();
|
|
@@ -502,7 +547,8 @@ export class ProjectService {
|
|
|
502
547
|
* initiator's conversation ("shared"). The leader is the root of the project's tree, so it is
|
|
503
548
|
* "project" by the same rule as everybody else - there is no special case for it. */
|
|
504
549
|
const projectTreeId = p.ownerConversationId ? this.store.conversation(p.ownerConversationId)?.treeId : undefined;
|
|
505
|
-
const
|
|
550
|
+
const memberConversations = state.conversations.filter((c) => c.workspaceId === p.workspaceId && members.has(c.id));
|
|
551
|
+
const sessions = memberConversations.map((c) => ({
|
|
506
552
|
id: c.id, title: c.title, status: c.status, tokens: tokens(c.usage), contextPercent: c.contextUsage?.percent,
|
|
507
553
|
// THE TREE SKELETON. The client draws the agents of a project as a tree, and the only
|
|
508
554
|
// thing that knows who spawned whom is the conversation's own parent link. It is sent
|
|
@@ -529,12 +575,33 @@ export class ProjectService {
|
|
|
529
575
|
Now it is counted once - through the session - and a project's budget therefore includes what
|
|
530
576
|
its one-shot calls cost, which is the whole point of a budget a person can predict.
|
|
531
577
|
See `Store.accountSubagentUsage`. */
|
|
532
|
-
|
|
578
|
+
/* THE SAME USAGE, TWICE: ONCE IN THE BILLING-EQUIVALENT UNITS THE BUDGET IS SPENT IN, AND
|
|
579
|
+
ONCE IN THE RAW 1:1 UNITS THE READER'S OWN TOOLING COUNTS IN.
|
|
580
|
+
The panel used to show only the equivalent number, and the two differ by a factor of
|
|
581
|
+
two and a half on a real run - so a reader who compared the project's cost with their
|
|
582
|
+
own dashboard saw two different numbers and no way to tell which one the panel meant.
|
|
583
|
+
So the raw components are sent as well: 输入 + 输出 + 缓存写入 + 缓存读取, which the reader
|
|
584
|
+
can add up, and can also feed into the published formula themselves.
|
|
585
|
+
The equivalent number is computed from the SUMMED components and rounded ONCE, not by
|
|
586
|
+
adding up each session's own rounded number: that way the formula printed next to the
|
|
587
|
+
number reproduces the number exactly, instead of being off by a token per session. */
|
|
588
|
+
const usage = memberConversations.reduce((acc, c) => {
|
|
589
|
+
if (!c.usage)
|
|
590
|
+
return acc;
|
|
591
|
+
acc.input += c.usage.input;
|
|
592
|
+
acc.output += c.usage.output;
|
|
593
|
+
acc.cacheRead += c.usage.cacheRead;
|
|
594
|
+
acc.cacheWrite += c.usage.cacheWrite;
|
|
595
|
+
return acc;
|
|
596
|
+
}, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
597
|
+
return { tokens: tokens(usage),
|
|
533
598
|
usageKnown: sessions.length > 0 && sessions.every((s) => Boolean(this.store.conversation(s.id)?.usage)) && runs.every((r) => Boolean(r.usage)),
|
|
534
599
|
activeWorkers: sessions.filter((s) => (!coordinatorIds.has(s.id) || (leaderIsWorker && s.id === p.ownerConversationId)) && busy(s.status)).length + runs.filter((r) => r.status === "running").length,
|
|
535
600
|
// THE PROJECT'S OWN MEMORY TREE, named once so the client does not have to infer it from
|
|
536
601
|
// the sessions. Absent only when the project has no leader session yet.
|
|
537
|
-
memoryTreeId: projectTreeId, sessions
|
|
602
|
+
memoryTreeId: projectTreeId, sessions,
|
|
603
|
+
// THE RAW 1:1 PICTURE OF THE SAME MONEY, next to the equivalent number it is spent in.
|
|
604
|
+
usage, rawTokens: usage.input + usage.output + usage.cacheRead + usage.cacheWrite };
|
|
538
605
|
}
|
|
539
606
|
list(workspaceId) {
|
|
540
607
|
const scope = this.workspace(workspaceId);
|
|
@@ -562,6 +629,12 @@ export class ProjectService {
|
|
|
562
629
|
const reason = blockedTask.blockedReason ? `(${blockedTask.blockedReason})` : "";
|
|
563
630
|
return `项目已阻塞:${blockedTask.title}${reason}`;
|
|
564
631
|
}
|
|
632
|
+
/* THE WORK IS REGISTERED AND THE PROJECT HAS NOT BEEN STARTED. The task is "ready to
|
|
633
|
+
dispatch", but dispatching needs a leader and there is no leader until the project is
|
|
634
|
+
started - so "waiting to dispatch" would point the reader at something nobody can do.
|
|
635
|
+
The headline says what is true, in the same words the card uses. */
|
|
636
|
+
if (total > 0 && !p.ownerConversationId)
|
|
637
|
+
return `已登记 ${total} 个任务,项目待启动`;
|
|
565
638
|
const inProgressTask = p.tasks.find((t) => t.status === "in_progress");
|
|
566
639
|
if (inProgressTask) {
|
|
567
640
|
return `${completed}/${total} 任务完成,正在执行${inProgressTask.title}`;
|
|
@@ -689,7 +762,12 @@ export class ProjectService {
|
|
|
689
762
|
// formula is printed next to the number: a budget nobody can predict is a budget nobody trusts.
|
|
690
763
|
// The sentence after the formula says what the number INCLUDES - a one-shot call (a sub-agent)
|
|
691
764
|
// is part of it, so a reader who dispatches children can see what that cost them.
|
|
692
|
-
|
|
765
|
+
// THE RAW 1:1 FIGURE FOLLOWS IT, because the two differ by a factor of two and a half on a
|
|
766
|
+
// real run, and a reader who checks the project's cost against their own tooling needs to
|
|
767
|
+
// see both numbers and the parts they are made of.
|
|
768
|
+
const usage = this.resources(p);
|
|
769
|
+
lines.push(`- 已用等效 Token:${usage.tokens}(口径:输入 + 输出 + 缓存写入×${CACHE_WRITE_WEIGHT} + 缓存读取×${CACHE_READ_WEIGHT},取整;含项目内的一次性调用(subagent))`);
|
|
770
|
+
lines.push(`- 原始口径(1:1):${usage.rawTokens}(输入 ${usage.usage?.input ?? 0} + 输出 ${usage.usage?.output ?? 0} + 缓存写入 ${usage.usage?.cacheWrite ?? 0} + 缓存读取 ${usage.usage?.cacheRead ?? 0})`);
|
|
693
771
|
lines.push(`- 任务:共 ${p.tasks.length} 个,已完成 ${p.tasks.filter((t) => t.status === "completed").length} 个`);
|
|
694
772
|
lines.push("", "## 交付与验收", "");
|
|
695
773
|
if (!p.tasks.length)
|
|
@@ -775,6 +853,12 @@ export class ProjectService {
|
|
|
775
853
|
const defaultLeaderProfile = profile(input.defaultLeaderProfile, "默认 Leader 配置");
|
|
776
854
|
const defaultExecutorProfile = profile(input.defaultExecutorProfile, "默认执行配置");
|
|
777
855
|
const defaultReviewerProfile = profile(input.defaultReviewerProfile, "默认复核配置");
|
|
856
|
+
// THE PROJECT'S OWN EMPLOYEE DEFAULTS ARE EMPLOYEES TOO. They are the profiles every task
|
|
857
|
+
// inherits when it names no one, so a subagent written here would become the employee of the
|
|
858
|
+
// whole project - and it would be refused at every task that inherited it, one by one, long
|
|
859
|
+
// after the person who wrote it had walked away.
|
|
860
|
+
await this.assertEmployee(defaultExecutorProfile, "defaultExecutorProfile");
|
|
861
|
+
await this.assertEmployee(defaultReviewerProfile, "defaultReviewerProfile");
|
|
778
862
|
const initiator = (initiatorConversationId ?? (typeof input.initiatorConversationId === "string" ? input.initiatorConversationId : undefined))?.trim() || undefined;
|
|
779
863
|
const now = stamp();
|
|
780
864
|
const p = { id: `project_${randomUUID()}`, workspaceId, title: text(input.title, "项目标题"), objective: text(input.objective, "项目目标"),
|
|
@@ -864,7 +948,7 @@ export class ProjectService {
|
|
|
864
948
|
});
|
|
865
949
|
}
|
|
866
950
|
async addTask(id, input, workspaceId, actor) {
|
|
867
|
-
return this.change(id, workspaceId, (p) => {
|
|
951
|
+
return this.change(id, workspaceId, async (p) => {
|
|
868
952
|
this.live(p);
|
|
869
953
|
const parentTaskId = optional(input.parentTaskId, "父任务 id");
|
|
870
954
|
// A LIGHTWEIGHT PROJECT HAS ONE SESSION, SO IT HAS NO TREE OF TASKS. Decomposing the work
|
|
@@ -916,6 +1000,11 @@ export class ProjectService {
|
|
|
916
1000
|
throw new Error("项目最多 100 个任务,请收敛分工范围");
|
|
917
1001
|
const executorProfile = profile(input.executorProfile, "执行配置");
|
|
918
1002
|
const reviewerProfile = profile(input.reviewerProfile, "复核配置");
|
|
1003
|
+
// THE EMPLOYEE IS CHECKED BEFORE THE TASK EXISTS. The refusal happens in the write itself,
|
|
1004
|
+
// so a project never carries a task whose employee is a subagent - not even for the moment
|
|
1005
|
+
// between the write and the next read.
|
|
1006
|
+
await this.assertEmployee(executorProfile, "executorProfile");
|
|
1007
|
+
await this.assertEmployee(reviewerProfile, "reviewerProfile");
|
|
919
1008
|
const now = stamp();
|
|
920
1009
|
const t = { id: `task_${randomUUID()}`, projectId: id, title: text(input.title, "任务标题"), objective: text(input.objective, "任务目标"),
|
|
921
1010
|
status: dependencies.length ? "pending" : "ready", accountableConversationId: manager, parentTaskId, role: input.role ?? "executor", dependencies,
|
|
@@ -1081,7 +1170,7 @@ export class ProjectService {
|
|
|
1081
1170
|
});
|
|
1082
1171
|
}
|
|
1083
1172
|
async updateTask(id, taskId, patch, actor) {
|
|
1084
|
-
const result = await this.change(id, patch.workspaceId, (p) => {
|
|
1173
|
+
const result = await this.change(id, patch.workspaceId, async (p) => {
|
|
1085
1174
|
this.live(p);
|
|
1086
1175
|
const t = this.task(p, taskId);
|
|
1087
1176
|
this.member(p, actor);
|
|
@@ -1115,6 +1204,9 @@ export class ProjectService {
|
|
|
1115
1204
|
if (t.ownerConversationId)
|
|
1116
1205
|
throw new Error("执行会话已创建,禁止修改执行配置");
|
|
1117
1206
|
const ep = profile(patch.executorProfile, "执行配置");
|
|
1207
|
+
// THE CHECK COMES BEFORE THE ASSIGNMENT. The task keeps the employee it had; what it must
|
|
1208
|
+
// not do is end up holding an employee the project is not allowed to have.
|
|
1209
|
+
await this.assertEmployee(ep, "executorProfile");
|
|
1118
1210
|
if (ep)
|
|
1119
1211
|
t.executorProfile = ep;
|
|
1120
1212
|
else
|
|
@@ -1124,6 +1216,7 @@ export class ProjectService {
|
|
|
1124
1216
|
if (t.reviewerConversationId)
|
|
1125
1217
|
throw new Error("复核会话已创建,禁止修改复核配置");
|
|
1126
1218
|
const rp = profile(patch.reviewerProfile, "复核配置");
|
|
1219
|
+
await this.assertEmployee(rp, "reviewerProfile");
|
|
1127
1220
|
if (rp)
|
|
1128
1221
|
t.reviewerProfile = rp;
|
|
1129
1222
|
else
|
|
@@ -1192,6 +1285,12 @@ export class ProjectService {
|
|
|
1192
1285
|
throw new Error("项目完成必须提交最终交付,其他状态由任务和例外推导");
|
|
1193
1286
|
if (status === "active" && p.escalations.some((e) => e.status === "open"))
|
|
1194
1287
|
throw new Error("请先处理未解决的例外");
|
|
1288
|
+
/* A PROJECT WITH NO LEADER SESSION HAS NOTHING RUNNING IN IT. The status is derived from
|
|
1289
|
+
the project's own state, and the API may not push it back into the contradiction that
|
|
1290
|
+
derivation exists to prevent: a project whose tasks were only registered is 待启动, and
|
|
1291
|
+
no caller - the panel included - may call it 执行中 until it has actually been started. */
|
|
1292
|
+
if (status === "active" && !p.ownerConversationId)
|
|
1293
|
+
throw new Error("项目尚未启动,请先启动项目负责人");
|
|
1195
1294
|
p.status = status;
|
|
1196
1295
|
if (closed(p))
|
|
1197
1296
|
p.closedAt = stamp();
|
|
@@ -1323,7 +1422,7 @@ export class ProjectService {
|
|
|
1323
1422
|
if (actor && actor !== p.ownerConversationId && (!t || ![t.ownerConversationId, t.reviewerConversationId, this.manager(p, t)].includes(actor)))
|
|
1324
1423
|
throw new Error("仅可汇报本人负责的任务");
|
|
1325
1424
|
const report = { id: randomUUID(), projectId: id, taskId: t?.id, authorConversationId: actor, kind: input.kind,
|
|
1326
|
-
progress: text(input.progress, "汇报进展"), variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
|
|
1425
|
+
progress: text(input.progress, "汇报进展"), summary: typeof input.summary === "string" && input.summary.trim() ? text(input.summary, "汇报标题") : undefined, variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
|
|
1327
1426
|
decisionNeeded: optional(input.decisionNeeded, "所需决策"), evidenceRefs: strings(input.evidenceRefs, "证据"), createdAt: stamp() };
|
|
1328
1427
|
p.reports.push(report);
|
|
1329
1428
|
if (report.kind === "blocked") {
|
|
@@ -1387,6 +1486,10 @@ export class ProjectService {
|
|
|
1387
1486
|
projectId: id,
|
|
1388
1487
|
taskId: t?.id,
|
|
1389
1488
|
reason: text(input.reason, "升级原因"),
|
|
1489
|
+
/* THE QUESTION IS KEPT BESIDE THE CASE. The title is what the reader decides ON - one
|
|
1490
|
+
sentence, in the reader's terms - and the reason is the case for it. An escalation
|
|
1491
|
+
* without a title is still a valid escalation; it simply has to be read in full. */
|
|
1492
|
+
...(typeof input.title === "string" && input.title.trim() ? { title: text(input.title, "升级标题") } : {}),
|
|
1390
1493
|
severity: input.severity,
|
|
1391
1494
|
status: "open",
|
|
1392
1495
|
requestedByConversationId: actor,
|
|
@@ -1395,19 +1498,19 @@ export class ProjectService {
|
|
|
1395
1498
|
createdAt: stamp(),
|
|
1396
1499
|
};
|
|
1397
1500
|
p.escalations.push(e);
|
|
1398
|
-
this.event(p, "escalated", e.reason, t?.id, actor);
|
|
1501
|
+
this.event(p, "escalated", e.title ? `${e.title}:${e.reason}` : e.reason, t?.id, actor);
|
|
1399
1502
|
this.reconcile(p);
|
|
1400
1503
|
return e;
|
|
1401
1504
|
}, actor);
|
|
1402
1505
|
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);
|
|
1506
|
+
await this.wake(supervisorToWake, `下级成员(会话:${actor},身份:${wakeMemberRole ?? "执行者"})发起例外升级(ID:${result.id}):\n${result.title ? `待决问题:${result.title}\n` : ""}原因:${result.reason}\n严重级别:${result.severity}\n请及时处理该例外(调用 project action="resolve_escalation" 记录处理意见),或按需向上级转述升级。`, `例外升级(ID:${result.id})`, id, workspaceId, actor);
|
|
1404
1507
|
}
|
|
1405
1508
|
return result;
|
|
1406
1509
|
}
|
|
1407
1510
|
async resolveEscalation(id, escalationId, decision, status, workspaceId, actor) {
|
|
1408
1511
|
const result = await this.change(id, workspaceId, (p) => {
|
|
1409
1512
|
this.live(p);
|
|
1410
|
-
if (status !== "resolved" && status !== "dismissed")
|
|
1513
|
+
if (status !== "resolved" && status !== "dismissed" && status !== "returned")
|
|
1411
1514
|
throw new Error("升级处理状态不合法");
|
|
1412
1515
|
const e = p.escalations.find((e) => e.id === escalationId);
|
|
1413
1516
|
if (!e)
|
|
@@ -1430,10 +1533,24 @@ export class ProjectService {
|
|
|
1430
1533
|
e.decision = text(decision, "处理意见");
|
|
1431
1534
|
e.resolvedAt = stamp();
|
|
1432
1535
|
const decider = audience === "boss" ? "Boss" : "直接上级";
|
|
1433
|
-
|
|
1536
|
+
/* A RETURN IS NOT A DECISION. It says the case was not ready - the reader is being asked
|
|
1537
|
+
* for more material, not for a ruling - so the timeline says so in those words. */
|
|
1538
|
+
this.event(p, "decision", status === "returned"
|
|
1539
|
+
? `${decider}退回例外,等待补充材料:${e.decision}`
|
|
1540
|
+
: `${decider}决策:${e.decision}`, e.taskId, actor);
|
|
1434
1541
|
this.reconcile(p);
|
|
1435
1542
|
return e;
|
|
1436
1543
|
}, actor);
|
|
1544
|
+
/* THE CASE GOES BACK TO WHO RAISED IT. A returned escalation leaves the inbox without being
|
|
1545
|
+
* answered, so nobody would ever know it was still open - unless the person who raised it is
|
|
1546
|
+
* woken up and told to bring the missing material. This is the one wake that is NOT about a
|
|
1547
|
+
* decision: it is about work coming back. */
|
|
1548
|
+
if (status === "returned") {
|
|
1549
|
+
if (result.requestedByConversationId) {
|
|
1550
|
+
await this.wake(result.requestedByConversationId, `例外已退回,等待对方补充材料(ID:${result.id}):${result.title ?? result.reason}\n退回意见:${result.decision}\n请补充材料后重新发起升级。`, `例外已退回(ID:${result.id})`, id, workspaceId, actor);
|
|
1551
|
+
}
|
|
1552
|
+
return result;
|
|
1553
|
+
}
|
|
1437
1554
|
const p = this.raw(id, workspaceId);
|
|
1438
1555
|
if (result.audience === "supervisor") {
|
|
1439
1556
|
if (result.requestedByConversationId) {
|
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { join, resolve } from "node:path";
|
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { openStateStore } from "./persistence/open-state-store.js";
|
|
11
11
|
import { PiRpcRuntime, WelcomeRuntime } from "./runtime.js";
|
|
12
|
-
import { resolveAgentDirectories } from "./agents.js";
|
|
12
|
+
import { agentModeOf, resolveAgentDirectories } from "./agents.js";
|
|
13
13
|
import { releaseAllCapabilities } from "./capability.js";
|
|
14
14
|
import { SubagentSupervisor } from "./subagents.js";
|
|
15
15
|
import { SubagentRunRecorder } from "./subagent-runs.js";
|
|
@@ -297,7 +297,18 @@ runService.setExecService(execService);
|
|
|
297
297
|
const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
|
|
298
298
|
const sessionLedger = new SessionLedger();
|
|
299
299
|
const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
|
|
300
|
-
|
|
300
|
+
/**
|
|
301
|
+
* THE PROJECT SERVICE IS TOLD HOW TO ASK WHAT AN AGENT IS.
|
|
302
|
+
*
|
|
303
|
+
* The employee rule of §1 - "a subagent cannot be a project employee" - is a fact about the agent
|
|
304
|
+
* DIRECTORY, and the directory is read through the same reader the picker and the runtime use
|
|
305
|
+
* (`readAgents`, through `agentModeOf`). The directories are asked of the catalogue service
|
|
306
|
+
* because THEY are the live ones: the user can add an agents directory at any time, and a guard
|
|
307
|
+
* that read a path captured at startup would answer about a directory that is no longer there.
|
|
308
|
+
*/
|
|
309
|
+
const projectService = new ProjectService(store, sessionOrchestratorService, {
|
|
310
|
+
agentModeOf: (agentId) => agentModeOf(catalogService.getAgentDirs(), agentId),
|
|
311
|
+
});
|
|
301
312
|
void sessionOrchestratorService.restoreWatchdogs().catch((error) => {
|
|
302
313
|
console.warn("Super Pi: restoring watchdogs failed:", error instanceof Error ? error.message : String(error));
|
|
303
314
|
});
|
package/dist/server/types.d.ts
CHANGED
|
@@ -939,7 +939,7 @@ export type TerminalShell = {
|
|
|
939
939
|
file: string;
|
|
940
940
|
args: string[];
|
|
941
941
|
};
|
|
942
|
-
export type ProjectStatus = "planning" | "active" | "blocked" | "awaiting_approval" | "awaiting_review" | "completed" | "failed" | "cancelled";
|
|
942
|
+
export type ProjectStatus = "planning" | "awaiting_start" | "active" | "blocked" | "awaiting_approval" | "awaiting_review" | "completed" | "failed" | "cancelled";
|
|
943
943
|
export type ProjectTaskStatus = "pending" | "ready" | "in_progress" | "blocked" | "awaiting_review" | "completed" | "failed" | "cancelled";
|
|
944
944
|
export type ProjectRisk = "low" | "medium" | "high";
|
|
945
945
|
/**
|
|
@@ -1011,6 +1011,7 @@ export type ProjectReport = {
|
|
|
1011
1011
|
authorConversationId?: string;
|
|
1012
1012
|
kind: "progress" | "blocked" | "delivery" | "decision_request" | "review";
|
|
1013
1013
|
progress: string;
|
|
1014
|
+
summary?: string;
|
|
1014
1015
|
variance?: string;
|
|
1015
1016
|
nextStep?: string;
|
|
1016
1017
|
risks?: string[];
|
|
@@ -1022,9 +1023,12 @@ export type ProjectEscalation = {
|
|
|
1022
1023
|
id: string;
|
|
1023
1024
|
projectId: string;
|
|
1024
1025
|
taskId?: string;
|
|
1025
|
-
reason: string;
|
|
1026
|
+
reason: string; /** THE QUESTION, IN ONE SENTENCE. What the Boss has to decide - as opposed to `reason`,
|
|
1027
|
+
* which is the case for it. The reader decides in a glance, and reads the case only if the
|
|
1028
|
+
* question is not obvious; an escalation without a title falls back to the reason. */
|
|
1029
|
+
title?: string;
|
|
1026
1030
|
severity: ProjectRisk;
|
|
1027
|
-
status: "open" | "resolved" | "dismissed";
|
|
1031
|
+
status: "open" | "resolved" | "dismissed" | "returned";
|
|
1028
1032
|
requestedByConversationId?: string;
|
|
1029
1033
|
decision?: string;
|
|
1030
1034
|
createdAt: string;
|
|
@@ -1095,6 +1099,13 @@ export type Project = {
|
|
|
1095
1099
|
usageKnown: boolean;
|
|
1096
1100
|
activeWorkers: number;
|
|
1097
1101
|
memoryTreeId?: string;
|
|
1102
|
+
usage?: {
|
|
1103
|
+
input: number;
|
|
1104
|
+
output: number;
|
|
1105
|
+
cacheRead: number;
|
|
1106
|
+
cacheWrite: number;
|
|
1107
|
+
};
|
|
1108
|
+
rawTokens?: number;
|
|
1098
1109
|
sessions: {
|
|
1099
1110
|
id: string;
|
|
1100
1111
|
title: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE order agent lists are shown in.
|
|
3
|
+
*
|
|
4
|
+
* WHY THERE IS A SINGLE RULE AND NOT A RULE PER SIDE. The picker's list, the settings page's grid, the
|
|
5
|
+
* sub-agent roster and the project panel's employee candidates are four views of the same catalogue, and
|
|
6
|
+
* they used to disagree about what a list of agents looks like. The server sorted what it read by
|
|
7
|
+
* "(built-in first, then id)", and the client re-sorted what the server sent by a rule of its own that
|
|
8
|
+
* added one more level - "roles before workers". A reader who saw the same agents in two orders had no
|
|
9
|
+
* way to tell which screen was wrong, because neither was: they were two answers to a question nobody
|
|
10
|
+
* had asked twice the same way.
|
|
11
|
+
*
|
|
12
|
+
* So the rule is stated once, HERE, and both sides apply it. The server imports it to order the
|
|
13
|
+
* catalogue it reads; the client imports it to order every list it draws. There is no second
|
|
14
|
+
* implementation to drift, because there is no second implementation.
|
|
15
|
+
*
|
|
16
|
+
* THE ORDER IS:
|
|
17
|
+
*
|
|
18
|
+
* 1. THE PACKAGE'S OWN agents first, then the user's. The ones that ship with Super Pi are drawn with
|
|
19
|
+
* a colour dot, and a dot scattered among the user's own reads as noise rather than as a group.
|
|
20
|
+
* 2. Inside a group, the CONVERSATION ROLES (mode: primary) before the DELEGATED ones (mode:
|
|
21
|
+
* subagent), so a list reads as "the roles, then the workers".
|
|
22
|
+
* 3. Inside a group and a role, alphabetical by id, so the order never depends on the order the
|
|
23
|
+
* filesystem happened to hand the files over in.
|
|
24
|
+
*
|
|
25
|
+
* WHY THE FLAG IS NOT GUESSED HERE. "Is this the package's own agent?" is a question about where the app
|
|
26
|
+
* is installed, and only the server knows that: it reads the definitions and marks them. A client that
|
|
27
|
+
* tried to work it out from the directory path would be guessing, and would guess wrong the moment the
|
|
28
|
+
* package is installed somewhere unusual - which is exactly the case where the answer matters.
|
|
29
|
+
*/
|
|
30
|
+
/** The minimum an agent has to carry for this ordering to be able to place it. */
|
|
31
|
+
export type OrderableAgent = {
|
|
32
|
+
id: string;
|
|
33
|
+
mode?: "primary" | "subagent";
|
|
34
|
+
/** True when the definition came from the package's own agents directory. */
|
|
35
|
+
builtin?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The built-in group first, then the user's, each group by role and then by id.
|
|
39
|
+
*
|
|
40
|
+
* An agent with no `builtin` flag at all (an older server, or a hand-built fixture) counts as the
|
|
41
|
+
* user's, which is the safe direction: it puts the unflagged one where a user's own would sit rather
|
|
42
|
+
* than promoting it into the group that means "this ships with Super Pi".
|
|
43
|
+
*/
|
|
44
|
+
export declare function orderAgentsForDisplay<T extends OrderableAgent>(agents: readonly T[]): T[];
|