@zhushanwen/pi-evolve-daily 0.1.2 → 0.1.4
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 +1 -1
- package/skills/evolve/SKILL.md +31 -0
- package/skills/evolve-report/SKILL.md +52 -0
- package/src/detectors/compact.ts +43 -0
- package/src/detectors/goal-quality.ts +59 -0
- package/src/detectors/param-error.ts +90 -0
- package/src/detectors/subagent-result.ts +65 -0
- package/src/index.ts +98 -4
- package/src/problems.ts +232 -0
- package/src/trackers/core.ts +459 -0
- package/src/trackers/run_tests.mjs +202 -0
- package/src/trackers/skill-execution.ts +126 -0
- package/src/trackers/types.ts +176 -0
package/src/problems.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// packages/evolve-daily/src/problems.ts
|
|
2
|
+
|
|
3
|
+
export interface ProblemDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
category: "skill" | "tool" | "user" | "workflow" | "context" | "subagent";
|
|
7
|
+
severity: SeverityRule;
|
|
8
|
+
detector: DetectorConfig;
|
|
9
|
+
analysis: AnalysisConfig;
|
|
10
|
+
suggestion: SuggestionTemplate;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SeverityRule {
|
|
14
|
+
metric: "error_count" | "frequency" | "rate" | "custom";
|
|
15
|
+
thresholds?: { medium: number; high: number };
|
|
16
|
+
custom?: (data: Record<string, unknown>) => "low" | "medium" | "high";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DetectorConfig {
|
|
20
|
+
events: Array<"tool_call" | "tool_result" | "user_message" | "turn_end" | "message_end">;
|
|
21
|
+
match: MatchCondition;
|
|
22
|
+
template: Partial<TrackedItemTemplate>;
|
|
23
|
+
steering: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MatchCondition {
|
|
27
|
+
eventType?: string;
|
|
28
|
+
toolName?: string | string[];
|
|
29
|
+
pathPattern?: string;
|
|
30
|
+
isError?: boolean;
|
|
31
|
+
contentRegex?: string;
|
|
32
|
+
custom?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnalysisConfig {
|
|
36
|
+
extractor: string;
|
|
37
|
+
minerRules: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SuggestionTemplate {
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
defaultSeverity: "low" | "medium" | "high";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Minimal template shape for ProblemDefinition.detector.template */
|
|
47
|
+
interface TrackedItemTemplate {
|
|
48
|
+
category: string;
|
|
49
|
+
status: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
53
|
+
{
|
|
54
|
+
id: "compact-frequency",
|
|
55
|
+
name: "Compact 频率",
|
|
56
|
+
category: "context",
|
|
57
|
+
severity: {
|
|
58
|
+
metric: "custom",
|
|
59
|
+
custom: (data) => {
|
|
60
|
+
const rate = data.compactsPerSession as number;
|
|
61
|
+
if (rate >= 3) return "high";
|
|
62
|
+
if (rate >= 2) return "medium";
|
|
63
|
+
return "low";
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
detector: {
|
|
67
|
+
events: ["message_end"],
|
|
68
|
+
match: { custom: "compactDetector" },
|
|
69
|
+
template: { category: "context-pressure" },
|
|
70
|
+
steering:
|
|
71
|
+
"检测到 Compact 触发(id={{id}})。请评估是否丢失了关键上下文。如有丢失,update status=error, detail='丢失的内容'。如无影响,update status=completed。",
|
|
72
|
+
},
|
|
73
|
+
analysis: {
|
|
74
|
+
extractor: "compact",
|
|
75
|
+
minerRules: ["compact-high-frequency", "compact-early-trigger"],
|
|
76
|
+
},
|
|
77
|
+
suggestion: {
|
|
78
|
+
title: "优化 Compact 频率",
|
|
79
|
+
description: "Compact 频率过高,说明上下文管理效率低",
|
|
80
|
+
defaultSeverity: "medium",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "context-utilization",
|
|
85
|
+
name: "上下文窗口利用率",
|
|
86
|
+
category: "context",
|
|
87
|
+
severity: {
|
|
88
|
+
metric: "rate",
|
|
89
|
+
thresholds: { medium: 0.7, high: 0.9 },
|
|
90
|
+
},
|
|
91
|
+
detector: {
|
|
92
|
+
events: ["turn_end"],
|
|
93
|
+
match: { custom: "contextUtilizationMatcher" },
|
|
94
|
+
template: { category: "context-pressure" },
|
|
95
|
+
steering:
|
|
96
|
+
"当前上下文利用率 {{usageRate}}(id={{id}})。如接近上限,update status=completed, detail='需要 compact'。如充足,update status=dismissed。",
|
|
97
|
+
},
|
|
98
|
+
analysis: {
|
|
99
|
+
extractor: "context",
|
|
100
|
+
minerRules: ["context-high-utilization"],
|
|
101
|
+
},
|
|
102
|
+
suggestion: {
|
|
103
|
+
title: "优化上下文利用率",
|
|
104
|
+
description: "上下文利用率持续偏高,会触发频繁 compact",
|
|
105
|
+
defaultSeverity: "medium",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "subagent-efficiency",
|
|
110
|
+
name: "Subagent 调度效率",
|
|
111
|
+
category: "subagent",
|
|
112
|
+
severity: {
|
|
113
|
+
metric: "rate",
|
|
114
|
+
thresholds: { medium: 0.2, high: 0.4 },
|
|
115
|
+
},
|
|
116
|
+
detector: {
|
|
117
|
+
events: ["tool_result"],
|
|
118
|
+
match: { toolName: "subagent", custom: "subagentResultMatcher" },
|
|
119
|
+
template: { category: "subagent" },
|
|
120
|
+
steering:
|
|
121
|
+
"Subagent 任务完成(id={{id}})。exitCode={{exitCode}}, 耗时={{duration}}。如结果满意,update status=completed。如需重做,update status=error, detail='问题原因'。",
|
|
122
|
+
},
|
|
123
|
+
analysis: {
|
|
124
|
+
extractor: "subagent",
|
|
125
|
+
minerRules: ["subagent-failure-rate", "subagent-high-retry"],
|
|
126
|
+
},
|
|
127
|
+
suggestion: {
|
|
128
|
+
title: "优化 Subagent 调度效率",
|
|
129
|
+
description: "Subagent 失败率或重试率过高",
|
|
130
|
+
defaultSeverity: "medium",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "tool-param-validation",
|
|
135
|
+
name: "工具参数校验失败",
|
|
136
|
+
category: "tool",
|
|
137
|
+
severity: {
|
|
138
|
+
metric: "rate",
|
|
139
|
+
thresholds: { medium: 0.1, high: 0.25 },
|
|
140
|
+
},
|
|
141
|
+
detector: {
|
|
142
|
+
events: ["tool_result"],
|
|
143
|
+
match: { isError: true, custom: "paramErrorMatcher" },
|
|
144
|
+
template: { category: "tool-error" },
|
|
145
|
+
steering:
|
|
146
|
+
"检测到 {{toolName}} 参数错误(id={{id}})。错误: {{errorPreview}}。如已理解原因,update status=completed, detail='错误原因和修正方式'。如不确定,update status=error。",
|
|
147
|
+
},
|
|
148
|
+
analysis: {
|
|
149
|
+
extractor: "tool_errors",
|
|
150
|
+
minerRules: ["param-error-rate", "edit-match-failure", "low-self-correction"],
|
|
151
|
+
},
|
|
152
|
+
suggestion: {
|
|
153
|
+
title: "降低工具参数错误率",
|
|
154
|
+
description: "参数错误率高,说明 AI 不理解工具用法",
|
|
155
|
+
defaultSeverity: "high",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "workflow-phase-duration",
|
|
160
|
+
name: "工作流阶段耗时",
|
|
161
|
+
category: "workflow",
|
|
162
|
+
severity: {
|
|
163
|
+
metric: "custom",
|
|
164
|
+
custom: (data) => {
|
|
165
|
+
const maxPhaseRatio = data.maxPhaseDurationRatio as number;
|
|
166
|
+
if (maxPhaseRatio > 0.7) return "high";
|
|
167
|
+
if (maxPhaseRatio > 0.5) return "medium";
|
|
168
|
+
return "low";
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
detector: {
|
|
172
|
+
events: ["tool_result"],
|
|
173
|
+
match: { toolName: ["coding-workflow-gate", "coding-workflow-phase-start"] },
|
|
174
|
+
template: { category: "workflow" },
|
|
175
|
+
steering:
|
|
176
|
+
"工作流阶段 {{phase}} 完成(id={{id}})。gate={{gateResult}}, 耗时={{duration}}。如阶段顺利,update status=completed。如有问题,update status=error, detail='问题描述'。",
|
|
177
|
+
},
|
|
178
|
+
analysis: {
|
|
179
|
+
extractor: "workflow",
|
|
180
|
+
minerRules: ["workflow-slow-phase", "workflow-gate-retry"],
|
|
181
|
+
},
|
|
182
|
+
suggestion: {
|
|
183
|
+
title: "优化工作流阶段效率",
|
|
184
|
+
description: "某阶段耗时占比过高或 gate 重试频繁",
|
|
185
|
+
defaultSeverity: "medium",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "goal-task-quality",
|
|
190
|
+
name: "Goal 任务拆分质量",
|
|
191
|
+
category: "workflow",
|
|
192
|
+
severity: {
|
|
193
|
+
metric: "custom",
|
|
194
|
+
custom: (data) => {
|
|
195
|
+
const completionRate = data.taskCompletionRate as number;
|
|
196
|
+
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";
|
|
199
|
+
return "low";
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
detector: {
|
|
203
|
+
events: ["tool_result"],
|
|
204
|
+
match: { toolName: "goal_manager", custom: "goalQualityMatcher" },
|
|
205
|
+
template: { category: "workflow" },
|
|
206
|
+
steering:
|
|
207
|
+
"Goal 任务更新(id={{id}})。任务完成率={{completionRate}}。如目标达成,update status=completed, detail='目标完成情况'。如遇到困难,update status=error, detail='困难描述'。",
|
|
208
|
+
},
|
|
209
|
+
analysis: {
|
|
210
|
+
extractor: "goal_quality",
|
|
211
|
+
minerRules: [
|
|
212
|
+
"goal-low-completion",
|
|
213
|
+
"goal-low-evidence",
|
|
214
|
+
"goal-stall-frequent",
|
|
215
|
+
"todo-high-abandon",
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
suggestion: {
|
|
219
|
+
title: "优化 Goal 任务拆分质量",
|
|
220
|
+
description: "任务完成率低或 Evidence 质量低",
|
|
221
|
+
defaultSeverity: "high",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
export function getProblemById(id: string): ProblemDefinition | undefined {
|
|
227
|
+
return PROBLEM_REGISTRY.find((p) => p.id === id);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function getProblemsByCategory(category: string): ProblemDefinition[] {
|
|
231
|
+
return PROBLEM_REGISTRY.filter((p) => p.category === category);
|
|
232
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Tracker Framework — createTracker 工厂函数
|
|
3
|
+
*
|
|
4
|
+
* 封装所有样板逻辑:事件注册、工具注册、状态持久化、
|
|
5
|
+
* steering 注入、GC、remind。在扩展工厂闭包内调用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CustomEntry,
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
SessionEntry,
|
|
13
|
+
Theme,
|
|
14
|
+
} from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
canTransition,
|
|
20
|
+
createInitialState,
|
|
21
|
+
deserializeState,
|
|
22
|
+
isTerminalStatus,
|
|
23
|
+
serializeState,
|
|
24
|
+
TrackerParams,
|
|
25
|
+
|
|
26
|
+
type TrackedItem,
|
|
27
|
+
type TrackerDetails,
|
|
28
|
+
type TrackerRuntimeState,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
// ── Tracker 配置接口(避免 types.ts 引入 Pi API 类型)──
|
|
32
|
+
|
|
33
|
+
export interface TrackerConfig<TMeta = Record<string, unknown>> {
|
|
34
|
+
name: string;
|
|
35
|
+
toolName: string;
|
|
36
|
+
label: string;
|
|
37
|
+
description: string;
|
|
38
|
+
promptSnippet: string;
|
|
39
|
+
promptGuidelines: string[];
|
|
40
|
+
triggerEvent: string;
|
|
41
|
+
triggerMatch: (
|
|
42
|
+
event: unknown,
|
|
43
|
+
) => { name: string; metadata: TMeta; summary: string } | null;
|
|
44
|
+
steering: {
|
|
45
|
+
onCreate: (item: TrackedItem<TMeta>) => string;
|
|
46
|
+
onRemind: (item: TrackedItem<TMeta>, turnsSinceLoad: number) => string;
|
|
47
|
+
onError: (item: TrackedItem<TMeta>) => string;
|
|
48
|
+
onContextRestore: (items: TrackedItem<TMeta>[]) => string;
|
|
49
|
+
};
|
|
50
|
+
entryType: string;
|
|
51
|
+
/** 旧版 entryType(用于向后兼容反序列化) */
|
|
52
|
+
legacyEntryTypes?: string[];
|
|
53
|
+
messageTypes: string[];
|
|
54
|
+
remindInterval: number;
|
|
55
|
+
errorThreshold: number;
|
|
56
|
+
renderResult?: (
|
|
57
|
+
details: TrackerDetails<TMeta>,
|
|
58
|
+
options: { expanded?: boolean },
|
|
59
|
+
theme: Theme,
|
|
60
|
+
) => Text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 类型守卫 ────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function isCustomEntry(
|
|
66
|
+
entry: SessionEntry,
|
|
67
|
+
customType: string,
|
|
68
|
+
): boolean {
|
|
69
|
+
return (
|
|
70
|
+
entry.type === "custom" &&
|
|
71
|
+
(entry as CustomEntry).customType === customType
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── 格式化 ──────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function formatItemList<TMeta>(
|
|
78
|
+
items: TrackedItem<TMeta>[],
|
|
79
|
+
trackerName: string,
|
|
80
|
+
): string {
|
|
81
|
+
if (items.length === 0) return `无活跃追踪(${trackerName})。`;
|
|
82
|
+
return items
|
|
83
|
+
.map(
|
|
84
|
+
(item) =>
|
|
85
|
+
`#${item.id} "${item.name}" status=${item.status} errorCount=${item.errorCount}` +
|
|
86
|
+
` loadedAtTurn=${item.loadedAtTurn}` +
|
|
87
|
+
(item.detail ? ` detail="${item.detail}"` : ""),
|
|
88
|
+
)
|
|
89
|
+
.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 工厂函数 ────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export function createTracker<TMeta>(
|
|
95
|
+
pi: ExtensionAPI,
|
|
96
|
+
config: TrackerConfig<TMeta>,
|
|
97
|
+
): void {
|
|
98
|
+
let state: TrackerRuntimeState<TMeta> = createInitialState<TMeta>();
|
|
99
|
+
|
|
100
|
+
// ── 持久化 + GC ───────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function persistState(ctx: ExtensionContext): void {
|
|
103
|
+
pi.appendEntry(config.entryType, serializeState(state));
|
|
104
|
+
const entries = ctx.sessionManager.getEntries();
|
|
105
|
+
const staleIndices: number[] = [];
|
|
106
|
+
let foundLatest = false;
|
|
107
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
108
|
+
if (isCustomEntry(entries[i], config.entryType)) {
|
|
109
|
+
if (!foundLatest) {
|
|
110
|
+
foundLatest = true;
|
|
111
|
+
} else {
|
|
112
|
+
staleIndices.push(i);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const idx of staleIndices) {
|
|
117
|
+
entries.splice(idx, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── 状态恢复 ──────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function reconstructState(ctx: ExtensionContext): void {
|
|
124
|
+
const entries = ctx.sessionManager.getEntries();
|
|
125
|
+
const allTypes = [config.entryType, ...(config.legacyEntryTypes ?? [])];
|
|
126
|
+
|
|
127
|
+
let latestData: Record<string, unknown> | undefined;
|
|
128
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
129
|
+
for (const et of allTypes) {
|
|
130
|
+
if (isCustomEntry(entries[i], et)) {
|
|
131
|
+
latestData = (entries[i] as CustomEntry).data as Record<
|
|
132
|
+
string,
|
|
133
|
+
unknown
|
|
134
|
+
>;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (latestData) break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!latestData) {
|
|
142
|
+
state = createInitialState<TMeta>();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
state = deserializeState<TMeta>(latestData);
|
|
147
|
+
// 过滤终态 item
|
|
148
|
+
state.items = state.items.filter(
|
|
149
|
+
(item) => !isTerminalStatus(item.status),
|
|
150
|
+
);
|
|
151
|
+
// 恢复 currentTurnIndex
|
|
152
|
+
let turnCount = 0;
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.type === "custom_message" || entry.type === "message") {
|
|
155
|
+
turnCount++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
state.currentTurnIndex = turnCount;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Event: session_start / session_tree ────────────
|
|
162
|
+
|
|
163
|
+
const handleSessionRestore = async (
|
|
164
|
+
_event: unknown,
|
|
165
|
+
ctx: ExtensionContext,
|
|
166
|
+
): Promise<void> => {
|
|
167
|
+
reconstructState(ctx);
|
|
168
|
+
const activeItems = state.items.filter(
|
|
169
|
+
(item) => !isTerminalStatus(item.status),
|
|
170
|
+
);
|
|
171
|
+
if (activeItems.length > 0) {
|
|
172
|
+
await pi.sendUserMessage(
|
|
173
|
+
config.steering.onContextRestore(activeItems),
|
|
174
|
+
{ deliverAs: "steer" },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
pi.on("session_start", handleSessionRestore);
|
|
179
|
+
pi.on("session_tree", handleSessionRestore);
|
|
180
|
+
|
|
181
|
+
// ── Event: triggerEvent (e.g. tool_call) ───────────
|
|
182
|
+
|
|
183
|
+
// Pi 事件系统支持任意字符串事件名,但类型定义不完整(与 session_compact 同)
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
185
|
+
(pi as any).on(
|
|
186
|
+
config.triggerEvent,
|
|
187
|
+
async (event: unknown, ctx: ExtensionContext) => {
|
|
188
|
+
const match = config.triggerMatch(event);
|
|
189
|
+
if (!match) return;
|
|
190
|
+
|
|
191
|
+
// 去重:非终态同名 item 存在时不重复创建
|
|
192
|
+
const existing = state.items.find(
|
|
193
|
+
(item) =>
|
|
194
|
+
item.name === match.name && !isTerminalStatus(item.status),
|
|
195
|
+
);
|
|
196
|
+
if (existing) return;
|
|
197
|
+
|
|
198
|
+
const turnIndex = state.currentTurnIndex;
|
|
199
|
+
const newItem: TrackedItem<TMeta> = {
|
|
200
|
+
id: state.nextId,
|
|
201
|
+
name: match.name,
|
|
202
|
+
status: "loaded",
|
|
203
|
+
errorCount: 0,
|
|
204
|
+
loadedAtTurn: turnIndex,
|
|
205
|
+
lastRemindAtTurn: -1,
|
|
206
|
+
detail: null,
|
|
207
|
+
metadata: match.metadata,
|
|
208
|
+
anchor: {
|
|
209
|
+
triggerType: config.triggerEvent,
|
|
210
|
+
triggerTurn: turnIndex,
|
|
211
|
+
triggerSummary: match.summary,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
state.items.push(newItem);
|
|
215
|
+
state.nextId++;
|
|
216
|
+
|
|
217
|
+
persistState(ctx);
|
|
218
|
+
await pi.sendUserMessage(config.steering.onCreate(newItem), {
|
|
219
|
+
deliverAs: "steer",
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// ── Event: turn_end(remind 检查)─────────────────
|
|
225
|
+
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
227
|
+
(pi as any).on(
|
|
228
|
+
"turn_end",
|
|
229
|
+
async (event: Record<string, unknown>, ctx: ExtensionContext) => {
|
|
230
|
+
const eventTurnIndex = event.turnIndex;
|
|
231
|
+
if (typeof eventTurnIndex === "number") {
|
|
232
|
+
state.currentTurnIndex = eventTurnIndex;
|
|
233
|
+
} else {
|
|
234
|
+
state.currentTurnIndex++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let needsPersist = false;
|
|
238
|
+
for (const item of state.items) {
|
|
239
|
+
if (isTerminalStatus(item.status)) continue;
|
|
240
|
+
|
|
241
|
+
const turnsSinceLoad =
|
|
242
|
+
state.currentTurnIndex - item.loadedAtTurn;
|
|
243
|
+
const turnsSinceRemind =
|
|
244
|
+
state.currentTurnIndex - item.lastRemindAtTurn;
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
turnsSinceLoad >= config.remindInterval &&
|
|
248
|
+
turnsSinceRemind >= config.remindInterval
|
|
249
|
+
) {
|
|
250
|
+
await pi.sendUserMessage(
|
|
251
|
+
config.steering.onRemind(item, turnsSinceLoad),
|
|
252
|
+
{ deliverAs: "steer" },
|
|
253
|
+
);
|
|
254
|
+
item.lastRemindAtTurn = state.currentTurnIndex;
|
|
255
|
+
needsPersist = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (needsPersist) {
|
|
260
|
+
persistState(ctx);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// ── Event: before_agent_start ──────────────────────
|
|
266
|
+
|
|
267
|
+
pi.on("before_agent_start", async () => {
|
|
268
|
+
const activeItems = state.items.filter(
|
|
269
|
+
(item) => !isTerminalStatus(item.status),
|
|
270
|
+
);
|
|
271
|
+
if (activeItems.length === 0) return undefined;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
message: {
|
|
275
|
+
customType: `${config.entryType}-context`,
|
|
276
|
+
content: config.steering.onContextRestore(activeItems),
|
|
277
|
+
display: false,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ── Message Renderers ──────────────────────────────
|
|
283
|
+
|
|
284
|
+
for (const customType of config.messageTypes) {
|
|
285
|
+
pi.registerMessageRenderer(
|
|
286
|
+
customType,
|
|
287
|
+
(
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
+
message: any,
|
|
290
|
+
_options: unknown,
|
|
291
|
+
theme: Theme,
|
|
292
|
+
) => {
|
|
293
|
+
const content =
|
|
294
|
+
typeof message.content === "string"
|
|
295
|
+
? message.content
|
|
296
|
+
: JSON.stringify(message.content);
|
|
297
|
+
return new Text(
|
|
298
|
+
theme.fg("accent", `[${config.name}] `) +
|
|
299
|
+
theme.fg("dim", content),
|
|
300
|
+
0,
|
|
301
|
+
0,
|
|
302
|
+
);
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Tool Registration ──────────────────────────────
|
|
308
|
+
|
|
309
|
+
pi.registerTool({
|
|
310
|
+
name: config.toolName,
|
|
311
|
+
label: config.label,
|
|
312
|
+
description: config.description,
|
|
313
|
+
promptGuidelines: config.promptGuidelines,
|
|
314
|
+
parameters: TrackerParams,
|
|
315
|
+
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
317
|
+
async execute(
|
|
318
|
+
_toolCallId: string,
|
|
319
|
+
params: any,
|
|
320
|
+
_signal: any,
|
|
321
|
+
_onUpdate: any,
|
|
322
|
+
ctx: ExtensionContext,
|
|
323
|
+
): Promise<any> {
|
|
324
|
+
// ── list ──
|
|
325
|
+
if (params.action === "list") {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text" as const,
|
|
330
|
+
text: formatItemList(state.items, config.name),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
details: {
|
|
334
|
+
action: "list",
|
|
335
|
+
items: [...state.items],
|
|
336
|
+
trackerName: config.name,
|
|
337
|
+
} satisfies TrackerDetails<TMeta>,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── update ──
|
|
342
|
+
if (params.id === undefined) {
|
|
343
|
+
throw new Error("update 操作需要 id 参数");
|
|
344
|
+
}
|
|
345
|
+
if (params.status === undefined) {
|
|
346
|
+
throw new Error("update 操作需要 status 参数");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const itemIndex = state.items.findIndex(
|
|
350
|
+
(item) => item.id === params.id,
|
|
351
|
+
);
|
|
352
|
+
if (itemIndex === -1) {
|
|
353
|
+
throw new Error(`TrackedItem id=${params.id} 不存在`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const item = state.items[itemIndex];
|
|
357
|
+
if (!canTransition(item.status, params.status)) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`非法转换: ${item.status} → ${params.status}(当前 ${item.status},终态不可变更或该路径不允许)`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 执行转换
|
|
364
|
+
item.status = params.status;
|
|
365
|
+
item.detail = params.detail ?? item.detail;
|
|
366
|
+
|
|
367
|
+
if (params.status === "error") {
|
|
368
|
+
item.errorCount += 1;
|
|
369
|
+
if (item.errorCount >= config.errorThreshold) {
|
|
370
|
+
await pi.sendUserMessage(config.steering.onError(item), {
|
|
371
|
+
deliverAs: "steer",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
persistState(ctx);
|
|
377
|
+
|
|
378
|
+
const statusText = isTerminalStatus(item.status) ? "(终态)" : "";
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: "text" as const,
|
|
383
|
+
text: `TrackedItem #${item.id} "${item.name}" → ${item.status}${statusText}`,
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
details: {
|
|
387
|
+
action: "update",
|
|
388
|
+
items: [...state.items],
|
|
389
|
+
trackerName: config.name,
|
|
390
|
+
updatedId: item.id,
|
|
391
|
+
} satisfies TrackerDetails<TMeta>,
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
396
|
+
renderCall(
|
|
397
|
+
args: any,
|
|
398
|
+
theme: Theme,
|
|
399
|
+
_context?: unknown,
|
|
400
|
+
) {
|
|
401
|
+
const parts = [
|
|
402
|
+
theme.fg("toolTitle", theme.bold(`${config.toolName} `)),
|
|
403
|
+
theme.fg("muted", String(args.action ?? "")),
|
|
404
|
+
];
|
|
405
|
+
if (args.id !== undefined)
|
|
406
|
+
parts.push(theme.fg("accent", `#${String(args.id)}`));
|
|
407
|
+
if (args.status !== undefined)
|
|
408
|
+
parts.push(theme.fg("warning", String(args.status)));
|
|
409
|
+
if (args.detail !== undefined)
|
|
410
|
+
parts.push(theme.fg("dim", `"${String(args.detail)}"`));
|
|
411
|
+
return new Text(parts.join(" "), 0, 0);
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
415
|
+
renderResult(
|
|
416
|
+
result: any,
|
|
417
|
+
options: any,
|
|
418
|
+
theme: Theme,
|
|
419
|
+
_context?: unknown,
|
|
420
|
+
) {
|
|
421
|
+
if (config.renderResult) {
|
|
422
|
+
return config.renderResult(
|
|
423
|
+
result.details as TrackerDetails<TMeta>,
|
|
424
|
+
options,
|
|
425
|
+
theme,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 框架默认渲染
|
|
430
|
+
const details = result.details as TrackerDetails<TMeta>;
|
|
431
|
+
if (details.error) {
|
|
432
|
+
return new Text(
|
|
433
|
+
theme.fg("error", `[${config.name}] 错误: ${details.error}`),
|
|
434
|
+
0,
|
|
435
|
+
0,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const prefix = theme.fg("accent", `[${config.name}] `);
|
|
440
|
+
const summary = `${details.action}: ${details.items.length} items`;
|
|
441
|
+
|
|
442
|
+
if (!options.expanded) {
|
|
443
|
+
return new Text(prefix + theme.fg("dim", summary), 0, 0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const items = details.items
|
|
447
|
+
.map((item) => {
|
|
448
|
+
const terminal = isTerminalStatus(item.status) ? " ✓" : "";
|
|
449
|
+
return ` #${item.id} ${item.name} [${item.status}]${terminal}`;
|
|
450
|
+
})
|
|
451
|
+
.join("\n");
|
|
452
|
+
return new Text(
|
|
453
|
+
prefix + summary + "\n" + theme.fg("dim", items),
|
|
454
|
+
0,
|
|
455
|
+
0,
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|