@evandu/super-pi 0.1.165 → 0.1.167
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 +10 -6
- package/agents/verifier.md +6 -2
- package/dist/client/.super-pi-client-sources.json +10 -9
- package/dist/client/assets/{CodeMirrorEditor-B3-4Qpd1.js → CodeMirrorEditor-CVWde5kc.js} +2 -2
- package/dist/client/assets/{dist-Bh9jVMEA.js → dist-4fGafgy-.js} +1 -1
- package/dist/client/assets/{dist-D45Y5G5v.js → dist-BCl45PhR.js} +1 -1
- package/dist/client/assets/{dist-CL9_Ff9R.js → dist-BHFNgK-o.js} +1 -1
- package/dist/client/assets/{dist-DGOgW2hA.js → dist-BNHp4wy-.js} +1 -1
- package/dist/client/assets/{dist-DtNwDyK1.js → dist-BmMZE70t.js} +1 -1
- package/dist/client/assets/{dist-BWsed8P8.js → dist-BmTCWOAf.js} +1 -1
- package/dist/client/assets/{dist-wW-2h0X5.js → dist-C44HyASP.js} +1 -1
- package/dist/client/assets/{dist-BhiW2HYK.js → dist-C5vtfmg_.js} +1 -1
- package/dist/client/assets/{dist-C2mhG_5m.js → dist-CC90sIpG.js} +1 -1
- package/dist/client/assets/{dist-2Ax3GOhT.js → dist-CH83_wek.js} +1 -1
- package/dist/client/assets/{dist-Cn4Q7-eG.js → dist-CYucB1mz.js} +1 -1
- package/dist/client/assets/{dist-BD4w6y25.js → dist-D7VBYFBQ.js} +1 -1
- package/dist/client/assets/{dist-Do72hsvz.js → dist-DInnP2s9.js} +1 -1
- package/dist/client/assets/{dist-DQnuHIqA.js → dist-DOYAWZox.js} +1 -1
- package/dist/client/assets/{dist-CN6rdrJA.js → dist-DQ97Hq65.js} +1 -1
- package/dist/client/assets/{dist-BIqjsoet.js → dist-DTOebxDL.js} +1 -1
- package/dist/client/assets/{dist-Di8I1Dsi.js → dist-DUgYAI4d.js} +1 -1
- package/dist/client/assets/{dist-QTqWnNGE.js → dist-DaEO5rUQ.js} +1 -1
- package/dist/client/assets/{dist-rFG_iRW3.js → dist-Dn-GoU5Q.js} +1 -1
- package/dist/client/assets/{dist-hUAwe2Kn.js → dist-FPYonm2j.js} +1 -1
- package/dist/client/assets/{dist-DRgOr5sP.js → dist-GZFbu49l.js} +1 -1
- package/dist/client/assets/{dist-DF05sEDO.js → dist-RLM_LWKf.js} +1 -1
- package/dist/client/assets/{index-BUTxkA3D.js → index-BptmWfVR.js} +19 -19
- package/dist/client/assets/index-DWjVnvsQ.css +1 -0
- 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.js +67 -6
- 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 +48 -2
- package/dist/shared/agent-order.d.ts +44 -0
- package/dist/shared/agent-order.js +50 -0
- package/package.json +12 -2
- package/dist/client/assets/index-Wtlj0s4R.css +0 -1
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
|
|
@@ -392,6 +393,18 @@ export function expandTilde(filepath) {
|
|
|
392
393
|
}
|
|
393
394
|
return filepath;
|
|
394
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* The agents directory that ships WITH the package: the definitions Super Pi itself carries.
|
|
398
|
+
*
|
|
399
|
+
* WHY IT IS A FUNCTION AND NOT A CONSTANT. The package root is not the same directory in every
|
|
400
|
+
* layout - it is the repository root in a checkout and the installed package root in an npm install
|
|
401
|
+
* - so the path is derived from the same `rootDir` the discovery uses, and both sides of the
|
|
402
|
+
* "is this built-in?" question ask THIS function. If the two ever disagreed, a user agent could be
|
|
403
|
+
* filed as built-in and a built-in one as the user's, and the picker would sort them wrongly.
|
|
404
|
+
*/
|
|
405
|
+
export function builtinAgentsDir(rootDir) {
|
|
406
|
+
return resolve(join(rootDir ?? resolve(import.meta.dirname, "../.."), "agents"));
|
|
407
|
+
}
|
|
395
408
|
/**
|
|
396
409
|
* Resolves the ordered list of directories to discover agents from:
|
|
397
410
|
* 1. Custom directories configured by the user in app settings (or options)
|
|
@@ -433,11 +446,25 @@ export function resolveAgentDirectories(options = {}) {
|
|
|
433
446
|
catch {
|
|
434
447
|
// homedir() might throw in restricted sandbox environments
|
|
435
448
|
}
|
|
436
|
-
|
|
437
|
-
add(join(root, "agents"));
|
|
449
|
+
add(builtinAgentsDir(options.rootDir));
|
|
438
450
|
}
|
|
439
451
|
return dirs;
|
|
440
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Whether two paths name the same directory.
|
|
455
|
+
*
|
|
456
|
+
* WHY THE COMPARISON IS NOT A PLAIN `===`. The paths that reach here were built by joining and
|
|
457
|
+
* resolving, so they are already absolute - but Windows paths are case-insensitive and a directory
|
|
458
|
+
* typed by hand (`d:\agents`) is the same directory as the one the app resolved itself
|
|
459
|
+
* (`D:\Agents`). Comparing them as strings would call the package's own agents directory "the user's"
|
|
460
|
+
* and put the built-in agents at the bottom of the picker, which is the defect this whole change is
|
|
461
|
+
* about.
|
|
462
|
+
*/
|
|
463
|
+
function sameDirectory(left, right) {
|
|
464
|
+
const a = resolve(left);
|
|
465
|
+
const b = resolve(right);
|
|
466
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
467
|
+
}
|
|
441
468
|
export async function readAgents(directory, options = {}) {
|
|
442
469
|
const mode = options.mode ?? "primary";
|
|
443
470
|
const rawDirs = Array.isArray(directory) ? directory : [directory];
|
|
@@ -445,6 +472,9 @@ export async function readAgents(directory, options = {}) {
|
|
|
445
472
|
const agents = [];
|
|
446
473
|
const problems = [];
|
|
447
474
|
const seenIds = new Set();
|
|
475
|
+
// The directory whose definitions are the PACKAGE'S OWN. Everything else is the user's, and the
|
|
476
|
+
// two are listed separately so the package's agents can come first (see the sort at the end).
|
|
477
|
+
const builtinDir = options.builtinDir ?? builtinAgentsDir();
|
|
448
478
|
for (const dir of dirs) {
|
|
449
479
|
let entries;
|
|
450
480
|
try {
|
|
@@ -455,6 +485,7 @@ export async function readAgents(directory, options = {}) {
|
|
|
455
485
|
continue;
|
|
456
486
|
throw error;
|
|
457
487
|
}
|
|
488
|
+
const isBuiltin = sameDirectory(dir, builtinDir);
|
|
458
489
|
for (const file of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
|
|
459
490
|
// A FILE THAT CANNOT BE READ IS THE SAME KIND OF PROBLEM AS ONE THAT CANNOT BE PARSED.
|
|
460
491
|
//
|
|
@@ -487,17 +518,30 @@ export async function readAgents(directory, options = {}) {
|
|
|
487
518
|
else {
|
|
488
519
|
if (!seenIds.has(parsed.id)) {
|
|
489
520
|
seenIds.add(parsed.id);
|
|
490
|
-
agents.push({ ...parsed, sourceDir: dir });
|
|
521
|
+
agents.push({ ...parsed, sourceDir: dir, builtin: isBuiltin });
|
|
491
522
|
}
|
|
492
523
|
}
|
|
493
524
|
}
|
|
494
525
|
}
|
|
495
526
|
// The half that was NOT asked for is filtered out here rather than marked in the UI: sub-agents are
|
|
496
527
|
// invoked BY an agent and never chosen by a human, so listing one in the picker would promise a role
|
|
497
|
-
// the runtime cannot honour.
|
|
498
|
-
//
|
|
528
|
+
// the runtime cannot honour.
|
|
529
|
+
//
|
|
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.
|
|
537
|
+
//
|
|
538
|
+
// WHY THE GROUP ORDER DOES NOT TOUCH THE OVERRIDE RULE. The rule that decides WHICH definition of
|
|
539
|
+
// an id survives was applied above, while reading: the first directory to offer an id wins, and the
|
|
540
|
+
// user's directories are read before the package's own. Sorting here only reorders definitions that
|
|
541
|
+
// already survived that decision, so a user's copy of a built-in agent still wins - and it is then
|
|
542
|
+
// listed as the user's, because it IS the user's file.
|
|
499
543
|
return {
|
|
500
|
-
agents: agents.filter((agent) => agent.mode === mode)
|
|
544
|
+
agents: orderAgentsForDisplay(agents).filter((agent) => agent.mode === mode),
|
|
501
545
|
problems,
|
|
502
546
|
};
|
|
503
547
|
}
|
|
@@ -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 = {
|
|
@@ -475,10 +475,22 @@ export class ProjectService {
|
|
|
475
475
|
const hasLeaderBlockedTask = p.tasks.some((t) => t.status === "blocked" && isLeader(t.blockedReportedByConversationId));
|
|
476
476
|
const lastLeaderProjectReport = [...p.reports].reverse().find((r) => !r.taskId && isLeader(r.authorConversationId));
|
|
477
477
|
const leaderProjectBlocked = lastLeaderProjectReport?.kind === "blocked";
|
|
478
|
+
/* A PROJECT THAT HAS WORK REGISTERED BUT NO LEADER SESSION YET IS NOT RUNNING.
|
|
479
|
+
"active" used to mean "there is at least one task", so a project whose tasks were only
|
|
480
|
+
REGISTERED - nobody had started it, nobody was working on it - claimed on its card to be
|
|
481
|
+
running while the panel's own team section said there were no sessions at all. The card
|
|
482
|
+
and the team list contradicted each other, and the card was the one that lied: the
|
|
483
|
+
project's own status has to say what is true about the project, not what the client
|
|
484
|
+
happens to translate it into. So the state is split:
|
|
485
|
+
planning - nothing has been broken down yet,
|
|
486
|
+
awaiting_start - the work is registered and the project has NOT been started,
|
|
487
|
+
active - the project is started and a leader session exists.
|
|
488
|
+
A project has a leader session only after `start`, so the leader is the signal - not the
|
|
489
|
+
task count, and not anything the client sends. */
|
|
478
490
|
p.status = openBoss.some((e) => e.severity === "high") ? "awaiting_approval"
|
|
479
491
|
: openBoss.length || hasLeaderBlockedTask || leaderProjectBlocked ? "blocked"
|
|
480
492
|
: p.tasks.some((t) => t.status === "awaiting_review") ? "awaiting_review"
|
|
481
|
-
: p.tasks.length ? "active" : "planning";
|
|
493
|
+
: p.tasks.length ? (p.ownerConversationId ? "active" : "awaiting_start") : "planning";
|
|
482
494
|
}
|
|
483
495
|
resources(p) {
|
|
484
496
|
const state = this.store.snapshot();
|
|
@@ -502,7 +514,8 @@ export class ProjectService {
|
|
|
502
514
|
* initiator's conversation ("shared"). The leader is the root of the project's tree, so it is
|
|
503
515
|
* "project" by the same rule as everybody else - there is no special case for it. */
|
|
504
516
|
const projectTreeId = p.ownerConversationId ? this.store.conversation(p.ownerConversationId)?.treeId : undefined;
|
|
505
|
-
const
|
|
517
|
+
const memberConversations = state.conversations.filter((c) => c.workspaceId === p.workspaceId && members.has(c.id));
|
|
518
|
+
const sessions = memberConversations.map((c) => ({
|
|
506
519
|
id: c.id, title: c.title, status: c.status, tokens: tokens(c.usage), contextPercent: c.contextUsage?.percent,
|
|
507
520
|
// THE TREE SKELETON. The client draws the agents of a project as a tree, and the only
|
|
508
521
|
// thing that knows who spawned whom is the conversation's own parent link. It is sent
|
|
@@ -521,12 +534,41 @@ export class ProjectService {
|
|
|
521
534
|
report zero running agents. The concurrency guard is not touched by this: the in-flight task
|
|
522
535
|
it counts (`reserved`) already covers the busy leader, so the cap behaves exactly as before. */
|
|
523
536
|
const leaderIsWorker = lightweight(p) && p.ownerConversationId !== undefined;
|
|
524
|
-
|
|
537
|
+
/* ONE ACCOUNTING PATH, AND IT IS THE SESSION TREE.
|
|
538
|
+
The number below is the sum of the member sessions' usage, and NOTHING ELSE. A one-shot call
|
|
539
|
+
(a sub-agent) used to be added here from the run records on top of that sum, which was a
|
|
540
|
+
SECOND path to the same money: the run's usage was already inside the conversation's usage
|
|
541
|
+
(the store folds it in when the run settles), so the same tokens were counted twice.
|
|
542
|
+
Now it is counted once - through the session - and a project's budget therefore includes what
|
|
543
|
+
its one-shot calls cost, which is the whole point of a budget a person can predict.
|
|
544
|
+
See `Store.accountSubagentUsage`. */
|
|
545
|
+
/* THE SAME USAGE, TWICE: ONCE IN THE BILLING-EQUIVALENT UNITS THE BUDGET IS SPENT IN, AND
|
|
546
|
+
ONCE IN THE RAW 1:1 UNITS THE READER'S OWN TOOLING COUNTS IN.
|
|
547
|
+
The panel used to show only the equivalent number, and the two differ by a factor of
|
|
548
|
+
two and a half on a real run - so a reader who compared the project's cost with their
|
|
549
|
+
own dashboard saw two different numbers and no way to tell which one the panel meant.
|
|
550
|
+
So the raw components are sent as well: 输入 + 输出 + 缓存写入 + 缓存读取, which the reader
|
|
551
|
+
can add up, and can also feed into the published formula themselves.
|
|
552
|
+
The equivalent number is computed from the SUMMED components and rounded ONCE, not by
|
|
553
|
+
adding up each session's own rounded number: that way the formula printed next to the
|
|
554
|
+
number reproduces the number exactly, instead of being off by a token per session. */
|
|
555
|
+
const usage = memberConversations.reduce((acc, c) => {
|
|
556
|
+
if (!c.usage)
|
|
557
|
+
return acc;
|
|
558
|
+
acc.input += c.usage.input;
|
|
559
|
+
acc.output += c.usage.output;
|
|
560
|
+
acc.cacheRead += c.usage.cacheRead;
|
|
561
|
+
acc.cacheWrite += c.usage.cacheWrite;
|
|
562
|
+
return acc;
|
|
563
|
+
}, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
564
|
+
return { tokens: tokens(usage),
|
|
525
565
|
usageKnown: sessions.length > 0 && sessions.every((s) => Boolean(this.store.conversation(s.id)?.usage)) && runs.every((r) => Boolean(r.usage)),
|
|
526
566
|
activeWorkers: sessions.filter((s) => (!coordinatorIds.has(s.id) || (leaderIsWorker && s.id === p.ownerConversationId)) && busy(s.status)).length + runs.filter((r) => r.status === "running").length,
|
|
527
567
|
// THE PROJECT'S OWN MEMORY TREE, named once so the client does not have to infer it from
|
|
528
568
|
// the sessions. Absent only when the project has no leader session yet.
|
|
529
|
-
memoryTreeId: projectTreeId, sessions
|
|
569
|
+
memoryTreeId: projectTreeId, sessions,
|
|
570
|
+
// THE RAW 1:1 PICTURE OF THE SAME MONEY, next to the equivalent number it is spent in.
|
|
571
|
+
usage, rawTokens: usage.input + usage.output + usage.cacheRead + usage.cacheWrite };
|
|
530
572
|
}
|
|
531
573
|
list(workspaceId) {
|
|
532
574
|
const scope = this.workspace(workspaceId);
|
|
@@ -554,6 +596,12 @@ export class ProjectService {
|
|
|
554
596
|
const reason = blockedTask.blockedReason ? `(${blockedTask.blockedReason})` : "";
|
|
555
597
|
return `项目已阻塞:${blockedTask.title}${reason}`;
|
|
556
598
|
}
|
|
599
|
+
/* THE WORK IS REGISTERED AND THE PROJECT HAS NOT BEEN STARTED. The task is "ready to
|
|
600
|
+
dispatch", but dispatching needs a leader and there is no leader until the project is
|
|
601
|
+
started - so "waiting to dispatch" would point the reader at something nobody can do.
|
|
602
|
+
The headline says what is true, in the same words the card uses. */
|
|
603
|
+
if (total > 0 && !p.ownerConversationId)
|
|
604
|
+
return `已登记 ${total} 个任务,项目待启动`;
|
|
557
605
|
const inProgressTask = p.tasks.find((t) => t.status === "in_progress");
|
|
558
606
|
if (inProgressTask) {
|
|
559
607
|
return `${completed}/${total} 任务完成,正在执行${inProgressTask.title}`;
|
|
@@ -679,7 +727,14 @@ export class ProjectService {
|
|
|
679
727
|
lines.push(`- 预算上限:${p.budget?.maxTokens ?? DEFAULT_MAX_TOKENS} Token`);
|
|
680
728
|
// THE USAGE IS REPORTED IN THE SAME BILLING-EQUIVALENT UNITS THE BUDGET IS SPENT IN, and the
|
|
681
729
|
// formula is printed next to the number: a budget nobody can predict is a budget nobody trusts.
|
|
682
|
-
|
|
730
|
+
// The sentence after the formula says what the number INCLUDES - a one-shot call (a sub-agent)
|
|
731
|
+
// is part of it, so a reader who dispatches children can see what that cost them.
|
|
732
|
+
// THE RAW 1:1 FIGURE FOLLOWS IT, because the two differ by a factor of two and a half on a
|
|
733
|
+
// real run, and a reader who checks the project's cost against their own tooling needs to
|
|
734
|
+
// see both numbers and the parts they are made of.
|
|
735
|
+
const usage = this.resources(p);
|
|
736
|
+
lines.push(`- 已用等效 Token:${usage.tokens}(口径:输入 + 输出 + 缓存写入×${CACHE_WRITE_WEIGHT} + 缓存读取×${CACHE_READ_WEIGHT},取整;含项目内的一次性调用(subagent))`);
|
|
737
|
+
lines.push(`- 原始口径(1:1):${usage.rawTokens}(输入 ${usage.usage?.input ?? 0} + 输出 ${usage.usage?.output ?? 0} + 缓存写入 ${usage.usage?.cacheWrite ?? 0} + 缓存读取 ${usage.usage?.cacheRead ?? 0})`);
|
|
683
738
|
lines.push(`- 任务:共 ${p.tasks.length} 个,已完成 ${p.tasks.filter((t) => t.status === "completed").length} 个`);
|
|
684
739
|
lines.push("", "## 交付与验收", "");
|
|
685
740
|
if (!p.tasks.length)
|
|
@@ -1182,6 +1237,12 @@ export class ProjectService {
|
|
|
1182
1237
|
throw new Error("项目完成必须提交最终交付,其他状态由任务和例外推导");
|
|
1183
1238
|
if (status === "active" && p.escalations.some((e) => e.status === "open"))
|
|
1184
1239
|
throw new Error("请先处理未解决的例外");
|
|
1240
|
+
/* A PROJECT WITH NO LEADER SESSION HAS NOTHING RUNNING IN IT. The status is derived from
|
|
1241
|
+
the project's own state, and the API may not push it back into the contradiction that
|
|
1242
|
+
derivation exists to prevent: a project whose tasks were only registered is 待启动, and
|
|
1243
|
+
no caller - the panel included - may call it 执行中 until it has actually been started. */
|
|
1244
|
+
if (status === "active" && !p.ownerConversationId)
|
|
1245
|
+
throw new Error("项目尚未启动,请先启动项目负责人");
|
|
1185
1246
|
p.status = status;
|
|
1186
1247
|
if (closed(p))
|
|
1187
1248
|
p.closedAt = stamp();
|
package/dist/server/store.d.ts
CHANGED
|
@@ -924,6 +924,23 @@ export declare class Store {
|
|
|
924
924
|
* as durable as the conversation it belongs to.
|
|
925
925
|
*/
|
|
926
926
|
setSubagentRun(runId: string, changes: SubagentRunUpdate): Promise<SubagentRun>;
|
|
927
|
+
/**
|
|
928
|
+
* Adds ONE settled run's usage to the ledger of the conversation that dispatched it.
|
|
929
|
+
*
|
|
930
|
+
* CALLED AT THE SETTLE, ONCE PER RUN, and the run's FINAL accumulated usage is what it reads - the
|
|
931
|
+
* run record already holds the total of every finished message the child produced, so nothing is
|
|
932
|
+
* re-derived here and no message is counted twice.
|
|
933
|
+
*
|
|
934
|
+
* THE FLAG AND THE LEDGER ENTRY ARE WRITTEN IN ONE WRITE, and that is the whole crash-safety
|
|
935
|
+
* argument: `foldRunUsage` mutates both in the same synchronous block and this persists the state
|
|
936
|
+
* once, so a server that dies here cannot leave the money counted twice (flag written, entry not)
|
|
937
|
+
* or not at all (entry written, flag not). A run that was folded already is left alone, so calling
|
|
938
|
+
* this twice is a no-op rather than a second bill.
|
|
939
|
+
*
|
|
940
|
+
* IT PERSISTS THE WHOLE STATE rather than the two rows, because the two rows ARE one fact: the
|
|
941
|
+
* run's flag and the conversation's ledger entry have to become true together or not at all.
|
|
942
|
+
*/
|
|
943
|
+
accountSubagentUsage(runId: string): Promise<boolean>;
|
|
927
944
|
/**
|
|
928
945
|
* Replaces the model's task list, and persists it.
|
|
929
946
|
*
|
package/dist/server/store.js
CHANGED
|
@@ -105,6 +105,71 @@ function reconcileSubagentRuns(state) {
|
|
|
105
105
|
}
|
|
106
106
|
return orphaned;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* ONE RUN'S USAGE, FOLDED INTO THE LEDGER OF THE CONVERSATION THAT DISPATCHED IT.
|
|
110
|
+
*
|
|
111
|
+
* WHY THE CHILD'S MONEY BELONGS TO THE PARENT'S LEDGER. A one-shot call spends real money on a
|
|
112
|
+
* child Pi, and the only number a budget is compared against is a conversation's `usage`. A ledger
|
|
113
|
+
* that held the parent's own turns alone would let a conversation multiply its real cost past its
|
|
114
|
+
* own cap simply by dispatching children instead of answering - which is precisely what design §5
|
|
115
|
+
* exists to prevent ("写预算的人必须能自己算出这个数是多少钱"). So the child's FINAL accumulated
|
|
116
|
+
* usage is added to the parent's, once, when the run settles.
|
|
117
|
+
*
|
|
118
|
+
* ONCE, AND ONLY ONCE, AND WHY THE FLAG IS THE WHOLE MECHANISM. A run reports its usage on every
|
|
119
|
+
* finished assistant message, and the recorder settles the run with the run's TOTAL - so folding
|
|
120
|
+
* per message would bill every message twice. The fold therefore happens once, on the settled run,
|
|
121
|
+
* and `usageAccounted` says which side of that line the run is on. The flag and the ledger entry
|
|
122
|
+
* are written in ONE store write, so a server that dies between them cannot leave the money
|
|
123
|
+
* counted twice or not at all.
|
|
124
|
+
*
|
|
125
|
+
* THE FIELDS ARE ADDED, NOT RE-DERIVED. A conversation's usage and a run's usage are the same
|
|
126
|
+
* shape (`input` / `output` / `cacheRead` / `cacheWrite` / `cost` / `turns`), so the sum is a sum -
|
|
127
|
+
* no second pricing formula, and no place where the two could disagree about what a token costs.
|
|
128
|
+
*
|
|
129
|
+
* A RUN WITH NO CONVERSATION IS NOT FOLDED, and is not an error: the conversation it belonged to
|
|
130
|
+
* was deleted while the child was running, so there is no ledger left to add to. Deleting a
|
|
131
|
+
* conversation deletes its runs too, so the money is not counted twice either - it is simply not
|
|
132
|
+
* counted, because nobody is left to be billed for it.
|
|
133
|
+
*/
|
|
134
|
+
function foldRunUsage(state, run) {
|
|
135
|
+
if (!run?.usage || run.usageAccounted)
|
|
136
|
+
return false;
|
|
137
|
+
const conversation = (state.conversations ?? []).find((item) => item?.id === run.conversationId);
|
|
138
|
+
if (!conversation)
|
|
139
|
+
return false;
|
|
140
|
+
const previous = conversation.usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
141
|
+
conversation.usage = {
|
|
142
|
+
input: previous.input + run.usage.input,
|
|
143
|
+
output: previous.output + run.usage.output,
|
|
144
|
+
cacheRead: previous.cacheRead + run.usage.cacheRead,
|
|
145
|
+
cacheWrite: previous.cacheWrite + run.usage.cacheWrite,
|
|
146
|
+
cost: previous.cost + run.usage.cost,
|
|
147
|
+
turns: previous.turns + run.usage.turns,
|
|
148
|
+
};
|
|
149
|
+
conversation.usageUpdatedAt = now();
|
|
150
|
+
run.usageAccounted = true;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* THE STARTUP SWEEP FOR RUNS THAT PREDATE THE LEDGER.
|
|
155
|
+
*
|
|
156
|
+
* A `state.json` written before the conversation ledger existed holds runs whose usage was never
|
|
157
|
+
* added to the conversation that dispatched them - because at the time, nothing did that. Left
|
|
158
|
+
* alone, the project's budget would silently DROP for every project that had already spent money
|
|
159
|
+
* on one-shot calls, which is the kind of change nobody can explain afterwards.
|
|
160
|
+
*
|
|
161
|
+
* So every run that carries usage and has not been accounted for is folded in here, once, on the
|
|
162
|
+
* next boot. The flag makes the sweep idempotent: a run folded by this sweep is flagged, and the
|
|
163
|
+
* next boot leaves it alone. Mutated in place, like every other sweep in this file, and persisted
|
|
164
|
+
* by the single write `open` performs when this reports a change.
|
|
165
|
+
*/
|
|
166
|
+
function foldUnaccountedRunUsage(state) {
|
|
167
|
+
let folded = 0;
|
|
168
|
+
for (const run of state.subagentRuns ?? [])
|
|
169
|
+
if (foldRunUsage(state, run))
|
|
170
|
+
folded += 1;
|
|
171
|
+
return folded;
|
|
172
|
+
}
|
|
108
173
|
/**
|
|
109
174
|
* Why a conversation the previous process left `running` is failed, in the same spirit and for the same
|
|
110
175
|
* audience as `ORPHANED_RUN_ERROR`: this lands in a field the chat pane renders verbatim, so it names the
|
|
@@ -634,11 +699,15 @@ export class Store {
|
|
|
634
699
|
const interrupted = reconcileConversations(state);
|
|
635
700
|
const requeued = reconcileWaitingMessages(state);
|
|
636
701
|
const todos = reconcileTodos(state);
|
|
702
|
+
// THE USAGE SWEEP. A run recorded before the conversation ledger existed has its money sitting in
|
|
703
|
+
// the run record and nowhere else, so the ledger is brought up to date here - once, and only
|
|
704
|
+
// for the runs that carry usage and have not been accounted for.
|
|
705
|
+
const foldedUsage = foldUnaccountedRunUsage(state);
|
|
637
706
|
const store = new Store(repository, normalizeMessageTimestamps(state));
|
|
638
707
|
// Persisted here, in `open`, rather than left for the first unrelated write: a reconciled record that
|
|
639
708
|
// is only rewritten by a later mutation would still read `running` to anything that opened the file
|
|
640
709
|
// in the meantime, and would be reconciled a SECOND time on the next restart.
|
|
641
|
-
if (defaulted || orphaned.length || orphanedWorkflows.length || orphanedTasks.changed || interrupted.length || requeued.recovered.length || requeued.changed || todos.changed) {
|
|
710
|
+
if (defaulted || orphaned.length || orphanedWorkflows.length || orphanedTasks.changed || interrupted.length || requeued.recovered.length || requeued.changed || todos.changed || foldedUsage) {
|
|
642
711
|
for (const run of orphaned)
|
|
643
712
|
console.warn(`Super Pi: sub-agent run ${run.id} was still marked running when the last server stopped - it is now failed, because no child process survived it.`);
|
|
644
713
|
for (const plan of orphanedWorkflows)
|
|
@@ -660,6 +729,10 @@ export class Store {
|
|
|
660
729
|
console.warn(`Super Pi: message ${entry.message.id} of conversation ${entry.conversationId} was still marked sending when the last server stopped - it is waiting again and cancellable, because the Pi that would have delivered it is gone.`);
|
|
661
730
|
for (const entry of requeued.folded)
|
|
662
731
|
console.warn(`Super Pi: message ${entry.message.id} of conversation ${entry.conversationId} was waiting in the old queue format - it has been moved into that conversation's transcript.`);
|
|
732
|
+
// Announced, because a budget that silently changed shape is a budget nobody can explain: this
|
|
733
|
+
// says the money those runs spent is now part of the conversations that dispatched them.
|
|
734
|
+
if (foldedUsage)
|
|
735
|
+
console.warn(`Super Pi: the usage of ${foldedUsage} sub-agent run(s) recorded before the usage ledger existed was added to the conversations that dispatched them.`);
|
|
663
736
|
await store.persist();
|
|
664
737
|
}
|
|
665
738
|
return store;
|
|
@@ -2542,6 +2615,29 @@ export class Store {
|
|
|
2542
2615
|
await this.persistTransaction({ kind: "subagent.replace", run: structuredClone(run) });
|
|
2543
2616
|
return structuredClone(run);
|
|
2544
2617
|
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Adds ONE settled run's usage to the ledger of the conversation that dispatched it.
|
|
2620
|
+
*
|
|
2621
|
+
* CALLED AT THE SETTLE, ONCE PER RUN, and the run's FINAL accumulated usage is what it reads - the
|
|
2622
|
+
* run record already holds the total of every finished message the child produced, so nothing is
|
|
2623
|
+
* re-derived here and no message is counted twice.
|
|
2624
|
+
*
|
|
2625
|
+
* THE FLAG AND THE LEDGER ENTRY ARE WRITTEN IN ONE WRITE, and that is the whole crash-safety
|
|
2626
|
+
* argument: `foldRunUsage` mutates both in the same synchronous block and this persists the state
|
|
2627
|
+
* once, so a server that dies here cannot leave the money counted twice (flag written, entry not)
|
|
2628
|
+
* or not at all (entry written, flag not). A run that was folded already is left alone, so calling
|
|
2629
|
+
* this twice is a no-op rather than a second bill.
|
|
2630
|
+
*
|
|
2631
|
+
* IT PERSISTS THE WHOLE STATE rather than the two rows, because the two rows ARE one fact: the
|
|
2632
|
+
* run's flag and the conversation's ledger entry have to become true together or not at all.
|
|
2633
|
+
*/
|
|
2634
|
+
async accountSubagentUsage(runId) {
|
|
2635
|
+
const run = this.state.subagentRuns.find((item) => item.id === runId);
|
|
2636
|
+
if (!foldRunUsage(this.state, run))
|
|
2637
|
+
return false;
|
|
2638
|
+
await this.persist();
|
|
2639
|
+
return true;
|
|
2640
|
+
}
|
|
2545
2641
|
/**
|
|
2546
2642
|
* Replaces the model's task list, and persists it.
|
|
2547
2643
|
*
|
|
@@ -305,7 +305,7 @@ export class SubagentRunRecorder {
|
|
|
305
305
|
// `output`; the streamed copy is only a fallback for a child that never sent a `done`.
|
|
306
306
|
const output = result.finalText || streamed;
|
|
307
307
|
const truncated = output.length > PER_TASK_OUTPUT_CAP;
|
|
308
|
-
|
|
308
|
+
const settled = await settle({
|
|
309
309
|
status: "completed",
|
|
310
310
|
output: truncated ? output.slice(0, PER_TASK_OUTPUT_CAP) : output,
|
|
311
311
|
// `undefined` rather than `false`: the key is absent on an untruncated run, so it serializes
|
|
@@ -321,6 +321,24 @@ export class SubagentRunRecorder {
|
|
|
321
321
|
failure: undefined,
|
|
322
322
|
finishedAt: new Date().toISOString(),
|
|
323
323
|
});
|
|
324
|
+
// THE CHILD'S MONEY IS THE CALLER'S MONEY, AND IT IS ADDED ONCE.
|
|
325
|
+
//
|
|
326
|
+
// The run has just been settled with its FINAL accumulated usage, and that is the number the
|
|
327
|
+
// dispatching conversation's ledger is given - not one message's worth, and not one addition
|
|
328
|
+
// per message the child produced. A project budget reads the conversation's ledger, so a child
|
|
329
|
+
// that is not in there is a child nobody is charged for.
|
|
330
|
+
//
|
|
331
|
+
// A FAILURE HERE IS ANNOUNCED AND SWALLOWED: the run record is already on disk, so the work is
|
|
332
|
+
// real and recorded, and losing the ledger entry must not turn a finished run into a failed one.
|
|
333
|
+
// The next start folds it in - the startup sweep accounts for every run whose usage has not been
|
|
334
|
+
// accounted for - so the money is late rather than lost.
|
|
335
|
+
try {
|
|
336
|
+
await this.hooks.store.accountSubagentUsage(id);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.warn(`Super Pi: the usage of sub-agent run ${id} could not be added to the usage of conversation ${conversationId}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
340
|
+
}
|
|
341
|
+
return settled;
|
|
324
342
|
}
|
|
325
343
|
catch (error) {
|
|
326
344
|
const stopped = error instanceof SubagentCancelled;
|
|
@@ -262,7 +262,7 @@ export declare const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
|
262
262
|
export declare const SESSION_REPORT_TOOL_NAME = "session_report";
|
|
263
263
|
export declare const SESSION_STOP_SUBTREE_TOOL_NAME = "session_stop_subtree";
|
|
264
264
|
/** Model-facing description for session_spawn. */
|
|
265
|
-
export declare const SESSION_SPAWN_DESCRIPTION = "Create a
|
|
265
|
+
export declare const SESSION_SPAWN_DESCRIPTION = "Create a child conversation in the current workspace and immediately dispatch its initial task. You can specify a primary agent as the child's role.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent child CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Multi-phase or complex work where the user needs each step to be visible and available for inspection or intervention: create a child session with session_spawn.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a single one-off delegation with a single answer: use 'task'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.\n4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.\n5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.\n6. Optionally pass 'model' to choose which model the NEW session runs on: {\"provider\":\"...\",\"modelId\":\"...\"} - the same \"provider/modelId\" pair the model picker shows, and BOTH halves are required. Leave 'model' out and the new session starts on the workspace default model.\n7. Optionally pass 'watchdogTimeoutSeconds' to declare how long YOU are willing to wait for a report before you want to be reminded: a positive number of seconds. If the new session stays silent that long, you receive a '[Watchdog \u63D0\u9192]' message naming it. A report from the child (session_report) restarts the clock; the watchdog never stops the child. Leave it out to run without a watchdog.";
|
|
266
266
|
/** Model-facing description for session_status. */
|
|
267
267
|
export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of sibling conversations whose direct parent is 'parentConversationId' (the calling session by default).\n3. By default it never returns full transcripts; concise summaries keep your context window lean. Set 'includeOutput' with a conversationId to read the final output from its result file, capped to the last 64Ki characters with an explicit truncation notice.";
|
|
268
268
|
/** Model-facing description for session_dispatch. */
|
package/dist/server/task-tool.js
CHANGED
|
@@ -367,21 +367,22 @@ export const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
|
367
367
|
export const SESSION_REPORT_TOOL_NAME = "session_report";
|
|
368
368
|
export const SESSION_STOP_SUBTREE_TOOL_NAME = "session_stop_subtree";
|
|
369
369
|
/** Model-facing description for session_spawn. */
|
|
370
|
-
export const SESSION_SPAWN_DESCRIPTION = `Create a
|
|
370
|
+
export const SESSION_SPAWN_DESCRIPTION = `Create a child conversation in the current workspace and immediately dispatch its initial task. You can specify a primary agent as the child's role.
|
|
371
371
|
|
|
372
372
|
DIFFERENCE FROM THE 'task' TOOL:
|
|
373
373
|
- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.
|
|
374
|
-
- 'session_spawn' creates a real, persistent
|
|
374
|
+
- 'session_spawn' creates a real, persistent child CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.
|
|
375
375
|
|
|
376
376
|
WHEN TO USE THIS:
|
|
377
377
|
- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.
|
|
378
|
+
- Multi-phase or complex work where the user needs each step to be visible and available for inspection or intervention: create a child session with session_spawn.
|
|
378
379
|
- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.
|
|
379
380
|
- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.
|
|
380
381
|
|
|
381
382
|
WHEN NOT TO USE THIS:
|
|
382
383
|
- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.
|
|
383
384
|
- For running shell commands or build watchers: use 'run_async'.
|
|
384
|
-
- For a
|
|
385
|
+
- For a single one-off delegation with a single answer: use 'task'.
|
|
385
386
|
|
|
386
387
|
Usage notes:
|
|
387
388
|
1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.
|
package/dist/server/types.d.ts
CHANGED
|
@@ -464,7 +464,17 @@ export type Conversation = {
|
|
|
464
464
|
status: RunStatus;
|
|
465
465
|
messages: Message[];
|
|
466
466
|
archivedAt?: string;
|
|
467
|
-
/**
|
|
467
|
+
/**
|
|
468
|
+
* Cumulative usage observed for this conversation: its OWN assistant messages AND the one-shot
|
|
469
|
+
* (sub-agent) calls it dispatched.
|
|
470
|
+
*
|
|
471
|
+
* THE SUB-AGENT'S USAGE IS IN HERE ON PURPOSE. A child Pi spends real money, and a budget that
|
|
472
|
+
* only counts the parent's own turns is a budget that can be multiplied past its own cap by
|
|
473
|
+
* dispatching children instead of answering - which is exactly what design §5 forbids, because
|
|
474
|
+
* "写预算的人必须能自己算出这个数是多少钱" is only true if the number includes every call the
|
|
475
|
+
* conversation made. The child's final accumulated usage is folded in ONCE, when the run settles
|
|
476
|
+
* (see `Store.accountSubagentUsage`), so the sum here equals what was really spent.
|
|
477
|
+
*/
|
|
468
478
|
usage?: SubagentUsage;
|
|
469
479
|
/** False means cost is only a sum of reported costs, not a complete price (zero is not "free"). */
|
|
470
480
|
usageCostKnown?: boolean;
|
|
@@ -601,6 +611,21 @@ export type AgentDefinition = {
|
|
|
601
611
|
prompt: string;
|
|
602
612
|
/** The directory this agent definition was discovered from. */
|
|
603
613
|
sourceDir?: string;
|
|
614
|
+
/**
|
|
615
|
+
* Whether this definition came from the PACKAGE'S OWN agents directory - the ones Super Pi ships -
|
|
616
|
+
* rather than from a directory the user pointed the app at.
|
|
617
|
+
*
|
|
618
|
+
* WHY IT IS DECIDED BY THE SERVER AND NOT BY THE CLIENT. The picker used to interleave the two
|
|
619
|
+
* kinds alphabetically, so the built-in agents - the ones with a colour dot - were scattered among
|
|
620
|
+
* the user's own. The client cannot work that out on its own: it is told where each definition came
|
|
621
|
+
* from, but "is this directory the package's own" is a question about the app's installation, and
|
|
622
|
+
* only the server knows where it is installed. So the server answers it here, at read time, and the
|
|
623
|
+
* client just orders by the answer.
|
|
624
|
+
*
|
|
625
|
+
* It describes the definition that WON, not the id: when a user's directory shadows a built-in
|
|
626
|
+
* agent of the same id, the user's file is the one that is listed, and it is the user's.
|
|
627
|
+
*/
|
|
628
|
+
builtin?: boolean;
|
|
604
629
|
};
|
|
605
630
|
/**
|
|
606
631
|
* What the picker receives. The prompt body is deliberately NOT sent: it is a whole system prompt,
|
|
@@ -830,6 +855,20 @@ export type SubagentRun = {
|
|
|
830
855
|
/** True when `output` was cut for the model's benefit; the browser still saw all of it. */
|
|
831
856
|
outputTruncated?: boolean;
|
|
832
857
|
usage?: SubagentUsage;
|
|
858
|
+
/**
|
|
859
|
+
* TRUE once this run's usage has been folded into the usage ledger of the conversation that
|
|
860
|
+
* dispatched it.
|
|
861
|
+
*
|
|
862
|
+
* IT IS THE FLAG THAT MAKES THE FOLD HAPPEN EXACTLY ONCE. The child's money is added to the
|
|
863
|
+
* parent conversation's `usage` when the run settles, and a project budget reads that ledger -
|
|
864
|
+
* so a run whose usage were folded twice would be billed twice, and a run whose usage were never
|
|
865
|
+
* folded would be free. The flag says which side of that line this run is on, and it is written
|
|
866
|
+
* in the SAME store write as the ledger entry, so there is no window in which the two disagree.
|
|
867
|
+
*
|
|
868
|
+
* ABSENT means "not accounted for yet", which is what a `state.json` written before this feature
|
|
869
|
+
* existed says about every run in it - and what the startup sweep repairs, once, on the next boot.
|
|
870
|
+
*/
|
|
871
|
+
usageAccounted?: boolean;
|
|
833
872
|
error?: string;
|
|
834
873
|
/**
|
|
835
874
|
* How long the run took, measured by THIS server between writing the `running` row and settling it.
|
|
@@ -900,7 +939,7 @@ export type TerminalShell = {
|
|
|
900
939
|
file: string;
|
|
901
940
|
args: string[];
|
|
902
941
|
};
|
|
903
|
-
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";
|
|
904
943
|
export type ProjectTaskStatus = "pending" | "ready" | "in_progress" | "blocked" | "awaiting_review" | "completed" | "failed" | "cancelled";
|
|
905
944
|
export type ProjectRisk = "low" | "medium" | "high";
|
|
906
945
|
/**
|
|
@@ -1056,6 +1095,13 @@ export type Project = {
|
|
|
1056
1095
|
usageKnown: boolean;
|
|
1057
1096
|
activeWorkers: number;
|
|
1058
1097
|
memoryTreeId?: string;
|
|
1098
|
+
usage?: {
|
|
1099
|
+
input: number;
|
|
1100
|
+
output: number;
|
|
1101
|
+
cacheRead: number;
|
|
1102
|
+
cacheWrite: number;
|
|
1103
|
+
};
|
|
1104
|
+
rawTokens?: number;
|
|
1059
1105
|
sessions: {
|
|
1060
1106
|
id: string;
|
|
1061
1107
|
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[];
|