paseo-req-kit 1.13.0-beta.3 → 1.13.0-beta.4
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/CHANGELOG.md +17 -0
- package/contracts/cli-json.md +3 -1
- package/contracts/state.md +3 -1
- package/contracts/usage-record.md +2 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +4 -1
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +15 -2
- package/engine/src/api.ts +29 -5
- package/engine/src/card-cmd.ts +2 -0
- package/engine/src/config-view.ts +5 -0
- package/engine/src/config.ts +13 -0
- package/engine/src/estimate.ts +95 -0
- package/engine/src/factory.ts +13 -30
- package/engine/src/launch.ts +5 -0
- package/engine/src/main.ts +5 -1
- package/engine/src/pipeline.ts +89 -1
- package/engine/src/records.ts +6 -0
- package/engine/src/state.ts +15 -0
- package/engine/src/stats.ts +83 -0
- package/engine/src/watchdog.ts +105 -0
- package/package.json +1 -1
- package/paseo-plugin/client/detail.tsx +10 -1
- package/paseo-plugin/client/format.ts +1 -0
- package/paseo-plugin/client/inbox.tsx +7 -4
- package/paseo-plugin/client/overview.tsx +1 -1
- package/paseo-plugin/client/stats.tsx +42 -0
- package/paseo-plugin/index.server.ts +16 -0
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/shared/contracts.ts +23 -1
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.8viPUf6o.js → app.n_bkEV-X.js} +1 -1
- package/site-dist/assets/changelog.md.RqcFias_.js +1 -0
- package/site-dist/assets/{changelog.md.fsAikR0t.lean.js → changelog.md.RqcFias_.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.DQEikTmr.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.C-itfeI3.js → VPLocalSearchBox.B54kK3Ge.js} +1 -1
- package/site-dist/assets/chunks/{theme.CmA1-Qwc.js → theme.BH3XHrL3.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.DU5hdLl6.js → guide_getting-started.md.Di4587Bu.js} +1 -1
- package/site-dist/assets/{guide_project.md.Cc6JFJ5R.js → guide_project.md.DnD2-Dsd.js} +3 -3
- package/site-dist/assets/{guide_project.md.Cc6JFJ5R.lean.js → guide_project.md.DnD2-Dsd.lean.js} +3 -3
- package/site-dist/assets/{guide_user.md.8tCspVEK.js → guide_user.md.Cenm9vlN.js} +1 -1
- package/site-dist/assets/{guide_user.md.8tCspVEK.lean.js → guide_user.md.Cenm9vlN.lean.js} +1 -1
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.js +1 -0
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.lean.js +1 -0
- package/site-dist/assets/reference_contracts_state.md.TRfzD5bc.js +1 -0
- package/site-dist/assets/{reference_contracts_state.md.5bNTC3ow.lean.js → reference_contracts_state.md.TRfzD5bc.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_usage-record.md._C0OXWeq.js → reference_contracts_usage-record.md.DBMNY4OP.js} +2 -2
- package/site-dist/assets/{reference_contracts_usage-record.md._C0OXWeq.lean.js → reference_contracts_usage-record.md.DBMNY4OP.lean.js} +2 -2
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +6 -6
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +5 -5
- package/site-dist/reference/contracts/usage-record.html +6 -6
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/site-dist/assets/changelog.md.fsAikR0t.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.D_9Dq76F.js +0 -1
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.js +0 -1
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.lean.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.5bNTC3ow.js +0 -1
- /package/site-dist/assets/{guide_getting-started.md.DU5hdLl6.lean.js → guide_getting-started.md.Di4587Bu.lean.js} +0 -0
package/engine/src/pipeline.ts
CHANGED
|
@@ -170,6 +170,45 @@ export function isTransientError(message: string): boolean {
|
|
|
170
170
|
return TRANSIENT_ERROR.test(message);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** 1.13: the requirement used its token budget (config budget); stops before the next step. */
|
|
174
|
+
class BudgetReached extends Error {
|
|
175
|
+
constructor(
|
|
176
|
+
public readonly used: number,
|
|
177
|
+
public readonly allowance: number,
|
|
178
|
+
) {
|
|
179
|
+
super(`token 用量 ${tokenText(used)} 已到预算 ${tokenText(allowance)}`);
|
|
180
|
+
this.name = "BudgetReached";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function tokenText(n: number): string {
|
|
185
|
+
return n >= 10_000 ? `${Math.round(n / 1000) / 10} 万` : String(n);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Tokens (input + output) of every step of the requirement so far, all attempts. */
|
|
189
|
+
export function tokensUsed(state: Pick<PipelineState, "history">): number {
|
|
190
|
+
return state.history.reduce((sum, h) => sum + (h.usage?.inputTokens ?? 0) + (h.usage?.outputTokens ?? 0), 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Before a new step agent starts: warns once per budget level at warnPercent, and stops at the
|
|
195
|
+
* allowance (the step that crossed it is finished, the next one waits for a person).
|
|
196
|
+
*/
|
|
197
|
+
async function checkBudget(ctx: PipelineContext, stepTitle: string): Promise<void> {
|
|
198
|
+
const limit = ctx.config.budget.tokens;
|
|
199
|
+
if (limit === null) return;
|
|
200
|
+
const budget = (ctx.state.budget ??= { allowance: limit, warnedAt: null });
|
|
201
|
+
const used = tokensUsed(ctx.state);
|
|
202
|
+
if (used >= budget.allowance) throw new BudgetReached(used, budget.allowance);
|
|
203
|
+
if (used >= (budget.allowance * ctx.config.budget.warnPercent) / 100 && budget.warnedAt !== budget.allowance) {
|
|
204
|
+
budget.warnedAt = budget.allowance;
|
|
205
|
+
save(ctx);
|
|
206
|
+
const text = `【需求开发流水线通知】${shownFeatureDir(ctx)} 已用 ${tokenText(used)} tokens,到了预算 ${tokenText(budget.allowance)} 的 ${Math.round((used / budget.allowance) * 100)}%。流水线继续(下一步:${stepTitle});用完预算时会做完当前步骤后停下等你决定。这条消息只是告知,不需要处理。`;
|
|
207
|
+
await notifyOrigin(ctx, text);
|
|
208
|
+
await notifyExternal(ctx, "milestone", `需求 ${featureName(ctx.featureDir)} 的 token 用量已到预算的 ${ctx.config.budget.warnPercent}%`, { used, budget: budget.allowance });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
173
212
|
class BusinessDecisionNeeded extends Error {
|
|
174
213
|
constructor(
|
|
175
214
|
public readonly questions: string[],
|
|
@@ -616,6 +655,12 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
616
655
|
await ctx.sleep(Math.min(60_000, 5_000 * consecutiveErrors));
|
|
617
656
|
continue;
|
|
618
657
|
}
|
|
658
|
+
// 1.13: whether the step waits for a permission, for the plugin's "等你处理".
|
|
659
|
+
const waitingNow = outcome.status === "permission";
|
|
660
|
+
if (!waitingNow && ctx.state.waitingPermission) {
|
|
661
|
+
ctx.state.waitingPermission = null;
|
|
662
|
+
save(ctx);
|
|
663
|
+
}
|
|
619
664
|
switch (outcome.status) {
|
|
620
665
|
case "idle":
|
|
621
666
|
return outcome.usage ?? null;
|
|
@@ -624,6 +669,12 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
624
669
|
case "error":
|
|
625
670
|
throw new AgentErrored(def.title, `agent 报告错误:${outcome.error ?? "未知错误"}`);
|
|
626
671
|
case "permission": {
|
|
672
|
+
const requests = outcome.pendingPermissions.map((p) => p.title);
|
|
673
|
+
const w = ctx.state.waitingPermission;
|
|
674
|
+
if (!w || w.agentId !== agent.id || w.requests.join("\n") !== requests.join("\n")) {
|
|
675
|
+
ctx.state.waitingPermission = { agentId: agent.id, stepTitle: def.title, requests, since: w?.agentId === agent.id ? w.since : new Date().toISOString() };
|
|
676
|
+
save(ctx);
|
|
677
|
+
}
|
|
627
678
|
const fresh = outcome.pendingPermissions.filter((p) => !notifiedPermissions.has(p.id));
|
|
628
679
|
if (fresh.length > 0) {
|
|
629
680
|
for (const p of fresh) notifiedPermissions.add(p.id);
|
|
@@ -768,6 +819,7 @@ async function runStepOnce(
|
|
|
768
819
|
const agentTitle =
|
|
769
820
|
`[${featureName(ctx.featureDir)}] ` +
|
|
770
821
|
(extras.title ?? `${def.number}-${def.title}${round !== null && round > 1 ? ` 第${round}轮` : ""}${target ? `(${target.name})` : ""}`);
|
|
822
|
+
if (!leftover.agent) await checkBudget(ctx, def.title);
|
|
771
823
|
const record: StepRecord = {
|
|
772
824
|
key,
|
|
773
825
|
stepId: id,
|
|
@@ -788,6 +840,8 @@ async function runStepOnce(
|
|
|
788
840
|
}
|
|
789
841
|
ctx.log(leftover.agent ? `继续等待 ${key}(${def.title})上一次的 agent ${leftover.agent.id}` : `开始 ${key}(${def.title})`);
|
|
790
842
|
|
|
843
|
+
const stepProfile = extras.profile ?? phaseAgent(ctx, id) ?? ctx.config.agents[def.role];
|
|
844
|
+
if (!leftover.agent) record.agent = agentName(stepProfile);
|
|
791
845
|
try {
|
|
792
846
|
const agent = leftover.agent ?? await ctx.runtime.startAgent({
|
|
793
847
|
role: def.role,
|
|
@@ -801,7 +855,7 @@ async function runStepOnce(
|
|
|
801
855
|
},
|
|
802
856
|
parentAgentId: ctx.config.attachToOriginChat ? ctx.state.originAgentId : null,
|
|
803
857
|
// Always passed: the run's config has Paseo profile names resolved; the runtime's may not.
|
|
804
|
-
profile:
|
|
858
|
+
profile: stepProfile,
|
|
805
859
|
workspaceId: target ? target.workspaceId : (ctx.state.worktree?.workspaceId ?? null),
|
|
806
860
|
});
|
|
807
861
|
record.agentId = agent.id;
|
|
@@ -961,6 +1015,12 @@ function mergeExtras(
|
|
|
961
1015
|
return { contextLines: [...(a?.contextLines ?? []), ...(b?.contextLines ?? [])], sections: [...(a?.sections ?? []), ...(b?.sections ?? [])] };
|
|
962
1016
|
}
|
|
963
1017
|
|
|
1018
|
+
/** How a step's agent is named in records and statistics (1.13). */
|
|
1019
|
+
export function agentName(profile: AgentProfile | null | undefined): string | undefined {
|
|
1020
|
+
if (!profile) return undefined;
|
|
1021
|
+
return profile.paseoProfile?.trim() || profile.provider?.trim() || undefined;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
964
1024
|
/** Steps that check the result the way a user would. */
|
|
965
1025
|
const BROWSER_STEPS: readonly StepId[] = ["verify", "defect_fix", "reverify"];
|
|
966
1026
|
|
|
@@ -2215,6 +2275,34 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
2215
2275
|
});
|
|
2216
2276
|
return "waiting_for_business";
|
|
2217
2277
|
}
|
|
2278
|
+
if (error instanceof BudgetReached) {
|
|
2279
|
+
// Continuing (req resume, or "继续开发") gives one more budget.
|
|
2280
|
+
const limit = ctx.config.budget.tokens ?? error.allowance;
|
|
2281
|
+
ctx.state.status = "failed";
|
|
2282
|
+
ctx.state.stopKind = "budget";
|
|
2283
|
+
ctx.state.stopReason = `${error.message},已停下等你决定是否继续`;
|
|
2284
|
+
ctx.state.budget = { allowance: error.allowance + limit, warnedAt: ctx.state.budget?.warnedAt ?? null };
|
|
2285
|
+
save(ctx);
|
|
2286
|
+
await notifyOrigin(
|
|
2287
|
+
ctx,
|
|
2288
|
+
[
|
|
2289
|
+
"【需求开发流水线通知 · 用完 token 预算】",
|
|
2290
|
+
"",
|
|
2291
|
+
`功能目录:${shownFeatureDir(ctx)}`,
|
|
2292
|
+
`情况:这个需求已用 ${tokenText(error.used)} tokens,到了预算 ${tokenText(error.allowance)}。已完成的步骤都保留,流水线停在下一步开始之前。`,
|
|
2293
|
+
"",
|
|
2294
|
+
"请这样处理:把用量告诉用户(可以运行查看进度的命令看各步骤),问是否继续。",
|
|
2295
|
+
`- 继续:执行下面的命令,会再给一份预算(到 ${tokenText(error.allowance + limit)} 时再停):\n\n${ctx.resumeCommand}`,
|
|
2296
|
+
"- 不继续:不用执行任何命令;需要时可以缩小需求范围再继续。",
|
|
2297
|
+
].join("\n"),
|
|
2298
|
+
);
|
|
2299
|
+
await integrate(ctx, "failed");
|
|
2300
|
+
await notifyExternal(ctx, "failed", `需求 ${featureName(ctx.featureDir)} 用完了 token 预算(${tokenText(error.used)}),已暂停等需求方决定`, {
|
|
2301
|
+
reason: error.message,
|
|
2302
|
+
resumeCommand: ctx.resumeCommand,
|
|
2303
|
+
});
|
|
2304
|
+
return "failed";
|
|
2305
|
+
}
|
|
2218
2306
|
const stepTitle = error instanceof StepFailure ? error.stepTitle : "流水线";
|
|
2219
2307
|
ctx.state.status = "failed";
|
|
2220
2308
|
ctx.state.stopReason = `${stepTitle}:${(error as Error).message}`;
|
package/engine/src/records.ts
CHANGED
|
@@ -134,6 +134,10 @@ export interface StepUsageRecord {
|
|
|
134
134
|
inputTokens: number | null;
|
|
135
135
|
outputTokens: number | null;
|
|
136
136
|
costUsd: number | null;
|
|
137
|
+
/** 1.13: input tokens served from the prompt cache (part of inputTokens), when the agent reports them. */
|
|
138
|
+
cachedInputTokens?: number | null;
|
|
139
|
+
/** 1.13: the agent the step ran with (Paseo profile name or "provider/model"). */
|
|
140
|
+
agent?: string | null;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
/** One pipeline run: a process from its start to its end (contracts/usage-record.md). */
|
|
@@ -217,6 +221,8 @@ export function runRecord(input: {
|
|
|
217
221
|
inputTokens: h.usage?.inputTokens ?? null,
|
|
218
222
|
outputTokens: h.usage?.outputTokens ?? null,
|
|
219
223
|
costUsd: h.usage?.totalCostUsd ?? null,
|
|
224
|
+
cachedInputTokens: h.usage?.cachedInputTokens ?? null,
|
|
225
|
+
agent: h.agent ?? null,
|
|
220
226
|
}),
|
|
221
227
|
);
|
|
222
228
|
const gateRuns = [...(state.gate?.runs ?? []), ...(state.joint ?? []).flatMap((j) => j.gate?.runs ?? [])].filter(
|
package/engine/src/state.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type StopKind =
|
|
|
43
43
|
| "transient"
|
|
44
44
|
| "stopped"
|
|
45
45
|
| "process_exit"
|
|
46
|
+
| "budget"
|
|
46
47
|
| "other";
|
|
47
48
|
|
|
48
49
|
/** Something the pipeline handled by itself (1.12). */
|
|
@@ -78,6 +79,16 @@ export interface StepRecord {
|
|
|
78
79
|
reattached?: number;
|
|
79
80
|
/** 1.12.1: the step's time limit was already given its one extra period (selfHeal); a later run waiting for the same agent gets none. */
|
|
80
81
|
extendedTimeout?: boolean;
|
|
82
|
+
/** 1.13: the agent the step ran with: the Paseo profile name, or "provider/model". */
|
|
83
|
+
agent?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 1.13: the requirement's token budget so far (config budget). */
|
|
87
|
+
export interface BudgetState {
|
|
88
|
+
/** Tokens allowed before stopping: the budget, plus one more each time the person continued after a stop. */
|
|
89
|
+
allowance: number;
|
|
90
|
+
/** The allowance a warning was given for, so each level warns once. */
|
|
91
|
+
warnedAt: number | null;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export interface PipelineState {
|
|
@@ -97,6 +108,10 @@ export interface PipelineState {
|
|
|
97
108
|
results: Record<string, Record<string, unknown>>;
|
|
98
109
|
history: StepRecord[];
|
|
99
110
|
stopReason: string | null;
|
|
111
|
+
/** 1.13: a step agent is waiting for someone to allow or deny an action; null otherwise. */
|
|
112
|
+
waitingPermission?: { agentId: string; stepTitle: string; requests: string[]; since: string } | null;
|
|
113
|
+
/** 1.13: token budget (config budget); absent until the first check. */
|
|
114
|
+
budget?: BudgetState;
|
|
100
115
|
/** 1.12: why the run stopped for a person, as a code (contracts/usage-record.md); null while running or done. */
|
|
101
116
|
stopKind?: StopKind | null;
|
|
102
117
|
/** 1.12: what the pipeline handled by itself instead of stopping (retries, waiting longer, a fallback model…). */
|
package/engine/src/stats.ts
CHANGED
|
@@ -121,6 +121,7 @@ export const STOP_LABELS: Record<string, string> = {
|
|
|
121
121
|
transient: "限流、网络等临时错误(自动重试用完)",
|
|
122
122
|
stopped: "被手动停止",
|
|
123
123
|
process_exit: "后台进程意外退出",
|
|
124
|
+
budget: "用完 token 预算",
|
|
124
125
|
other: "其他中断",
|
|
125
126
|
};
|
|
126
127
|
|
|
@@ -236,6 +237,8 @@ export interface StatsSummary {
|
|
|
236
237
|
/** Average minutes per requirement that waited, by kind, in the period's runs. */
|
|
237
238
|
waits: Array<{ kind: WaitKind; label: string; requirements: number; averageMinutes: number }>;
|
|
238
239
|
usage: { agentMinutes: number; inputTokens: number | null; outputTokens: number | null; costUsd: number | null };
|
|
240
|
+
/** 1.13: where the tokens (input + output) of the period's runs went. */
|
|
241
|
+
tokens: TokenBreakdown;
|
|
239
242
|
byBuilder: Array<{ builder: string; requirements: number; averageReviewRounds: number | null; averageCostUsd: number | null }>;
|
|
240
243
|
/** 1.12: why the period's runs stopped for a person, most frequent first. */
|
|
241
244
|
stops: Array<{ kind: string; label: string; necessary: boolean; runs: number }>;
|
|
@@ -255,6 +258,66 @@ export interface StatsSummary {
|
|
|
255
258
|
requirements: RequirementStats[];
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
export interface TokenBreakdown {
|
|
262
|
+
/** Input + output of the period's steps that reported usage; null when none did. */
|
|
263
|
+
total: number | null;
|
|
264
|
+
/** Share of input tokens served from the prompt cache, when agents report it. */
|
|
265
|
+
cachedShare: number | null;
|
|
266
|
+
byStep: Array<{ stepId: string; title: string; steps: number; tokens: number; share: number }>;
|
|
267
|
+
byAgent: Array<{ agent: string; steps: number; tokens: number; share: number }>;
|
|
268
|
+
/** Fixing and re-checking: revisions, review fixes, defect fixes, re-verification, and review rounds after the first. */
|
|
269
|
+
rework: { tokens: number; share: number } | null;
|
|
270
|
+
/** Tokens per completed requirement (all its runs). */
|
|
271
|
+
perRequirement: { median: number | null; byProfile: Array<{ profile: string; requirements: number; median: number | null }> };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const REWORK_STEPS = new Set(["design_revise", "review_fix", "defect_fix", "reverify"]);
|
|
275
|
+
|
|
276
|
+
export function isRework(s: { stepId: string; round: number | null }): boolean {
|
|
277
|
+
return REWORK_STEPS.has(s.stepId) || ((s.stepId === "design_review" || s.stepId === "code_review") && (s.round ?? 1) > 1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const stepTokens = (s: { inputTokens: number | null; outputTokens: number | null }) =>
|
|
281
|
+
s.inputTokens === null && s.outputTokens === null ? null : (s.inputTokens ?? 0) + (s.outputTokens ?? 0);
|
|
282
|
+
|
|
283
|
+
export function tokenBreakdown(steps: RunRecord["steps"], done: RequirementStats[]): TokenBreakdown {
|
|
284
|
+
const counted = steps.flatMap((s) => {
|
|
285
|
+
const t = stepTokens(s);
|
|
286
|
+
return t === null ? [] : [{ s, t }];
|
|
287
|
+
});
|
|
288
|
+
const total = counted.length ? counted.reduce((a, x) => a + x.t, 0) : null;
|
|
289
|
+
const share = (n: number) => (total ? Math.round((n / total) * 1000) / 1000 : 0);
|
|
290
|
+
const group = (key: (s: RunRecord["steps"][number]) => string) => {
|
|
291
|
+
const m = new Map<string, { steps: number; tokens: number }>();
|
|
292
|
+
for (const { s, t } of counted) {
|
|
293
|
+
const k = key(s);
|
|
294
|
+
const g = m.get(k) ?? { steps: 0, tokens: 0 };
|
|
295
|
+
m.set(k, { steps: g.steps + 1, tokens: g.tokens + t });
|
|
296
|
+
}
|
|
297
|
+
return [...m.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
|
|
298
|
+
};
|
|
299
|
+
const reworkTokens = counted.filter(({ s }) => isRework(s)).reduce((a, x) => a + x.t, 0);
|
|
300
|
+
const input = steps.reduce((a, s) => a + (s.inputTokens ?? 0), 0);
|
|
301
|
+
const cached = steps.reduce<number | null>((a, s) => (typeof s.cachedInputTokens === "number" ? (a ?? 0) + s.cachedInputTokens : a), null);
|
|
302
|
+
const reqTokens = (q: RequirementStats) => (q.inputTokens === null && q.outputTokens === null ? null : (q.inputTokens ?? 0) + (q.outputTokens ?? 0));
|
|
303
|
+
const profiles = new Map<string, number[]>();
|
|
304
|
+
for (const q of done) {
|
|
305
|
+
const t = reqTokens(q);
|
|
306
|
+
if (t !== null) profiles.set(q.profile, [...(profiles.get(q.profile) ?? []), t]);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
total,
|
|
310
|
+
cachedShare: cached !== null && input > 0 ? Math.round((cached / input) * 1000) / 1000 : null,
|
|
311
|
+
byStep: group((s) => s.stepId).map(([stepId, g]) => ({ stepId, title: STEPS[stepId as StepId]?.title ?? stepId, steps: g.steps, tokens: g.tokens, share: share(g.tokens) })),
|
|
312
|
+
byAgent: group((s) => s.agent ?? "(未记录)").map(([agent, g]) => ({ agent, steps: g.steps, tokens: g.tokens, share: share(g.tokens) })),
|
|
313
|
+
rework: total ? { tokens: reworkTokens, share: share(reworkTokens) } : null,
|
|
314
|
+
perRequirement: {
|
|
315
|
+
median: median([...profiles.values()].flat()),
|
|
316
|
+
byProfile: [...profiles.entries()].map(([profile, ts]) => ({ profile, requirements: ts.length, median: median(ts) })),
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
258
321
|
/** The summary of a period from run records (any machines). */
|
|
259
322
|
export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
260
323
|
const runs = dedupe(all);
|
|
@@ -341,6 +404,7 @@ export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
|
341
404
|
outputTokens: steps.reduce<number | null>((s, x) => add(s, x.outputTokens), null),
|
|
342
405
|
costUsd: steps.reduce<number | null>((s, x) => add(s, x.costUsd), null),
|
|
343
406
|
},
|
|
407
|
+
tokens: tokenBreakdown(steps, done),
|
|
344
408
|
byBuilder: [...builders.entries()].map(([builder, qs]) => ({
|
|
345
409
|
builder,
|
|
346
410
|
requirements: qs.length,
|
|
@@ -411,6 +475,25 @@ export function describeStats(s: StatsSummary): string {
|
|
|
411
475
|
`- 完成:${s.completed.total} 个(${list(s.completed.byOutcome, OUTCOME_LABELS)})`,
|
|
412
476
|
s.cycleHours.median !== null ? `- 交付周期(首次开始到完成):中位数 ${s.cycleHours.median} 小时,平均 ${s.cycleHours.average} 小时` : "- 交付周期:这段时间没有完成的需求",
|
|
413
477
|
`- agent 合计用时:${hours(s.usage.agentMinutes)};输入 ${tokens(s.usage.inputTokens)} tokens,输出 ${tokens(s.usage.outputTokens)} tokens${s.usage.costUsd !== null ? `,约 $${s.usage.costUsd.toFixed(2)}` : ""}`,
|
|
478
|
+
);
|
|
479
|
+
const t = s.tokens;
|
|
480
|
+
if (t.total) {
|
|
481
|
+
const pct = (x: number) => `${Math.round(x * 100)}%`;
|
|
482
|
+
lines.push("", "## token 花在哪", "", `- 合计 ${tokens(t.total)} tokens(输入 + 输出)${t.cachedShare !== null ? `;输入中 ${pct(t.cachedShare)} 来自提示缓存` : ""}`);
|
|
483
|
+
if (t.rework) lines.push(`- 返工(方案修订、评审问题修复、缺陷修复、复验,以及第 2 轮起的审核和评审):${tokens(t.rework.tokens)},占 ${pct(t.rework.share)}`);
|
|
484
|
+
if (t.perRequirement.median !== null) {
|
|
485
|
+
lines.push(
|
|
486
|
+
`- 每个完成的需求:中位数 ${tokens(t.perRequirement.median)}${t.perRequirement.byProfile.length > 1 ? `(${t.perRequirement.byProfile.map((p) => `${p.profile === "quick" ? "快速档" : p.profile === "standard" ? "标准档" : p.profile} ${tokens(p.median)},${p.requirements} 个`).join(";")})` : ""}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
lines.push("", "| 步骤 | 次数 | tokens | 占比 |", "| --- | --- | --- | --- |");
|
|
490
|
+
for (const r of t.byStep) lines.push(`| ${r.title} | ${r.steps} | ${tokens(r.tokens)} | ${pct(r.share)} |`);
|
|
491
|
+
if (t.byAgent.length > 1 || t.byAgent[0]?.agent !== "(未记录)") {
|
|
492
|
+
lines.push("", "| 模型 | 步骤数 | tokens | 占比 |", "| --- | --- | --- | --- |");
|
|
493
|
+
for (const r of t.byAgent) lines.push(`| ${r.agent} | ${r.steps} | ${tokens(r.tokens)} | ${pct(r.share)} |`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
lines.push(
|
|
414
497
|
"",
|
|
415
498
|
"## 返工",
|
|
416
499
|
"",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Continuing pipelines whose background process died (1.13), without asking a model: a plain
|
|
2
|
+
// check of this machine's requirements that runs `req resume` for each one whose state says
|
|
3
|
+
// "running" but whose process is gone. The Paseo plugin runs it every few minutes inside the
|
|
4
|
+
// daemon (`req api watchdog`), and the card factory on each poll. Before 1.13 this was a Paseo
|
|
5
|
+
// schedule that woke the chat agent every hour to look — a model turn each time, and a recovery
|
|
6
|
+
// that depended on the model understanding the prompt.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { kitHome } from "./locations.js";
|
|
10
|
+
import { readIndex } from "./records.js";
|
|
11
|
+
import { isAlive, readState, statePath, writeState } from "./state.js";
|
|
12
|
+
|
|
13
|
+
/** A run is left alone this long after its last write (it may be starting or finishing). */
|
|
14
|
+
const SETTLE_MS = 2 * 60_000;
|
|
15
|
+
/** At most this many automatic continuations per requirement per day. */
|
|
16
|
+
const MAX_PER_DAY = 3;
|
|
17
|
+
/** The plugin's watchdog counts as running when it checked within this time. */
|
|
18
|
+
export const HEARTBEAT_FRESH_MS = 15 * 60_000;
|
|
19
|
+
|
|
20
|
+
export const heartbeatFile = (env: NodeJS.ProcessEnv = process.env) => path.join(kitHome(env), "watchdog.json");
|
|
21
|
+
|
|
22
|
+
interface Memory {
|
|
23
|
+
/** Last check, ISO time, and who ran it. */
|
|
24
|
+
at?: string;
|
|
25
|
+
by?: string;
|
|
26
|
+
/** Per requirement: the dead process already handled, and when continuations happened. */
|
|
27
|
+
handled?: Record<string, { pid: number; times: string[] }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readMemory(env: NodeJS.ProcessEnv): Memory {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(heartbeatFile(env), "utf8")) as Memory;
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeMemory(env: NodeJS.ProcessEnv, m: Memory): void {
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(path.dirname(heartbeatFile(env)), { recursive: true });
|
|
41
|
+
const tmp = `${heartbeatFile(env)}.${process.pid}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmp, JSON.stringify(m, null, 2));
|
|
43
|
+
fs.renameSync(tmp, heartbeatFile(env));
|
|
44
|
+
} catch {
|
|
45
|
+
// Best effort: the next check tries again.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether the plugin's watchdog has checked recently (so the pipeline need not ask the chat to). */
|
|
50
|
+
export function watchdogActive(env: NodeJS.ProcessEnv = process.env, now = Date.now()): boolean {
|
|
51
|
+
const m = readMemory(env);
|
|
52
|
+
return m.by === "plugin" && !!m.at && now - new Date(m.at).getTime() < HEARTBEAT_FRESH_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WatchdogResult {
|
|
56
|
+
checked: number;
|
|
57
|
+
resumed: Array<{ featureDir: string; repo: string; message: string }>;
|
|
58
|
+
failed: Array<{ featureDir: string; repo: string; message: string }>;
|
|
59
|
+
skipped: Array<{ featureDir: string; reason: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type Resume = (repo: string, featureDir: string) => { ok: boolean; message: string };
|
|
63
|
+
|
|
64
|
+
export function watchdogPass(input: { env?: NodeJS.ProcessEnv; by: "plugin" | "factory" | "manual"; resume: Resume; now?: Date; dryRun?: boolean }): WatchdogResult {
|
|
65
|
+
const env = input.env ?? process.env;
|
|
66
|
+
const now = input.now ?? new Date();
|
|
67
|
+
const memory = readMemory(env);
|
|
68
|
+
const handled = memory.handled ?? {};
|
|
69
|
+
const result: WatchdogResult = { checked: 0, resumed: [], failed: [], skipped: [] };
|
|
70
|
+
for (const entry of readIndex(env)) {
|
|
71
|
+
if (!entry.present) continue;
|
|
72
|
+
const file = statePath(entry.repo, entry.featureDir);
|
|
73
|
+
const state = readState(file);
|
|
74
|
+
if (!state) continue;
|
|
75
|
+
result.checked++;
|
|
76
|
+
if (state.status !== "running" || isAlive(state.pid)) continue;
|
|
77
|
+
if (now.getTime() - new Date(state.updatedAt).getTime() < SETTLE_MS) continue;
|
|
78
|
+
const h = handled[entry.key];
|
|
79
|
+
if (h?.pid === state.pid) continue;
|
|
80
|
+
const recent = (h?.times ?? []).filter((t) => now.getTime() - new Date(t).getTime() < 24 * 3600_000);
|
|
81
|
+
if (recent.length >= MAX_PER_DAY) {
|
|
82
|
+
result.skipped.push({ featureDir: entry.featureDir, reason: `24 小时内已自动继续 ${recent.length} 次,不再自动继续,请人看一下日志` });
|
|
83
|
+
handled[entry.key] = { pid: state.pid, times: recent };
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (input.dryRun) {
|
|
87
|
+
result.resumed.push({ featureDir: entry.featureDir, repo: entry.repo, message: "(试运行,没有继续)" });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Say why in the state first: the continued run reads it, and statistics count it.
|
|
91
|
+
state.stopKind = "process_exit";
|
|
92
|
+
state.stopReason = "后台进程意外退出";
|
|
93
|
+
state.recoveries = [...(state.recoveries ?? []), { at: now.toISOString(), key: "pipeline", kind: "auto_resume", detail: `后台进程 ${state.pid} 意外退出,${input.by === "plugin" ? "插件" : input.by === "factory" ? "接单器" : "检查"}自动继续` }];
|
|
94
|
+
try {
|
|
95
|
+
writeState(file, state);
|
|
96
|
+
} catch {
|
|
97
|
+
/* the resume still runs */
|
|
98
|
+
}
|
|
99
|
+
const r = input.resume(entry.sourceRepo ?? entry.repo, entry.featureDir);
|
|
100
|
+
handled[entry.key] = { pid: state.pid, times: [...recent, now.toISOString()] };
|
|
101
|
+
(r.ok ? result.resumed : result.failed).push({ featureDir: entry.featureDir, repo: entry.repo, message: r.message.split("\n").find((l) => l.trim()) ?? "" });
|
|
102
|
+
}
|
|
103
|
+
writeMemory(env, { at: now.toISOString(), by: input.by === "manual" ? (memory.by ?? "manual") : input.by, handled });
|
|
104
|
+
return result;
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.13.0-beta.
|
|
3
|
+
"version": "1.13.0-beta.4",
|
|
4
4
|
"description": "req-kit: a requirement-development workflow for Paseo. /req aligns a requirement in chat; the engine runs design, implementation, review and verification as Paseo agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -174,13 +174,22 @@ export function RequirementView({
|
|
|
174
174
|
<View style={s.row}>
|
|
175
175
|
<Text style={s.title}>{d.title}</Text>
|
|
176
176
|
{d.attention ? (
|
|
177
|
-
<Badge theme={theme} tone={d.attention === "approval" || d.attention === "business" ? "warning" : "danger"}>
|
|
177
|
+
<Badge theme={theme} tone={d.attention === "approval" || d.attention === "business" || d.attention === "permission" ? "warning" : "danger"}>
|
|
178
178
|
{ATTENTION_LABELS[d.attention]}
|
|
179
179
|
</Badge>
|
|
180
180
|
) : null}
|
|
181
181
|
</View>
|
|
182
182
|
<Text style={s.muted}>{facts(d)}</Text>
|
|
183
183
|
<Text style={s.text}>{d.stage}</Text>
|
|
184
|
+
{d.estimate && d.attention !== "approval" ? (
|
|
185
|
+
<Text style={s.muted}>
|
|
186
|
+
{d.estimate.over
|
|
187
|
+
? `已超过同档位需求通常的用时(已用 ${minutes(d.estimate.usedAgentMinutes)},通常 ${minutes(d.estimate.typicalAgentMinutes)})`
|
|
188
|
+
: `预计还要约 ${minutes(Math.max(5, d.estimate.leftMinutes))}`}
|
|
189
|
+
{d.estimate.usedTokens !== null ? ` · 已用 ${tokens(d.estimate.usedTokens)} tokens${d.estimate.typicalTokens !== null ? `(通常 ${tokens(d.estimate.typicalTokens)})` : ""}` : ""}
|
|
190
|
+
{` · 按本机最近 ${d.estimate.basis} 个同档位需求,不含等人的时间`}
|
|
191
|
+
</Text>
|
|
192
|
+
) : null}
|
|
184
193
|
{d.run?.stopReason && !d.running ? <Text style={s.danger}>{d.run.stopReason}</Text> : null}
|
|
185
194
|
{d.attention === "business" ? <Text style={s.muted}>需求方的决定请在发起需求的对话中回答,回答后再继续。</Text> : null}
|
|
186
195
|
</View>
|
|
@@ -4,6 +4,7 @@ import type { Attention, Requirement } from "../shared/contracts";
|
|
|
4
4
|
export const ATTENTION_LABELS: Record<Exclude<Attention, null>, string> = {
|
|
5
5
|
approval: "等待确认方案",
|
|
6
6
|
business: "等待需求方决定",
|
|
7
|
+
permission: "等待批准权限",
|
|
7
8
|
interrupted: "已中断,可以继续",
|
|
8
9
|
not_completed: "结束但未完成",
|
|
9
10
|
};
|
|
@@ -17,6 +17,7 @@ type Layout = PluginHostProps["layout"];
|
|
|
17
17
|
const KIND: Record<Exclude<Attention, null>, { label: string; tone: "warning" | "danger" | "muted"; todo: string }> = {
|
|
18
18
|
approval: { label: "确认方案", tone: "warning", todo: "方案已通过审核。看一下“做完后会看到什么”,没问题就确认,开始写代码;要改就写下来。" },
|
|
19
19
|
business: { label: "回答问题", tone: "warning", todo: "方案审核有问题只有你能决定,回答后开发会接着进行。" },
|
|
20
|
+
permission: { label: "批准权限", tone: "warning", todo: "有一个步骤在等你允许它做一项操作,批准前会一直停在这里。打开这个 agent,在它的对话里选择允许或拒绝;拿不准时请开发同事看一下。" },
|
|
20
21
|
interrupted: { label: "已中断", tone: "danger", todo: "开发中断了,自动处理没能解决。看一下原因,能继续就继续。" },
|
|
21
22
|
not_completed: { label: "没有完成", tone: "muted", todo: "流水线结束了,但有部分没有做完。完成报告里写了原因和建议。" },
|
|
22
23
|
};
|
|
@@ -37,7 +38,7 @@ export function inboxItems(items: Requirement[]): Requirement[] {
|
|
|
37
38
|
.sort((a, b) => (a.waiting?.since ?? a.updatedAt).localeCompare(b.waiting?.since ?? b.updatedAt));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function InboxCard({ theme, layout, item, open, readOnly }: { theme: Theme; layout: Layout; item: Requirement; open(): void; readOnly: boolean }) {
|
|
41
|
+
function InboxCard({ theme, layout, item, open, readOnly, openAgent }: { theme: Theme; layout: Layout; item: Requirement; open(): void; readOnly: boolean; openAgent?: (agentId: string) => void }) {
|
|
41
42
|
const s = useStyles(theme, layout.compact);
|
|
42
43
|
const run = useAction();
|
|
43
44
|
const toast = useToast();
|
|
@@ -77,8 +78,9 @@ function InboxCard({ theme, layout, item, open, readOnly }: { theme: Theme; layo
|
|
|
77
78
|
</Text>
|
|
78
79
|
<Text style={s.text}>{kind.todo}</Text>
|
|
79
80
|
|
|
80
|
-
{item.attention === "business" && w && w.questions.length > 0 ? (
|
|
81
|
+
{(item.attention === "business" || item.attention === "permission") && w && w.questions.length > 0 ? (
|
|
81
82
|
<View style={{ gap: 4 }}>
|
|
83
|
+
{item.attention === "permission" ? <Text style={s.muted}>{w.reason}。要批准的操作:</Text> : null}
|
|
82
84
|
{w.questions.map((q, i) => (
|
|
83
85
|
<Text key={i} style={s.text}>
|
|
84
86
|
{i + 1}. {q}
|
|
@@ -140,6 +142,7 @@ function InboxCard({ theme, layout, item, open, readOnly }: { theme: Theme; layo
|
|
|
140
142
|
{item.attention === "business" && w?.canReply ? (
|
|
141
143
|
<Button theme={theme} primary label={run.isPending ? "正在发送…" : "发送回答"} disabled={run.isPending || !text.trim()} onPress={() => void act("reply")} />
|
|
142
144
|
) : null}
|
|
145
|
+
{item.attention === "permission" && w?.agentId && openAgent ? <Button theme={theme} primary label="打开这个 agent" onPress={() => openAgent(w.agentId!)} /> : null}
|
|
143
146
|
{item.attention === "interrupted" ? (
|
|
144
147
|
<Button theme={theme} primary label={run.isPending ? "正在执行…" : "继续"} disabled={run.isPending} onPress={() => void act("resume")} />
|
|
145
148
|
) : null}
|
|
@@ -151,7 +154,7 @@ function InboxCard({ theme, layout, item, open, readOnly }: { theme: Theme; layo
|
|
|
151
154
|
);
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
export function Inbox({ theme, layout, items, open }: { theme: Theme; layout: Layout; items: Requirement[]; open(item: Requirement): void }) {
|
|
157
|
+
export function Inbox({ theme, layout, items, open, openAgent }: { theme: Theme; layout: Layout; items: Requirement[]; open(item: Requirement): void; openAgent?: (agentId: string) => void }) {
|
|
155
158
|
const s = useStyles(theme, layout.compact);
|
|
156
159
|
const { readOnly } = usePreferences();
|
|
157
160
|
const waiting = inboxItems(items);
|
|
@@ -169,7 +172,7 @@ export function Inbox({ theme, layout, items, open }: { theme: Theme; layout: La
|
|
|
169
172
|
<Text style={s.muted}>现在没有需要你处理的事。需要你确认或回答时,会出现在这里。</Text>
|
|
170
173
|
</View>
|
|
171
174
|
) : (
|
|
172
|
-
waiting.map((item) => <InboxCard key={item.key} theme={theme} layout={layout} item={item} open={() => open(item)} readOnly={readOnly} />)
|
|
175
|
+
waiting.map((item) => <InboxCard key={item.key} theme={theme} layout={layout} item={item} open={() => open(item)} readOnly={readOnly} openAgent={openAgent} />)
|
|
173
176
|
)}
|
|
174
177
|
</View>
|
|
175
178
|
);
|
|
@@ -85,7 +85,7 @@ export function OverviewScreen(props: PluginScreenProps & { openScreen: ScreenOp
|
|
|
85
85
|
error={query.error}
|
|
86
86
|
empty={query.data && items.length === 0 ? "这台机器上还没有记录任何需求。在 Paseo 对话中用 /req 开始一个;1.9 之前运行过的需求,在项目目录运行 req index --rebuild。" : null}
|
|
87
87
|
/>
|
|
88
|
-
{query.data && items.length > 0 ? <Inbox theme={theme} layout={layout} items={shown} open={open} /> : null}
|
|
88
|
+
{query.data && items.length > 0 ? <Inbox theme={theme} layout={layout} items={shown} open={open} openAgent={props.navigation ? (agentId) => props.navigation!.openAgent({ agentId }) : undefined} /> : null}
|
|
89
89
|
<RequirementGroups theme={theme} layout={layout} items={shown} open={open} skip={["attention"]} />
|
|
90
90
|
</ScrollView>
|
|
91
91
|
</View>
|
|
@@ -7,6 +7,47 @@ import { useStats } from "./data";
|
|
|
7
7
|
import { minutes, tokens, TYPE_LABELS } from "./format";
|
|
8
8
|
import { Chip, Status, useStyles } from "./ui";
|
|
9
9
|
|
|
10
|
+
type Tokens = NonNullable<import("../shared/contracts").StatsSummary["tokens"]>;
|
|
11
|
+
|
|
12
|
+
const pct = (x: number) => `${Math.round(x * 100)}%`;
|
|
13
|
+
|
|
14
|
+
/** Where the tokens went (1.13): a bar per step and per model, and the share spent on rework. */
|
|
15
|
+
function TokenShare({ theme, compact, t }: { theme: import("@getpaseo/plugin/client").PluginHostProps["theme"]; compact: boolean; t: Tokens }) {
|
|
16
|
+
const s = useStyles(theme, compact);
|
|
17
|
+
const bar = (key: string, label: string, value: number, share: number, note: string) => (
|
|
18
|
+
<View key={key} style={{ gap: 2 }}>
|
|
19
|
+
<View style={[s.row, { justifyContent: "space-between", flexWrap: "nowrap" }]}>
|
|
20
|
+
<Text style={[s.text, { flexShrink: 1 }]}>{label}</Text>
|
|
21
|
+
<Text style={s.muted}>
|
|
22
|
+
{tokens(value)} · {pct(share)}
|
|
23
|
+
{note}
|
|
24
|
+
</Text>
|
|
25
|
+
</View>
|
|
26
|
+
<View style={{ height: 6, borderRadius: 3, backgroundColor: theme.colors.surface2, overflow: "hidden" }}>
|
|
27
|
+
<View style={{ width: `${Math.max(1, Math.round(share * 100))}%`, height: 6, backgroundColor: theme.colors.accent }} />
|
|
28
|
+
</View>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
return (
|
|
32
|
+
<View style={[s.card, { gap: 10 }]}>
|
|
33
|
+
<Text style={s.heading}>token 花在哪</Text>
|
|
34
|
+
<Text style={s.text}>
|
|
35
|
+
合计 {tokens(t.total)} tokens
|
|
36
|
+
{t.rework ? `,其中返工(修订、修复、复验和第 2 轮起的评审)占 ${pct(t.rework.share)}` : ""}
|
|
37
|
+
{t.cachedShare !== null ? `;输入中 ${pct(t.cachedShare)} 来自提示缓存` : ""}
|
|
38
|
+
</Text>
|
|
39
|
+
{t.perRequirement.median !== null ? <Text style={s.muted}>每个完成的需求中位数 {tokens(t.perRequirement.median)}</Text> : null}
|
|
40
|
+
<View style={{ gap: 8 }}>{t.byStep.slice(0, 12).map((r) => bar(r.stepId, r.title, r.tokens, r.share, `(${r.steps} 次)`))}</View>
|
|
41
|
+
{t.byAgent.some((a) => a.agent !== "(未记录)") ? (
|
|
42
|
+
<>
|
|
43
|
+
<Text style={[s.text, { fontWeight: "600" }]}>按模型</Text>
|
|
44
|
+
<View style={{ gap: 8 }}>{t.byAgent.map((r) => bar(r.agent, r.agent, r.tokens, r.share, `(${r.steps} 步)`))}</View>
|
|
45
|
+
</>
|
|
46
|
+
) : null}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
10
51
|
function month(offset: number, now = new Date()): string {
|
|
11
52
|
const d = new Date(now.getFullYear(), now.getMonth() + offset, 1);
|
|
12
53
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
@@ -50,6 +91,7 @@ export function StatsScreen({ theme, layout }: PluginScreenProps) {
|
|
|
50
91
|
{card("agent 合计用时", minutes(d.usage.agentMinutes), `输入 ${tokens(d.usage.inputTokens)},输出 ${tokens(d.usage.outputTokens)} tokens`)}
|
|
51
92
|
{d.usage.costUsd !== null ? card("费用", `$${d.usage.costUsd.toFixed(2)}`) : null}
|
|
52
93
|
</View>
|
|
94
|
+
{d.tokens?.total ? <TokenShare theme={theme} compact={layout.compact} t={d.tokens} /> : null}
|
|
53
95
|
{d.waits.length > 0 ? (
|
|
54
96
|
<View style={s.card}>
|
|
55
97
|
<Text style={s.heading}>等待</Text>
|
|
@@ -39,7 +39,23 @@ export default function contribute(server: PluginServerContext) {
|
|
|
39
39
|
server.handle(configRpc, (input) => handlers.config(input));
|
|
40
40
|
server.handle(setAgentsRpc, (input) => handlers.setAgents(input));
|
|
41
41
|
server.handle(reviewUrlRpc, (input) => handlers.reviewUrl(input));
|
|
42
|
+
// 1.13: every few minutes, continue this machine's pipelines whose background process died
|
|
43
|
+
// (req api watchdog). Deterministic, no model involved; while it runs, pipelines do not ask
|
|
44
|
+
// their chats to check on them. Off when the plugin is read-only.
|
|
45
|
+
const check = async () => {
|
|
46
|
+
try {
|
|
47
|
+
await load();
|
|
48
|
+
if (readOnly) return;
|
|
49
|
+
await reqApi(kitHome)(["watchdog", "--by", "plugin"], { timeoutMs: 240_000 });
|
|
50
|
+
} catch {
|
|
51
|
+
// req-kit missing or a check failing: the next one tries again.
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const first = setTimeout(() => void check(), 60_000);
|
|
55
|
+
const timer = setInterval(() => void check(), 5 * 60_000);
|
|
42
56
|
return () => {
|
|
57
|
+
clearTimeout(first);
|
|
58
|
+
clearInterval(timer);
|
|
43
59
|
unsubscribe();
|
|
44
60
|
};
|
|
45
61
|
}
|