paseo-req-kit 1.11.0 → 1.12.0-beta.2
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 +26 -0
- package/bin/req.mjs +1 -0
- package/config.example.json +3 -1
- package/contracts/cli-json.md +2 -0
- package/contracts/state.md +3 -1
- package/contracts/usage-record.md +4 -1
- package/docs/adr/0031-fewer-interventions.md +33 -0
- package/docs/adr/README.md +1 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +3 -2
- 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 +21 -3
- package/engine/prompts/04b-design-revise.md +15 -0
- package/engine/src/api.ts +33 -1
- package/engine/src/card-cmd.ts +8 -1
- package/engine/src/card.ts +9 -2
- package/engine/src/config.ts +23 -2
- package/engine/src/configure.ts +161 -36
- package/engine/src/control.ts +1 -0
- package/engine/src/factory.ts +48 -4
- package/engine/src/main.ts +1 -0
- package/engine/src/paseo-profiles.ts +2 -0
- package/engine/src/pipeline.ts +161 -26
- package/engine/src/records.ts +8 -0
- package/engine/src/state.ts +30 -0
- package/engine/src/stats.ts +90 -0
- package/engine/src/steps.ts +4 -1
- package/install.mjs +5 -1
- package/package.json +2 -2
- package/paseo-plugin/OVERVIEW.md +2 -0
- package/paseo-plugin/client/data.ts +15 -1
- package/paseo-plugin/client/models.tsx +117 -0
- package/paseo-plugin/client/settings.tsx +3 -1
- package/paseo-plugin/index.server.ts +3 -1
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +14 -1
- package/paseo-plugin/shared/contracts.ts +37 -0
- package/skills/req/playbooks/new.md +14 -14
- package/skills/req/playbooks/small.md +3 -3
- package/skills/req/playbooks/unattended.md +4 -2
package/engine/src/pipeline.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { describeExternal, type IntegrationEvent, type Runner, syncIntegration }
|
|
|
37
37
|
import { cardSlug, planAppendix, publishCard, readSource } from "./card.js";
|
|
38
38
|
import { principlesSection, projectAdditions, type PromptLocations, readToolPrompt } from "./prompts.js";
|
|
39
39
|
import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent, WaitOutcome } from "./runtime.js";
|
|
40
|
-
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type RunPhases, type StepRecord, writeState } from "./state.js";
|
|
40
|
+
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type Recovery, type RunPhases, type StepRecord, type StopKind, writeState } from "./state.js";
|
|
41
41
|
import { type PanelStep, STEPS, type StepDef, type StepId, stepKey, validateResult, VERIFY_PHASE_STEPS } from "./steps.js";
|
|
42
42
|
|
|
43
43
|
export interface PipelineContext {
|
|
@@ -125,7 +125,38 @@ class StepFailure extends Error {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/** The step gave up on its agent while the agent may still be working (1.11). */
|
|
128
|
-
class AgentLeftRunning extends StepFailure {
|
|
128
|
+
class AgentLeftRunning extends StepFailure {
|
|
129
|
+
constructor(
|
|
130
|
+
stepTitle: string,
|
|
131
|
+
message: string,
|
|
132
|
+
readonly timedOut: boolean,
|
|
133
|
+
) {
|
|
134
|
+
super(stepTitle, message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The agent's turn ended with an error status (1.12). */
|
|
139
|
+
class AgentErrored extends StepFailure {}
|
|
140
|
+
|
|
141
|
+
/** The agent did not write a valid result file, even after the reminders (1.12). */
|
|
142
|
+
class ResultInvalid extends StepFailure {}
|
|
143
|
+
|
|
144
|
+
/** Notes something the pipeline handled by itself (state.recoveries, the log). */
|
|
145
|
+
function recover(ctx: PipelineContext, key: string, kind: Recovery["kind"], detail: string): void {
|
|
146
|
+
ctx.state.recoveries = [...(ctx.state.recoveries ?? []), { at: new Date().toISOString(), key, kind, detail: detail.slice(0, 300) }].slice(-200);
|
|
147
|
+
save(ctx);
|
|
148
|
+
ctx.log(`自动处理(${key}):${detail}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Why a run stopped, as a code for the usage record (1.12). */
|
|
152
|
+
function failureKind(error: unknown): StopKind {
|
|
153
|
+
if (error instanceof AgentLeftRunning) return error.timedOut ? "timeout" : "transient";
|
|
154
|
+
const message = (error as Error).message ?? "";
|
|
155
|
+
if (error instanceof StepFailure && isTransientError(message)) return "transient";
|
|
156
|
+
if (error instanceof AgentErrored) return "agent_error";
|
|
157
|
+
if (error instanceof ResultInvalid) return "invalid_result";
|
|
158
|
+
return "other";
|
|
159
|
+
}
|
|
129
160
|
|
|
130
161
|
/**
|
|
131
162
|
* Errors that usually go away by themselves: provider rate limits and overload,
|
|
@@ -487,15 +518,31 @@ async function milestone(ctx: PipelineContext, id: string, text: string): Promis
|
|
|
487
518
|
await notifyExternal(ctx, "milestone", text, { milestone: id });
|
|
488
519
|
}
|
|
489
520
|
|
|
490
|
-
|
|
521
|
+
/** A step's time limit; `extended` once it was given a second period (selfHeal). */
|
|
522
|
+
interface Deadline {
|
|
523
|
+
at: number;
|
|
524
|
+
extended: boolean;
|
|
525
|
+
key: string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, deadline: Deadline): Promise<AgentUsage | null> {
|
|
491
529
|
const notifiedPermissions = new Set<string>();
|
|
492
530
|
let consecutiveErrors = 0;
|
|
493
531
|
for (;;) {
|
|
494
|
-
|
|
532
|
+
let remaining = deadline.at - Date.now();
|
|
533
|
+
if (remaining <= 0 && ctx.config.selfHeal && !deadline.extended) {
|
|
534
|
+
// Usually a big step, not a stuck one: the agent keeps its progress and gets as long again.
|
|
535
|
+
deadline.extended = true;
|
|
536
|
+
deadline.at = Date.now() + ctx.config.stepTimeoutMinutes[def.id] * 60_000;
|
|
537
|
+
remaining = deadline.at - Date.now();
|
|
538
|
+
recover(ctx, deadline.key, "extend_timeout", `超过 ${ctx.config.stepTimeoutMinutes[def.id]} 分钟仍未完成,agent 还在做,再给它同样长的时间`);
|
|
539
|
+
}
|
|
495
540
|
if (remaining <= 0) {
|
|
541
|
+
const total = ctx.config.stepTimeoutMinutes[def.id] * (deadline.extended ? 2 : 1);
|
|
496
542
|
throw new AgentLeftRunning(
|
|
497
543
|
def.title,
|
|
498
|
-
`超过 ${
|
|
544
|
+
`超过 ${total} 分钟仍未完成。agent「${agentTitle}」还在运行,没有停止它;继续时会先看它的情况:做完了就用它的结果,还在做就接着等它,不会另起一个 agent 重复做。`,
|
|
545
|
+
true,
|
|
499
546
|
);
|
|
500
547
|
}
|
|
501
548
|
let outcome;
|
|
@@ -506,7 +553,7 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
506
553
|
consecutiveErrors += 1;
|
|
507
554
|
ctx.log(`等待 ${agentTitle} 时出错(第 ${consecutiveErrors} 次):${(error as Error).message}`);
|
|
508
555
|
if (consecutiveErrors >= 5) {
|
|
509
|
-
throw new AgentLeftRunning(def.title, `连续 ${consecutiveErrors} 次无法获取 agent 状态:${(error as Error).message}
|
|
556
|
+
throw new AgentLeftRunning(def.title, `连续 ${consecutiveErrors} 次无法获取 agent 状态:${(error as Error).message}`, false);
|
|
510
557
|
}
|
|
511
558
|
await ctx.sleep(Math.min(60_000, 5_000 * consecutiveErrors));
|
|
512
559
|
continue;
|
|
@@ -517,7 +564,7 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
517
564
|
case "timeout":
|
|
518
565
|
continue;
|
|
519
566
|
case "error":
|
|
520
|
-
throw new
|
|
567
|
+
throw new AgentErrored(def.title, `agent 报告错误:${outcome.error ?? "未知错误"}`);
|
|
521
568
|
case "permission": {
|
|
522
569
|
const fresh = outcome.pendingPermissions.filter((p) => !notifiedPermissions.has(p.id));
|
|
523
570
|
if (fresh.length > 0) {
|
|
@@ -562,31 +609,63 @@ async function runStep(
|
|
|
562
609
|
extras: StepExtras = {},
|
|
563
610
|
): Promise<Record<string, unknown>> {
|
|
564
611
|
const { attempts, backoffSeconds } = ctx.config.retry;
|
|
612
|
+
const key = extras.key ?? keyFor(stepKey(id, round), extras.target);
|
|
613
|
+
const fallback = ctx.config.agents.fallback ?? null;
|
|
614
|
+
let redone = false;
|
|
615
|
+
let usedFallback = false;
|
|
565
616
|
for (let retry = 0; ; retry += 1) {
|
|
566
617
|
try {
|
|
567
618
|
return await runStepOnce(ctx, id, round, maxRounds, extras);
|
|
568
619
|
} catch (error) {
|
|
569
620
|
const message = (error as Error).message;
|
|
570
|
-
|
|
621
|
+
const transient = error instanceof StepFailure && isTransientError(message);
|
|
622
|
+
const healable = error instanceof AgentErrored || error instanceof ResultInvalid;
|
|
623
|
+
// 1.12: a step whose agent errored or wrote no valid result is redone once with a new
|
|
624
|
+
// agent; one that keeps hitting rate limits gets the fallback model, when there is one.
|
|
625
|
+
if (error instanceof StepFailure && ctx.config.selfHeal && !transient && healable && !redone) {
|
|
626
|
+
redone = true;
|
|
627
|
+
const switchModel = fallback !== null && !usedFallback && error instanceof AgentErrored;
|
|
628
|
+
if (switchModel) usedFallback = true;
|
|
629
|
+
recover(
|
|
630
|
+
ctx,
|
|
631
|
+
key,
|
|
632
|
+
switchModel ? "fallback" : "redo",
|
|
633
|
+
`${message.slice(0, 160)};${switchModel ? "换备用模型" : "新开一个 agent"}重做这一步`,
|
|
634
|
+
);
|
|
635
|
+
extras = withRetryNote({ ...extras, ...(switchModel ? { profile: fallback } : {}) }, message);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (error instanceof StepFailure && transient && retry >= attempts && fallback !== null && !usedFallback && !(error instanceof AgentLeftRunning)) {
|
|
639
|
+
usedFallback = true;
|
|
640
|
+
recover(ctx, key, "fallback", `重试 ${retry} 次后仍然${message.slice(0, 120)};换备用模型继续这一步`);
|
|
641
|
+
extras = withRetryNote({ ...extras, profile: fallback }, message);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (!(error instanceof StepFailure) || !transient || retry >= attempts) {
|
|
571
645
|
if (error instanceof StepFailure && retry > 0) {
|
|
572
|
-
throw new StepFailure(error.stepTitle, `${message}(已自动重试 ${retry} 次)`);
|
|
646
|
+
throw new (error.constructor as typeof StepFailure)(error.stepTitle, `${message}(已自动重试 ${retry} 次)`);
|
|
573
647
|
}
|
|
574
648
|
throw error;
|
|
575
649
|
}
|
|
576
650
|
const wait = backoffSeconds.at(Math.min(retry, backoffSeconds.length - 1)) ?? 60;
|
|
577
|
-
ctx.
|
|
651
|
+
recover(ctx, key, "retry", `遇到临时错误:${message.slice(0, 160)}。${wait} 秒后自动重试(第 ${retry + 1} 次,最多 ${attempts} 次)`);
|
|
578
652
|
await ctx.sleep(wait * 1000);
|
|
579
|
-
extras =
|
|
580
|
-
...extras,
|
|
581
|
-
contextLines: [
|
|
582
|
-
...(extras.contextLines ?? []).filter((l) => !l.startsWith("自动重试:")),
|
|
583
|
-
`自动重试:本步骤上一次因临时错误中断(${message.slice(0, 200)})。工作区里可能留有上一次没做完的改动,先用 git status 和 git diff 查看,在此基础上继续,不要重复提交`,
|
|
584
|
-
],
|
|
585
|
-
};
|
|
653
|
+
extras = withRetryNote(extras, message);
|
|
586
654
|
}
|
|
587
655
|
}
|
|
588
656
|
}
|
|
589
657
|
|
|
658
|
+
/** Tells the next agent of a step that an earlier one stopped part way. */
|
|
659
|
+
function withRetryNote(extras: StepExtras, message: string): StepExtras {
|
|
660
|
+
return {
|
|
661
|
+
...extras,
|
|
662
|
+
contextLines: [
|
|
663
|
+
...(extras.contextLines ?? []).filter((l) => !l.startsWith("自动重试:")),
|
|
664
|
+
`自动重试:本步骤上一次中断了(${message.slice(0, 200)})。工作区里可能留有上一次没做完的改动,先用 git status 和 git diff 查看,在此基础上继续,不要重复提交`,
|
|
665
|
+
],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
590
669
|
async function runStepOnce(
|
|
591
670
|
ctx: PipelineContext,
|
|
592
671
|
id: StepId,
|
|
@@ -671,7 +750,7 @@ async function runStepOnce(
|
|
|
671
750
|
ctx.state.history.push(record);
|
|
672
751
|
save(ctx);
|
|
673
752
|
|
|
674
|
-
const deadline = Date.now() + ctx.config.stepTimeoutMinutes[id] * 60_000;
|
|
753
|
+
const deadline: Deadline = { at: Date.now() + ctx.config.stepTimeoutMinutes[id] * 60_000, extended: false, key };
|
|
675
754
|
record.usage = (await waitForTurn(ctx, agent, def, agentTitle, deadline)) ?? record.usage ?? null;
|
|
676
755
|
let validation = readResultFile(resultAbs, def);
|
|
677
756
|
while (!validation.ok && record.followUps < ctx.config.resultFollowUps) {
|
|
@@ -683,7 +762,7 @@ async function runStepOnce(
|
|
|
683
762
|
validation = readResultFile(resultAbs, def);
|
|
684
763
|
}
|
|
685
764
|
if (!validation.ok) {
|
|
686
|
-
throw new
|
|
765
|
+
throw new ResultInvalid(def.title, `提醒 ${record.followUps} 次后仍没有得到有效的结果文件:${validation.error}`);
|
|
687
766
|
}
|
|
688
767
|
|
|
689
768
|
if (extras.gate) {
|
|
@@ -761,7 +840,7 @@ async function checkLeftover(
|
|
|
761
840
|
last.result = validation.value;
|
|
762
841
|
ctx.state.results[key] = validation.value;
|
|
763
842
|
save(ctx);
|
|
764
|
-
ctx
|
|
843
|
+
recover(ctx, key, "reuse_result", `上一次的 agent ${last.agentId} 已经做完,沿用它的结果`);
|
|
765
844
|
if (ctx.config.archiveStepAgents) await agent.archive().catch(() => undefined);
|
|
766
845
|
return { done: validation.value };
|
|
767
846
|
}
|
|
@@ -769,7 +848,10 @@ async function checkLeftover(
|
|
|
769
848
|
return {};
|
|
770
849
|
}
|
|
771
850
|
if (status === "timeout" || status === "permission") {
|
|
772
|
-
if ((last.reattached ?? 0) < 1)
|
|
851
|
+
if ((last.reattached ?? 0) < 1) {
|
|
852
|
+
recover(ctx, key, "reattach", `上一次的 agent ${last.agentId} 还在做,接着等它,不另起一个`);
|
|
853
|
+
return { agent, reattached: (last.reattached ?? 0) + 1 };
|
|
854
|
+
}
|
|
773
855
|
ctx.log(`${key} 上一次的 agent ${last.agentId} 已经等过两轮仍未完成,停止它并新建一个 agent`);
|
|
774
856
|
await ctx.runtime
|
|
775
857
|
.interruptAgent(last.agentId, "这一步已超时,流水线会另起一个 agent 重做。请立即停止当前工作,不要再修改任何文件,也不要写结果文件,直接结束。")
|
|
@@ -1023,6 +1105,26 @@ async function scheduleApprovalContinue(ctx: PipelineContext): Promise<string |
|
|
|
1023
1105
|
}
|
|
1024
1106
|
|
|
1025
1107
|
/** A Paseo schedule that has the chat continue the run later; returns the time as text, or null. */
|
|
1108
|
+
/** Rate limits and used-up subscriptions, as opposed to network trouble. */
|
|
1109
|
+
const QUOTA_ERROR = /rate.?limit|too many requests|\b429\b|quota|usage limit|额度|限流/i;
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* 1.12: when a step stopped on rate limits and Paseo says when a used-up subscription window
|
|
1113
|
+
* resets, the time to continue (5 minutes after the reset, within 24 hours); null otherwise.
|
|
1114
|
+
*/
|
|
1115
|
+
async function quotaResetTime(ctx: PipelineContext, reason: string): Promise<Date | null> {
|
|
1116
|
+
if (!QUOTA_ERROR.test(reason)) return null;
|
|
1117
|
+
const profiles = [ctx.config.agents.builder, ctx.config.agents.reviewer, ...(ctx.config.agents.fallback ? [ctx.config.agents.fallback] : [])];
|
|
1118
|
+
const providers = [...new Set(profiles.map((p) => (p.provider ?? "").split("/")[0]).filter(Boolean))];
|
|
1119
|
+
const quotas = await ctx.runtime.providerQuota(providers).catch(() => null);
|
|
1120
|
+
const now = Date.now();
|
|
1121
|
+
const resets = (quotas ?? [])
|
|
1122
|
+
.filter((q) => q.resetsAt && ((q.usedPct !== null && q.usedPct >= 95) || q.tone === "danger"))
|
|
1123
|
+
.map((q) => new Date(q.resetsAt!).getTime())
|
|
1124
|
+
.filter((t) => t > now && t < now + 24 * 3600_000);
|
|
1125
|
+
return resets.length === 0 ? null : new Date(Math.max(...resets) + 5 * 60_000);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1026
1128
|
async function scheduleAutoResume(ctx: PipelineContext, reason: string): Promise<string | null> {
|
|
1027
1129
|
const minutes = ctx.config.autoResumeMinutes;
|
|
1028
1130
|
const origin = ctx.state.originAgentId;
|
|
@@ -1032,7 +1134,8 @@ async function scheduleAutoResume(ctx: PipelineContext, reason: string): Promise
|
|
|
1032
1134
|
ctx.log(`这次尝试已经自动继续过 ${used} 次,不再预约`);
|
|
1033
1135
|
return null;
|
|
1034
1136
|
}
|
|
1035
|
-
const
|
|
1137
|
+
const reset = await quotaResetTime(ctx, reason);
|
|
1138
|
+
const at = reset ?? new Date(Date.now() + minutes * 60_000);
|
|
1036
1139
|
try {
|
|
1037
1140
|
const id = await ctx.runtime.scheduleMessage({
|
|
1038
1141
|
agentId: origin,
|
|
@@ -1049,7 +1152,9 @@ async function scheduleAutoResume(ctx: PipelineContext, reason: string): Promise
|
|
|
1049
1152
|
ctx.state.schedules = [...(ctx.state.schedules ?? []), { id, kind: "auto-resume", at: at.toISOString() }];
|
|
1050
1153
|
ctx.state.autoResumes = { attempt: ctx.state.attempt, count: used + 1 };
|
|
1051
1154
|
save(ctx);
|
|
1052
|
-
|
|
1155
|
+
const when = at.toLocaleString("zh-CN", { hour12: false });
|
|
1156
|
+
if (reset) recover(ctx, "pipeline", "quota_wait", `额度用完,Paseo 显示会恢复;已预约在 ${when} 自动继续(Paseo 定时任务 ${id})`);
|
|
1157
|
+
else recover(ctx, "pipeline", "auto_resume", `已预约 ${minutes} 分钟后自动继续(Paseo 定时任务 ${id})`);
|
|
1053
1158
|
return at.toLocaleString("zh-CN", { hour12: false, month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
1054
1159
|
} catch (error) {
|
|
1055
1160
|
ctx.log(`预约自动继续失败:${(error as Error).message}`);
|
|
@@ -1080,7 +1185,7 @@ async function checkQuota(ctx: PipelineContext): Promise<void> {
|
|
|
1080
1185
|
[
|
|
1081
1186
|
"【需求开发流水线通知】这条消息来自后台流水线,不是用户发的。",
|
|
1082
1187
|
`额度提醒:${items.join(";")}。`,
|
|
1083
|
-
"
|
|
1188
|
+
`流水线照常进行;额度用完时会遇到限流,流水线会先重试${ctx.config.agents.fallback ? "、再换备用模型" : ""},仍不行就预约在额度恢复后自动继续,不需要人处理。如果希望等额度恢复后再开发,请转告用户,用户同意的话可以先暂停,再预约在恢复时间开始。`,
|
|
1084
1189
|
].join("\n"),
|
|
1085
1190
|
);
|
|
1086
1191
|
}
|
|
@@ -1567,11 +1672,33 @@ async function assessmentPhase(ctx: PipelineContext): Promise<void> {
|
|
|
1567
1672
|
for (let r = 1; r <= rounds.designReview; r++) {
|
|
1568
1673
|
const review = await runReview(ctx, "design_review", r, rounds.designReview);
|
|
1569
1674
|
if (review.verdict === "approved") break;
|
|
1570
|
-
if (review.verdict === "blocked")
|
|
1675
|
+
if (review.verdict === "blocked") {
|
|
1676
|
+
await answerFirst(ctx, review, r, rounds.designReview);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1571
1679
|
await runStep(ctx, "design_revise", r, rounds.designReview);
|
|
1572
1680
|
}
|
|
1573
1681
|
}
|
|
1574
1682
|
|
|
1683
|
+
/**
|
|
1684
|
+
* The review says only the requester can decide (1.12): the builder first answers what the
|
|
1685
|
+
* code, the documents and the spec settle, in a revision of the design; what is left goes to
|
|
1686
|
+
* the requester, each with a recommended answer.
|
|
1687
|
+
*/
|
|
1688
|
+
async function answerFirst(ctx: PipelineContext, review: Record<string, unknown>, round: number, maxRounds: number): Promise<void> {
|
|
1689
|
+
const questions = (review.questions as string[]) ?? [];
|
|
1690
|
+
if (!ctx.config.answerQuestionsFirst) throw new BusinessDecisionNeeded(questions);
|
|
1691
|
+
const revised = await runStep(ctx, "design_revise", round, maxRounds, {
|
|
1692
|
+
contextLines: [`审核结论:需要需求方决定(${questions.length} 个问题,见 design-review.md)。按本步骤说明中"审核要求需求方决定时"一节,先自己找答案`],
|
|
1693
|
+
});
|
|
1694
|
+
const left = Array.isArray(revised.business_questions) ? (revised.business_questions as string[]).map((q) => q.trim()).filter(Boolean) : [];
|
|
1695
|
+
const answered = Math.max(0, questions.length - left.length);
|
|
1696
|
+
if (answered > 0) {
|
|
1697
|
+
recover(ctx, `design_revise-r${round}`, "self_answer", `方案审核提出的 ${questions.length} 个问题中,${answered} 个由 builder 从代码和文档中确定了答案${left.length > 0 ? `,${left.length} 个交给需求方` : ",不用等需求方"}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (left.length > 0) throw new BusinessDecisionNeeded(left);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1575
1702
|
/** Develop phase: design → design review ⇄ revise → implement → code review ⇄ fix → handoff. Returns the design result. */
|
|
1576
1703
|
async function developPhase(ctx: PipelineContext): Promise<Record<string, unknown>> {
|
|
1577
1704
|
const rounds = ctx.config.profiles[ctx.state.profile].rounds;
|
|
@@ -1602,7 +1729,10 @@ async function developPhase(ctx: PipelineContext): Promise<Record<string, unknow
|
|
|
1602
1729
|
const review = await runReview(ctx, "design_review", r, rounds.designReview);
|
|
1603
1730
|
designRounds = r;
|
|
1604
1731
|
if (review.verdict === "approved") break;
|
|
1605
|
-
if (review.verdict === "blocked")
|
|
1732
|
+
if (review.verdict === "blocked") {
|
|
1733
|
+
await answerFirst(ctx, review, r, rounds.designReview);
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1606
1736
|
// changes_required. The last round's revision goes straight to implementation;
|
|
1607
1737
|
// the implementer reads both the review and the revision record.
|
|
1608
1738
|
await runStep(ctx, "design_revise", r, rounds.designReview);
|
|
@@ -1876,6 +2006,7 @@ async function flow(ctx: PipelineContext): Promise<Record<string, unknown>> {
|
|
|
1876
2006
|
export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome> {
|
|
1877
2007
|
ctx.state.status = "running";
|
|
1878
2008
|
ctx.state.stopReason = null;
|
|
2009
|
+
ctx.state.stopKind = null;
|
|
1879
2010
|
save(ctx);
|
|
1880
2011
|
await integrate(ctx, phaseOf(ctx) === "verify" ? "verify" : "started");
|
|
1881
2012
|
try {
|
|
@@ -1917,6 +2048,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1917
2048
|
if (error instanceof PhaseGate) {
|
|
1918
2049
|
ctx.state.status = "phase_done";
|
|
1919
2050
|
ctx.state.stopReason = null;
|
|
2051
|
+
ctx.state.stopKind = "phase_gate";
|
|
1920
2052
|
save(ctx);
|
|
1921
2053
|
await stopManagedServices(ctx);
|
|
1922
2054
|
const verifyAt = await scheduleVerifyStart(ctx);
|
|
@@ -1947,6 +2079,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1947
2079
|
if (error instanceof ApprovalNeeded) {
|
|
1948
2080
|
ctx.state.status = "waiting_for_approval";
|
|
1949
2081
|
ctx.state.stopReason = null;
|
|
2082
|
+
ctx.state.stopKind = "approval";
|
|
1950
2083
|
save(ctx);
|
|
1951
2084
|
const autoContinueAt = await scheduleApprovalContinue(ctx);
|
|
1952
2085
|
const reminderHours = await scheduleReminder(ctx, "approval");
|
|
@@ -1974,6 +2107,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1974
2107
|
if (error instanceof BusinessDecisionNeeded) {
|
|
1975
2108
|
const reproduce = error.source === "reproduce";
|
|
1976
2109
|
ctx.state.status = "waiting_for_business";
|
|
2110
|
+
ctx.state.stopKind = reproduce ? "reproduce" : "business";
|
|
1977
2111
|
ctx.state.stopReason = `${reproduce ? "没能复现缺陷,需要需求方补充信息" : "方案审核需要需求方决定"}:${error.questions.join(";")}`;
|
|
1978
2112
|
save(ctx);
|
|
1979
2113
|
const reminderHours = await scheduleReminder(ctx, error.questions);
|
|
@@ -1994,6 +2128,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1994
2128
|
const stepTitle = error instanceof StepFailure ? error.stepTitle : "流水线";
|
|
1995
2129
|
ctx.state.status = "failed";
|
|
1996
2130
|
ctx.state.stopReason = `${stepTitle}:${(error as Error).message}`;
|
|
2131
|
+
ctx.state.stopKind = failureKind(error);
|
|
1997
2132
|
save(ctx);
|
|
1998
2133
|
const autoResumeAt =
|
|
1999
2134
|
error instanceof StepFailure && isTransientError((error as Error).message) ? await scheduleAutoResume(ctx, (error as Error).message) : null;
|
package/engine/src/records.ts
CHANGED
|
@@ -165,6 +165,10 @@ export interface RunRecord {
|
|
|
165
165
|
/** The report's verdict when the run completed (completed, completed_with_issues, not_completed). */
|
|
166
166
|
outcome: string | null;
|
|
167
167
|
stopReason: string | null;
|
|
168
|
+
/** 1.12: why the run stopped for a person, as a code (absent in older records). */
|
|
169
|
+
stopKind?: string | null;
|
|
170
|
+
/** 1.12: what the pipeline handled by itself during the run, by kind (absent in older records). */
|
|
171
|
+
recoveries?: Record<string, number>;
|
|
168
172
|
};
|
|
169
173
|
agents: { builder: string | null; reviewer: string | null };
|
|
170
174
|
steps: StepUsageRecord[];
|
|
@@ -241,6 +245,10 @@ export function runRecord(input: {
|
|
|
241
245
|
status: state.status,
|
|
242
246
|
outcome: state.status === "completed" ? state.outcome : null,
|
|
243
247
|
stopReason: state.stopReason,
|
|
248
|
+
stopKind: state.status === "completed" || state.status === "running" ? null : (state.stopKind ?? null),
|
|
249
|
+
recoveries: (state.recoveries ?? [])
|
|
250
|
+
.filter((r) => new Date(r.at).getTime() >= since)
|
|
251
|
+
.reduce<Record<string, number>>((m, r) => ({ ...m, [r.kind]: (m[r.kind] ?? 0) + 1 }), {}),
|
|
244
252
|
},
|
|
245
253
|
agents: input.agents,
|
|
246
254
|
steps,
|
package/engine/src/state.ts
CHANGED
|
@@ -27,6 +27,32 @@ export interface RunPhases {
|
|
|
27
27
|
defects: DefectHandling;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Why a run stopped for a person (1.12). Necessary decisions: approval (the plan), business
|
|
32
|
+
* (a question only the requester can answer), reproduce (a defect that could not be reproduced),
|
|
33
|
+
* phase_gate (development done, waiting for testing). The rest are interruptions.
|
|
34
|
+
*/
|
|
35
|
+
export type StopKind =
|
|
36
|
+
| "approval"
|
|
37
|
+
| "business"
|
|
38
|
+
| "reproduce"
|
|
39
|
+
| "phase_gate"
|
|
40
|
+
| "timeout"
|
|
41
|
+
| "agent_error"
|
|
42
|
+
| "invalid_result"
|
|
43
|
+
| "transient"
|
|
44
|
+
| "stopped"
|
|
45
|
+
| "process_exit"
|
|
46
|
+
| "other";
|
|
47
|
+
|
|
48
|
+
/** Something the pipeline handled by itself (1.12). */
|
|
49
|
+
export interface Recovery {
|
|
50
|
+
at: string;
|
|
51
|
+
key: string;
|
|
52
|
+
kind: "retry" | "reattach" | "reuse_result" | "extend_timeout" | "redo" | "fallback" | "auto_resume" | "quota_wait" | "self_answer";
|
|
53
|
+
detail: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export interface StepRecord {
|
|
31
57
|
key: string;
|
|
32
58
|
stepId: StepId;
|
|
@@ -69,6 +95,10 @@ export interface PipelineState {
|
|
|
69
95
|
results: Record<string, Record<string, unknown>>;
|
|
70
96
|
history: StepRecord[];
|
|
71
97
|
stopReason: string | null;
|
|
98
|
+
/** 1.12: why the run stopped for a person, as a code (contracts/usage-record.md); null while running or done. */
|
|
99
|
+
stopKind?: StopKind | null;
|
|
100
|
+
/** 1.12: what the pipeline handled by itself instead of stopping (retries, waiting longer, a fallback model…). */
|
|
101
|
+
recoveries?: Recovery[];
|
|
72
102
|
outcome: string | null;
|
|
73
103
|
/** "quick" or "standard"; decides loop rounds and optional steps. */
|
|
74
104
|
profile: ProfileName;
|
package/engine/src/stats.ts
CHANGED
|
@@ -109,6 +109,35 @@ function waitKind(status: string): WaitKind {
|
|
|
109
109
|
return "other";
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Why runs stopped for a person (1.12, state.stopKind). */
|
|
113
|
+
export const STOP_LABELS: Record<string, string> = {
|
|
114
|
+
approval: "确认方案",
|
|
115
|
+
phase_gate: "研发完成,等待测试",
|
|
116
|
+
business: "方案审核的问题等需求方决定",
|
|
117
|
+
reproduce: "缺陷复现不了,等需求方补充",
|
|
118
|
+
timeout: "步骤超时",
|
|
119
|
+
agent_error: "agent 报错",
|
|
120
|
+
invalid_result: "结果文件无效",
|
|
121
|
+
transient: "限流、网络等临时错误(自动重试用完)",
|
|
122
|
+
stopped: "被手动停止",
|
|
123
|
+
process_exit: "后台进程意外退出",
|
|
124
|
+
other: "其他中断",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Stops that are the decisions people are meant to make; the rest are what 1.12 works to remove. */
|
|
128
|
+
export const NECESSARY_STOPS = ["approval", "phase_gate"];
|
|
129
|
+
|
|
130
|
+
/** The stop code of a run; older records (before 1.12) are inferred from the status. */
|
|
131
|
+
export function stopKindOf(r: RunRecord): string | null {
|
|
132
|
+
if (r.run.stopKind !== undefined) return r.run.stopKind;
|
|
133
|
+
if (r.run.status === "waiting_for_approval") return "approval";
|
|
134
|
+
if (r.run.status === "waiting_for_business") return "business";
|
|
135
|
+
if (r.run.status === "phase_done") return "phase_gate";
|
|
136
|
+
if (r.run.status === "failed") return /手动停止|被 SIG/.test(r.run.stopReason ?? "") ? "stopped" : "other";
|
|
137
|
+
if (r.run.status === "running") return "process_exit";
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
/** Records of the same run seen twice (merged exports) count once. */
|
|
113
142
|
export function dedupe(runs: RunRecord[]): RunRecord[] {
|
|
114
143
|
const seen = new Map<string, RunRecord>();
|
|
@@ -208,6 +237,19 @@ export interface StatsSummary {
|
|
|
208
237
|
waits: Array<{ kind: WaitKind; label: string; requirements: number; averageMinutes: number }>;
|
|
209
238
|
usage: { agentMinutes: number; inputTokens: number | null; outputTokens: number | null; costUsd: number | null };
|
|
210
239
|
byBuilder: Array<{ builder: string; requirements: number; averageReviewRounds: number | null; averageCostUsd: number | null }>;
|
|
240
|
+
/** 1.12: why the period's runs stopped for a person, most frequent first. */
|
|
241
|
+
stops: Array<{ kind: string; label: string; necessary: boolean; runs: number }>;
|
|
242
|
+
/** 1.12: how much went without a person. */
|
|
243
|
+
automation: {
|
|
244
|
+
/** Of the requirements that reached testing (or completion) in the period, the share with no stop other than the necessary ones. */
|
|
245
|
+
unattendedShare: number | null;
|
|
246
|
+
/** Problems the pipeline handled by itself in the period's runs, by kind. */
|
|
247
|
+
recovered: Record<string, number>;
|
|
248
|
+
/** Runs that ended in an interruption (not a necessary decision). */
|
|
249
|
+
interrupted: number;
|
|
250
|
+
/** recovered / (recovered + interrupted). */
|
|
251
|
+
autoRecoveryShare: number | null;
|
|
252
|
+
};
|
|
211
253
|
/** Requirements that started from cards, by who asked for them (1.10); empty when there are none. */
|
|
212
254
|
byRequester: Array<{ requester: string; requirements: number; completed: number; medianCycleHours: number | null }>;
|
|
213
255
|
requirements: RequirementStats[];
|
|
@@ -257,6 +299,24 @@ export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
|
257
299
|
const requesters = new Map<string, RequirementStats[]>();
|
|
258
300
|
for (const q of active) if (q.card) requesters.set(q.requester ?? "(未记录)", [...(requesters.get(q.requester ?? "(未记录)") ?? []), q]);
|
|
259
301
|
|
|
302
|
+
const stopCounts = count(periodRuns.flatMap((r) => (stopKindOf(r) ? [stopKindOf(r)!] : [])));
|
|
303
|
+
const recovered: Record<string, number> = {};
|
|
304
|
+
for (const r of periodRuns) for (const [k, v] of Object.entries(r.run.recoveries ?? {})) recovered[k] = (recovered[k] ?? 0) + v;
|
|
305
|
+
const recoveredTotal = Object.values(recovered).reduce((a, b) => a + b, 0);
|
|
306
|
+
const interrupted = periodRuns.filter((r) => {
|
|
307
|
+
const k = stopKindOf(r);
|
|
308
|
+
return k !== null && !NECESSARY_STOPS.includes(k) && k !== "business" && k !== "reproduce";
|
|
309
|
+
}).length;
|
|
310
|
+
const reachedTesting = active.filter((q) => q.status === "completed" || q.status === "phase_done");
|
|
311
|
+
const unattended = reachedTesting.filter((q) =>
|
|
312
|
+
runs
|
|
313
|
+
.filter((r) => r.requirement.key === q.key && r.requirement.project === q.project)
|
|
314
|
+
.every((r) => {
|
|
315
|
+
const k = stopKindOf(r);
|
|
316
|
+
return k === null || NECESSARY_STOPS.includes(k);
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
260
320
|
return {
|
|
261
321
|
schema: 1,
|
|
262
322
|
period: { from: period.from?.toISOString() ?? null, to: period.to?.toISOString() ?? null, label: period.label },
|
|
@@ -287,6 +347,15 @@ export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
|
287
347
|
averageReviewRounds: avg(qs.map((q) => q.reviewRounds)),
|
|
288
348
|
averageCostUsd: avg(qs.flatMap((q) => (q.costUsd === null ? [] : [q.costUsd]))),
|
|
289
349
|
})),
|
|
350
|
+
stops: Object.entries(stopCounts)
|
|
351
|
+
.map(([kind, n]) => ({ kind, label: STOP_LABELS[kind] ?? kind, necessary: NECESSARY_STOPS.includes(kind), runs: n }))
|
|
352
|
+
.sort((a, b) => b.runs - a.runs || a.kind.localeCompare(b.kind)),
|
|
353
|
+
automation: {
|
|
354
|
+
unattendedShare: reachedTesting.length === 0 ? null : Math.round((unattended.length / reachedTesting.length) * 100) / 100,
|
|
355
|
+
recovered,
|
|
356
|
+
interrupted,
|
|
357
|
+
autoRecoveryShare: recoveredTotal + interrupted === 0 ? null : Math.round((recoveredTotal / (recoveredTotal + interrupted)) * 100) / 100,
|
|
358
|
+
},
|
|
290
359
|
byRequester: [...requesters.entries()]
|
|
291
360
|
.map(([requester, qs]) => {
|
|
292
361
|
const finished = qs.filter((q) => done.includes(q));
|
|
@@ -349,6 +418,27 @@ export function describeStats(s: StatsSummary): string {
|
|
|
349
418
|
`- 需要缺陷修复的需求占比:${s.rework.withDefectRounds === null ? "无数据" : `${Math.round(s.rework.withDefectRounds * 100)}%`}`,
|
|
350
419
|
`- 质量门:运行 ${s.rework.gateRuns} 次,其中发现新失败 ${s.rework.gateFailed} 次`,
|
|
351
420
|
);
|
|
421
|
+
const RECOVERY_LABELS: Record<string, string> = {
|
|
422
|
+
retry: "临时错误重试",
|
|
423
|
+
reattach: "接着等还在做的 agent",
|
|
424
|
+
reuse_result: "沿用旧 agent 的结果",
|
|
425
|
+
extend_timeout: "超时后多等一轮",
|
|
426
|
+
redo: "新开 agent 重做",
|
|
427
|
+
fallback: "换备用模型",
|
|
428
|
+
auto_resume: "自动继续",
|
|
429
|
+
quota_wait: "等额度恢复后继续",
|
|
430
|
+
};
|
|
431
|
+
lines.push(
|
|
432
|
+
"",
|
|
433
|
+
"## 人工介入",
|
|
434
|
+
"",
|
|
435
|
+
`- 到达提测(或完成)的需求中,除了确认方案外没有停下等人的:${s.automation.unattendedShare === null ? "无数据" : `${Math.round(s.automation.unattendedShare * 100)}%`}`,
|
|
436
|
+
`- 自动处理的问题 ${Object.values(s.automation.recovered).reduce((a, b) => a + b, 0)} 次(${list(s.automation.recovered, RECOVERY_LABELS)}),中断等人 ${s.automation.interrupted} 次${s.automation.autoRecoveryShare === null ? "" : `,自动处理的比例 ${Math.round(s.automation.autoRecoveryShare * 100)}%`}`,
|
|
437
|
+
);
|
|
438
|
+
if (s.stops.length > 0) {
|
|
439
|
+
lines.push("", "| 停下等人的原因 | 次数 | 必要的决定 |", "| --- | --- | --- |");
|
|
440
|
+
for (const st of s.stops) lines.push(`| ${st.label} | ${st.runs} | ${st.necessary ? "是" : "否"} |`);
|
|
441
|
+
}
|
|
352
442
|
if (s.waits.length > 0) {
|
|
353
443
|
lines.push("", "## 等待", "", "| 等待 | 需求数 | 平均 |", "| --- | --- | --- |");
|
|
354
444
|
for (const w of s.waits) lines.push(`| ${w.label} | ${w.requirements} | ${hours(w.averageMinutes)} |`);
|
package/engine/src/steps.ts
CHANGED
|
@@ -132,7 +132,10 @@ export const STEPS: Record<StepId, StepDef> = {
|
|
|
132
132
|
title: "方案修订",
|
|
133
133
|
role: "builder",
|
|
134
134
|
promptFile: "04b-design-revise.md",
|
|
135
|
-
result: {
|
|
135
|
+
result: {
|
|
136
|
+
required: done.required,
|
|
137
|
+
optional: { slices: { kind: "string[]" }, files: { kind: "string[]" }, ui_change: { kind: "boolean" }, business_questions: { kind: "string[]" } },
|
|
138
|
+
},
|
|
136
139
|
},
|
|
137
140
|
implement: {
|
|
138
141
|
id: "implement",
|
package/install.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// node install.mjs --prune list old version directories that can be deleted
|
|
10
10
|
// node install.mjs --uninstall remove the skills req-kit installed (keeps ~/.req-kit)
|
|
11
11
|
// node install.mjs --configure configuration wizard: pick the agents from what Paseo offers
|
|
12
|
+
// node install.mjs --configure --auto the same without questions: the recommended agents (1.12)
|
|
12
13
|
// node install.mjs --upgrade [version] download a version (default: the newest) next to this one,
|
|
13
14
|
// install its dependencies and switch to it
|
|
14
15
|
// node install.mjs --latest print the newest published version
|
|
@@ -47,6 +48,9 @@ const { values, positionals } = parseArgs({
|
|
|
47
48
|
allowPositionals: true,
|
|
48
49
|
options: {
|
|
49
50
|
configure: { type: "boolean", default: false },
|
|
51
|
+
auto: { type: "boolean", default: false },
|
|
52
|
+
"catalog-json": { type: "boolean", default: false },
|
|
53
|
+
set: { type: "string" },
|
|
50
54
|
upgrade: { type: "boolean", default: false },
|
|
51
55
|
latest: { type: "boolean", default: false },
|
|
52
56
|
from: { type: "string" },
|
|
@@ -844,7 +848,7 @@ function configureCommand() {
|
|
|
844
848
|
console.error(`依赖未安装:在 ${KIT_ROOT} 中执行 npm install 后再运行配置向导。`);
|
|
845
849
|
return 1;
|
|
846
850
|
}
|
|
847
|
-
const result = spawnSync(process.execPath, [tsx, path.join(KIT_ROOT, "engine", "src", "configure.ts"), "--config", CONFIG], {
|
|
851
|
+
const result = spawnSync(process.execPath, [tsx, path.join(KIT_ROOT, "engine", "src", "configure.ts"), "--config", CONFIG, ...(values.auto ? ["--auto"] : []), ...(values["catalog-json"] ? ["--catalog-json"] : []), ...(values.set !== undefined ? ["--set", values.set] : [])], {
|
|
848
852
|
stdio: "inherit",
|
|
849
853
|
windowsHide: true,
|
|
850
854
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0-beta.2",
|
|
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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"test": "node --import tsx --import ./engine/test/isolate.ts --test --test-concurrency=1 engine/test/*.test.ts test/*.test.ts",
|
|
34
|
+
"test": "node --import tsx --import ./engine/test/isolate.ts --test --test-concurrency=1 --test-timeout=300000 engine/test/*.test.ts test/*.test.ts",
|
|
35
35
|
"smoke": "tsx engine/src/smoke-test.ts",
|
|
36
36
|
"doctor": "node bin/req-doctor.mjs",
|
|
37
37
|
"eval": "tsx engine/src/eval.ts",
|
package/paseo-plugin/OVERVIEW.md
CHANGED
|
@@ -2,6 +2,8 @@ req-kit 的需求在 Paseo 里的样子:本机所有项目的需求一页看
|
|
|
2
2
|
|
|
3
3
|
需要人处理的时候,可以直接在这里确认方案、停止、继续或开始测试验证。这些操作和在终端运行 `req approve`、`req stop`、`req resume`、`req verify` 完全一样,流水线本身不受插件影响;不想让人在网页上操作时,在插件设置中打开"只读"。
|
|
4
4
|
|
|
5
|
+
在插件设置的"模型"一页,可以从 Paseo 中已登录的 agent 里选择 builder、reviewer 和备用模型,保存到 req-kit 的本机配置,和在终端运行 `req configure` 一样。
|
|
6
|
+
|
|
5
7
|
还有:侧边栏的"需求"(带待处理数)、每个工作区头部的"需求"按钮和面板、步骤 agent 输入框旁的"需求"按钮,以及使用统计(交付周期、返工、等待、用量)。
|
|
6
8
|
|
|
7
9
|
需要这台 daemon 上安装 req-kit 1.9 或更新的版本,插件通过本机的 `req api` 读取需求;它不连接任何外部服务。用 `req plugin install` 安装和更新。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Data hooks: every read is an RPC to the daemon side (req api), cached by React Query.
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { useRpc, useSettings } from "@getpaseo/plugin/client";
|
|
4
|
-
import { fileRpc, listRpc, preferences, runRpc, statsRpc, statusRpc, versionRpc, type Action } from "../shared/contracts";
|
|
4
|
+
import { fileRpc, listRpc, modelsRpc, preferences, runRpc, setAgentsRpc, statsRpc, statusRpc, versionRpc, type Action, type AgentProfileData } from "../shared/contracts";
|
|
5
5
|
|
|
6
6
|
export function usePreferences(): { readOnly: boolean; refreshMs: number } {
|
|
7
7
|
const settings = useSettings(preferences);
|
|
@@ -53,3 +53,17 @@ export function useAction() {
|
|
|
53
53
|
onSettled: () => queries.invalidateQueries({ queryKey: ["req"] }),
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
export function useModels() {
|
|
58
|
+
const models = useRpc(modelsRpc);
|
|
59
|
+
return useQuery({ queryKey: ["req", "models"], queryFn: () => models({}), retry: false });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useSetAgents() {
|
|
63
|
+
const set = useRpc(setAgentsRpc);
|
|
64
|
+
const queries = useQueryClient();
|
|
65
|
+
return useMutation({
|
|
66
|
+
mutationFn: (input: { builder: AgentProfileData; reviewer: AgentProfileData; fallback: AgentProfileData | null }) => set(input),
|
|
67
|
+
onSettled: () => queries.invalidateQueries({ queryKey: ["req", "models"] }),
|
|
68
|
+
});
|
|
69
|
+
}
|