@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 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 命令参数解析(仅识别 --tokens flag)
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.5",
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.5"
32
+ "@zhushanwen/pi-pending-notifications": "0.7.6"
33
33
  },
34
34
  "dependencies": {
35
- "@zhushanwen/extension-protocol": "0.12.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" | "sendUser";
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
- const userSends = h.piCalls.filter((c) => c.kind === "sendUser");
303
- expect(userSends).toHaveLength(1); // FR-8.12 触发 AI
304
-
305
- // 恢复后:下一次 agent_end 正常发 continuation(新周期额度)
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
- expect(followUpSends(h)).toHaveLength(1);
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 === "sendUser")).toHaveLength(0);
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" | "sendUser";
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, _options?: unknown): void {
64
- const msg = message as { customType?: string; content?: string };
65
- piCalls.push({ kind: "sendContext", content: msg.content, customType: msg.customType });
66
- },
67
- sendUserMessage(content: string | unknown[], _options?: unknown): void {
68
- piCalls.push({ kind: "sendUser", content: typeof content === "string" ? content : undefined });
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
- expect(h.piCalls.filter((c) => c.kind === "sendUser")).toHaveLength(1);
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(提示词触发器:sendUserMessage 让 AI 调 goal_control create)──
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
- // 发送 sendUserMessage 引导 AI 调 create
496
- const sendUserCalls = h.piCalls.filter((c) => c.kind === "sendUser");
497
- expect(sendUserCalls).toHaveLength(1);
498
- expect(sendUserCalls[0]?.content).toContain("my objective");
499
- expect(sendUserCalls[0]?.content).toContain("goal_control");
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.piCalls.filter((c) => c.kind === "sendUser")).toHaveLength(0); // 不触发 AI
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.piCalls.filter((c) => c.kind === "sendUser")).toHaveLength(1);
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 sendUserCalls = h.piCalls.filter((c) => c.kind === "sendUser");
551
- expect(sendUserCalls).toHaveLength(1);
552
- expect(sendUserCalls[0]?.content).toContain("obj");
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 sendUserCalls = h.piCalls.filter((c) => c.kind === "sendUser");
560
- expect(sendUserCalls).toHaveLength(1);
561
- expect(sendUserCalls[0]?.content).toContain("5000");
581
+ const sends = sendContextCalls(h);
582
+ expect(sends).toHaveLength(1);
583
+ expect(sends[0]?.content).toContain("5000");
562
584
  });
563
585
  });
564
586
 
@@ -43,7 +43,6 @@ function makeFakePorts(): ServicePorts & {
43
43
  } as UiPort,
44
44
  messaging: {
45
45
  sendContextMessage: () => {},
46
- sendUserMessage: () => {},
47
46
  },
48
47
  session: {
49
48
  getEntries: () => [],
@@ -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" | "sendUser" | "setStatus" | "setWidget";
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/sendUser)
123
- expect(all.filter((c) => c.kind === "sendContext" || c.kind === "sendUser")).toHaveLength(0);
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 不变)
@@ -60,7 +60,6 @@ function makeFakePorts(): ServicePorts & {
60
60
  } as UiPort,
61
61
  messaging: {
62
62
  sendContextMessage: () => {},
63
- sendUserMessage: () => {},
64
63
  },
65
64
  session: {
66
65
  getEntries: () => [],
@@ -69,7 +69,6 @@ function makeFixture(mode: "rpc" | "tui" | "json" | "print" = "rpc"): FakeFixtur
69
69
  registerMessageRenderer: () => {},
70
70
  appendEntry: () => {},
71
71
  sendMessage: () => {},
72
- sendUserMessage: () => {},
73
72
  } as unknown as ExtensionAPI;
74
73
 
75
74
  const ctx = {
@@ -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/sendUserMessage */
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, sendUser, commands };
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 = {
@@ -42,7 +42,6 @@ function makeFakePorts(): ServicePorts & {
42
42
  },
43
43
  messaging: {
44
44
  sendContextMessage: () => {},
45
- sendUserMessage: () => {},
46
45
  },
47
46
  session: {
48
47
  getEntries: () => [],
@@ -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 后 sendUserMessage 触发 AI。
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
- pi.sendUserMessage(
195
+ ports.messaging.sendContextMessage(
195
196
  `Goal resumed. Continuing toward the objective.${blockerNote}${capNote}\n\nObjective: ${state.objective}`,
196
- { deliverAs: "followUp" },
197
+ "followUp",
197
198
  );
198
199
  }
199
200
 
@@ -347,7 +348,7 @@ function handleUpdate(
347
348
 
348
349
  /**
349
350
  * /goal <objective> 改为「提示词触发器」:不直接 createGoal,
350
- * 而是 sendUserMessage 引导 AI 调 goal_control create(slug 由 AI 生成)。
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)—— AI 消化后调 goal_control create
404
- pi.sendUserMessage(message, { deliverAs: "followUp" });
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,deliverAs:"steer"}) 自然唤醒主 agent;
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 不调 sendUserMessage:toolcall 时 AI 已在 turn 中,返回结果后自行续跑
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 中,**不**调 sendUserMessage
154
+ * 全解耦:不读 todo/plan。toolcall 时 AI 已在 turn 中,**不**发消息注入
155
155
  * (AI 返回后自行续跑,对齐 Codex create_goal)。
156
156
  *
157
157
  * 守卫用 D25 严格语义:非终态 active/paused/blocked 全挡,提示用 /goal resume 或
@@ -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 映射到 sendContextMessage / sendUserMessage
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
- /** 发送 custom message(goal-context 等) */
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
- * 而是 sendUserMessage 让 AI 调 goal_control create(slug + successCriteria 由 AI 生成)。
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 指导完成验证)