paseo-req-kit 1.11.0-beta.1 → 1.11.0
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 +15 -0
- 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 +1 -1
- package/engine/src/paseo-runtime.ts +3 -0
- package/engine/src/pipeline.ts +82 -7
- package/engine/src/runtime.ts +2 -0
- package/engine/src/state.ts +7 -0
- package/package.json +1 -1
- package/paseo-plugin/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.11.0
|
|
6
|
+
|
|
7
|
+
稳定版:包含 1.10 的"从卡片开始的需求"和 1.11 的接单器(详见下面的 beta 记录),并修正步骤超时后会重复开 agent 的问题。
|
|
8
|
+
|
|
9
|
+
### 使用者会看到的变化
|
|
10
|
+
|
|
11
|
+
- **从卡片开始的需求**(1.10):`req start --from <系统>:<卡号>` 读卡片、建需求会话;卡片描述末尾的 AI 区和需求方回复区代替对话;按子系统自动选仓库;工厂模式下研发完成后可以推送 `req/` 分支;统计按需求方汇总。
|
|
12
|
+
- **接单器**(1.11):`req factory` 定时接标题带 `[AI]`/`【AI】` 的新卡片,并把需求方的回复转给需求会话。
|
|
13
|
+
- **修正:步骤超时后不再重复开 agent**。以前超时只判定失败、不看 agent 实际状态,继续时会新建一个 agent,而旧的仍在运行,两个 agent 同时改同一批文件、在同一个工作区提交、写同一个结果文件,需要手动停止旧的。现在继续(`--resume`、自动重试)时先看上一次的 agent:做完了就用它的结果;还在做就接着等它一轮;第二次仍超时才停止它,再新建。连续读不到 agent 状态时同样处理。
|
|
14
|
+
|
|
15
|
+
### 升级须知
|
|
16
|
+
|
|
17
|
+
- 从 1.9 升级:先看 1.10.0-beta.1 至 1.11.0-beta.1 的升级须知(对接配置的 `query`、`describe`、`list`,项目配置的 `cardSource`,本机或团队配置的 `push`、`factory`)。不用卡片的项目不需要改任何配置。
|
|
18
|
+
- 状态文件的步骤记录新增 `leftRunning`、`reattached` 两项,旧状态文件照常可用。
|
|
19
|
+
|
|
5
20
|
## 1.11.0-beta.1
|
|
6
21
|
|
|
7
22
|
接单器:一台共享研发机器自动接卡片、转达回复,需求方只在卡片上操作。
|
|
@@ -471,7 +471,7 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
471
471
|
|
|
472
472
|
**结果文件**:每步必须写一个 JSON 结果文件,流水线只看它,不看 agent 的回复文字。文件缺失或格式不对时,会在同一个会话里提醒 agent 补写,最多 2 次,仍不对则判定该步骤失败。
|
|
473
473
|
|
|
474
|
-
**等待与超时**:每步有总时限(`stepTimeoutMinutes
|
|
474
|
+
**等待与超时**:每步有总时限(`stepTimeoutMinutes`),超时判定失败,但不停止 agent。继续(`--resume`、自动重试)时先看这一步上一次的 agent:已经做完、结果文件有效就直接用它的结果;还在做就接着等它一轮;第二次仍超时才停止它、另起一个 agent。同一步不会有两个 agent 同时在做(它们会改同样的文件、在同一个工作区提交、写同一个结果文件)。连续读不到 agent 状态时同样处理。等待被拆成每段 10 分钟,连接断开时自动重试。agent 等待权限批准时,同一个请求只通知一次,然后继续等待。
|
|
475
475
|
|
|
476
476
|
**临时错误自动重试**:步骤因限流(429)、服务暂时不可用(502/503/504/529、overloaded)、网络中断等临时错误失败时,等待后用新的 agent 重做这一步,并告诉它工作区里可能留有上一次没做完的改动。默认最多重试 2 次,分别等 1 分钟和 5 分钟:
|
|
477
477
|
|
|
@@ -199,6 +199,9 @@ export function createRuntimeWithClient(
|
|
|
199
199
|
async notifyAgent(agentId, text) {
|
|
200
200
|
await client.agents.ref(agentId).send(text, { activeTurnBehavior: "steer" });
|
|
201
201
|
},
|
|
202
|
+
attachAgent(agentId) {
|
|
203
|
+
return wrap(client.agents.ref(agentId));
|
|
204
|
+
},
|
|
202
205
|
async interruptAgent(agentId, text) {
|
|
203
206
|
await client.agents.ref(agentId).send(text, { activeTurnBehavior: "interrupt" });
|
|
204
207
|
},
|
package/engine/src/pipeline.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { dispatchNotifiers, type NotifierEventType } from "./notifiers.js";
|
|
|
36
36
|
import { describeExternal, type IntegrationEvent, type Runner, syncIntegration } from "./integrations.js";
|
|
37
37
|
import { cardSlug, planAppendix, publishCard, readSource } from "./card.js";
|
|
38
38
|
import { principlesSection, projectAdditions, type PromptLocations, readToolPrompt } from "./prompts.js";
|
|
39
|
-
import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent } from "./runtime.js";
|
|
39
|
+
import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent, WaitOutcome } from "./runtime.js";
|
|
40
40
|
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type RunPhases, type StepRecord, writeState } from "./state.js";
|
|
41
41
|
import { type PanelStep, STEPS, type StepDef, type StepId, stepKey, validateResult, VERIFY_PHASE_STEPS } from "./steps.js";
|
|
42
42
|
|
|
@@ -124,6 +124,9 @@ class StepFailure extends Error {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** The step gave up on its agent while the agent may still be working (1.11). */
|
|
128
|
+
class AgentLeftRunning extends StepFailure {}
|
|
129
|
+
|
|
127
130
|
/**
|
|
128
131
|
* Errors that usually go away by themselves: provider rate limits and overload,
|
|
129
132
|
* dropped connections, a restarting daemon. A step that fails this way is started
|
|
@@ -490,9 +493,9 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
490
493
|
for (;;) {
|
|
491
494
|
const remaining = deadline - Date.now();
|
|
492
495
|
if (remaining <= 0) {
|
|
493
|
-
throw new
|
|
496
|
+
throw new AgentLeftRunning(
|
|
494
497
|
def.title,
|
|
495
|
-
`超过 ${ctx.config.stepTimeoutMinutes[def.id]}
|
|
498
|
+
`超过 ${ctx.config.stepTimeoutMinutes[def.id]} 分钟仍未完成。agent「${agentTitle}」还在运行,没有停止它;继续时会先看它的情况:做完了就用它的结果,还在做就接着等它,不会另起一个 agent 重复做。`,
|
|
496
499
|
);
|
|
497
500
|
}
|
|
498
501
|
let outcome;
|
|
@@ -503,7 +506,7 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
503
506
|
consecutiveErrors += 1;
|
|
504
507
|
ctx.log(`等待 ${agentTitle} 时出错(第 ${consecutiveErrors} 次):${(error as Error).message}`);
|
|
505
508
|
if (consecutiveErrors >= 5) {
|
|
506
|
-
throw new
|
|
509
|
+
throw new AgentLeftRunning(def.title, `连续 ${consecutiveErrors} 次无法获取 agent 状态:${(error as Error).message}`);
|
|
507
510
|
}
|
|
508
511
|
await ctx.sleep(Math.min(60_000, 5_000 * consecutiveErrors));
|
|
509
512
|
continue;
|
|
@@ -604,7 +607,11 @@ async function runStepOnce(
|
|
|
604
607
|
const resultRel = `${ctx.featureDir}/.pipeline/results/${key}.json`;
|
|
605
608
|
const resultAbs = path.join(stepRepo, resultRel);
|
|
606
609
|
fs.mkdirSync(path.dirname(resultAbs), { recursive: true });
|
|
607
|
-
|
|
610
|
+
|
|
611
|
+
// An earlier run of this step may have left its agent working: use it, never run a second one.
|
|
612
|
+
const leftover = await checkLeftover(ctx, key, def, resultAbs);
|
|
613
|
+
if (leftover.done) return leftover.done;
|
|
614
|
+
if (!leftover.agent) fs.rmSync(resultAbs, { force: true });
|
|
608
615
|
|
|
609
616
|
// Services, the browser and the deployed environment belong to the steps in this repository.
|
|
610
617
|
const managed = target
|
|
@@ -638,10 +645,14 @@ async function runStepOnce(
|
|
|
638
645
|
error: null,
|
|
639
646
|
phase: phaseOf(ctx),
|
|
640
647
|
};
|
|
641
|
-
|
|
648
|
+
if (leftover.agent) {
|
|
649
|
+
record.agentId = leftover.agent.id;
|
|
650
|
+
record.reattached = leftover.reattached;
|
|
651
|
+
}
|
|
652
|
+
ctx.log(leftover.agent ? `继续等待 ${key}(${def.title})上一次的 agent ${leftover.agent.id}` : `开始 ${key}(${def.title})`);
|
|
642
653
|
|
|
643
654
|
try {
|
|
644
|
-
const agent = await ctx.runtime.startAgent({
|
|
655
|
+
const agent = leftover.agent ?? await ctx.runtime.startAgent({
|
|
645
656
|
role: def.role,
|
|
646
657
|
title: agentTitle,
|
|
647
658
|
prompt: buildPrompt(ctx, def, round, maxRounds, resultRel, extras),
|
|
@@ -699,6 +710,7 @@ async function runStepOnce(
|
|
|
699
710
|
record.outcome = "failed";
|
|
700
711
|
record.endedAt = new Date().toISOString();
|
|
701
712
|
record.error = (error as Error).message;
|
|
713
|
+
if (error instanceof AgentLeftRunning) record.leftRunning = true;
|
|
702
714
|
if (!ctx.state.history.includes(record)) ctx.state.history.push(record);
|
|
703
715
|
save(ctx);
|
|
704
716
|
if (error instanceof StepFailure) throw error;
|
|
@@ -706,6 +718,69 @@ async function runStepOnce(
|
|
|
706
718
|
}
|
|
707
719
|
}
|
|
708
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Before a step starts an agent, looks at the agent an earlier run of the same step left
|
|
723
|
+
* working (a timeout, unreadable status, or a pipeline process that died mid-step):
|
|
724
|
+
* - it has finished and written a valid result: that result is the step's result;
|
|
725
|
+
* - it is still working: the step waits for it again (once); the second time it is stopped
|
|
726
|
+
* and a new agent starts, so a stuck agent does not hold the step forever;
|
|
727
|
+
* - it cannot be reached, or finished without a valid result: a new agent starts.
|
|
728
|
+
* Two agents on one step would edit the same files, commit in the same worktree and write
|
|
729
|
+
* the same result file, so a new one never starts while the old one may be working.
|
|
730
|
+
*/
|
|
731
|
+
async function checkLeftover(
|
|
732
|
+
ctx: PipelineContext,
|
|
733
|
+
key: string,
|
|
734
|
+
def: StepDef,
|
|
735
|
+
resultAbs: string,
|
|
736
|
+
): Promise<{ done?: Record<string, unknown>; agent?: StepAgent; reattached?: number }> {
|
|
737
|
+
const last = [...ctx.state.history].reverse().find((h) => h.key === key);
|
|
738
|
+
if (!last?.agentId || last.outcome === "completed") return {};
|
|
739
|
+
if (last.outcome !== "running" && !last.leftRunning) return {};
|
|
740
|
+
last.leftRunning = false;
|
|
741
|
+
if (last.outcome === "running") {
|
|
742
|
+
last.outcome = "failed";
|
|
743
|
+
last.endedAt = new Date().toISOString();
|
|
744
|
+
last.error ??= "流水线进程在这一步运行时退出";
|
|
745
|
+
}
|
|
746
|
+
save(ctx);
|
|
747
|
+
const agent = ctx.runtime.attachAgent(last.agentId);
|
|
748
|
+
let status: WaitOutcome["status"] | "unreachable";
|
|
749
|
+
try {
|
|
750
|
+
status = (await agent.wait(Math.min(5_000, ctx.config.waitChunkSeconds * 1000))).status;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
ctx.log(`无法读取 ${key} 上一次的 agent ${last.agentId} 的状态(${(error as Error).message}),新建一个 agent`);
|
|
753
|
+
status = "unreachable";
|
|
754
|
+
}
|
|
755
|
+
if (status === "idle") {
|
|
756
|
+
const validation = readResultFile(resultAbs, def);
|
|
757
|
+
if (validation.ok) {
|
|
758
|
+
last.outcome = "completed";
|
|
759
|
+
last.endedAt = new Date().toISOString();
|
|
760
|
+
last.error = null;
|
|
761
|
+
last.result = validation.value;
|
|
762
|
+
ctx.state.results[key] = validation.value;
|
|
763
|
+
save(ctx);
|
|
764
|
+
ctx.log(`${key} 上一次的 agent ${last.agentId} 已经做完,沿用它的结果:${JSON.stringify(validation.value)}`);
|
|
765
|
+
if (ctx.config.archiveStepAgents) await agent.archive().catch(() => undefined);
|
|
766
|
+
return { done: validation.value };
|
|
767
|
+
}
|
|
768
|
+
ctx.log(`${key} 上一次的 agent ${last.agentId} 已经停下,但没有有效的结果文件(${validation.error}),新建一个 agent`);
|
|
769
|
+
return {};
|
|
770
|
+
}
|
|
771
|
+
if (status === "timeout" || status === "permission") {
|
|
772
|
+
if ((last.reattached ?? 0) < 1) return { agent, reattached: (last.reattached ?? 0) + 1 };
|
|
773
|
+
ctx.log(`${key} 上一次的 agent ${last.agentId} 已经等过两轮仍未完成,停止它并新建一个 agent`);
|
|
774
|
+
await ctx.runtime
|
|
775
|
+
.interruptAgent(last.agentId, "这一步已超时,流水线会另起一个 agent 重做。请立即停止当前工作,不要再修改任何文件,也不要写结果文件,直接结束。")
|
|
776
|
+
.catch((error: Error) => ctx.log(`无法停止 agent ${last.agentId}:${error.message}`));
|
|
777
|
+
// Give it a moment to wind down before the new agent touches the same files.
|
|
778
|
+
await agent.wait(Math.min(60_000, ctx.config.waitChunkSeconds * 1000 * 2)).catch(() => undefined);
|
|
779
|
+
return {};
|
|
780
|
+
}
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
|
|
709
784
|
/** phases.verify.agents applies to the verify-phase steps. */
|
|
710
785
|
function phaseAgent(ctx: PipelineContext, id: StepId): AgentProfile | undefined {
|
|
711
786
|
if (!VERIFY_PHASE_STEPS.includes(id)) return undefined;
|
package/engine/src/runtime.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface AgentRuntime {
|
|
|
111
111
|
*/
|
|
112
112
|
verifyOrigin(agentId: string, cwd: string): Promise<void>;
|
|
113
113
|
startAgent(input: StartAgentInput): Promise<StepAgent>;
|
|
114
|
+
/** An agent started earlier (possibly by another pipeline process), to check on, wait for or follow up. */
|
|
115
|
+
attachAgent(agentId: string): StepAgent;
|
|
114
116
|
/**
|
|
115
117
|
* Starts a requirement's own chat (1.10): a top-level agent working in the repository, given
|
|
116
118
|
* `prompt` and not waited for. It is the chat the requirement's pipeline is later launched
|
package/engine/src/state.ts
CHANGED
|
@@ -43,6 +43,13 @@ export interface StepRecord {
|
|
|
43
43
|
usage?: AgentUsage | null;
|
|
44
44
|
/** Phase the step ran in; absent before 1.7 (develop or verify by step). */
|
|
45
45
|
phase?: PhaseName;
|
|
46
|
+
/**
|
|
47
|
+
* 1.11: the step failed while its agent was still at work (timed out, or its status could
|
|
48
|
+
* not be read). The next run of the step checks that agent before starting another one.
|
|
49
|
+
*/
|
|
50
|
+
leftRunning?: boolean;
|
|
51
|
+
/** 1.11: how many times a later run went on waiting for this record's agent instead of starting a new one. */
|
|
52
|
+
reattached?: number;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export interface PipelineState {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.11.0
|
|
3
|
+
"version": "1.11.0",
|
|
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",
|