@zhushanwen/pi-evolve-daily 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-evolve-daily",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Daily evolution data collector — runs Python analyzer on first session of the day.",
5
5
  "type": "module",
6
+ "license": "MIT",
6
7
  "main": "src/index.ts",
7
8
  "pi": {
8
9
  "extensions": [
@@ -12,6 +12,12 @@ export interface CompactTrackedItem {
12
12
  detail?: string;
13
13
  }
14
14
 
15
+ // ── ID generation constants ──────────────────────────
16
+
17
+ const RANDOM_ID_RADIX = 36;
18
+ const RANDOM_ID_SLICE_START = 2;
19
+ const RANDOM_ID_SLICE_END = 7;
20
+
15
21
  export function createCompactDetector(problem: ProblemDefinition) {
16
22
  return {
17
23
  problemId: problem.id,
@@ -25,7 +31,7 @@ export function createCompactDetector(problem: ProblemDefinition) {
25
31
  compactionEntry?: { tokensBefore?: number };
26
32
  }): CompactTrackedItem {
27
33
  return {
28
- id: `compact-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
34
+ id: `compact-${Date.now()}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START, RANDOM_ID_SLICE_END)}`,
29
35
  problemId: problem.id as "compact-frequency",
30
36
  sessionId: "",
31
37
  tokensBefore: event.compactionEntry?.tokensBefore ?? 0,
@@ -2,6 +2,12 @@
2
2
 
3
3
  import type { ProblemDefinition } from "../problems";
4
4
 
5
+ // ── ID generation constants ──────────────────────────
6
+
7
+ const RANDOM_ID_RADIX = 36;
8
+ const RANDOM_ID_SLICE_START = 2;
9
+ const RANDOM_ID_SLICE_END = 7;
10
+
5
11
  export interface GoalQualityTrackedItem {
6
12
  id: string;
7
13
  problemId: "goal-task-quality";
@@ -37,7 +43,7 @@ export function createGoalQualityDetector(problem: ProblemDefinition) {
37
43
  const total = tasks.length;
38
44
 
39
45
  return {
40
- id: `goal-quality-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
46
+ id: `goal-quality-${Date.now()}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START, RANDOM_ID_SLICE_END)}`,
41
47
  problemId: problem.id as "goal-task-quality",
42
48
  sessionId: "",
43
49
  goalId: "",
@@ -2,6 +2,13 @@
2
2
 
3
3
  import type { ProblemDefinition } from "../problems";
4
4
 
5
+ // ── ID generation constants ──────────────────────────
6
+
7
+ const RANDOM_ID_RADIX = 36;
8
+ const RANDOM_ID_SLICE_START = 2;
9
+ const RANDOM_ID_SLICE_END = 7;
10
+ const ERROR_PREVIEW_MAX_LENGTH = 200;
11
+
5
12
  export interface ParamErrorTrackedItem {
6
13
  id: string;
7
14
  problemId: "tool-param-validation";
@@ -70,12 +77,12 @@ export function createParamErrorDetector(problem: ProblemDefinition) {
70
77
  }): ParamErrorTrackedItem {
71
78
  const errorMessage = event.content ?? "";
72
79
  return {
73
- id: `param-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
80
+ id: `param-error-${Date.now()}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START, RANDOM_ID_SLICE_END)}`,
74
81
  problemId: problem.id as "tool-param-validation",
75
82
  sessionId: "",
76
83
  toolName: event.toolName ?? "unknown",
77
84
  errorType: classifyError(errorMessage),
78
- errorPreview: errorMessage.slice(0, 200),
85
+ errorPreview: errorMessage.slice(0, ERROR_PREVIEW_MAX_LENGTH),
79
86
  status: "pending",
80
87
  };
81
88
  },
@@ -2,6 +2,12 @@
2
2
 
3
3
  import type { ProblemDefinition } from "../problems";
4
4
 
5
+ // ── ID generation constants ──────────────────────────
6
+
7
+ const RANDOM_ID_RADIX = 36;
8
+ const RANDOM_ID_SLICE_START = 2;
9
+ const RANDOM_ID_SLICE_END = 7;
10
+
5
11
  export interface SubagentTrackedItem {
6
12
  id: string;
7
13
  problemId: "subagent-efficiency";
@@ -45,7 +51,7 @@ export function createSubagentDetector(problem: ProblemDefinition) {
45
51
  taskPrompt?: string;
46
52
  }): SubagentTrackedItem {
47
53
  return {
48
- id: `subagent-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
54
+ id: `subagent-${Date.now()}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START, RANDOM_ID_SLICE_END)}`,
49
55
  problemId: problem.id as "subagent-efficiency",
50
56
  sessionId: "",
51
57
  taskType: classifyTaskType(event.taskPrompt ?? ""),
package/src/index.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  // packages/evolve-daily/src/index.ts
2
2
 
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
3
  import { existsSync, unlinkSync } from "node:fs";
5
4
  import { homedir } from "node:os";
6
5
  import { dirname, join } from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
 
9
- import { PROBLEM_REGISTRY } from "./problems";
8
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+
10
10
  import { createCompactDetector } from "./detectors/compact";
11
- import { createSubagentDetector } from "./detectors/subagent-result";
12
- import { createParamErrorDetector } from "./detectors/param-error";
13
11
  import { createGoalQualityDetector } from "./detectors/goal-quality";
12
+ import { createParamErrorDetector } from "./detectors/param-error";
13
+ import { createSubagentDetector } from "./detectors/subagent-result";
14
+ import { PROBLEM_REGISTRY } from "./problems";
14
15
  import { createTracker } from "./trackers/core";
15
16
  import { skillExecutionConfig } from "./trackers/skill-execution";
16
17
 
@@ -21,6 +22,12 @@ const ANALYZER_PATH = join(EXT_DIR, "..", "analyzer", "analyze.py");
21
22
  // 运行时数据目录使用 Pi 平台约定路径(homedir + .pi/agent/)
22
23
  const REPORTS_DIR = join(homedir(), ".pi", "agent", "evolution-data", "daily-reports");
23
24
 
25
+ const DATE_SLICE_END = 10;
26
+ const ANALYZER_TIMEOUT_MS = 30_000;
27
+
28
+ /** Pi API 的 on 方法重载签名(覆盖非标事件名) */
29
+ type PiOnAny = { on(event: string, handler: (...args: unknown[]) => Promise<void> | void): void };
30
+
24
31
  /** tool_result 事件中匹配的工具结果 detector */
25
32
  interface ToolResultDetector {
26
33
  problemId: string;
@@ -30,8 +37,8 @@ interface ToolResultDetector {
30
37
 
31
38
  export default function evolveDailyExtension(pi: ExtensionAPI) {
32
39
  // ── L1: session_start 时调用 Python analyzer ──
33
- pi.on("session_start", async () => {
34
- const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
40
+ pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
41
+ const today = new Date().toISOString().slice(0, DATE_SLICE_END); // YYYY-MM-DD
35
42
  const reportPath = join(REPORTS_DIR, `${today}.json`);
36
43
 
37
44
  if (existsSync(reportPath)) return;
@@ -48,12 +55,13 @@ export default function evolveDailyExtension(pi: ExtensionAPI) {
48
55
  "--output",
49
56
  reportPath,
50
57
  ],
51
- { timeout: 30_000 }
58
+ { timeout: ANALYZER_TIMEOUT_MS, signal: ctx.signal }
52
59
  );
53
60
  } catch (e) {
54
61
  // Clean up partial output if analyzer failed mid-write
55
62
  try {
56
63
  unlinkSync(reportPath);
64
+ // eslint-disable-next-line taste/no-silent-catch
57
65
  } catch {
58
66
  /* already gone */
59
67
  }
@@ -69,9 +77,10 @@ export default function evolveDailyExtension(pi: ExtensionAPI) {
69
77
  PROBLEM_REGISTRY.find((p) => p.id === "compact-frequency")!
70
78
  );
71
79
 
72
- (pi.on as any)("session_compact", async (event: Record<string, unknown>) => {
80
+ (pi as unknown as PiOnAny).on("session_compact", async (event: unknown) => {
81
+ const ev = event as Record<string, unknown>;
73
82
  try {
74
- const item = compactDetector.createItem(event);
83
+ const item = compactDetector.createItem(ev);
75
84
  pi.appendEntry("evolve-feedback", {
76
85
  problemId: item.problemId,
77
86
  itemId: item.id,
@@ -79,6 +88,7 @@ export default function evolveDailyExtension(pi: ExtensionAPI) {
79
88
  detail: item.detail ?? null,
80
89
  timestamp: new Date().toISOString(),
81
90
  });
91
+ // eslint-disable-next-line taste/no-silent-catch
82
92
  } catch (e) {
83
93
  console.error(
84
94
  `[evolve-daily] compact detector error:`,
@@ -101,13 +111,14 @@ export default function evolveDailyExtension(pi: ExtensionAPI) {
101
111
  ),
102
112
  ];
103
113
 
104
- (pi.on as any)(
114
+ (pi as unknown as PiOnAny).on(
105
115
  "tool_result",
106
- async (event: Record<string, unknown>, _ctx?: unknown) => {
116
+ async (event: unknown, _ctx?: unknown) => {
117
+ const ev = event as Record<string, unknown>;
107
118
  for (const detector of toolDetectors) {
108
119
  try {
109
- if (detector.match(event)) {
110
- const item = detector.createItem(event);
120
+ if (detector.match(ev)) {
121
+ const item = detector.createItem(ev);
111
122
  pi.appendEntry("evolve-feedback", {
112
123
  problemId: item.problemId,
113
124
  itemId: item.id,
@@ -116,6 +127,7 @@ export default function evolveDailyExtension(pi: ExtensionAPI) {
116
127
  timestamp: new Date().toISOString(),
117
128
  });
118
129
  }
130
+ // eslint-disable-next-line taste/no-silent-catch
119
131
  } catch (e) {
120
132
  console.error(
121
133
  `[evolve-daily] detector ${detector.problemId} error:`,
package/src/problems.ts CHANGED
@@ -49,17 +49,34 @@ interface TrackedItemTemplate {
49
49
  status: string;
50
50
  }
51
51
 
52
+ // ── Severity thresholds ──────────────────────────────
53
+
54
+ const COMPACT_HIGH_THRESHOLD = 3;
55
+ const COMPACT_MEDIUM_THRESHOLD = 2;
56
+ const CONTEXT_UTILIZATION_MEDIUM = 0.7;
57
+ const CONTEXT_UTILIZATION_HIGH = 0.9;
58
+ const GOAL_LOW_COMPLETION_HIGH = 0.5;
59
+ const GOAL_HIGH_CANCEL = 0.4;
60
+ const GOAL_LOW_COMPLETION_MEDIUM = 0.7;
61
+ const GOAL_MEDIUM_CANCEL = 0.2;
62
+ const SUBAGENT_FAILURE_MEDIUM = 0.2;
63
+ const SUBAGENT_FAILURE_HIGH = 0.4;
64
+ const PARAM_ERROR_MEDIUM = 0.1;
65
+ const PARAM_ERROR_HIGH = 0.25;
66
+ const WORKFLOW_PHASE_RATIO_HIGH = 0.7;
67
+ const WORKFLOW_PHASE_RATIO_MEDIUM = 0.5;
68
+
52
69
  export const PROBLEM_REGISTRY: ProblemDefinition[] = [
53
70
  {
54
71
  id: "compact-frequency",
55
- name: "Compact 频率",
72
+ name: "Compact Frequency",
56
73
  category: "context",
57
74
  severity: {
58
75
  metric: "custom",
59
76
  custom: (data) => {
60
77
  const rate = data.compactsPerSession as number;
61
- if (rate >= 3) return "high";
62
- if (rate >= 2) return "medium";
78
+ if (rate >= COMPACT_HIGH_THRESHOLD) return "high";
79
+ if (rate >= COMPACT_MEDIUM_THRESHOLD) return "medium";
63
80
  return "low";
64
81
  },
65
82
  },
@@ -68,103 +85,103 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
68
85
  match: { custom: "compactDetector" },
69
86
  template: { category: "context-pressure" },
70
87
  steering:
71
- "检测到 Compact 触发(id={{id}})。请评估是否丢失了关键上下文。如有丢失,update status=error, detail='丢失的内容'。如无影响,update status=completed",
88
+ "Compact triggered (id={{id}}). Evaluate whether critical context was lost. If lost, update status=error, detail='what was lost'. If unaffected, update status=completed.",
72
89
  },
73
90
  analysis: {
74
91
  extractor: "compact",
75
92
  minerRules: ["compact-high-frequency", "compact-early-trigger"],
76
93
  },
77
94
  suggestion: {
78
- title: "优化 Compact 频率",
79
- description: "Compact 频率过高,说明上下文管理效率低",
95
+ title: "Optimize Compact Frequency",
96
+ description: "High compact frequency indicates inefficient context management",
80
97
  defaultSeverity: "medium",
81
98
  },
82
99
  },
83
100
  {
84
101
  id: "context-utilization",
85
- name: "上下文窗口利用率",
102
+ name: "Context Window Utilization",
86
103
  category: "context",
87
104
  severity: {
88
105
  metric: "rate",
89
- thresholds: { medium: 0.7, high: 0.9 },
106
+ thresholds: { medium: CONTEXT_UTILIZATION_MEDIUM, high: CONTEXT_UTILIZATION_HIGH },
90
107
  },
91
108
  detector: {
92
109
  events: ["turn_end"],
93
110
  match: { custom: "contextUtilizationMatcher" },
94
111
  template: { category: "context-pressure" },
95
112
  steering:
96
- "当前上下文利用率 {{usageRate}}(id={{id}})。如接近上限,update status=completed, detail='需要 compact'。如充足,update status=dismissed",
113
+ "Current context utilization {{usageRate}} (id={{id}}). If near limit, update status=completed, detail='needs compact'. If sufficient, update status=dismissed.",
97
114
  },
98
115
  analysis: {
99
116
  extractor: "context",
100
117
  minerRules: ["context-high-utilization"],
101
118
  },
102
119
  suggestion: {
103
- title: "优化上下文利用率",
104
- description: "上下文利用率持续偏高,会触发频繁 compact",
120
+ title: "Optimize Context Utilization",
121
+ description: "Persistently high context utilization triggers frequent compacts",
105
122
  defaultSeverity: "medium",
106
123
  },
107
124
  },
108
125
  {
109
126
  id: "subagent-efficiency",
110
- name: "Subagent 调度效率",
127
+ name: "Subagent Scheduling Efficiency",
111
128
  category: "subagent",
112
129
  severity: {
113
130
  metric: "rate",
114
- thresholds: { medium: 0.2, high: 0.4 },
131
+ thresholds: { medium: SUBAGENT_FAILURE_MEDIUM, high: SUBAGENT_FAILURE_HIGH },
115
132
  },
116
133
  detector: {
117
134
  events: ["tool_result"],
118
135
  match: { toolName: "subagent", custom: "subagentResultMatcher" },
119
136
  template: { category: "subagent" },
120
137
  steering:
121
- "Subagent 任务完成(id={{id}})exitCode={{exitCode}}, 耗时={{duration}}。如结果满意,update status=completed。如需重做,update status=error, detail='问题原因'",
138
+ "Subagent task completed (id={{id}}). exitCode={{exitCode}}, duration={{duration}}. If satisfactory, update status=completed. If retry needed, update status=error, detail='reason'.",
122
139
  },
123
140
  analysis: {
124
141
  extractor: "subagent",
125
142
  minerRules: ["subagent-failure-rate", "subagent-high-retry"],
126
143
  },
127
144
  suggestion: {
128
- title: "优化 Subagent 调度效率",
129
- description: "Subagent 失败率或重试率过高",
145
+ title: "Optimize Subagent Scheduling",
146
+ description: "High subagent failure or retry rate",
130
147
  defaultSeverity: "medium",
131
148
  },
132
149
  },
133
150
  {
134
151
  id: "tool-param-validation",
135
- name: "工具参数校验失败",
152
+ name: "Tool Parameter Validation Failure",
136
153
  category: "tool",
137
154
  severity: {
138
155
  metric: "rate",
139
- thresholds: { medium: 0.1, high: 0.25 },
156
+ thresholds: { medium: PARAM_ERROR_MEDIUM, high: PARAM_ERROR_HIGH },
140
157
  },
141
158
  detector: {
142
159
  events: ["tool_result"],
143
160
  match: { isError: true, custom: "paramErrorMatcher" },
144
161
  template: { category: "tool-error" },
145
162
  steering:
146
- "检测到 {{toolName}} 参数错误(id={{id}})。错误: {{errorPreview}}。如已理解原因,update status=completed, detail='错误原因和修正方式'。如不确定,update status=error",
163
+ "Detected {{toolName}} parameter error (id={{id}}). Error: {{errorPreview}}. If cause understood, update status=completed, detail='cause and fix'. If unclear, update status=error.",
147
164
  },
148
165
  analysis: {
149
166
  extractor: "tool_errors",
150
167
  minerRules: ["param-error-rate", "edit-match-failure", "low-self-correction"],
151
168
  },
152
169
  suggestion: {
153
- title: "降低工具参数错误率",
154
- description: "参数错误率高,说明 AI 不理解工具用法",
170
+ title: "Reduce Tool Parameter Error Rate",
171
+ description: "High parameter error rate suggests the AI does not understand tool usage",
155
172
  defaultSeverity: "high",
156
173
  },
157
174
  },
158
175
  {
159
176
  id: "workflow-phase-duration",
160
- name: "工作流阶段耗时",
177
+ name: "Workflow Phase Duration",
161
178
  category: "workflow",
162
179
  severity: {
163
180
  metric: "custom",
164
181
  custom: (data) => {
165
182
  const maxPhaseRatio = data.maxPhaseDurationRatio as number;
166
- if (maxPhaseRatio > 0.7) return "high";
167
- if (maxPhaseRatio > 0.5) return "medium";
183
+ if (maxPhaseRatio > WORKFLOW_PHASE_RATIO_HIGH) return "high";
184
+ if (maxPhaseRatio > WORKFLOW_PHASE_RATIO_MEDIUM) return "medium";
168
185
  return "low";
169
186
  },
170
187
  },
@@ -173,29 +190,29 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
173
190
  match: { toolName: ["coding-workflow-gate", "coding-workflow-phase-start"] },
174
191
  template: { category: "workflow" },
175
192
  steering:
176
- "工作流阶段 {{phase}} 完成(id={{id}})gate={{gateResult}}, 耗时={{duration}}。如阶段顺利,update status=completed。如有问题,update status=error, detail='问题描述'",
193
+ "Workflow phase {{phase}} completed (id={{id}}). gate={{gateResult}}, duration={{duration}}. If smooth, update status=completed. If issues, update status=error, detail='issue description'.",
177
194
  },
178
195
  analysis: {
179
196
  extractor: "workflow",
180
197
  minerRules: ["workflow-slow-phase", "workflow-gate-retry"],
181
198
  },
182
199
  suggestion: {
183
- title: "优化工作流阶段效率",
184
- description: "某阶段耗时占比过高或 gate 重试频繁",
200
+ title: "Optimize Workflow Phase Efficiency",
201
+ description: "Disproportionate phase duration or frequent gate retries",
185
202
  defaultSeverity: "medium",
186
203
  },
187
204
  },
188
205
  {
189
206
  id: "goal-task-quality",
190
- name: "Goal 任务拆分质量",
207
+ name: "Goal Task Decomposition Quality",
191
208
  category: "workflow",
192
209
  severity: {
193
210
  metric: "custom",
194
211
  custom: (data) => {
195
212
  const completionRate = data.taskCompletionRate as number;
196
213
  const cancelRate = data.taskCancelRate as number;
197
- if (completionRate < 0.5 || cancelRate > 0.4) return "high";
198
- if (completionRate < 0.7 || cancelRate > 0.2) return "medium";
214
+ if (completionRate < GOAL_LOW_COMPLETION_HIGH || cancelRate > GOAL_HIGH_CANCEL) return "high";
215
+ if (completionRate < GOAL_LOW_COMPLETION_MEDIUM || cancelRate > GOAL_MEDIUM_CANCEL) return "medium";
199
216
  return "low";
200
217
  },
201
218
  },
@@ -204,7 +221,7 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
204
221
  match: { toolName: "goal_manager", custom: "goalQualityMatcher" },
205
222
  template: { category: "workflow" },
206
223
  steering:
207
- "Goal 任务更新(id={{id}})。任务完成率={{completionRate}}。如目标达成,update status=completed, detail='目标完成情况'。如遇到困难,update status=error, detail='困难描述'",
224
+ "Goal task updated (id={{id}}). Completion rate={{completionRate}}. If objective met, update status=completed, detail='completion summary'. If blocked, update status=error, detail='blocker description'.",
208
225
  },
209
226
  analysis: {
210
227
  extractor: "goal_quality",
@@ -216,8 +233,8 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
216
233
  ],
217
234
  },
218
235
  suggestion: {
219
- title: "优化 Goal 任务拆分质量",
220
- description: "任务完成率低或 Evidence 质量低",
236
+ title: "Optimize Goal Task Decomposition",
237
+ description: "Low task completion rate or poor evidence quality",
221
238
  defaultSeverity: "high",
222
239
  },
223
240
  },
@@ -13,7 +13,7 @@ import type {
13
13
  Theme,
14
14
  } from "@mariozechner/pi-coding-agent";
15
15
  import { Text } from "@mariozechner/pi-tui";
16
-
16
+ import type { Static } from "typebox";
17
17
 
18
18
  import {
19
19
  canTransition,
@@ -21,13 +21,46 @@ import {
21
21
  deserializeState,
22
22
  isTerminalStatus,
23
23
  serializeState,
24
- TrackerParams,
25
-
26
24
  type TrackedItem,
25
+ type TrackedItemStatus,
27
26
  type TrackerDetails,
27
+ TrackerParams,
28
28
  type TrackerRuntimeState,
29
29
  } from "./types";
30
30
 
31
+ // ── Pi SDK custom event API type ──────────────────────
32
+
33
+ type PiOnAny = {
34
+ on(event: string, handler: (...args: unknown[]) => unknown): void;
35
+ };
36
+
37
+ // ── Stale context detection ──────────────────────────
38
+
39
+ const STALE_CONTEXT_PATTERNS = [
40
+ "Extension context no longer active",
41
+ "aborted",
42
+ "context canceled",
43
+ "stale context",
44
+ "stalecontext",
45
+ ];
46
+
47
+ /** Detect errors that indicate the Pi session has been torn down (compact/reload/exit). */
48
+ function isStaleContextError(error: unknown): boolean {
49
+ if (!(error instanceof Error)) return false;
50
+ const msg = error.message.toLowerCase();
51
+ return STALE_CONTEXT_PATTERNS.some((p) => msg.includes(p));
52
+ }
53
+
54
+ // ── Tool execute/render param types ──────────────────
55
+
56
+ type RenderOptions = { expanded?: boolean };
57
+
58
+ type ToolResult = {
59
+ content: Array<{ type: "text"; text: string }>;
60
+ details: Record<string, unknown> | undefined;
61
+ isError?: boolean;
62
+ };
63
+
31
64
  // ── Tracker 配置接口(避免 types.ts 引入 Pi API 类型)──
32
65
 
33
66
  export interface TrackerConfig<TMeta = Record<string, unknown>> {
@@ -78,7 +111,7 @@ function formatItemList<TMeta>(
78
111
  items: TrackedItem<TMeta>[],
79
112
  trackerName: string,
80
113
  ): string {
81
- if (items.length === 0) return `无活跃追踪(${trackerName})。`;
114
+ if (items.length === 0) return `No active tracked items (${trackerName}).`;
82
115
  return items
83
116
  .map(
84
117
  (item) =>
@@ -89,6 +122,74 @@ function formatItemList<TMeta>(
89
122
  .join("\n");
90
123
  }
91
124
 
125
+ // ── Tool render helpers (extracted to keep createTracker ≤300 lines) ──
126
+
127
+ function renderTrackerCall<TMeta>(
128
+ args: Record<string, unknown>,
129
+ config: TrackerConfig<TMeta>,
130
+ theme: Theme,
131
+ ): Text {
132
+ const parts = [
133
+ theme.fg("toolTitle", theme.bold(`${config.toolName} `)),
134
+ theme.fg("muted", String(args.action ?? "")),
135
+ ];
136
+ if (args.id !== undefined)
137
+ parts.push(theme.fg("accent", `#${String(args.id)}`));
138
+ if (args.status !== undefined)
139
+ parts.push(theme.fg("warning", String(args.status)));
140
+ if (args.detail !== undefined)
141
+ parts.push(theme.fg("dim", `"${String(args.detail)}"`));
142
+ return new Text(parts.join(" "), 0, 0);
143
+ }
144
+
145
+ function renderTrackerResult<TMeta>(
146
+ result: ToolResult,
147
+ options: RenderOptions,
148
+ config: TrackerConfig<TMeta>,
149
+ theme: Theme,
150
+ ): Text {
151
+ if (config.renderResult && result.details) {
152
+ return config.renderResult(
153
+ result.details as unknown as TrackerDetails<TMeta>,
154
+ options,
155
+ theme,
156
+ );
157
+ }
158
+
159
+ if (!result.details) {
160
+ return new Text(theme.fg("dim", "(no details)"), 0, 0);
161
+ }
162
+
163
+ // 框架默认渲染
164
+ const details = result.details as unknown as TrackerDetails<TMeta>;
165
+ if (details.error) {
166
+ return new Text(
167
+ theme.fg("error", `[${config.name}] Error: ${details.error}`),
168
+ 0,
169
+ 0,
170
+ );
171
+ }
172
+
173
+ const prefix = theme.fg("accent", `[${config.name}] `);
174
+ const summary = `${details.action}: ${details.items.length} items`;
175
+
176
+ if (!options.expanded) {
177
+ return new Text(prefix + theme.fg("dim", summary), 0, 0);
178
+ }
179
+
180
+ const items = details.items
181
+ .map((item) => {
182
+ const terminal = isTerminalStatus(item.status) ? " ✓" : "";
183
+ return ` #${item.id} ${item.name} [${item.status}]${terminal}`;
184
+ })
185
+ .join("\n");
186
+ return new Text(
187
+ prefix + summary + "\n" + theme.fg("dim", items),
188
+ 0,
189
+ 0,
190
+ );
191
+ }
192
+
92
193
  // ── 工厂函数 ────────────────────────────────────────
93
194
 
94
195
  export function createTracker<TMeta>(
@@ -100,8 +201,19 @@ export function createTracker<TMeta>(
100
201
  // ── 持久化 + GC ───────────────────────────────────
101
202
 
102
203
  function persistState(ctx: ExtensionContext): void {
103
- pi.appendEntry(config.entryType, serializeState(state));
104
- const entries = ctx.sessionManager.getEntries();
204
+ let entries: SessionEntry[];
205
+ try {
206
+ pi.appendEntry(config.entryType, serializeState(state));
207
+ entries = ctx.sessionManager.getEntries();
208
+ } catch (e) {
209
+ if (isStaleContextError(e)) {
210
+ console.warn(
211
+ `[${config.name}] skip persist: stale context (${(e as Error).message})`,
212
+ );
213
+ return;
214
+ }
215
+ throw e;
216
+ }
105
217
  const staleIndices: number[] = [];
106
218
  let foundLatest = false;
107
219
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -121,7 +233,19 @@ export function createTracker<TMeta>(
121
233
  // ── 状态恢复 ──────────────────────────────────────
122
234
 
123
235
  function reconstructState(ctx: ExtensionContext): void {
124
- const entries = ctx.sessionManager.getEntries();
236
+ let entries: SessionEntry[];
237
+ try {
238
+ entries = ctx.sessionManager.getEntries();
239
+ } catch (e) {
240
+ if (isStaleContextError(e)) {
241
+ console.warn(
242
+ `[${config.name}] skip reconstruct: stale context (${(e as Error).message})`,
243
+ );
244
+ state = createInitialState<TMeta>();
245
+ return;
246
+ }
247
+ throw e;
248
+ }
125
249
  const allTypes = [config.entryType, ...(config.legacyEntryTypes ?? [])];
126
250
 
127
251
  let latestData: Record<string, unknown> | undefined;
@@ -181,10 +305,10 @@ export function createTracker<TMeta>(
181
305
  // ── Event: triggerEvent (e.g. tool_call) ───────────
182
306
 
183
307
  // Pi 事件系统支持任意字符串事件名,但类型定义不完整(与 session_compact 同)
184
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
- (pi as any).on(
308
+ (pi as unknown as PiOnAny).on(
186
309
  config.triggerEvent,
187
- async (event: unknown, ctx: ExtensionContext) => {
310
+ async (event, nextCtx) => {
311
+ const ctx = nextCtx as ExtensionContext;
188
312
  const match = config.triggerMatch(event);
189
313
  if (!match) return;
190
314
 
@@ -223,10 +347,11 @@ export function createTracker<TMeta>(
223
347
 
224
348
  // ── Event: turn_end(remind 检查)─────────────────
225
349
 
226
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
- (pi as any).on(
350
+ (pi as unknown as PiOnAny).on(
228
351
  "turn_end",
229
- async (event: Record<string, unknown>, ctx: ExtensionContext) => {
352
+ async (rawEvent, nextCtx) => {
353
+ const event = rawEvent as Record<string, unknown>;
354
+ const ctx = nextCtx as ExtensionContext;
230
355
  const eventTurnIndex = event.turnIndex;
231
356
  if (typeof eventTurnIndex === "number") {
232
357
  state.currentTurnIndex = eventTurnIndex;
@@ -285,8 +410,7 @@ export function createTracker<TMeta>(
285
410
  pi.registerMessageRenderer(
286
411
  customType,
287
412
  (
288
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
289
- message: any,
413
+ message: { content: string | unknown },
290
414
  _options: unknown,
291
415
  theme: Theme,
292
416
  ) => {
@@ -313,14 +437,13 @@ export function createTracker<TMeta>(
313
437
  promptGuidelines: config.promptGuidelines,
314
438
  parameters: TrackerParams,
315
439
 
316
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
440
  async execute(
318
441
  _toolCallId: string,
319
- params: any,
320
- _signal: any,
321
- _onUpdate: any,
442
+ params: Static<typeof TrackerParams>,
443
+ _signal: AbortSignal | undefined,
444
+ _onUpdate: unknown,
322
445
  ctx: ExtensionContext,
323
- ): Promise<any> {
446
+ ): Promise<ToolResult> {
324
447
  // ── list ──
325
448
  if (params.action === "list") {
326
449
  return {
@@ -339,30 +462,32 @@ export function createTracker<TMeta>(
339
462
  }
340
463
 
341
464
  // ── update ──
342
- if (params.id === undefined) {
343
- return { content: [{ type: "text", text: "update 操作需要 id 参数" }], isError: true };
465
+ const updateId = params.id as number | undefined;
466
+ const updateStatus = params.status as string | undefined;
467
+ if (updateId === undefined) {
468
+ return { content: [{ type: "text", text: "update action requires id parameter" }], details: undefined, isError: true };
344
469
  }
345
- if (params.status === undefined) {
346
- return { content: [{ type: "text", text: "update 操作需要 status 参数" }], isError: true };
470
+ if (updateStatus === undefined) {
471
+ return { content: [{ type: "text", text: "update action requires status parameter" }], details: undefined, isError: true };
347
472
  }
348
473
 
349
474
  const itemIndex = state.items.findIndex(
350
- (item) => item.id === params.id,
475
+ (item) => item.id === updateId,
351
476
  );
352
477
  if (itemIndex === -1) {
353
- return { content: [{ type: "text", text: `TrackedItem id=${params.id} 不存在` }], isError: true };
478
+ return { content: [{ type: "text", text: `TrackedItem id=${updateId} not found` }], details: undefined, isError: true };
354
479
  }
355
480
 
356
481
  const item = state.items[itemIndex];
357
- if (!canTransition(item.status, params.status)) {
358
- return { content: [{ type: "text", text: `非法转换: ${item.status} → ${params.status}(当前 ${item.status},终态不可变更或该路径不允许)` }], isError: true };
482
+ if (!canTransition(item.status, updateStatus as TrackedItemStatus)) {
483
+ return { content: [{ type: "text", text: `Invalid transition: ${item.status} → ${updateStatus} (current: ${item.status}, terminal states are immutable or path not allowed)` }], details: undefined, isError: true };
359
484
  }
360
485
 
361
486
  // 执行转换
362
- item.status = params.status;
363
- item.detail = params.detail ?? item.detail;
487
+ item.status = updateStatus as TrackedItemStatus;
488
+ item.detail = (params.detail as string | undefined | null) ?? item.detail;
364
489
 
365
- if (params.status === "error") {
490
+ if (updateStatus === "error") {
366
491
  item.errorCount += 1;
367
492
  if (item.errorCount >= config.errorThreshold) {
368
493
  await pi.sendUserMessage(config.steering.onError(item), {
@@ -373,7 +498,7 @@ export function createTracker<TMeta>(
373
498
 
374
499
  persistState(ctx);
375
500
 
376
- const statusText = isTerminalStatus(item.status) ? "(终态)" : "";
501
+ const statusText = isTerminalStatus(item.status) ? " (terminal)" : "";
377
502
  return {
378
503
  content: [
379
504
  {
@@ -390,68 +515,21 @@ export function createTracker<TMeta>(
390
515
  };
391
516
  },
392
517
 
393
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
394
518
  renderCall(
395
- args: any,
519
+ args: Record<string, unknown>,
396
520
  theme: Theme,
397
521
  _context?: unknown,
398
522
  ) {
399
- const parts = [
400
- theme.fg("toolTitle", theme.bold(`${config.toolName} `)),
401
- theme.fg("muted", String(args.action ?? "")),
402
- ];
403
- if (args.id !== undefined)
404
- parts.push(theme.fg("accent", `#${String(args.id)}`));
405
- if (args.status !== undefined)
406
- parts.push(theme.fg("warning", String(args.status)));
407
- if (args.detail !== undefined)
408
- parts.push(theme.fg("dim", `"${String(args.detail)}"`));
409
- return new Text(parts.join(" "), 0, 0);
523
+ return renderTrackerCall(args, config, theme);
410
524
  },
411
525
 
412
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
413
526
  renderResult(
414
- result: any,
415
- options: any,
527
+ result: { content: Array<{ type: "text"; text?: string } | { type: "image"; data: string; mimeType: string }>; details?: Record<string, unknown> },
528
+ _options: unknown,
416
529
  theme: Theme,
417
530
  _context?: unknown,
418
531
  ) {
419
- if (config.renderResult) {
420
- return config.renderResult(
421
- result.details as TrackerDetails<TMeta>,
422
- options,
423
- theme,
424
- );
425
- }
426
-
427
- // 框架默认渲染
428
- const details = result.details as TrackerDetails<TMeta>;
429
- if (details.error) {
430
- return new Text(
431
- theme.fg("error", `[${config.name}] 错误: ${details.error}`),
432
- 0,
433
- 0,
434
- );
435
- }
436
-
437
- const prefix = theme.fg("accent", `[${config.name}] `);
438
- const summary = `${details.action}: ${details.items.length} items`;
439
-
440
- if (!options.expanded) {
441
- return new Text(prefix + theme.fg("dim", summary), 0, 0);
442
- }
443
-
444
- const items = details.items
445
- .map((item) => {
446
- const terminal = isTerminalStatus(item.status) ? " ✓" : "";
447
- return ` #${item.id} ${item.name} [${item.status}]${terminal}`;
448
- })
449
- .join("\n");
450
- return new Text(
451
- prefix + summary + "\n" + theme.fg("dim", items),
452
- 0,
453
- 0,
454
- );
532
+ return renderTrackerResult(result as ToolResult, { expanded: (_options as Record<string, unknown> | undefined)?.expanded as boolean | undefined }, config, theme);
455
533
  },
456
534
  });
457
535
  }
@@ -5,8 +5,8 @@
5
5
  * 追踪 skill 的加载、执行、异常、记录全生命周期。
6
6
  */
7
7
 
8
- import type { TrackedItem } from "./types";
9
8
  import type { TrackerConfig } from "./core";
9
+ import type { TrackedItem } from "./types";
10
10
 
11
11
  // ── Metadata 类型 ───────────────────────────────────
12
12
 
@@ -32,9 +32,9 @@ export function extractSkillName(path: string): string | null {
32
32
 
33
33
  function loadedSteeringPrompt(name: string, id: number): string {
34
34
  return (
35
- `[SKILL-STATE] skill "${name}" 已加载并开始追踪(id=${id})。\n` +
36
- `执行完成后调用 skill_state(action=update, id=${id}, status=completed)。\n` +
37
- `遇到困难时调用 skill_state(action=update, id=${id}, status=error, detail="原因")。`
35
+ `[SKILL-STATE] skill "${name}" loaded and tracking started (id=${id}).\n` +
36
+ `When done, call skill_state(action=update, id=${id}, status=completed).\n` +
37
+ `If blocked, call skill_state(action=update, id=${id}, status=error, detail="reason").`
38
38
  );
39
39
  }
40
40
 
@@ -42,17 +42,17 @@ function remindSteeringPrompt(
42
42
  name: string,
43
43
  turnsSinceLoad: number,
44
44
  ): string {
45
- return `[SKILL-STATE] skill "${name}" 已加载 ${turnsSinceLoad} turn 未终态,请调用 skill_state 工具流转状态。`;
45
+ return `[SKILL-STATE] skill "${name}" loaded ${turnsSinceLoad} turns ago without reaching terminal state. Please call skill_state to update its status.`;
46
46
  }
47
47
 
48
48
  function errorForceRecordPrompt(item: TrackedItem<SkillMeta>): string {
49
49
  return (
50
- `[SKILL-STATE] skill "${item.name}" 异常次数已达 ${item.errorCount} 次,需要记录问题。\n` +
51
- `请立即调用 subagent 工具(background 模式),任务如下:\n` +
52
- `1. 读取 ${item.metadata.skillMdPath}\n` +
53
- `2. 根据当前 session 上下文分析 skill "${item.name}" 执行中遇到的问题\n` +
54
- `3. 生成结构化问题记录(skill 名称、异常次数、问题描述、改进建议)\n` +
55
- `完成后调用 skill_state(action=update, id=${item.id}, status=recorded) 标记记录完成。`
50
+ `[SKILL-STATE] skill "${item.name}" has reached ${item.errorCount} errors — issue recording required.\n` +
51
+ `Immediately call the subagent tool (background mode) with this task:\n` +
52
+ `1. Read ${item.metadata.skillMdPath}\n` +
53
+ `2. Analyze issues encountered during skill "${item.name}" execution based on current session context\n` +
54
+ `3. Generate a structured issue record (skill name, error count, issue description, improvement suggestions)\n` +
55
+ `After completion, call skill_state(action=update, id=${item.id}, status=recorded).`
56
56
  );
57
57
  }
58
58
 
@@ -62,7 +62,7 @@ function agentStartContextPrompt(items: TrackedItem<SkillMeta>[]): string {
62
62
  (item) => ` - "${item.name}" (id=${item.id}, status=${item.status})`,
63
63
  );
64
64
  return (
65
- `[SKILL-STATE] 以下 skill 正在追踪中,请适时调用 skill_state 工具流转状态:\n` +
65
+ `[SKILL-STATE] The following skills are being tracked — call skill_state to update their status when appropriate:\n` +
66
66
  lines.join("\n")
67
67
  );
68
68
  }
@@ -74,17 +74,17 @@ export const skillExecutionConfig: TrackerConfig<SkillMeta> = {
74
74
  toolName: "skill_state",
75
75
  label: "Skill State",
76
76
  description:
77
- "管理 skill 执行追踪状态。" +
78
- "\n\n可用 action:" +
79
- "\n- list:查看所有 TrackedItem" +
80
- "\n- update:更新 TrackedItem 状态(需要 id status",
81
- promptSnippet: "追踪 skill 执行状态,自动检测 skill 加载",
77
+ "Manage skill execution tracking state." +
78
+ "\n\nAvailable actions:" +
79
+ "\n- list: View all TrackedItems" +
80
+ "\n- update: Update TrackedItem status (requires id and status)",
81
+ promptSnippet: "Track skill execution status with automatic skill load detection",
82
82
  promptGuidelines: [
83
- "[触发] skill 加载时自动创建追踪,无需手动创建",
84
- "[流转] 执行完成后用 update status=completed 标记成功",
85
- "[异常] 遇到困难时用 update status=error 标记异常",
86
- "[记录] 异常累积 2 次后系统会要求记录问题,完成后 update status=recorded",
87
- "[查询] 随时用 list 查看所有追踪状态",
83
+ "[Trigger] Tracking is auto-created when a skill loads — no manual creation needed",
84
+ "[Transition] After execution, use update status=completed to mark success",
85
+ "[Error] When blocked, use update status=error to mark the exception",
86
+ "[Record] After 2 accumulated errors, the system requests issue recording — when done, update status=recorded",
87
+ "[Query] Use list anytime to view all tracking states",
88
88
  ],
89
89
 
90
90
  triggerEvent: "tool_call",
@@ -79,15 +79,15 @@ export interface TrackerDetails<
79
79
  export const TrackerParams = Type.Object({
80
80
  action: StringEnum(["update", "list"] as const),
81
81
  id: Type.Optional(
82
- Type.Number({ description: "TrackedItem IDupdate 必填)" }),
82
+ Type.Number({ description: "TrackedItem ID (required for update)" }),
83
83
  ),
84
84
  status: Type.Optional(
85
85
  StringEnum(["completed", "error", "recorded"] as const, {
86
- description: "目标状态(update 必填)",
86
+ description: "Target status (required for update)",
87
87
  }),
88
88
  ),
89
89
  detail: Type.Optional(
90
- Type.String({ description: "附加说明(如 error 原因)" }),
90
+ Type.String({ description: "Additional notes (e.g. error reason)" }),
91
91
  ),
92
92
  });
93
93