pi-langfuse 1.5.19 → 1.6.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.
- package/README.md +6 -0
- package/README_CN.md +6 -0
- package/index.ts +52 -49
- package/package.json +3 -3
- package/src/handlers/agent.ts +102 -1
- package/src/handlers/cache.ts +147 -0
- package/src/handlers/generation.ts +39 -2
- package/src/handlers/session.ts +90 -0
- package/src/handlers/system-state.ts +186 -0
- package/src/handlers/tool.ts +1 -1
- package/src/handlers/turn.ts +1 -1
- package/src/observation.ts +1 -1
- package/src/state.ts +18 -3
- package/src/types.ts +13 -2
- package/src/utils.ts +75 -0
- package/types/langfuse-runtime-shims.d.ts +0 -49
- package/types/node-shims.d.ts +0 -29
- package/types/pi-coding-agent.d.ts +0 -16
package/README.md
CHANGED
|
@@ -269,6 +269,12 @@ This command makes a timeout-bounded authenticated request to Langfuse and, if i
|
|
|
269
269
|
- Tool runs appear as tool observations with arguments, results, and error state.
|
|
270
270
|
- LLM requests appear as generation observations, including usage and cost when the provider exposes them.
|
|
271
271
|
Reasoning tokens are reported as their own usage bucket when `PI_LANGFUSE_SPLIT_REASONING_TOKENS` is enabled.
|
|
272
|
+
- Pi 0.86 transcript-aware prompt/tool state appears as `system-state` events. Generations reference the
|
|
273
|
+
effective prompt/tool state hashes, active tool count, update transport, and cache hit ratio.
|
|
274
|
+
- Automatic retries are grouped under `agent-attempt` spans and the trace remains open until `agent_settled`.
|
|
275
|
+
- Cache-warming decisions and persisted `cache_warm` usage are reported, including standalone session-grouped
|
|
276
|
+
traces when warming happens while no user-request trace is active.
|
|
277
|
+
- Compaction is recorded as `session-compaction`, with a nested `compaction-summary` generation when usage is available.
|
|
272
278
|
- Trace-level scores include tool counts, tool success rate, and whether the run had errors.
|
|
273
279
|
|
|
274
280
|
The package also includes a Langfuse CLI skill, so Langfuse data can be queried directly from Pi:
|
package/README_CN.md
CHANGED
|
@@ -250,6 +250,12 @@ pi list
|
|
|
250
250
|
- 工具执行会以工具观察节点展示参数、结果和错误状态。
|
|
251
251
|
- 模型请求会以生成观察节点展示;如果提供商暴露相关信息,还会包含用量和成本。
|
|
252
252
|
开启 `PI_LANGFUSE_SPLIT_REASONING_TOKENS` 后,推理 token 会作为独立用量桶上报。
|
|
253
|
+
- Pi 0.86 的 transcript-aware system/tool 状态会作为 `system-state` 事件上报;generation 会引用有效的
|
|
254
|
+
prompt/tool 状态指纹、活动工具数、更新传输方式和缓存命中率。
|
|
255
|
+
- 自动重试会归入 `agent-attempt` span,trace 会持续到 `agent_settled` 才结束。
|
|
256
|
+
- cache warming 决策及持久化的 `cache_warm` 用量会被记录;若 warming 发生时没有活动的用户请求 trace,
|
|
257
|
+
会创建按相同 session 分组的独立 trace。
|
|
258
|
+
- compaction 会记录为 `session-compaction`;若存在摘要用量,则包含 `compaction-summary` generation。
|
|
253
259
|
- trace 级别会记录工具调用次数、工具成功率和是否出现错误。
|
|
254
260
|
|
|
255
261
|
此包还包含一个内置 Langfuse 技能,可直接在 Pi 中查询 Langfuse 数据:
|
package/index.ts
CHANGED
|
@@ -14,10 +14,23 @@ import { state, resetRunState, runWithSession, setCurrentSession } from "./src/s
|
|
|
14
14
|
import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
|
|
15
15
|
import { shutdownRuntime } from "./src/langfuse.js";
|
|
16
16
|
import { handleLangfusePrivacyCommand, handleLangfuseStatusCommand, handleLangfuseTestCommand } from "./src/commands.js";
|
|
17
|
-
import { getMessageFromEvent, extractAssistantOutput
|
|
18
|
-
import {
|
|
19
|
-
|
|
17
|
+
import { getMessageFromEvent, extractAssistantOutput } from "./src/utils.js";
|
|
18
|
+
import {
|
|
19
|
+
startAgentRun,
|
|
20
|
+
finishAgentRun,
|
|
21
|
+
finishAgentAttempt,
|
|
22
|
+
cancelAgentRun,
|
|
23
|
+
recordSystemPrompt,
|
|
24
|
+
startAgentAttempt,
|
|
25
|
+
} from "./src/handlers/agent.js";
|
|
20
26
|
import { startTurnObservation, finishTurnObservation } from "./src/handlers/turn.js";
|
|
27
|
+
import { initializeSystemStateTracking, recordSystemState } from "./src/handlers/system-state.js";
|
|
28
|
+
import {
|
|
29
|
+
initializeUsageTracking,
|
|
30
|
+
recordCacheWarmingDecision,
|
|
31
|
+
recordNewUsageEntries,
|
|
32
|
+
} from "./src/handlers/cache.js";
|
|
33
|
+
import { recordSessionCompaction } from "./src/handlers/session.js";
|
|
21
34
|
import {
|
|
22
35
|
startGeneration,
|
|
23
36
|
updateGenerationMetadata,
|
|
@@ -28,7 +41,6 @@ import {
|
|
|
28
41
|
import {
|
|
29
42
|
startToolObservation,
|
|
30
43
|
finishToolObservation,
|
|
31
|
-
closeDanglingObservations,
|
|
32
44
|
} from "./src/handlers/tool.js";
|
|
33
45
|
|
|
34
46
|
// ============================================
|
|
@@ -36,6 +48,7 @@ import {
|
|
|
36
48
|
// ============================================
|
|
37
49
|
|
|
38
50
|
export default async function (pi: ExtensionAPI) {
|
|
51
|
+
const asRecord = (value: object): Record<string, unknown> => value as unknown as Record<string, unknown>;
|
|
39
52
|
if (!state.config) {
|
|
40
53
|
state.config = loadConfig();
|
|
41
54
|
}
|
|
@@ -97,6 +110,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
97
110
|
state.setupAttemptedThisSession = false;
|
|
98
111
|
await ensureConfig(ctx);
|
|
99
112
|
resetRunState();
|
|
113
|
+
initializeSystemStateTracking(ctx);
|
|
114
|
+
initializeUsageTracking(ctx);
|
|
100
115
|
}));
|
|
101
116
|
|
|
102
117
|
pi.on("model_select", async (event, ctx) => withSession(ctx, async () => {
|
|
@@ -105,84 +120,87 @@ export default async function (pi: ExtensionAPI) {
|
|
|
105
120
|
}));
|
|
106
121
|
|
|
107
122
|
pi.on("before_agent_start", async (event, ctx) => withSession(ctx, async () => {
|
|
108
|
-
await
|
|
123
|
+
await recordNewUsageEntries(ctx);
|
|
124
|
+
await startAgentRun(asRecord(event), ctx);
|
|
109
125
|
}));
|
|
110
126
|
|
|
111
127
|
pi.on("agent_start", async (event, ctx) => withSession(ctx, async () => {
|
|
112
128
|
if (!state.agentState?.root) {
|
|
113
|
-
await startAgentRun(event, ctx);
|
|
129
|
+
await startAgentRun(asRecord(event), ctx);
|
|
114
130
|
}
|
|
131
|
+
await startAgentAttempt();
|
|
115
132
|
// The system prompt is only final here: before_agent_start handlers that
|
|
116
133
|
// run after this extension may still rewrite it.
|
|
117
134
|
await recordSystemPrompt(ctx);
|
|
135
|
+
await recordSystemState(ctx, pi.getActiveTools());
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
pi.on("cache_warming_decision", async (event, ctx) => withSession(ctx, async () => {
|
|
139
|
+
await recordNewUsageEntries(ctx);
|
|
140
|
+
await recordCacheWarmingDecision(asRecord(event), ctx);
|
|
118
141
|
}));
|
|
119
142
|
|
|
120
143
|
pi.on("turn_start", async (event, ctx) => withSession(ctx, async () => {
|
|
121
|
-
await startTurnObservation(event);
|
|
144
|
+
await startTurnObservation(asRecord(event));
|
|
122
145
|
}));
|
|
123
146
|
|
|
124
147
|
pi.on("before_provider_request", async (event, ctx) => withSession(ctx, async () => {
|
|
125
|
-
await startGeneration(event);
|
|
148
|
+
await startGeneration(asRecord(event));
|
|
126
149
|
}));
|
|
127
150
|
|
|
128
151
|
pi.on("after_provider_response", async (event, ctx) => withSession(ctx, async () => {
|
|
129
|
-
updateGenerationMetadata(event);
|
|
152
|
+
updateGenerationMetadata(asRecord(event));
|
|
130
153
|
}));
|
|
131
154
|
|
|
132
155
|
pi.on("message_update", async (event, ctx) => withSession(ctx, async () => {
|
|
133
|
-
recordTTFT(event);
|
|
134
|
-
const message = getMessageFromEvent(event);
|
|
156
|
+
recordTTFT(asRecord(event));
|
|
157
|
+
const message = getMessageFromEvent(asRecord(event));
|
|
135
158
|
if (message?.role === "assistant" && state.agentState) {
|
|
136
159
|
state.agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
137
160
|
}
|
|
138
161
|
}));
|
|
139
162
|
|
|
140
163
|
pi.on("message_end", async (event, ctx) => withSession(ctx, async () => {
|
|
141
|
-
await finishGenerationFromMessage(event);
|
|
164
|
+
await finishGenerationFromMessage(asRecord(event));
|
|
142
165
|
}));
|
|
143
166
|
|
|
144
167
|
pi.on("tool_execution_start", async (event, ctx) => withSession(ctx, async () => {
|
|
145
|
-
await startToolObservation(event);
|
|
168
|
+
await startToolObservation(asRecord(event));
|
|
146
169
|
}));
|
|
147
170
|
|
|
148
171
|
pi.on("tool_call", async (event, ctx) => withSession(ctx, async () => {
|
|
149
|
-
await startToolObservation(event);
|
|
172
|
+
await startToolObservation(asRecord(event));
|
|
150
173
|
}));
|
|
151
174
|
|
|
152
175
|
pi.on("tool_result", async (event, ctx) => withSession(ctx, async () => {
|
|
153
|
-
await finishToolObservation(event);
|
|
176
|
+
await finishToolObservation(asRecord(event));
|
|
154
177
|
}));
|
|
155
178
|
|
|
156
179
|
pi.on("tool_execution_end", async (event, ctx) => withSession(ctx, async () => {
|
|
157
|
-
await finishToolObservation(event);
|
|
180
|
+
await finishToolObservation(asRecord(event));
|
|
158
181
|
}));
|
|
159
182
|
|
|
160
183
|
pi.on("turn_end", async (event, ctx) => withSession(ctx, async () => {
|
|
161
184
|
state.turnCount++;
|
|
162
|
-
const
|
|
185
|
+
const record = asRecord(event);
|
|
186
|
+
const message = getMessageFromEvent(record);
|
|
163
187
|
if (message?.role === "assistant") {
|
|
164
|
-
await createFallbackGenerationFromTurn(
|
|
165
|
-
await finishGenerationFromMessage(
|
|
188
|
+
await createFallbackGenerationFromTurn(record, message);
|
|
189
|
+
await finishGenerationFromMessage(record);
|
|
166
190
|
}
|
|
167
|
-
finishTurnObservation(
|
|
191
|
+
finishTurnObservation(record);
|
|
168
192
|
}));
|
|
169
193
|
|
|
170
194
|
pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.warn("📊 Langfuse: Shutdown failed", error);
|
|
177
|
-
}
|
|
195
|
+
finishAgentAttempt(asRecord(event));
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
pi.on("agent_settled", async (_event, ctx) => withSession(ctx, async () => {
|
|
199
|
+
await finishAgentRun();
|
|
178
200
|
}));
|
|
179
201
|
|
|
180
202
|
const handleSessionInterruption = (reason: string) => {
|
|
181
|
-
|
|
182
|
-
closeDanglingObservations(reason);
|
|
183
|
-
state.agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
184
|
-
}
|
|
185
|
-
resetRunState();
|
|
203
|
+
cancelAgentRun(reason);
|
|
186
204
|
};
|
|
187
205
|
|
|
188
206
|
pi.on("session_before_switch", async (_event, ctx) => {
|
|
@@ -200,26 +218,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
200
218
|
});
|
|
201
219
|
|
|
202
220
|
pi.on("session_compact", async (event, ctx) => withSession(ctx, async () => {
|
|
203
|
-
|
|
204
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
205
|
-
try {
|
|
206
|
-
const observation = parent.startObservation ? parent.startObservation(
|
|
207
|
-
"session_compact",
|
|
208
|
-
{
|
|
209
|
-
level: "DEFAULT",
|
|
210
|
-
statusMessage: "Context was compacted",
|
|
211
|
-
metadata: applyCapturePolicy({ metadata: { ...event } }, getCapturePolicy()).metadata
|
|
212
|
-
},
|
|
213
|
-
{ asType: "span" }
|
|
214
|
-
) : undefined;
|
|
215
|
-
observation?.end();
|
|
216
|
-
} catch (e) {
|
|
217
|
-
// ignore
|
|
218
|
-
}
|
|
219
|
-
}
|
|
221
|
+
await recordSessionCompaction(asRecord(event));
|
|
220
222
|
}));
|
|
221
223
|
|
|
222
224
|
pi.on("session_shutdown", async (_event, ctx) => withSession(ctx, async () => {
|
|
225
|
+
await recordNewUsageEntries(ctx);
|
|
223
226
|
handleSessionInterruption("Session shutdown before agent completed");
|
|
224
227
|
await shutdownRuntime();
|
|
225
228
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"index.ts",
|
|
18
18
|
"src/",
|
|
19
|
-
"types/",
|
|
20
19
|
"README.md",
|
|
21
20
|
"README_CN.md",
|
|
22
21
|
"image.png",
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
|
53
52
|
},
|
|
54
53
|
"peerDependencies": {
|
|
55
|
-
"@earendil-works/pi-coding-agent": "
|
|
54
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0"
|
|
56
55
|
},
|
|
57
56
|
"publishConfig": {
|
|
58
57
|
"access": "public",
|
|
@@ -63,6 +62,7 @@
|
|
|
63
62
|
"node": ">=22"
|
|
64
63
|
},
|
|
65
64
|
"devDependencies": {
|
|
65
|
+
"@earendil-works/pi-coding-agent": "^0.86.0",
|
|
66
66
|
"tsx": "^4.19.0",
|
|
67
67
|
"typescript": "^6.0.3"
|
|
68
68
|
}
|
package/src/handlers/agent.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput,
|
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
6
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
7
|
import { collectSourceMetadata } from "../source-metadata.js";
|
|
8
|
+
import { startChildObservation } from "../observation.js";
|
|
8
9
|
|
|
9
10
|
function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
10
11
|
if (!metadata) {
|
|
@@ -70,6 +71,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
70
71
|
...(state.currentModel ? { model: state.currentModel } : {}),
|
|
71
72
|
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
72
73
|
sessionId: state.currentSessionId || undefined,
|
|
74
|
+
sessionLeafId: ctx?.sessionManager?.getLeafId?.() || undefined,
|
|
73
75
|
},
|
|
74
76
|
},
|
|
75
77
|
capturePolicy,
|
|
@@ -84,6 +86,11 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
84
86
|
activeTools: new Map(),
|
|
85
87
|
sourceMetadata,
|
|
86
88
|
providerMetadataByRequest: new Map(),
|
|
89
|
+
attemptCount: 0,
|
|
90
|
+
systemStateChangeCount: 0,
|
|
91
|
+
cacheReadTokens: 0,
|
|
92
|
+
cacheWriteTokens: 0,
|
|
93
|
+
uncachedInputTokens: 0,
|
|
87
94
|
};
|
|
88
95
|
|
|
89
96
|
const root = rt.propagateAttributes(
|
|
@@ -112,6 +119,82 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
export async function startAgentAttempt() {
|
|
123
|
+
const agent = state.agentState;
|
|
124
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
125
|
+
|
|
126
|
+
if (agent.activeAttempt) {
|
|
127
|
+
agent.activeAttempt
|
|
128
|
+
.update({ level: "WARNING", statusMessage: "A new agent attempt started before the previous attempt ended" })
|
|
129
|
+
.end();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
agent.attemptCount++;
|
|
134
|
+
agent.activeAttempt = await startChildObservation({
|
|
135
|
+
parent: agent.root,
|
|
136
|
+
runtime: getRuntime,
|
|
137
|
+
name: "agent-attempt",
|
|
138
|
+
body: { metadata: { attemptIndex: agent.attemptCount } },
|
|
139
|
+
asType: "span",
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn("📊 Langfuse: Failed to start agent attempt", error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function finishAgentAttempt(event: Record<string, unknown> = {}) {
|
|
147
|
+
const agent = state.agentState;
|
|
148
|
+
if (!agent) return;
|
|
149
|
+
agent.lastAgentEndEvent = event;
|
|
150
|
+
|
|
151
|
+
closeDanglingObservations("Agent attempt ended before observation finalized");
|
|
152
|
+
if (agent.activeTurn) {
|
|
153
|
+
agent.activeTurn
|
|
154
|
+
.update({ level: "WARNING", statusMessage: "Agent attempt ended before turn finalized" })
|
|
155
|
+
.end();
|
|
156
|
+
agent.activeTurn = undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!agent.activeAttempt) return;
|
|
160
|
+
try {
|
|
161
|
+
const lastAssistant = extractFinalAssistant(event.messages);
|
|
162
|
+
const captured = applyCapturePolicy(
|
|
163
|
+
{ output: lastAssistant ? extractAssistantOutput(lastAssistant) : undefined },
|
|
164
|
+
getCapturePolicy(),
|
|
165
|
+
);
|
|
166
|
+
agent.activeAttempt.update({ output: captured.output }).end();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn("📊 Langfuse: Failed to finish agent attempt", error);
|
|
169
|
+
} finally {
|
|
170
|
+
agent.activeAttempt = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function cancelAgentRun(reason: string) {
|
|
175
|
+
const agent = state.agentState;
|
|
176
|
+
if (!agent?.root) {
|
|
177
|
+
resetRunState();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
closeDanglingObservations(reason);
|
|
182
|
+
if (agent.activeTurn) {
|
|
183
|
+
agent.activeTurn.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
|
|
184
|
+
agent.activeTurn = undefined;
|
|
185
|
+
}
|
|
186
|
+
if (agent.activeAttempt) {
|
|
187
|
+
agent.activeAttempt.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
|
|
188
|
+
agent.activeAttempt = undefined;
|
|
189
|
+
}
|
|
190
|
+
agent.root.update({
|
|
191
|
+
level: "WARNING",
|
|
192
|
+
statusMessage: reason,
|
|
193
|
+
metadata: { completed: false, cancelled: true },
|
|
194
|
+
}).end();
|
|
195
|
+
resetRunState();
|
|
196
|
+
}
|
|
197
|
+
|
|
115
198
|
/**
|
|
116
199
|
* Records the effective system prompt on the root agent observation.
|
|
117
200
|
*
|
|
@@ -161,7 +244,8 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
161
244
|
return;
|
|
162
245
|
}
|
|
163
246
|
|
|
164
|
-
const
|
|
247
|
+
const finalEvent = Object.keys(event).length > 0 ? event : state.agentState.lastAgentEndEvent ?? {};
|
|
248
|
+
const lastAssistant = extractFinalAssistant(finalEvent.messages);
|
|
165
249
|
const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
|
|
166
250
|
const captured = applyCapturePolicy(
|
|
167
251
|
{
|
|
@@ -173,6 +257,19 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
173
257
|
model: state.currentModel || undefined,
|
|
174
258
|
provider: state.currentProvider || undefined,
|
|
175
259
|
totalTools: state.toolCallCount,
|
|
260
|
+
agentAttemptCount: state.agentState.attemptCount,
|
|
261
|
+
systemStateChangeCount: state.agentState.systemStateChangeCount,
|
|
262
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
263
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
264
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
265
|
+
cacheReadTokens: state.agentState.cacheReadTokens,
|
|
266
|
+
cacheWriteTokens: state.agentState.cacheWriteTokens,
|
|
267
|
+
uncachedInputTokens: state.agentState.uncachedInputTokens,
|
|
268
|
+
cacheHitRatio:
|
|
269
|
+
state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens > 0
|
|
270
|
+
? state.agentState.cacheReadTokens /
|
|
271
|
+
(state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens)
|
|
272
|
+
: undefined,
|
|
176
273
|
...computeEvaluationScores(),
|
|
177
274
|
},
|
|
178
275
|
},
|
|
@@ -181,6 +278,10 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
181
278
|
const scores = computeEvaluationScores();
|
|
182
279
|
|
|
183
280
|
closeDanglingObservations("Agent run ended before observation finalized");
|
|
281
|
+
if (state.agentState.activeAttempt) {
|
|
282
|
+
state.agentState.activeAttempt.end();
|
|
283
|
+
state.agentState.activeAttempt = undefined;
|
|
284
|
+
}
|
|
184
285
|
|
|
185
286
|
try {
|
|
186
287
|
state.agentState.root
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { getSessionRunState, state } from "../state.js";
|
|
5
|
+
import { extractCostDetails, extractUsage, getCapturePolicy, truncate } from "../utils.js";
|
|
6
|
+
|
|
7
|
+
type RecordLike = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function sessionIdForTrace(): string | undefined {
|
|
10
|
+
return state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function initializeUsageTracking(ctx: any): void {
|
|
14
|
+
const session = getSessionRunState();
|
|
15
|
+
try {
|
|
16
|
+
const entries = ctx?.sessionManager?.getEntries?.();
|
|
17
|
+
if (!Array.isArray(entries)) return;
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry && typeof entry === "object" && (entry as RecordLike).type === "usage") {
|
|
20
|
+
const id = (entry as RecordLike).id;
|
|
21
|
+
if (typeof id === "string") session.seenUsageEntryIds.add(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Optional for SDK hosts without persisted sessions.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function recordCacheWarmingDecision(event: RecordLike, ctx: any): Promise<void> {
|
|
30
|
+
if (state.isTracingDisabled || !state.config) return;
|
|
31
|
+
const contextUsage = ctx?.getContextUsage?.();
|
|
32
|
+
const captured = applyCapturePolicy(
|
|
33
|
+
{
|
|
34
|
+
metadata: {
|
|
35
|
+
action: event.action,
|
|
36
|
+
warmCostEstimate: event.warmCost,
|
|
37
|
+
missCostEstimate: event.missCost,
|
|
38
|
+
continuationProbability: event.continuationProbability,
|
|
39
|
+
contextTokens: contextUsage?.tokens,
|
|
40
|
+
contextWindow: contextUsage?.contextWindow,
|
|
41
|
+
contextPercent: contextUsage?.percent,
|
|
42
|
+
idle: ctx?.isIdle?.(),
|
|
43
|
+
model: ctx?.model?.id,
|
|
44
|
+
provider: ctx?.model?.provider,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
getCapturePolicy(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const parent = state.agentState?.activeTurn ?? state.agentState?.activeAttempt ?? state.agentState?.root;
|
|
52
|
+
if (parent) {
|
|
53
|
+
const observation = await startChildObservation({
|
|
54
|
+
parent,
|
|
55
|
+
runtime: getRuntime,
|
|
56
|
+
name: "cache-warming-decision",
|
|
57
|
+
body: { metadata: captured.metadata },
|
|
58
|
+
asType: "event",
|
|
59
|
+
});
|
|
60
|
+
observation.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const runtime = await getRuntime();
|
|
65
|
+
const create = () => runtime.propagateAttributes(
|
|
66
|
+
{
|
|
67
|
+
sessionId: sessionIdForTrace(),
|
|
68
|
+
traceName: "pi-cache-warming",
|
|
69
|
+
metadata: {
|
|
70
|
+
...(ctx?.model?.id ? { model: String(ctx.model.id) } : {}),
|
|
71
|
+
...(ctx?.model?.provider ? { provider: String(ctx.model.provider) } : {}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
() => runtime.startObservation(
|
|
75
|
+
"cache-warming-decision",
|
|
76
|
+
{ metadata: captured.metadata },
|
|
77
|
+
{ asType: "event" },
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const observation = runtime.withRootContext ? runtime.withRootContext(create) : create();
|
|
81
|
+
observation.end();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn("📊 Langfuse: Failed to record cache warming decision", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function recordNewUsageEntries(ctx: any): Promise<void> {
|
|
88
|
+
if (state.isTracingDisabled || !state.config) return;
|
|
89
|
+
const session = getSessionRunState();
|
|
90
|
+
let entries: unknown;
|
|
91
|
+
try {
|
|
92
|
+
entries = ctx?.sessionManager?.getEntries?.();
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(entries)) return;
|
|
97
|
+
|
|
98
|
+
for (const rawEntry of entries) {
|
|
99
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
100
|
+
const entry = rawEntry as RecordLike;
|
|
101
|
+
if (entry.type !== "usage" || typeof entry.id !== "string" || session.seenUsageEntryIds.has(entry.id)) continue;
|
|
102
|
+
if (entry.kind !== "cache_warm") {
|
|
103
|
+
session.seenUsageEntryIds.add(entry.id);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const usageDetails = extractUsage(entry);
|
|
108
|
+
const costDetails = extractCostDetails(entry);
|
|
109
|
+
const captured = applyCapturePolicy(
|
|
110
|
+
{
|
|
111
|
+
metadata: {
|
|
112
|
+
usageEntryId: entry.id,
|
|
113
|
+
kind: entry.kind,
|
|
114
|
+
note: entry.note,
|
|
115
|
+
provider: entry.provider,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
getCapturePolicy(),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const runtime = await getRuntime();
|
|
123
|
+
const parent = state.agentState?.activeTurn ?? state.agentState?.activeAttempt ?? state.agentState?.root;
|
|
124
|
+
const body = {
|
|
125
|
+
model: typeof entry.model === "string" ? entry.model : undefined,
|
|
126
|
+
usageDetails,
|
|
127
|
+
...(costDetails ? { costDetails } : {}),
|
|
128
|
+
metadata: captured.metadata,
|
|
129
|
+
};
|
|
130
|
+
const observation = parent
|
|
131
|
+
? await startChildObservation({ parent, runtime: getRuntime, name: "cache-warm", body, asType: "generation" })
|
|
132
|
+
: (runtime.withRootContext
|
|
133
|
+
? runtime.withRootContext(() => runtime.propagateAttributes(
|
|
134
|
+
{ sessionId: sessionIdForTrace(), traceName: "pi-cache-warm" },
|
|
135
|
+
() => runtime.startObservation("cache-warm", body, { asType: "generation" }),
|
|
136
|
+
))
|
|
137
|
+
: runtime.propagateAttributes(
|
|
138
|
+
{ sessionId: sessionIdForTrace(), traceName: "pi-cache-warm" },
|
|
139
|
+
() => runtime.startObservation("cache-warm", body, { asType: "generation" }),
|
|
140
|
+
));
|
|
141
|
+
observation.end();
|
|
142
|
+
session.seenUsageEntryIds.add(entry.id);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.warn("📊 Langfuse: Failed to record cache warming usage", error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
extractCostDetails,
|
|
13
13
|
getCapturePolicy,
|
|
14
14
|
extractModelParameters,
|
|
15
|
+
extractCacheMetrics,
|
|
16
|
+
inferToolUpdateTransport,
|
|
15
17
|
} from "../utils.js";
|
|
16
18
|
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
17
19
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
@@ -43,11 +45,26 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
43
45
|
const modelParameters = extractModelParameters(payload);
|
|
44
46
|
const model = String(event.model ?? event.modelId ?? state.currentModel ?? "");
|
|
45
47
|
const provider = String(event.provider ?? state.currentProvider ?? "");
|
|
48
|
+
const toolUpdateTransport = inferToolUpdateTransport(payload, (state.agentState.systemStateSequence ?? 0) > 1);
|
|
46
49
|
const metadata = shapePayload({
|
|
47
50
|
provider,
|
|
48
51
|
requestId: key,
|
|
49
52
|
url: event.url,
|
|
50
53
|
method: event.method,
|
|
54
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
55
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
56
|
+
systemStateSequence: state.agentState.systemStateSequence,
|
|
57
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
58
|
+
toolUpdateTransport,
|
|
59
|
+
cachePreservationExpected:
|
|
60
|
+
toolUpdateTransport === "anthropic-native" ||
|
|
61
|
+
toolUpdateTransport === "openai-additional-tools" ||
|
|
62
|
+
toolUpdateTransport === "openai-tool-search" ||
|
|
63
|
+
toolUpdateTransport === "mid-conversation-system"
|
|
64
|
+
? true
|
|
65
|
+
: toolUpdateTransport === "collapsed-leading-system"
|
|
66
|
+
? false
|
|
67
|
+
: undefined,
|
|
51
68
|
}) as Record<string, unknown>;
|
|
52
69
|
const captured = applyCapturePolicy(
|
|
53
70
|
{
|
|
@@ -57,7 +74,7 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
57
74
|
getCapturePolicy(),
|
|
58
75
|
);
|
|
59
76
|
|
|
60
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
77
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
61
78
|
const generation = await startChildObservation({
|
|
62
79
|
parent,
|
|
63
80
|
runtime: getRuntime,
|
|
@@ -177,6 +194,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
177
194
|
|
|
178
195
|
const usageDetails = extractUsage({ ...event, message });
|
|
179
196
|
const costDetails = extractCostDetails({ ...event, message });
|
|
197
|
+
const cacheMetrics = extractCacheMetrics({ ...event, message });
|
|
180
198
|
const modelParameters = extractModelParameters(getProviderPayload(event)) ?? generation.modelParameters;
|
|
181
199
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
182
200
|
const update: ObservationUpdate = {
|
|
@@ -188,6 +206,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
188
206
|
metadata: {
|
|
189
207
|
...generation.metadata,
|
|
190
208
|
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
209
|
+
...cacheMetrics,
|
|
191
210
|
},
|
|
192
211
|
};
|
|
193
212
|
update.metadata = applyCapturePolicy({ metadata: update.metadata }, getCapturePolicy()).metadata;
|
|
@@ -195,6 +214,12 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
195
214
|
try {
|
|
196
215
|
generation.observation.update(update).end();
|
|
197
216
|
generation.ended = true;
|
|
217
|
+
if (cacheMetrics) {
|
|
218
|
+
state.agentState.cacheReadTokens = (state.agentState.cacheReadTokens ?? 0) + cacheMetrics.cacheReadTokens;
|
|
219
|
+
state.agentState.cacheWriteTokens = (state.agentState.cacheWriteTokens ?? 0) + cacheMetrics.cacheWriteTokens;
|
|
220
|
+
state.agentState.uncachedInputTokens =
|
|
221
|
+
(state.agentState.uncachedInputTokens ?? 0) + cacheMetrics.uncachedInputTokens;
|
|
222
|
+
}
|
|
198
223
|
} catch (e) {
|
|
199
224
|
console.warn("📊 Langfuse: Failed to finish generation", e);
|
|
200
225
|
}
|
|
@@ -208,6 +233,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
208
233
|
try {
|
|
209
234
|
const usageDetails = extractUsage({ ...event, message });
|
|
210
235
|
const costDetails = extractCostDetails({ ...event, message });
|
|
236
|
+
const cacheMetrics = extractCacheMetrics({ ...event, message });
|
|
211
237
|
const modelParameters = extractModelParameters(getProviderPayload(event));
|
|
212
238
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
213
239
|
const captured = applyCapturePolicy(
|
|
@@ -217,11 +243,16 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
217
243
|
metadata: {
|
|
218
244
|
provider: state.currentProvider || undefined,
|
|
219
245
|
sourceEvent: "turn_end",
|
|
246
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
247
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
248
|
+
systemStateSequence: state.agentState.systemStateSequence,
|
|
249
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
250
|
+
...cacheMetrics,
|
|
220
251
|
},
|
|
221
252
|
},
|
|
222
253
|
getCapturePolicy(),
|
|
223
254
|
);
|
|
224
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
255
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
225
256
|
const generation = await startChildObservation({
|
|
226
257
|
parent,
|
|
227
258
|
runtime: getRuntime,
|
|
@@ -240,6 +271,12 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
240
271
|
|
|
241
272
|
generation.end();
|
|
242
273
|
state.agentState.generationOrder.push("turn-end-fallback");
|
|
274
|
+
if (cacheMetrics) {
|
|
275
|
+
state.agentState.cacheReadTokens = (state.agentState.cacheReadTokens ?? 0) + cacheMetrics.cacheReadTokens;
|
|
276
|
+
state.agentState.cacheWriteTokens = (state.agentState.cacheWriteTokens ?? 0) + cacheMetrics.cacheWriteTokens;
|
|
277
|
+
state.agentState.uncachedInputTokens =
|
|
278
|
+
(state.agentState.uncachedInputTokens ?? 0) + cacheMetrics.uncachedInputTokens;
|
|
279
|
+
}
|
|
243
280
|
} catch (e) {
|
|
244
281
|
console.warn("📊 Langfuse: Failed to create fallback generation", e);
|
|
245
282
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { state } from "../state.js";
|
|
5
|
+
import {
|
|
6
|
+
extractCostDetails,
|
|
7
|
+
extractUsage,
|
|
8
|
+
getCapturePolicy,
|
|
9
|
+
shapePayload,
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
|
|
12
|
+
type RecordLike = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function namesFromTools(value: unknown): string[] {
|
|
15
|
+
if (!Array.isArray(value)) return [];
|
|
16
|
+
return value.flatMap((tool) => {
|
|
17
|
+
if (typeof tool === "string") return [tool];
|
|
18
|
+
if (!tool || typeof tool !== "object") return [];
|
|
19
|
+
const name = (tool as RecordLike).name;
|
|
20
|
+
return typeof name === "string" ? [name] : [];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function recordSessionCompaction(event: RecordLike): Promise<void> {
|
|
25
|
+
const agent = state.agentState;
|
|
26
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
27
|
+
|
|
28
|
+
const entry = event.compactionEntry && typeof event.compactionEntry === "object"
|
|
29
|
+
? event.compactionEntry as RecordLike
|
|
30
|
+
: {};
|
|
31
|
+
const checkpoint = entry.systemMessage && typeof entry.systemMessage === "object"
|
|
32
|
+
? entry.systemMessage as RecordLike
|
|
33
|
+
: undefined;
|
|
34
|
+
const checkpointTools = namesFromTools(checkpoint?.toolsAdded);
|
|
35
|
+
const checkpointSections = checkpoint?.sections && typeof checkpoint.sections === "object"
|
|
36
|
+
? Object.keys(checkpoint.sections as RecordLike)
|
|
37
|
+
: [];
|
|
38
|
+
const captured = applyCapturePolicy(
|
|
39
|
+
{
|
|
40
|
+
input: shapePayload({ summary: entry.summary }),
|
|
41
|
+
metadata: {
|
|
42
|
+
reason: event.reason,
|
|
43
|
+
willRetry: event.willRetry,
|
|
44
|
+
fromExtension: event.fromExtension,
|
|
45
|
+
compactionEntryId: entry.id,
|
|
46
|
+
firstKeptEntryId: entry.firstKeptEntryId,
|
|
47
|
+
tokensBefore: entry.tokensBefore,
|
|
48
|
+
checkpointSectionNames: checkpointSections,
|
|
49
|
+
checkpointToolNames: checkpointTools,
|
|
50
|
+
checkpointToolCount: checkpointTools.length,
|
|
51
|
+
promptStateHash: agent.promptStateHash,
|
|
52
|
+
toolStateHash: agent.toolStateHash,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
getCapturePolicy(),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const parent = agent.activeTurn ?? agent.activeAttempt ?? agent.root;
|
|
60
|
+
const compaction = await startChildObservation({
|
|
61
|
+
parent,
|
|
62
|
+
runtime: getRuntime,
|
|
63
|
+
name: "session-compaction",
|
|
64
|
+
body: { input: captured.input, metadata: captured.metadata },
|
|
65
|
+
asType: "span",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (entry.usage && typeof entry.usage === "object") {
|
|
69
|
+
const summary = applyCapturePolicy({ output: entry.summary }, getCapturePolicy());
|
|
70
|
+
const costDetails = extractCostDetails({ usage: entry.usage });
|
|
71
|
+
const generation = await startChildObservation({
|
|
72
|
+
parent: compaction,
|
|
73
|
+
runtime: getRuntime,
|
|
74
|
+
name: "compaction-summary",
|
|
75
|
+
body: {
|
|
76
|
+
output: summary.output,
|
|
77
|
+
model: state.currentModel || undefined,
|
|
78
|
+
usageDetails: extractUsage({ usage: entry.usage }),
|
|
79
|
+
...(costDetails ? { costDetails } : {}),
|
|
80
|
+
metadata: { provider: state.currentProvider || undefined },
|
|
81
|
+
},
|
|
82
|
+
asType: "generation",
|
|
83
|
+
});
|
|
84
|
+
generation.end();
|
|
85
|
+
}
|
|
86
|
+
compaction.end();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn("📊 Langfuse: Failed to record session compaction", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
4
|
+
import { getRuntime } from "../langfuse.js";
|
|
5
|
+
import { startChildObservation } from "../observation.js";
|
|
6
|
+
import { getSessionRunState, state } from "../state.js";
|
|
7
|
+
import { getCapturePolicy, shapePayload } from "../utils.js";
|
|
8
|
+
|
|
9
|
+
type RecordLike = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function fingerprint(value: string): string {
|
|
12
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toolName(tool: unknown): string | undefined {
|
|
16
|
+
if (typeof tool === "string") return tool;
|
|
17
|
+
if (!tool || typeof tool !== "object") return undefined;
|
|
18
|
+
const name = (tool as RecordLike).name;
|
|
19
|
+
return typeof name === "string" && name ? name : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sortedUnique(values: Iterable<string>): string[] {
|
|
23
|
+
return [...new Set(values)].sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readNewSystemMessages(ctx: any): Array<{ entryId: string; message: RecordLike }> {
|
|
27
|
+
const session = getSessionRunState();
|
|
28
|
+
let branch: unknown;
|
|
29
|
+
try {
|
|
30
|
+
branch = ctx?.sessionManager?.getBranch?.();
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(branch)) return [];
|
|
35
|
+
|
|
36
|
+
const messages: Array<{ entryId: string; message: RecordLike }> = [];
|
|
37
|
+
for (const rawEntry of branch) {
|
|
38
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
39
|
+
const entry = rawEntry as RecordLike;
|
|
40
|
+
const message = entry.message;
|
|
41
|
+
if (entry.type !== "message" || !message || typeof message !== "object") continue;
|
|
42
|
+
if ((message as RecordLike).role !== "system") continue;
|
|
43
|
+
const entryId = typeof entry.id === "string" ? entry.id : "";
|
|
44
|
+
if (!entryId || session.seenSystemEntryIds.has(entryId)) continue;
|
|
45
|
+
session.seenSystemEntryIds.add(entryId);
|
|
46
|
+
messages.push({ entryId, message: message as RecordLike });
|
|
47
|
+
}
|
|
48
|
+
return messages;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mark restored transcript state as already seen so the first trace emits one snapshot, not the full history. */
|
|
52
|
+
export function initializeSystemStateTracking(ctx: any): void {
|
|
53
|
+
const session = getSessionRunState();
|
|
54
|
+
try {
|
|
55
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
56
|
+
if (!Array.isArray(branch)) return;
|
|
57
|
+
for (const rawEntry of branch) {
|
|
58
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
59
|
+
const entry = rawEntry as RecordLike;
|
|
60
|
+
const message = entry.message;
|
|
61
|
+
if (
|
|
62
|
+
entry.type === "message" &&
|
|
63
|
+
typeof entry.id === "string" &&
|
|
64
|
+
message &&
|
|
65
|
+
typeof message === "object" &&
|
|
66
|
+
(message as RecordLike).role === "system"
|
|
67
|
+
) {
|
|
68
|
+
session.seenSystemEntryIds.add(entry.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Session history is optional in SDK/headless hosts.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function recordSystemState(ctx: any, activeTools: string[]): Promise<void> {
|
|
77
|
+
const agent = state.agentState;
|
|
78
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
79
|
+
|
|
80
|
+
let prompt = "";
|
|
81
|
+
try {
|
|
82
|
+
prompt = String((await ctx?.getSystemPrompt?.()) ?? "");
|
|
83
|
+
} catch {
|
|
84
|
+
// The tool state is still useful when a host cannot expose the rendered prompt.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tools = sortedUnique(activeTools);
|
|
88
|
+
const promptStateHash = prompt ? fingerprint(prompt) : undefined;
|
|
89
|
+
const toolStateHash = fingerprint(JSON.stringify(tools));
|
|
90
|
+
const session = getSessionRunState();
|
|
91
|
+
const systemMessages = readNewSystemMessages(ctx);
|
|
92
|
+
|
|
93
|
+
const transcriptToolsAdded: string[] = [];
|
|
94
|
+
const transcriptToolsRemoved: string[] = [];
|
|
95
|
+
const sectionsChanged = new Set<string>();
|
|
96
|
+
const sectionsRemoved = new Set<string>();
|
|
97
|
+
const entryIds: string[] = [];
|
|
98
|
+
for (const { entryId, message } of systemMessages) {
|
|
99
|
+
entryIds.push(entryId);
|
|
100
|
+
const sections = message.sections;
|
|
101
|
+
if (sections && typeof sections === "object" && !Array.isArray(sections)) {
|
|
102
|
+
for (const [name, value] of Object.entries(sections as RecordLike)) {
|
|
103
|
+
(value === null ? sectionsRemoved : sectionsChanged).add(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(message.toolsAdded)) {
|
|
107
|
+
for (const tool of message.toolsAdded) {
|
|
108
|
+
const name = toolName(tool);
|
|
109
|
+
if (name) transcriptToolsAdded.push(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(message.toolsRemoved)) {
|
|
113
|
+
for (const tool of message.toolsRemoved) {
|
|
114
|
+
const name = toolName(tool);
|
|
115
|
+
if (name) transcriptToolsRemoved.push(name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const previousTools = new Set(session.lastActiveTools);
|
|
121
|
+
const currentTools = new Set(tools);
|
|
122
|
+
const toolsAdded = sortedUnique([
|
|
123
|
+
...tools.filter((name) => !previousTools.has(name)),
|
|
124
|
+
...transcriptToolsAdded,
|
|
125
|
+
]);
|
|
126
|
+
const toolsRemoved = sortedUnique([
|
|
127
|
+
...session.lastActiveTools.filter((name) => !currentTools.has(name)),
|
|
128
|
+
...transcriptToolsRemoved,
|
|
129
|
+
]);
|
|
130
|
+
const promptChanged = session.lastPromptStateHash !== promptStateHash;
|
|
131
|
+
const toolsChanged = session.lastToolStateHash !== toolStateHash;
|
|
132
|
+
const isInitial = session.lastPromptStateHash === undefined && session.lastToolStateHash === undefined;
|
|
133
|
+
const changed = isInitial || promptChanged || toolsChanged || systemMessages.length > 0;
|
|
134
|
+
if (changed) session.systemStateSequence++;
|
|
135
|
+
|
|
136
|
+
const capturePolicy = getCapturePolicy();
|
|
137
|
+
const rawSystemUpdate = systemMessages.length > 0
|
|
138
|
+
? systemMessages.map(({ entryId, message }) => ({ entryId, ...message }))
|
|
139
|
+
: { role: "system", content: prompt };
|
|
140
|
+
const captured = applyCapturePolicy(
|
|
141
|
+
{
|
|
142
|
+
systemPrompt: shapePayload(rawSystemUpdate),
|
|
143
|
+
metadata: {
|
|
144
|
+
stateSequence: session.systemStateSequence,
|
|
145
|
+
changeKind: isInitial ? "initial" : changed ? "updated" : "unchanged",
|
|
146
|
+
source: systemMessages.length > 0 ? "transcript" : "effective-snapshot",
|
|
147
|
+
transcriptEntryIds: entryIds,
|
|
148
|
+
promptStateHash,
|
|
149
|
+
toolStateHash,
|
|
150
|
+
promptChars: prompt.length,
|
|
151
|
+
activeToolCount: tools.length,
|
|
152
|
+
activeTools: tools,
|
|
153
|
+
toolsAdded,
|
|
154
|
+
toolsRemoved,
|
|
155
|
+
sectionsChanged: sortedUnique(sectionsChanged),
|
|
156
|
+
sectionsRemoved: sortedUnique(sectionsRemoved),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
capturePolicy,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const observation = await startChildObservation({
|
|
164
|
+
parent: agent.activeAttempt ?? agent.root,
|
|
165
|
+
runtime: getRuntime,
|
|
166
|
+
name: "system-state",
|
|
167
|
+
body: {
|
|
168
|
+
input: captured.systemPrompt,
|
|
169
|
+
metadata: captured.metadata,
|
|
170
|
+
},
|
|
171
|
+
asType: "event",
|
|
172
|
+
});
|
|
173
|
+
observation.end();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn("📊 Langfuse: Failed to record system state", error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (changed) agent.systemStateChangeCount = (agent.systemStateChangeCount ?? 0) + 1;
|
|
179
|
+
agent.promptStateHash = promptStateHash;
|
|
180
|
+
agent.toolStateHash = toolStateHash;
|
|
181
|
+
agent.systemStateSequence = session.systemStateSequence;
|
|
182
|
+
agent.activeToolCount = tools.length;
|
|
183
|
+
session.lastPromptStateHash = promptStateHash;
|
|
184
|
+
session.lastToolStateHash = toolStateHash;
|
|
185
|
+
session.lastActiveTools = tools;
|
|
186
|
+
}
|
package/src/handlers/tool.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
37
37
|
getCapturePolicy(),
|
|
38
38
|
);
|
|
39
39
|
const inputBytes = estimatePayloadBytes(captured.toolInput, getLimits().maxToolPayload);
|
|
40
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
40
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
41
41
|
const tool = await startChildObservation({
|
|
42
42
|
parent,
|
|
43
43
|
runtime: getRuntime,
|
package/src/handlers/turn.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function startTurnObservation(event: Record<string, unknown>) {
|
|
|
25
25
|
getCapturePolicy(),
|
|
26
26
|
);
|
|
27
27
|
const observation = await startChildObservation({
|
|
28
|
-
parent: state.agentState.root,
|
|
28
|
+
parent: state.agentState.activeAttempt ?? state.agentState.root,
|
|
29
29
|
runtime: getRuntime,
|
|
30
30
|
name: "turn",
|
|
31
31
|
body: {
|
package/src/observation.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function startChildObservation({
|
|
|
11
11
|
runtime: () => Promise<LangfuseRuntime>;
|
|
12
12
|
name: string;
|
|
13
13
|
body?: ObservationUpdate;
|
|
14
|
-
asType: "generation" | "tool" | "span";
|
|
14
|
+
asType: "event" | "generation" | "tool" | "span";
|
|
15
15
|
}): Promise<LangfuseObservation> {
|
|
16
16
|
if (parent.startObservation) {
|
|
17
17
|
return parent.startObservation(name, body, { asType });
|
package/src/state.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface SessionRunState {
|
|
|
10
10
|
turnCount: number;
|
|
11
11
|
tracingDisabled: boolean;
|
|
12
12
|
setupAttemptedThisSession: boolean;
|
|
13
|
+
lastPromptStateHash?: string;
|
|
14
|
+
lastToolStateHash?: string;
|
|
15
|
+
lastActiveTools: string[];
|
|
16
|
+
systemStateSequence: number;
|
|
17
|
+
seenSystemEntryIds: Set<string>;
|
|
18
|
+
seenUsageEntryIds: Set<string>;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
|
|
@@ -27,6 +33,10 @@ function createSessionRunState(): SessionRunState {
|
|
|
27
33
|
turnCount: 0,
|
|
28
34
|
tracingDisabled: false,
|
|
29
35
|
setupAttemptedThisSession: false,
|
|
36
|
+
lastActiveTools: [],
|
|
37
|
+
systemStateSequence: 0,
|
|
38
|
+
seenSystemEntryIds: new Set(),
|
|
39
|
+
seenUsageEntryIds: new Set(),
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -130,11 +140,16 @@ export const state = {
|
|
|
130
140
|
|
|
131
141
|
export function resetRunState(sessionId = getActiveSessionId()) {
|
|
132
142
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
133
|
-
const
|
|
134
|
-
state.sessionStates.get(normalizedSessionId)?.setupAttemptedThisSession ?? false;
|
|
143
|
+
const previous = state.sessionStates.get(normalizedSessionId);
|
|
135
144
|
state.sessionStates.set(normalizedSessionId, {
|
|
136
145
|
...createSessionRunState(),
|
|
137
|
-
setupAttemptedThisSession,
|
|
146
|
+
setupAttemptedThisSession: previous?.setupAttemptedThisSession ?? false,
|
|
147
|
+
lastPromptStateHash: previous?.lastPromptStateHash,
|
|
148
|
+
lastToolStateHash: previous?.lastToolStateHash,
|
|
149
|
+
lastActiveTools: previous?.lastActiveTools ?? [],
|
|
150
|
+
systemStateSequence: previous?.systemStateSequence ?? 0,
|
|
151
|
+
seenSystemEntryIds: previous?.seenSystemEntryIds ?? new Set(),
|
|
152
|
+
seenUsageEntryIds: previous?.seenUsageEntryIds ?? new Set(),
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
|
package/src/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface LangfuseObservation {
|
|
|
19
19
|
startObservation?(
|
|
20
20
|
name: string,
|
|
21
21
|
body?: ObservationUpdate,
|
|
22
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
22
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
23
23
|
): LangfuseObservation;
|
|
24
24
|
setTraceIO?(body?: { input?: unknown; output?: unknown }): void;
|
|
25
25
|
}
|
|
@@ -82,7 +82,7 @@ export interface LangfuseRuntime {
|
|
|
82
82
|
startObservation: (
|
|
83
83
|
name: string,
|
|
84
84
|
body?: ObservationUpdate,
|
|
85
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
85
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
86
86
|
) => LangfuseObservation;
|
|
87
87
|
propagateAttributes: (
|
|
88
88
|
params: {
|
|
@@ -136,6 +136,7 @@ export interface ToolState {
|
|
|
136
136
|
|
|
137
137
|
export interface AgentState {
|
|
138
138
|
root?: LangfuseObservation;
|
|
139
|
+
activeAttempt?: LangfuseObservation;
|
|
139
140
|
activeTurn?: LangfuseObservation;
|
|
140
141
|
traceId?: string;
|
|
141
142
|
promptInput?: unknown;
|
|
@@ -147,4 +148,14 @@ export interface AgentState {
|
|
|
147
148
|
latestAssistantOutput?: unknown;
|
|
148
149
|
sourceMetadata?: Record<string, unknown>;
|
|
149
150
|
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
151
|
+
attemptCount: number;
|
|
152
|
+
lastAgentEndEvent?: Record<string, unknown>;
|
|
153
|
+
promptStateHash?: string;
|
|
154
|
+
toolStateHash?: string;
|
|
155
|
+
systemStateSequence?: number;
|
|
156
|
+
activeToolCount?: number;
|
|
157
|
+
systemStateChangeCount: number;
|
|
158
|
+
cacheReadTokens: number;
|
|
159
|
+
cacheWriteTokens: number;
|
|
160
|
+
uncachedInputTokens: number;
|
|
150
161
|
}
|
package/src/utils.ts
CHANGED
|
@@ -334,6 +334,55 @@ export function getProviderPayload(event: Record<string, unknown>): unknown {
|
|
|
334
334
|
return event.request ?? event.payload ?? event.body ?? event.providerPayload ?? event.messages ?? event;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
export type ToolUpdateTransport =
|
|
338
|
+
| "anthropic-native"
|
|
339
|
+
| "openai-additional-tools"
|
|
340
|
+
| "openai-tool-search"
|
|
341
|
+
| "mid-conversation-system"
|
|
342
|
+
| "collapsed-leading-system";
|
|
343
|
+
|
|
344
|
+
/** Classify the provider payload observed by this hook rather than guessing from the model name alone. */
|
|
345
|
+
export function inferToolUpdateTransport(payload: unknown, hasPriorSystemState: boolean): ToolUpdateTransport | undefined {
|
|
346
|
+
let sawToolChangeBlock = false;
|
|
347
|
+
let sawAdditionalTools = false;
|
|
348
|
+
let sawToolSearch = false;
|
|
349
|
+
let sawTopLevelTools = false;
|
|
350
|
+
let sawMidConversationSystem = false;
|
|
351
|
+
let visited = 0;
|
|
352
|
+
|
|
353
|
+
const walk = (value: unknown, key?: string) => {
|
|
354
|
+
if (++visited > 2_000 || value === null || value === undefined) return;
|
|
355
|
+
if (key === "additional_tools") sawAdditionalTools = true;
|
|
356
|
+
if (key === "tools") sawTopLevelTools = true;
|
|
357
|
+
if (key === "tool_search" || key === "tool_search_output") sawToolSearch = true;
|
|
358
|
+
if (typeof value !== "object") return;
|
|
359
|
+
if (Array.isArray(value)) {
|
|
360
|
+
let sawNonSystem = false;
|
|
361
|
+
for (const item of value) {
|
|
362
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
363
|
+
const role = (item as Record<string, unknown>).role;
|
|
364
|
+
if (role === "system" && sawNonSystem) sawMidConversationSystem = true;
|
|
365
|
+
if (role && role !== "system") sawNonSystem = true;
|
|
366
|
+
}
|
|
367
|
+
walk(item);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const record = value as Record<string, unknown>;
|
|
372
|
+
const type = record.type;
|
|
373
|
+
if (type === "tool_addition" || type === "tool_removal") sawToolChangeBlock = true;
|
|
374
|
+
for (const [childKey, child] of Object.entries(record)) walk(child, childKey);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
walk(payload);
|
|
378
|
+
if (sawToolChangeBlock) return "anthropic-native";
|
|
379
|
+
if (sawAdditionalTools) return "openai-additional-tools";
|
|
380
|
+
if (sawToolSearch) return "openai-tool-search";
|
|
381
|
+
if (sawMidConversationSystem) return "mid-conversation-system";
|
|
382
|
+
if (hasPriorSystemState && sawTopLevelTools) return "collapsed-leading-system";
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
337
386
|
export function extractModelParameters(payload: unknown): Record<string, string | number> | undefined {
|
|
338
387
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
339
388
|
return undefined;
|
|
@@ -487,6 +536,32 @@ export function extractUsage(
|
|
|
487
536
|
};
|
|
488
537
|
}
|
|
489
538
|
|
|
539
|
+
export function extractCacheMetrics(messageOrEvent: Record<string, unknown>): {
|
|
540
|
+
cacheReadTokens: number;
|
|
541
|
+
cacheWriteTokens: number;
|
|
542
|
+
uncachedInputTokens: number;
|
|
543
|
+
cacheHitRatio?: number;
|
|
544
|
+
} | undefined {
|
|
545
|
+
const usage = (messageOrEvent.usage ??
|
|
546
|
+
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
547
|
+
? (messageOrEvent.message as Record<string, unknown>).usage
|
|
548
|
+
: undefined)) as Record<string, unknown> | undefined;
|
|
549
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
550
|
+
|
|
551
|
+
const uncachedInputTokens = Number(
|
|
552
|
+
usage.input ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens ?? 0,
|
|
553
|
+
);
|
|
554
|
+
const cacheReadTokens = Number(usage.cacheRead ?? usage.cache_read ?? usage.cachedTokens ?? 0);
|
|
555
|
+
const cacheWriteTokens = Number(usage.cacheWrite ?? usage.cache_write ?? 0);
|
|
556
|
+
const denominator = uncachedInputTokens + cacheReadTokens;
|
|
557
|
+
return {
|
|
558
|
+
cacheReadTokens,
|
|
559
|
+
cacheWriteTokens,
|
|
560
|
+
uncachedInputTokens,
|
|
561
|
+
...(denominator > 0 ? { cacheHitRatio: cacheReadTokens / denominator } : {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
490
565
|
export function extractCostDetails(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
|
|
491
566
|
const usage = (messageOrEvent.usage ??
|
|
492
567
|
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
declare module "@opentelemetry/sdk-trace-base" {
|
|
2
|
-
export class BasicTracerProvider {
|
|
3
|
-
constructor(options?: { resource?: unknown; spanProcessors?: unknown[] });
|
|
4
|
-
forceFlush?(): Promise<void>;
|
|
5
|
-
shutdown?(): Promise<void>;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
declare module "@langfuse/otel" {
|
|
10
|
-
export class LangfuseSpanProcessor {
|
|
11
|
-
constructor(options: {
|
|
12
|
-
publicKey: string;
|
|
13
|
-
secretKey: string;
|
|
14
|
-
baseUrl: string;
|
|
15
|
-
});
|
|
16
|
-
forceFlush?(): Promise<void>;
|
|
17
|
-
shutdown?(): Promise<void>;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
declare module "@langfuse/tracing" {
|
|
22
|
-
export function setLangfuseTracerProvider(provider: unknown): void;
|
|
23
|
-
|
|
24
|
-
export function startObservation(
|
|
25
|
-
name: string,
|
|
26
|
-
body?: Record<string, unknown>,
|
|
27
|
-
options?: { asType?: string },
|
|
28
|
-
): unknown;
|
|
29
|
-
|
|
30
|
-
export function propagateAttributes<T>(
|
|
31
|
-
params: {
|
|
32
|
-
sessionId?: string;
|
|
33
|
-
traceName?: string;
|
|
34
|
-
metadata?: Record<string, string>;
|
|
35
|
-
tags?: string[];
|
|
36
|
-
},
|
|
37
|
-
fn: () => T,
|
|
38
|
-
): T;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
declare module "@langfuse/client" {
|
|
42
|
-
export class LangfuseClient {
|
|
43
|
-
constructor(options: {
|
|
44
|
-
publicKey: string;
|
|
45
|
-
secretKey: string;
|
|
46
|
-
baseUrl: string;
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
package/types/node-shims.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
declare module "node:fs" {
|
|
2
|
-
export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
|
|
3
|
-
export function readFileSync(path: string, encoding: string): string;
|
|
4
|
-
export function existsSync(path: string): boolean;
|
|
5
|
-
export function writeFileSync(path: string, data: string, encoding: string): void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
declare module "node:crypto" {
|
|
9
|
-
export function randomUUID(): string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
declare module "node:os" {
|
|
13
|
-
export function homedir(): string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
declare module "node:path" {
|
|
17
|
-
export function resolve(...paths: string[]): string;
|
|
18
|
-
export function dirname(path: string): string;
|
|
19
|
-
export function basename(path: string, suffix?: string): string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
declare module "node:url" {
|
|
23
|
-
export function fileURLToPath(url: string | URL): string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
declare const process: {
|
|
27
|
-
cwd(): string;
|
|
28
|
-
env: Record<string, string | undefined>;
|
|
29
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
declare module "@earendil-works/pi-coding-agent" {
|
|
2
|
-
export interface ExtensionContext {
|
|
3
|
-
sessionManager?: {
|
|
4
|
-
getSessionId?: () => unknown;
|
|
5
|
-
getSessionFile?: () => unknown;
|
|
6
|
-
};
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ExtensionAPI {
|
|
10
|
-
on(event: string, handler: (event: any, ctx: any) => unknown): void;
|
|
11
|
-
registerCommand(
|
|
12
|
-
name: string,
|
|
13
|
-
options: { description?: string; handler: (args: string, ctx: any) => unknown },
|
|
14
|
-
): void;
|
|
15
|
-
}
|
|
16
|
-
}
|