@zhushanwen/pi-evolve-daily 0.1.7 → 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 +2 -1
- package/src/detectors/compact.ts +7 -1
- package/src/detectors/goal-quality.ts +7 -1
- package/src/detectors/param-error.ts +9 -2
- package/src/detectors/subagent-result.ts +7 -1
- package/src/index.ts +25 -13
- package/src/problems.ts +26 -9
- package/src/trackers/core.ts +160 -82
- package/src/trackers/skill-execution.ts +1 -1
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-evolve-daily",
|
|
3
|
-
"version": "0.1.
|
|
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": [
|
package/src/detectors/compact.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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 {
|
|
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,
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
114
|
+
(pi as unknown as PiOnAny).on(
|
|
105
115
|
"tool_result",
|
|
106
|
-
async (event:
|
|
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(
|
|
110
|
-
const item = detector.createItem(
|
|
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,6 +49,23 @@ 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",
|
|
@@ -58,8 +75,8 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
58
75
|
metric: "custom",
|
|
59
76
|
custom: (data) => {
|
|
60
77
|
const rate = data.compactsPerSession as number;
|
|
61
|
-
if (rate >=
|
|
62
|
-
if (rate >=
|
|
78
|
+
if (rate >= COMPACT_HIGH_THRESHOLD) return "high";
|
|
79
|
+
if (rate >= COMPACT_MEDIUM_THRESHOLD) return "medium";
|
|
63
80
|
return "low";
|
|
64
81
|
},
|
|
65
82
|
},
|
|
@@ -86,7 +103,7 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
86
103
|
category: "context",
|
|
87
104
|
severity: {
|
|
88
105
|
metric: "rate",
|
|
89
|
-
thresholds: { medium:
|
|
106
|
+
thresholds: { medium: CONTEXT_UTILIZATION_MEDIUM, high: CONTEXT_UTILIZATION_HIGH },
|
|
90
107
|
},
|
|
91
108
|
detector: {
|
|
92
109
|
events: ["turn_end"],
|
|
@@ -111,7 +128,7 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
111
128
|
category: "subagent",
|
|
112
129
|
severity: {
|
|
113
130
|
metric: "rate",
|
|
114
|
-
thresholds: { medium:
|
|
131
|
+
thresholds: { medium: SUBAGENT_FAILURE_MEDIUM, high: SUBAGENT_FAILURE_HIGH },
|
|
115
132
|
},
|
|
116
133
|
detector: {
|
|
117
134
|
events: ["tool_result"],
|
|
@@ -136,7 +153,7 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
136
153
|
category: "tool",
|
|
137
154
|
severity: {
|
|
138
155
|
metric: "rate",
|
|
139
|
-
thresholds: { medium:
|
|
156
|
+
thresholds: { medium: PARAM_ERROR_MEDIUM, high: PARAM_ERROR_HIGH },
|
|
140
157
|
},
|
|
141
158
|
detector: {
|
|
142
159
|
events: ["tool_result"],
|
|
@@ -163,8 +180,8 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
163
180
|
metric: "custom",
|
|
164
181
|
custom: (data) => {
|
|
165
182
|
const maxPhaseRatio = data.maxPhaseDurationRatio as number;
|
|
166
|
-
if (maxPhaseRatio >
|
|
167
|
-
if (maxPhaseRatio >
|
|
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
|
},
|
|
@@ -194,8 +211,8 @@ export const PROBLEM_REGISTRY: ProblemDefinition[] = [
|
|
|
194
211
|
custom: (data) => {
|
|
195
212
|
const completionRate = data.taskCompletionRate as number;
|
|
196
213
|
const cancelRate = data.taskCancelRate as number;
|
|
197
|
-
if (completionRate <
|
|
198
|
-
if (completionRate <
|
|
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
|
},
|
package/src/trackers/core.ts
CHANGED
|
@@ -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>> {
|
|
@@ -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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
(pi as any).on(
|
|
308
|
+
(pi as unknown as PiOnAny).on(
|
|
186
309
|
config.triggerEvent,
|
|
187
|
-
async (event
|
|
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
|
-
|
|
227
|
-
(pi as any).on(
|
|
350
|
+
(pi as unknown as PiOnAny).on(
|
|
228
351
|
"turn_end",
|
|
229
|
-
async (
|
|
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
|
-
|
|
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:
|
|
320
|
-
_signal:
|
|
321
|
-
_onUpdate:
|
|
442
|
+
params: Static<typeof TrackerParams>,
|
|
443
|
+
_signal: AbortSignal | undefined,
|
|
444
|
+
_onUpdate: unknown,
|
|
322
445
|
ctx: ExtensionContext,
|
|
323
|
-
): Promise<
|
|
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
|
-
|
|
343
|
-
|
|
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 (
|
|
346
|
-
return { content: [{ type: "text", text: "update action requires status parameter" }], 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 ===
|
|
475
|
+
(item) => item.id === updateId,
|
|
351
476
|
);
|
|
352
477
|
if (itemIndex === -1) {
|
|
353
|
-
return { content: [{ type: "text", text: `TrackedItem id=${
|
|
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,
|
|
358
|
-
return { content: [{ type: "text", text: `Invalid transition: ${item.status} → ${
|
|
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 =
|
|
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 (
|
|
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), {
|
|
@@ -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:
|
|
519
|
+
args: Record<string, unknown>,
|
|
396
520
|
theme: Theme,
|
|
397
521
|
_context?: unknown,
|
|
398
522
|
) {
|
|
399
|
-
|
|
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:
|
|
415
|
-
|
|
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
|
-
|
|
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}] Error: ${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
|
}
|