pi-firecode 0.8.1 → 1.0.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.
Files changed (79) hide show
  1. package/README.md +35 -15
  2. package/activity.ts +92 -0
  3. package/busy.ts +188 -0
  4. package/config.example.jsonc +38 -47
  5. package/config.ts +198 -172
  6. package/deliver.ts +100 -13
  7. package/flame.ts +149 -0
  8. package/format.ts +84 -31
  9. package/header.ts +208 -82
  10. package/index.ts +17 -11
  11. package/master/actions.ts +293 -0
  12. package/master/activity-list.ts +281 -0
  13. package/master/event-card.ts +18 -37
  14. package/master/event-format.ts +71 -40
  15. package/master/guard.ts +46 -0
  16. package/master/index.ts +88 -1036
  17. package/master/list-view.ts +129 -0
  18. package/master/outbox.ts +188 -0
  19. package/master/prompts/master.zh.md +17 -12
  20. package/master/prompts/worker.zh.md +1 -1
  21. package/master/role.ts +6 -1
  22. package/master/run.ts +315 -0
  23. package/master/runtime.ts +259 -0
  24. package/master/spawn.ts +79 -38
  25. package/master/state.ts +30 -6
  26. package/master/worker-view.ts +517 -0
  27. package/package.json +1 -1
  28. package/provider/claude-sub.ts +35 -13
  29. package/provider/openai-native/src/compact-client.ts +2 -14
  30. package/provider/openai-native/src/options.ts +4 -2
  31. package/review/advisor.ts +0 -2
  32. package/review/card.ts +36 -23
  33. package/review/checkpoint.ts +2 -8
  34. package/review/evidence.ts +42 -44
  35. package/review/index.ts +271 -525
  36. package/review/occupancy.ts +25 -0
  37. package/review/outcome.ts +31 -7
  38. package/review/prompt.ts +7 -8
  39. package/review/prompts/review.en.md +7 -6
  40. package/review/prompts/review.zh.md +7 -6
  41. package/review/reviewer.ts +2 -5
  42. package/review/session.ts +11 -30
  43. package/review/state.ts +32 -60
  44. package/review/ui.ts +30 -469
  45. package/round-recorder.ts +18 -0
  46. package/session/herdr-display.ts +1 -1
  47. package/session/presets.ts +130 -87
  48. package/session/quota.ts +123 -0
  49. package/session/stats.ts +4 -1
  50. package/statusbar/index.ts +242 -104
  51. package/statusbar/render.ts +95 -163
  52. package/theme.ts +2 -41
  53. package/tools/actions.ts +53 -0
  54. package/tools/assistant-view.ts +64 -0
  55. package/tools/click-anchor.ts +56 -0
  56. package/tools/group-view.ts +420 -0
  57. package/tools/grouping.ts +146 -221
  58. package/tools/host.ts +369 -0
  59. package/tools/index.ts +108 -105
  60. package/tools/line.ts +95 -58
  61. package/tools/machine.ts +78 -0
  62. package/tools/parts.ts +2 -2
  63. package/tools/round.ts +95 -0
  64. package/tools/turn-clock.ts +41 -0
  65. package/tools/turn-summary.ts +109 -0
  66. package/truncated-write.ts +18 -0
  67. package/watcher/card.ts +28 -22
  68. package/watcher/index.ts +16 -44
  69. package/watcher/observer.ts +3 -3
  70. package/watcher/prompts/watch.zh.md +1 -1
  71. package/watcher/transcript.ts +4 -12
  72. package/flame-frames.ts +0 -460
  73. package/review/progress.ts +0 -324
  74. package/session/bark.ts +0 -157
  75. package/session/working-flame.ts +0 -116
  76. package/statusbar/quota-cache.ts +0 -60
  77. package/statusbar/quota-parse.ts +0 -85
  78. package/statusbar/quota.ts +0 -203
  79. package/statusbar/tps.ts +0 -87
package/watcher/card.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * 观察员建议的展示:单通道单一样式,随投递消息一起渲染。
3
- * 渲染器永不抛异常;校验失败降级纯文本。
2
+ * 观察员建议的展示:单通道单一样式,从消息正文里的信封渲染(deliver.ts 拥有信封格式)。
3
+ * 渲染器永不抛异常;不是信封的消息降级纯文本。
4
4
  */
