@zhushanwen/pi-goal 0.14.5 → 0.14.6
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/README.md +1 -1
- package/package.json +3 -3
- package/src/__tests__/circuit-breaker.test.ts +47 -12
- package/src/__tests__/command-adapter.test.ts +46 -24
- package/src/__tests__/criteria-array-adapter.test.ts +0 -1
- package/src/__tests__/event-adapter.test.ts +3 -10
- package/src/__tests__/goal-control-adapter.test.ts +0 -1
- package/src/__tests__/goal-control-rpc.test.ts +0 -1
- package/src/__tests__/index.test.ts +2 -8
- package/src/__tests__/service.test.ts +0 -1
- package/src/adapters/command-adapter.ts +9 -7
- package/src/adapters/event-handlers/agent-end.ts +1 -1
- package/src/adapters/goal-control-adapter.ts +2 -2
- package/src/adapters/ports.ts +4 -5
- package/src/ports.ts +4 -3
- package/src/service.ts +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ goal/
|
|
|
46
46
|
├── index.ts # 入口 — re-export src/index.ts
|
|
47
47
|
└── src/
|
|
48
48
|
├── index.ts # 工厂入口(注册 command/tool/events + goalInit slot,全部委托 adapters)
|
|
49
|
-
├── commands.ts # /goal
|
|
49
|
+
├── commands.ts # /goal 命令参数解析(识别 --tokens / --criteria flag)
|
|
50
50
|
├── constants.ts # 语义常量
|
|
51
51
|
├── ports.ts # Pi 能力抽象(Persistence/Ui/Messaging/Session)
|
|
52
52
|
├── service.ts # 协调层 — createGoal / finalizeAndPersist / applyEvent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.6",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
30
30
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
31
31
|
"typebox": "*",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.7.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.7.6"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhushanwen/extension-protocol": "0.
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.13.0",
|
|
36
36
|
"@zhushanwen/pi-extension-logger": "0.6.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
@@ -28,11 +28,13 @@ import { createGoalSession, type GoalSession } from "../session";
|
|
|
28
28
|
// ── Fake pi / ctx(entries 数组可变,模拟 append-only 会话流)──
|
|
29
29
|
|
|
30
30
|
interface RecordedCall {
|
|
31
|
-
kind: "appendState" | "appendHistory" | "notify" | "sendContext"
|
|
31
|
+
kind: "appendState" | "appendHistory" | "notify" | "sendContext";
|
|
32
32
|
payload?: unknown;
|
|
33
33
|
text?: string;
|
|
34
34
|
level?: string;
|
|
35
35
|
content?: unknown;
|
|
36
|
+
customType?: unknown;
|
|
37
|
+
display?: unknown;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface FakeHarness {
|
|
@@ -69,11 +71,8 @@ function makeHarness(): FakeHarness {
|
|
|
69
71
|
piCalls.push({ kind: customType === "goal-history" ? "appendHistory" : "appendState", payload: data });
|
|
70
72
|
},
|
|
71
73
|
sendMessage(message: unknown, options?: unknown): void {
|
|
72
|
-
const msg = message as { content?: unknown };
|
|
73
|
-
piCalls.push({ kind: "sendContext", content: msg.content, payload: options });
|
|
74
|
-
},
|
|
75
|
-
sendUserMessage(content: string | unknown[], _options?: unknown): void {
|
|
76
|
-
piCalls.push({ kind: "sendUser", content });
|
|
74
|
+
const msg = message as { content?: unknown; customType?: unknown; display?: unknown };
|
|
75
|
+
piCalls.push({ kind: "sendContext", content: msg.content, customType: msg.customType, display: msg.display, payload: options });
|
|
77
76
|
},
|
|
78
77
|
} as unknown as ExtensionAPI;
|
|
79
78
|
|
|
@@ -299,12 +298,17 @@ describe("/goal resume 恢复通道", () => {
|
|
|
299
298
|
expect(session.state!.continuationsSent).toBe(0); // 新激活周期
|
|
300
299
|
expect(session.state!.continuationCapNotified).toBe(false);
|
|
301
300
|
expect(session.state!.status).toBe("active");
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
// FR-8.12 触发 AI(custom message,非 user message)
|
|
302
|
+
const resumeSends = h.piCalls.filter((c) => c.kind === "sendContext");
|
|
303
|
+
expect(resumeSends).toHaveLength(1);
|
|
304
|
+
expect(resumeSends[0]?.customType).toBe("goal-context");
|
|
305
|
+
|
|
306
|
+
// 恢复后:下一次 agent_end 正常发 continuation(新周期额度)。
|
|
307
|
+
// resume 触发消息同为 followUp custom message(改造后形态),按 [GOAL] 前缀
|
|
308
|
+
// 区分 continuation 本体(continuationPrompt 专属前缀)
|
|
306
309
|
await runTurn(h, session, { tokenDelta: 200 });
|
|
307
|
-
|
|
310
|
+
const continuations = followUpSends(h).filter((c) => String(c.content).includes("[GOAL]"));
|
|
311
|
+
expect(continuations).toHaveLength(1);
|
|
308
312
|
});
|
|
309
313
|
|
|
310
314
|
it("未封顶的普通 active goal → resume 提示无需恢复(原语义不变)", async () => {
|
|
@@ -315,7 +319,7 @@ describe("/goal resume 恢复通道", () => {
|
|
|
315
319
|
await handleGoalCommand(h.pi, session, "resume", h.ctx);
|
|
316
320
|
|
|
317
321
|
expect(notifyTexts(h).some((t) => t.includes("no need to resume"))).toBe(true);
|
|
318
|
-
expect(h.piCalls.filter((c) => c.kind === "
|
|
322
|
+
expect(h.piCalls.filter((c) => c.kind === "sendContext")).toHaveLength(0);
|
|
319
323
|
});
|
|
320
324
|
});
|
|
321
325
|
|
|
@@ -447,3 +451,34 @@ describe("MF-6③ timer 清理面:session_start / before_agent_start 取消旧
|
|
|
447
451
|
expect(followUpSends(h)).toHaveLength(4);
|
|
448
452
|
});
|
|
449
453
|
});
|
|
454
|
+
|
|
455
|
+
// ── A8:退避通道激活(sendContextMessage 端口 triggerTurn 改造)──
|
|
456
|
+
//
|
|
457
|
+
// 设计 docs/design/send-user-message-to-custom-message.md(已删除,git 可追溯;决策见 ADR-0068)
|
|
458
|
+
// §2.2 端口改造 + §3.1 A8:
|
|
459
|
+
// 端口改造前 backoff timer 到期伪装用户消息投递;改造后
|
|
460
|
+
// deliverContinuation 经端口发 custom message 且 options 含 triggerTurn: true——
|
|
461
|
+
// 非 streaming idle 场景真实开轮(改造前 append 不开轮 = 死消息)。
|
|
462
|
+
// isIdle=false 拦截见 MF-6①;封顶记账守卫见「主判据」describe;此处锁定到期
|
|
463
|
+
// 发出路径的 options 契约与记账恰好 +1。
|
|
464
|
+
describe("A8:退避 timer 到期 → triggerTurn: true 开轮发出", () => {
|
|
465
|
+
it("isIdle 守卫通过后发出:custom message 四要素齐备,记账恰好 +1", async () => {
|
|
466
|
+
const { h, session } = await enterBackoff();
|
|
467
|
+
|
|
468
|
+
vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true → 守卫放行
|
|
469
|
+
|
|
470
|
+
const sends = followUpSends(h);
|
|
471
|
+
expect(sends).toHaveLength(5); // 4 立即 + 1 到期发出
|
|
472
|
+
const fired = sends[4]!;
|
|
473
|
+
// message 形态:customType + display:false(custom message,非用户气泡)
|
|
474
|
+
expect(fired.customType).toBe("goal-context");
|
|
475
|
+
expect(fired.display).toBe(false);
|
|
476
|
+
// options:deliverAs + triggerTurn(A8 核心断言——退避通道真实开轮)
|
|
477
|
+
const opts = fired.payload as { deliverAs?: string; triggerTurn?: boolean };
|
|
478
|
+
expect(opts.deliverAs).toBe("followUp");
|
|
479
|
+
expect(opts.triggerTurn).toBe(true);
|
|
480
|
+
// budget 记账不重复触发:主判据只在实发时 +1,本次到期恰好一次
|
|
481
|
+
expect(session.state!.continuationsSent).toBe(5);
|
|
482
|
+
expect(session.continuationTimer).toBeNull(); // 无残留待发 timer
|
|
483
|
+
});
|
|
484
|
+
});
|
|
@@ -27,12 +27,14 @@ import { createGoalSession } from "../session";
|
|
|
27
27
|
// ── Fake pi / ctx ────────────────────────────────────
|
|
28
28
|
|
|
29
29
|
interface RecordedCall {
|
|
30
|
-
kind: "appendState" | "appendHistory" | "notify" | "sendContext"
|
|
30
|
+
kind: "appendState" | "appendHistory" | "notify" | "sendContext";
|
|
31
31
|
text?: string;
|
|
32
32
|
level?: string;
|
|
33
33
|
content?: string;
|
|
34
|
-
deliverAs?: string;
|
|
35
34
|
customType?: string;
|
|
35
|
+
display?: boolean;
|
|
36
|
+
deliverAs?: string;
|
|
37
|
+
triggerTurn?: boolean;
|
|
36
38
|
payload?: unknown;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -60,12 +62,17 @@ function makeHarness(entries: SessionEntryLike[] = []): FakeHarness {
|
|
|
60
62
|
payload: data,
|
|
61
63
|
});
|
|
62
64
|
},
|
|
63
|
-
sendMessage(message: unknown,
|
|
64
|
-
const msg = message as { customType?: string; content?: string };
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
sendMessage(message: unknown, options?: unknown): void {
|
|
66
|
+
const msg = message as { customType?: string; content?: string; display?: boolean };
|
|
67
|
+
const opts = (options ?? {}) as { deliverAs?: string; triggerTurn?: boolean };
|
|
68
|
+
piCalls.push({
|
|
69
|
+
kind: "sendContext",
|
|
70
|
+
content: msg.content,
|
|
71
|
+
customType: msg.customType,
|
|
72
|
+
display: msg.display,
|
|
73
|
+
deliverAs: opts.deliverAs,
|
|
74
|
+
triggerTurn: opts.triggerTurn,
|
|
75
|
+
});
|
|
69
76
|
},
|
|
70
77
|
} as unknown as ExtensionAPI;
|
|
71
78
|
|
|
@@ -121,6 +128,11 @@ function notifyText(h: FakeHarness): string[] {
|
|
|
121
128
|
.map((c) => c.text ?? "");
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
/** 发出的 custom message 调用(经 ports.messaging.sendContextMessage → pi.sendMessage) */
|
|
132
|
+
function sendContextCalls(h: FakeHarness): RecordedCall[] {
|
|
133
|
+
return h.piCalls.filter((c) => c.kind === "sendContext");
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
// ── /goal status ─────────────────────────────────────
|
|
125
137
|
|
|
126
138
|
describe("handleGoalCommand — status", () => {
|
|
@@ -200,8 +212,14 @@ describe("handleGoalCommand — resume (FR-3 paused/blocked→active + G-014)",
|
|
|
200
212
|
await handleGoalCommand(h.pi, session, "resume", h.ctx);
|
|
201
213
|
expect(session.state!.status).toBe("active");
|
|
202
214
|
expect(h.states).toHaveLength(1); // persist 恰好 1 次
|
|
203
|
-
// FR-8.12: resume 后触发 AI
|
|
204
|
-
|
|
215
|
+
// FR-8.12: resume 后触发 AI(custom message 四要素:customType/display/deliverAs/triggerTurn)
|
|
216
|
+
const sends = sendContextCalls(h);
|
|
217
|
+
expect(sends).toHaveLength(1);
|
|
218
|
+
expect(sends[0]?.customType).toBe("goal-context");
|
|
219
|
+
expect(sends[0]?.display).toBe(false);
|
|
220
|
+
expect(sends[0]?.deliverAs).toBe("followUp");
|
|
221
|
+
expect(sends[0]?.triggerTurn).toBe(true);
|
|
222
|
+
expect(sends[0]?.content).toContain("Goal resumed");
|
|
205
223
|
});
|
|
206
224
|
|
|
207
225
|
it("resume 重置 timeStartedAt=now(FR-3.2 重启计时器)", async () => {
|
|
@@ -482,7 +500,7 @@ describe("parseGoalArgs — 子命令路由", () => {
|
|
|
482
500
|
});
|
|
483
501
|
});
|
|
484
502
|
|
|
485
|
-
// ── /goal set(提示词触发器:
|
|
503
|
+
// ── /goal set(提示词触发器:custom message 让 AI 调 goal_control create)──
|
|
486
504
|
|
|
487
505
|
describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终态)", () => {
|
|
488
506
|
it("无旧 goal → 发触发消息(不直接创建 state)", async () => {
|
|
@@ -492,11 +510,15 @@ describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终
|
|
|
492
510
|
// 提示词触发器:不直接 createGoal,state 仍为 null(由 AI 后续 toolcall 创建)
|
|
493
511
|
expect(session.state).toBeNull();
|
|
494
512
|
expect(h.states).toHaveLength(0); // 不写 state
|
|
495
|
-
//
|
|
496
|
-
const
|
|
497
|
-
expect(
|
|
498
|
-
expect(
|
|
499
|
-
expect(
|
|
513
|
+
// 发 custom message 引导 AI 调 create(四要素 + content 含 objective 与工具名)
|
|
514
|
+
const sends = sendContextCalls(h);
|
|
515
|
+
expect(sends).toHaveLength(1);
|
|
516
|
+
expect(sends[0]?.customType).toBe("goal-context");
|
|
517
|
+
expect(sends[0]?.display).toBe(false);
|
|
518
|
+
expect(sends[0]?.deliverAs).toBe("followUp");
|
|
519
|
+
expect(sends[0]?.triggerTurn).toBe(true);
|
|
520
|
+
expect(sends[0]?.content).toContain("my objective");
|
|
521
|
+
expect(sends[0]?.content).toContain("goal_control");
|
|
500
522
|
});
|
|
501
523
|
|
|
502
524
|
const SET_REJECT_STATUSES: GoalRuntimeState["status"][] = ["active", "paused"];
|
|
@@ -515,7 +537,7 @@ describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终
|
|
|
515
537
|
expect(notifyText(h).join("\n")).toContain("clear");
|
|
516
538
|
expect(session.state!.status).toBe(status); // 状态不变
|
|
517
539
|
expect(session.state!.objective).toBe(`old ${status} goal`); // 旧 goal 保留
|
|
518
|
-
expect(h
|
|
540
|
+
expect(sendContextCalls(h)).toHaveLength(0); // 不触发 AI
|
|
519
541
|
},
|
|
520
542
|
);
|
|
521
543
|
|
|
@@ -527,7 +549,7 @@ describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终
|
|
|
527
549
|
await handleGoalCommand(h.pi, session, "new objective", h.ctx);
|
|
528
550
|
expect(h.history.length).toBe(historyBefore); // 不写 history(触发器不直接创建)
|
|
529
551
|
// 终态旧 goal 不挡触发器,发消息让 AI 创建
|
|
530
|
-
expect(h
|
|
552
|
+
expect(sendContextCalls(h)).toHaveLength(1);
|
|
531
553
|
});
|
|
532
554
|
|
|
533
555
|
it("空 objective → usage 提示", async () => {
|
|
@@ -547,18 +569,18 @@ describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终
|
|
|
547
569
|
await handleGoalCommand(h.pi, session, "obj --tokens 0", h.ctx);
|
|
548
570
|
// 提示词触发器不直接创建 state
|
|
549
571
|
expect(session.state).toBeNull();
|
|
550
|
-
const
|
|
551
|
-
expect(
|
|
552
|
-
expect(
|
|
572
|
+
const sends = sendContextCalls(h);
|
|
573
|
+
expect(sends).toHaveLength(1);
|
|
574
|
+
expect(sends[0]?.content).toContain("obj");
|
|
553
575
|
});
|
|
554
576
|
|
|
555
577
|
it("--tokens N → 触发消息含 budget 值", async () => {
|
|
556
578
|
const h = makeHarness();
|
|
557
579
|
const session = createGoalSession();
|
|
558
580
|
await handleGoalCommand(h.pi, session, "obj --tokens 5000", h.ctx);
|
|
559
|
-
const
|
|
560
|
-
expect(
|
|
561
|
-
expect(
|
|
581
|
+
const sends = sendContextCalls(h);
|
|
582
|
+
expect(sends).toHaveLength(1);
|
|
583
|
+
expect(sends[0]?.content).toContain("5000");
|
|
562
584
|
});
|
|
563
585
|
});
|
|
564
586
|
|
|
@@ -21,7 +21,7 @@ import { createGoalSession } from "../session";
|
|
|
21
21
|
// ── Fake pi / ctx ────────────────────────────────────
|
|
22
22
|
|
|
23
23
|
interface RecordedCall {
|
|
24
|
-
kind: "appendState" | "appendHistory" | "notify" | "sendContext" | "
|
|
24
|
+
kind: "appendState" | "appendHistory" | "notify" | "sendContext" | "setStatus" | "setWidget";
|
|
25
25
|
payload?: unknown;
|
|
26
26
|
text?: string;
|
|
27
27
|
level?: string;
|
|
@@ -50,13 +50,6 @@ function makeFakePi(): { pi: ExtensionAPI; calls: RecordedCall[]; states: unknow
|
|
|
50
50
|
payload: _options,
|
|
51
51
|
});
|
|
52
52
|
},
|
|
53
|
-
sendUserMessage(content: string | unknown[], options?: unknown): void {
|
|
54
|
-
calls.push({
|
|
55
|
-
kind: "sendUser",
|
|
56
|
-
content: typeof content === "string" ? content : undefined,
|
|
57
|
-
payload: options,
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
53
|
} as unknown as ExtensionAPI;
|
|
61
54
|
return { pi, calls, states, history };
|
|
62
55
|
}
|
|
@@ -119,8 +112,8 @@ describe("handleAgentEnd — FR-6.7 ESC 守卫", () => {
|
|
|
119
112
|
await handleAgentEnd(pi, session, ctx);
|
|
120
113
|
const all = allCalls(piCalls, ctxCalls);
|
|
121
114
|
|
|
122
|
-
// 不发 continuation(无 sendContext
|
|
123
|
-
expect(all.filter((c) => c.kind === "sendContext"
|
|
115
|
+
// 不发 continuation(无 sendContext custom message)
|
|
116
|
+
expect(all.filter((c) => c.kind === "sendContext")).toHaveLength(0);
|
|
124
117
|
// 不做 budget 检查(无 notify 关于 budget)
|
|
125
118
|
expect(all.filter((c) => c.kind === "notify")).toHaveLength(0);
|
|
126
119
|
// goal 保持 active(status 不变)
|
|
@@ -38,15 +38,13 @@ interface FactoryFixture {
|
|
|
38
38
|
ctx: ExtensionContext;
|
|
39
39
|
states: unknown[];
|
|
40
40
|
history: unknown[];
|
|
41
|
-
sendUser: unknown[];
|
|
42
41
|
commands: Record<string, { description?: string }>;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
/** 工厂所需的最小 pi:registerCommand/registerTool/on/registerMessageRenderer + appendEntry/sendMessage
|
|
44
|
+
/** 工厂所需的最小 pi:registerCommand/registerTool/on/registerMessageRenderer + appendEntry/sendMessage */
|
|
46
45
|
function makeFactoryFixture(): FactoryFixture {
|
|
47
46
|
const states: unknown[] = [];
|
|
48
47
|
const history: unknown[] = [];
|
|
49
|
-
const sendUser: unknown[] = [];
|
|
50
48
|
const commands: Record<string, { description?: string }> = {};
|
|
51
49
|
const pi = {
|
|
52
50
|
registerCommand: (name: string, opts: { description?: string }) => {
|
|
@@ -60,9 +58,6 @@ function makeFactoryFixture(): FactoryFixture {
|
|
|
60
58
|
else states.push(data);
|
|
61
59
|
},
|
|
62
60
|
sendMessage: () => {},
|
|
63
|
-
sendUserMessage(content: unknown): void {
|
|
64
|
-
sendUser.push(content);
|
|
65
|
-
},
|
|
66
61
|
} as unknown as ExtensionAPI;
|
|
67
62
|
|
|
68
63
|
const ctx = {
|
|
@@ -78,7 +73,7 @@ function makeFactoryFixture(): FactoryFixture {
|
|
|
78
73
|
sessionManager: { getEntries: () => [], getBranch: () => undefined },
|
|
79
74
|
} as unknown as ExtensionContext;
|
|
80
75
|
|
|
81
|
-
return { pi, ctx, states, history,
|
|
76
|
+
return { pi, ctx, states, history, commands };
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
// ── goalInit slot(NFR-AC-8 / T1.8)──────────────────
|
|
@@ -181,7 +176,6 @@ function makeE1Fixture(
|
|
|
181
176
|
appendFileSync(sessionFile, `${JSON.stringify({ type: "custom", customType, data })}\n`, "utf-8");
|
|
182
177
|
},
|
|
183
178
|
sendMessage: () => {},
|
|
184
|
-
sendUserMessage: () => {},
|
|
185
179
|
} as unknown as ExtensionAPI;
|
|
186
180
|
|
|
187
181
|
const ctx = {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 状态变更调 service(createGoal / finalizeAndPersist);
|
|
11
11
|
* ports 桥接复用 adapters/ports.buildPorts(DRY:单一 ports 构造点);
|
|
12
|
-
* FR-8.12: set/resume
|
|
12
|
+
* FR-8.12: set/resume 后经 messaging.sendContextMessage(followUp)触发 AI。
|
|
13
13
|
*
|
|
14
14
|
* adapters 层可 import Pi 类型(桥接 Pi 和 service)。
|
|
15
15
|
*/
|
|
@@ -184,16 +184,17 @@ function handleResume(pi: ExtensionAPI, session: GoalSession, ctx: ExtensionCont
|
|
|
184
184
|
persistState(session, ports);
|
|
185
185
|
updateWidget(session, ports.ui);
|
|
186
186
|
|
|
187
|
-
// FR-8.12 并行模式:resume 后触发 AI
|
|
187
|
+
// FR-8.12 并行模式:resume 后触发 AI 继续(custom message + followUp,
|
|
188
|
+
// 非 streaming 时经端口 triggerTurn 开轮)
|
|
188
189
|
const blockerNote = state.lastBlockerReason
|
|
189
190
|
? `\n\nPrevious blocker: ${state.lastBlockerReason}. Try a different approach.`
|
|
190
191
|
: "";
|
|
191
192
|
const capNote = cappedActive
|
|
192
193
|
? "\n\nContinuation circuit breaker was reached — pursue the objective with concrete tool-backed progress."
|
|
193
194
|
: "";
|
|
194
|
-
|
|
195
|
+
ports.messaging.sendContextMessage(
|
|
195
196
|
`Goal resumed. Continuing toward the objective.${blockerNote}${capNote}\n\nObjective: ${state.objective}`,
|
|
196
|
-
|
|
197
|
+
"followUp",
|
|
197
198
|
);
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -347,7 +348,7 @@ function handleUpdate(
|
|
|
347
348
|
|
|
348
349
|
/**
|
|
349
350
|
* /goal <objective> 改为「提示词触发器」:不直接 createGoal,
|
|
350
|
-
*
|
|
351
|
+
* 而是发 custom message(followUp)引导 AI 调 goal_control create(slug 由 AI 生成)。
|
|
351
352
|
*
|
|
352
353
|
* 这样 goal 创建的唯一路径是 goal_control toolcall(统一入口),
|
|
353
354
|
* slug/objective/budget 都由 AI 在 toolcall 时决定。
|
|
@@ -400,6 +401,7 @@ function handleSet(
|
|
|
400
401
|
`\nRaw objective from user: ${objective.trim()}${budgetLine}`;
|
|
401
402
|
|
|
402
403
|
ctx.ui.notify(`Requesting goal start: ${objective.trim()}`, "info");
|
|
403
|
-
// FR-8.12: 触发 AI(followUp
|
|
404
|
-
|
|
404
|
+
// FR-8.12: 触发 AI(custom message + followUp,非 streaming 时开轮)—— AI 消化后调 goal_control create
|
|
405
|
+
const ports = buildPorts(pi, ctx);
|
|
406
|
+
ports.messaging.sendContextMessage(message, "followUp");
|
|
405
407
|
}
|
|
@@ -189,7 +189,7 @@ async function handleContinuation(
|
|
|
189
189
|
);
|
|
190
190
|
|
|
191
191
|
// pending 守卫:有活跃 background subagent/workflow 时不发 continuation。
|
|
192
|
-
// 靠 subagent/workflow 完成时 sendMessage({triggerTurn:true
|
|
192
|
+
// 靠 subagent/workflow 完成时 sendMessage({triggerTurn:true},缺省 deliverAs 即 steer) 自然唤醒主 agent;
|
|
193
193
|
// goal 抢先催会与异步任务完成通知叠加,形成“停不下来”的死循环——continuation 排入
|
|
194
194
|
// _followUpMessages 队列 → _handlePostAgentRun 的 hasQueuedMessages() 返回 true → agent.continue()
|
|
195
195
|
// 新 turn → 又 agent_end → 又 continuation。
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - complete: finalizeAndPersist(state, "complete", ...)(内部已含 tickState → finalizeGoal → persist)
|
|
17
17
|
* - report_blocked: 手动 tickState(status 仍 active 才累加当前运行段)→ transitionStatus(active→blocked) → persistState
|
|
18
18
|
*
|
|
19
|
-
* create
|
|
19
|
+
* create 不发消息注入:toolcall 时 AI 已在 turn 中,返回结果后自行续跑
|
|
20
20
|
* (与 /goal set 的 followUp 触发区分;对齐 Codex create_goal 不自动续跑)。
|
|
21
21
|
*
|
|
22
22
|
* schema:扁平 Type.Object(OpenAI 兼容,C3)——parameters 顶层必须是 type:"object",
|
|
@@ -151,7 +151,7 @@ export interface GoalControlDetails {
|
|
|
151
151
|
* slug:AI 生成的短标识,仅 widget 标题 + history 用,不注入 prompt。真 optional。
|
|
152
152
|
* objective:完整描述,注入每轮 context prompt(保证方向感)。
|
|
153
153
|
*
|
|
154
|
-
* 全解耦:不读 todo/plan。toolcall 时 AI 已在 turn
|
|
154
|
+
* 全解耦:不读 todo/plan。toolcall 时 AI 已在 turn 中,**不**发消息注入
|
|
155
155
|
* (AI 返回后自行续跑,对齐 Codex create_goal)。
|
|
156
156
|
*
|
|
157
157
|
* 守卫用 D25 严格语义:非终态 active/paused/blocked 全挡,提示用 /goal resume 或
|
package/src/adapters/ports.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - persistence: pi.appendEntry 映射到 appendState / appendHistory(type 字符串区分)
|
|
7
7
|
* - ui: ctx.ui 的 setWidget/setStatus/notify + hasUI + theme(fg/bold 适配 ThemeLike)
|
|
8
|
-
* - messaging: pi.sendMessage
|
|
8
|
+
* - messaging: pi.sendMessage(custom message 形态)映射到 sendContextMessage
|
|
9
9
|
* - session: ctx.sessionManager.getEntries
|
|
10
10
|
*
|
|
11
11
|
* port/ctx 双通道惯例:port 对象仅用于传入 service/session 的实参;
|
|
@@ -74,6 +74,8 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
const messaging: MessagingPort = {
|
|
77
|
+
// triggerTurn: true——非 streaming 时真实开轮(不设则 pi 只 append entry 不开轮,
|
|
78
|
+
// 消息成死文);streaming 时走 deliverAs 队列(steer/followUp),triggerTurn 不额外生效。
|
|
77
79
|
sendContextMessage: (content, deliverAs, customType): void => {
|
|
78
80
|
pi.sendMessage(
|
|
79
81
|
{
|
|
@@ -81,12 +83,9 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
|
|
|
81
83
|
content,
|
|
82
84
|
display: false,
|
|
83
85
|
},
|
|
84
|
-
{ deliverAs },
|
|
86
|
+
{ deliverAs, triggerTurn: true },
|
|
85
87
|
);
|
|
86
88
|
},
|
|
87
|
-
sendUserMessage: (content, deliverAs): void => {
|
|
88
|
-
pi.sendUserMessage(content, { deliverAs });
|
|
89
|
-
},
|
|
90
89
|
};
|
|
91
90
|
|
|
92
91
|
const session: SessionPort = {
|
package/src/ports.ts
CHANGED
|
@@ -70,10 +70,11 @@ export interface UiPort {
|
|
|
70
70
|
// ── MessagingPort ────────────────────────────────────
|
|
71
71
|
|
|
72
72
|
export interface MessagingPort {
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 发送 custom message(goal-context 等)。实现侧固定 display:false +
|
|
75
|
+
* triggerTurn:true(非 streaming 真实开轮,streaming 走 deliverAs 队列)。
|
|
76
|
+
*/
|
|
74
77
|
sendContextMessage(content: string, deliverAs: "steer" | "followUp", customType?: string): void;
|
|
75
|
-
/** 发送 user message(触发 AI 开始工作,FR-8.12) */
|
|
76
|
-
sendUserMessage(content: string, deliverAs: "steer" | "followUp"): void;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// ── SessionPort ──────────────────────────────────────
|
package/src/service.ts
CHANGED
|
@@ -124,7 +124,7 @@ export function persistAndUpdate(
|
|
|
124
124
|
* - goalInit slot(index.ts,跨扩展编程式入口)
|
|
125
125
|
*
|
|
126
126
|
* 注:/goal <objective> 命令路径已改为提示词触发器——不直接调本函数,
|
|
127
|
-
*
|
|
127
|
+
* 而是发 custom message 让 AI 调 goal_control create(slug + successCriteria 由 AI 生成)。
|
|
128
128
|
*
|
|
129
129
|
* @param slug AI 生成的短标识(optional,仅 widget 标题 + history 用)
|
|
130
130
|
* @param successCriteria 成功标准(optional,由 AI 推导或外部传入;注入 prompt 指导完成验证)
|