5
5
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { Text, type Component } from "@earendil-works/pi-tui";
7
7
 
8
- import { clip, oneLine } from "../format.js";
8
+ import { parseEnvelopes, wrapEnvelope } from "../deliver.js";
9
+ import { clip, oneLine, textOf } from "../format.js";
9
10
  import { RAIL, paintBgLine } from "../tools/line.js";
10
11
 
11
12
  export const WATCHER_MESSAGE_TYPE = "firecode-watcher-note";
@@ -15,7 +16,7 @@ const WEIGH_NOTICE = "这是观察员供你权衡的第二意见,不是指令
15
16
  const LABEL = "👓 观察员";
16
17
 
17
18
  export function adviceMessage(card: WatcherCard): string {
18
- return `<firecode_watcher>\n${adviceHeadline(card)}\n${card.note}\n${WEIGH_NOTICE}\n</firecode_watcher>`;
19
+ return wrapEnvelope("firecode_watcher", `${adviceHeadline(card)}\n${card.note}\n${WEIGH_NOTICE}`);
19
20
  }
20
21
 
21
22
  export interface WatcherCard {
@@ -23,41 +24,52 @@ export interface WatcherCard {
23
24
  turnIndex: number;
24
25
  }
25
26
 
26
- export function adviceHeadline(card: WatcherCard): string {
27
+ function adviceHeadline(card: WatcherCard): string {
27
28
  return `${LABEL}(${timeMark(card.turnIndex)})`;
28
29
  }
29
30
 
30
31
  /** 建议自带时点标记:投递时主会话可能已经走远,读的人要知道它看的是哪一刻。 */
31
- export function timeMark(turnIndex: number): string {
32
+ function timeMark(turnIndex: number): string {
32
33
  return `基于第 ${turnIndex} 回合前的观察`;
33
34
  }
34
35
 
35
36
  export function registerWatcherCardRenderer(pi: ExtensionAPI): void {
36
- pi.registerMessageRenderer<WatcherCard>(WATCHER_MESSAGE_TYPE, (message, options, theme) =>
37
- isValidCard(message.details)
38
- ? new AdviceLine(message.details, options.expanded, theme)
39
- : new Text(String(message.content), 0, 0));
37
+ pi.registerMessageRenderer(WATCHER_MESSAGE_TYPE, (message, options, theme) => {
38
+ const advice = parseAdvice(message.content);
39
+ return advice ? new AdviceLine(advice, options.expanded, theme) : new Text(textOf(message.content), 0, 0);
40
+ });
41
+ }
42
+
43
+ interface Advice {
44
+ headline: string;
45
+ note: string;
46
+ }
47
+
48
+ /** 信封正文 = 标题行 + 建议 + 权衡声明(末行),与 adviceMessage 同构。 */
49
+ function parseAdvice(content: unknown): Advice | undefined {
50
+ const body = parseEnvelopes(textOf(content))?.[0]?.body;
51
+ if (body === undefined) return undefined;
52
+ const lines = body.split("\n");
53
+ return { headline: lines[0] ?? "", note: lines.slice(1, -1).join("\n") };
40
54
  }
41
55
 
42
56
  class AdviceLine implements Component {
43
57
  private readonly fallback: Component;
44
58
 
45
59
  constructor(
46
- private readonly card: WatcherCard,
60
+ private readonly card: Advice,
47
61
  private readonly expanded: boolean,
48
62
  private readonly theme: Theme,
49
63
  ) {
50
- this.fallback = new Text(`${adviceHeadline(card)} ${card.note}`, 0, 0);
64
+ this.fallback = new Text(`${card.headline} ${card.note}`, 0, 0);
51
65
  }
52
66
 
53
67
  render(width: number): string[] {
54
68
  const columns = Math.max(1, width);
55
69
  try {
56
- const bgFn = typeof this.theme.bg === "function"
57
- ? (text: string) => this.theme.bg("toolPendingBg", text)
58
- : undefined;
70
+ const bgFn = (text: string) => this.theme.bg("toolPendingBg", text);
59
71
  if (this.expanded) {
60
- const headline = this.theme.fg("warning", clip(oneLine(adviceHeadline(this.card)), columns));
72
+ const headline = this.theme.fg("warning", clip(oneLine(this.card.headline), columns));
61
73
  const body = new Text(this.theme.fg("dim", ` ${this.card.note}\n (供权衡,勿盲从)`), 0, 0);
62
74
  return [paintBgLine(headline, columns, bgFn), ...body.render(columns)];
63
75
  }
@@ -77,9 +89,3 @@ class AdviceLine implements Component {
77
89
  this.fallback.invalidate?.();
78
90
  }
79
91
  }
80
-
81
- function isValidCard(value: unknown): value is WatcherCard {
82
- if (!value || typeof value !== "object") return false;
83
- const card = value as Record<string, unknown>;
84
- return typeof card.note === "string" && typeof card.turnIndex === "number";
85
- }
package/watcher/index.ts CHANGED
@@ -2,17 +2,14 @@
2
2
  * Watcher 观察员:每个 turn 结束后异步评估主会话增量,要么沉默,要么发一条建议。
3
3
  * 与 Master、fire-review 各自独立注册;观察过程不落盘。
4
4
  */
5
- import type { Model } from "@earendil-works/pi-ai";
6
5
  import {
7
- getAgentDir,
8
- ModelRuntime,
9
6
  type ExtensionAPI,
10
7
  type ExtensionContext,
11
8
  } from "@earendil-works/pi-coding-agent";
12
- import { loadConfig, type WatcherConfig } from "../config.js";
9
+ import { loadConfig } from "../config.js";
13
10
  import { deliver } from "../deliver.js";
14
- import { formatModelName } from "../format.js";
15
11
  import { InProcessSessionPool } from "../master/spawn.js";
12
+ import { OCCUPANCY_CHANNEL, type OccupancyPayload } from "../review/occupancy.js";
16
13
  import {
17
14
  adviceMessage,
18
15
  registerWatcherCardRenderer,
@@ -22,13 +19,10 @@ import {
22
19
  import { createObserver, type Advice, type Observer } from "./observer.js";
23
20
  import { renderTurn } from "./transcript.js";
24
21
 
25
- /** review 模块发布的占用频道;观察员只订阅,不参与审查状态机。 */
26
- const REVIEW_OCCUPANCY_CHANNEL = "herdr:blocked";
27
22
  /** 观察会话自身上下文占比超过此值即重建。 */
28
23
  const CONTEXT_RESET_PERCENT = 70;
29
24
 
30
25
  interface WatcherDependencies {
31
- resolveModel?: (id: string) => Promise<Model<any>>;
32
26
  pool?: InProcessSessionPool;
33
27
  createObserver?: typeof createObserver;
34
28
  }
@@ -49,7 +43,7 @@ export function registerWatcher(
49
43
  // 子会话不带观察员:级联抑制是代码规则,不靠进程环境。
50
44
  if (subsession) return;
51
45
  registerWatcherCardRenderer(pi);
52
- const loaded = loadWatcherConfiguration();
46
+ const loaded = loadConfig().watcher;
53
47
  // 配置有问题时拒绝启动:静默回退会拿用户没配的模型真实发起观察。
54
48
  if ("error" in loaded) {
55
49
  pi.registerCommand("fire-watch", {
@@ -58,7 +52,7 @@ export function registerWatcher(
58
52
  });
59
53
  return;
60
54
  }
61
- const config = loaded;
55
+ const config = loaded.config;
62
56
  const pool = dependencies.pool ?? new InProcessSessionPool();
63
57
  const spawnObserver = dependencies.createObserver ?? createObserver;
64
58
  let runtime: WatcherRuntime | undefined;
@@ -85,13 +79,18 @@ export function registerWatcher(
85
79
  const activate = (ctx: ExtensionContext): WatcherRuntime => {
86
80
  const owner = { ctx, pending: [], lastTurnIndex: 0, evaluating: false };
87
81
  runtime = owner;
88
- ctx.ui.setStatus("watcher", ctx.ui.theme.fg("dim", `👓 ${formatModelName(config.model)}/${config.thinking}`));
82
+ ctx.ui.setStatus("watcher", ctx.ui.theme.fg("dim", "观察员"));
89
83
  return owner;
90
84
  };
91
85
  // 与指挥官事件同构:忙时卡片经 steer 队列句缝追加,歇透时走前门唤起(见 deliver.ts)。
92
- const speak = (owner: WatcherRuntime, advice: Advice, turnIndex: number) => {
86
+ // 建议是当下的第二意见,过时重投没有价值:投递失败只丢弃这一条并提示,观察员照常工作(Master 事件则重试)。
87
+ const speak = async (owner: WatcherRuntime, advice: Advice, turnIndex: number) => {
93
88
  const card: WatcherCard = { note: advice.note, turnIndex };
94
- return deliver(pi, owner.ctx, { customType: WATCHER_MESSAGE_TYPE, content: adviceMessage(card), details: card });
89
+ try {
90
+ await deliver(pi, owner.ctx, { customType: WATCHER_MESSAGE_TYPE, content: adviceMessage(card) });
91
+ } catch (error) {
92
+ if (runtime === owner) owner.ctx.ui.notify(`观察员这条建议投递失败,已丢弃:${error instanceof Error ? error.message : String(error)}`, "warning");
93
+ }
95
94
  };
96
95
  const evaluate = async (owner: WatcherRuntime) => {
97
96
  owner.evaluating = true;
@@ -103,11 +102,11 @@ export function registerWatcher(
103
102
  let observer = owner.observer;
104
103
  if (!observer) {
105
104
  const cwd = owner.ctx.cwd;
106
- const model = await (dependencies.resolveModel ?? resolveConfiguredModel)(config.model);
105
+ const model = await pool.resolveModel(config.model);
107
106
  if (runtime !== owner) return;
108
107
  observer = await spawnObserver({ cwd, model, thinking: config.thinking, pool });
109
108
  if (runtime !== owner) {
110
- observer.dispose();
109
+ await observer.dispose();
111
110
  return;
112
111
  }
113
112
  owner.observer = observer;
@@ -163,8 +162,8 @@ export function registerWatcher(
163
162
  });
164
163
 
165
164
  // fire-review 活跃期静默:不与对抗审查的反馈打架,增量留着审查完合并评估。
166
- pi.events?.on?.(REVIEW_OCCUPANCY_CHANNEL, (data: { active?: boolean } | undefined) => {
167
- reviewActive = Boolean(data?.active);
165
+ pi.events.on(OCCUPANCY_CHANNEL, (data) => {
166
+ reviewActive = (data as OccupancyPayload).active;
168
167
  const active = runtime;
169
168
  if (reviewActive || !active || active.evaluating || !active.pending.length) return;
170
169
  void evaluate(active);
@@ -175,30 +174,3 @@ export function registerWatcher(
175
174
  pi.on("session_shutdown", () => deactivate());
176
175
  }
177
176
 
178
- function loadWatcherConfiguration(): WatcherConfig | { error: string } {
179
- let loaded: ReturnType<typeof loadConfig>;
180
- try {
181
- loaded = loadConfig();
182
- } catch (error) {
183
- return { error: `观察员配置读取失败:${error instanceof Error ? error.message : String(error)}` };
184
- }
185
- // features 也算阻断集:开关写成字符串 "false" 时 `!== false` 仍会注册,
186
- // 而启用观察员意味着每个回合都对模型发起真实调用。
187
- const problems = loaded.problems.filter((problem) =>
188
- problem.startsWith("watcher") || problem.startsWith("未知字段 watcher.")
189
- || problem.startsWith("config.jsonc") || problem.startsWith("features"));
190
- if (problems.length) return { error: `观察员配置有问题,已停止:${problems.join(";")}` };
191
- return loaded.config.watcher;
192
- }
193
-
194
- async function resolveConfiguredModel(id: string): Promise<Model<any>> {
195
- const runtime = await ModelRuntime.create({
196
- authPath: `${getAgentDir()}/auth.json`,
197
- modelsPath: `${getAgentDir()}/models.json`,
198
- });
199
- const slash = id.indexOf("/");
200
- const model = slash > 0 ? runtime.getModel(id.slice(0, slash), id.slice(slash + 1)) : undefined;
201
- // 扩展注册的 provider 在无扩展子会话里不可解析(实测),必须明确引导而不是静默失败。
202
- if (!model) throw new Error(`找不到模型:${id};观察员只能使用内置 provider 的模型`);
203
- return model;
204
- }
@@ -22,7 +22,7 @@ export interface Observer {
22
22
  evaluate(increment: string): Promise<Advice | undefined>;
23
23
  /** 观察会话自身上下文占比(百分数),未知时 undefined。 */
24
24
  contextPercent(): number | undefined;
25
- dispose(): void;
25
+ dispose(): Promise<void>;
26
26
  }
27
27
 
28
28
  export interface ObserverOptions {
@@ -64,12 +64,12 @@ function adviseTool(current: () => Advice | undefined, capture: (advice: Advice)
64
64
  parameters: Type.Object({
65
65
  note: Type.String({ description: "一句话说清问题与定位。" }),
66
66
  }),
67
- execute(_id: string, params: Record<string, unknown>) {
67
+ async execute(_id: string, params: Record<string, unknown>) {
68
68
  if (current()) throw new Error("本次评估已提交过建议;余下的问题留到下一次评估");
69
69
  const note = typeof params.note === "string" ? params.note.trim() : "";
70
70
  if (!note) throw new Error("note 不能为空");
71
71
  capture({ note });
72
- return { content: [{ type: "text" as const, text: "已记录,本次评估结束。" }] };
72
+ return { content: [{ type: "text" as const, text: "已记录,本次评估结束。" }], details: undefined };
73
73
  },
74
74
  };
75
75
  }
@@ -18,7 +18,7 @@
18
18
 
19
19
  只在下面这几类问题上开口:
20
20
 
21
- - **偏离**:正在做的事与用户的需求、spec 或工单不一致,或悄悄扩大到没被要求的范围。
21
+ - **偏离**:正在做的事与用户的需求或工单不一致,或悄悄扩大到没被要求的范围。
22
22
  - **过度工程**:为不存在的需求加抽象、加配置、加兼容层、加防御分支。
23
23
  - **漏需求**:需求或工单里明确要求的部分被跳过、被忘记或被降级处理。
24
24
  - **未收口**:改动留下失效的死代码、过时文档、未更新的事实源、没跑的验证,或票据没有收尾。
@@ -4,12 +4,14 @@
4
4
  */
5
5
  import type { TurnEndEvent } from "@earendil-works/pi-coding-agent";
6
6
  import type { WatcherContext } from "../config.js";
7
+ import { textOf } from "../format.js";
7
8
 
8
9
  const RESULT_BUDGET = 400;
9
10
 
10
11
  export function renderTurn(turn: TurnEndEvent, context: WatcherContext): string {
11
12
  const lines: string[] = [];
12
- for (const part of blocks(turn.message.content)) {
13
+ const content = "content" in turn.message ? turn.message.content : undefined;
14
+ for (const part of blocks(content)) {
13
15
  if (part.type === "text" && typeof part.text === "string" && part.text.trim())
14
16
  lines.push(part.text.trim());
15
17
  if (part.type === "thinking" && context === "full" && typeof part.thinking === "string")
@@ -17,7 +19,7 @@ export function renderTurn(turn: TurnEndEvent, context: WatcherContext): string
17
19
  if (part.type === "toolCall") lines.push(toolCallLine(part, context));
18
20
  }
19
21
  for (const result of turn.toolResults) {
20
- const text = clipText(plainText(result.content), RESULT_BUDGET);
22
+ const text = clipText(textOf(result.content), RESULT_BUDGET);
21
23
  lines.push(`${result.isError ? "✗" : "→"} ${result.toolName}:${text || "(无输出)"}`);
22
24
  }
23
25
  return `<turn index="${turn.turnIndex}">\n${lines.join("\n")}\n</turn>`;
@@ -44,16 +46,6 @@ function blocks(content: unknown): Array<Record<string, any>> {
44
46
  return Array.isArray(content) ? content.filter((part) => !!part && typeof part === "object") : [];
45
47
  }
46
48
 
47
- function plainText(output: unknown): string {
48
- if (typeof output === "string") return output;
49
- if (Array.isArray(output))
50
- return output
51
- .map((part) => (part && typeof part === "object" && "text" in part ? String((part as any).text) : ""))
52
- .filter(Boolean)
53
- .join("\n");
54
- return "";
55
- }
56
-
57
49
  function oneLine(text: string): string {
58
50
  return text.replace(/\s+/gu, " ").trim();
59
51
  }