@code-yeongyu/senpi-codemode 2026.8.14 → 2026.8.16
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/CHANGELOG.md +14 -0
- package/README.md +5 -0
- package/package.json +4 -4
- package/src/index.ts +14 -1
- package/src/tool/call-capture.ts +64 -0
- package/src/tool/cell-handler.ts +45 -61
- package/src/tool/cell-runtime.ts +6 -0
- package/src/tool/eval-execution-event.ts +201 -0
- package/src/tool/eval-tool-options.ts +2 -0
- package/src/tool/eval-tool.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.8.16] - 2026-08-16
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Published one versioned `senpi.eval.execution` event per settled eval cell: the in-process bus receives bounded rich call details, while the external RPC projection exposes only byte-capped timing/count metadata for safe OMO analytics; total wall time, kernel runtime, pending calls, exact aggregate totals, and overflow accounting are reported separately ([#897](https://github.com/code-yeongyu/senpi/pull/897)).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
15
29
|
## [2026.8.14] - 2026-08-14
|
|
16
30
|
|
|
17
31
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@ task-tool names are known.
|
|
|
20
20
|
- Loopback, bearer-authenticated kernel bridge with bounded JSONL frames.
|
|
21
21
|
- Structured status events for file operations, environment access, phases,
|
|
22
22
|
bridge activity, and delegated task progress.
|
|
23
|
+
- One versioned `senpi.eval.execution` event at terminal cell settlement. The
|
|
24
|
+
in-process event bus receives bounded per-call arguments and result previews
|
|
25
|
+
for extension-owned consumers; external RPC clients receive a 32 KiB-capped
|
|
26
|
+
metadata-only projection with wall time, kernel time, exact call counts,
|
|
27
|
+
pending-call counts, and bounded per-tool aggregates.
|
|
23
28
|
- Bounded streaming output with head/tail previews, column clamping, and
|
|
24
29
|
session-adjacent spill files for large streams.
|
|
25
30
|
- TUI and HTML-export rendering for syntax-highlighted cells, status rows,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.16",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@babel/parser": "8.0.4",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.16",
|
|
34
34
|
"typebox": "1.3.8"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.8.16"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.8.16"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,11 @@ import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from
|
|
|
17
17
|
import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
|
|
18
18
|
import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
|
|
19
19
|
import { EvalDetachedCellManager, type EvalDetachedCellStatusEntry } from "./tool/detached-cell-manager.ts";
|
|
20
|
+
import {
|
|
21
|
+
EVAL_EXECUTION_EVENT,
|
|
22
|
+
type EvalExecutionEventPayload,
|
|
23
|
+
toEvalExecutionRpcPayload,
|
|
24
|
+
} from "./tool/eval-execution-event.ts";
|
|
20
25
|
import { createEvalTool } from "./tool/eval-tool.ts";
|
|
21
26
|
import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
|
|
22
27
|
|
|
@@ -39,8 +44,10 @@ export interface CodemodeExtensionAPI {
|
|
|
39
44
|
getActiveTools(): string[];
|
|
40
45
|
getAllTools(): readonly EvalSchemaToolInfo[];
|
|
41
46
|
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
42
|
-
/** Optional host event bus; a host without one turns
|
|
47
|
+
/** Optional host event bus; a host without one turns extension event emission into a harmless no-op. */
|
|
43
48
|
events?: { emit(name: string, data: unknown): void };
|
|
49
|
+
/** Optional host RPC surface for forwarding extension-owned events to connected clients. */
|
|
50
|
+
rpc?: { emit(name: string, data: unknown): void };
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export interface SenpiCodemodeOptions {
|
|
@@ -90,6 +97,11 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
90
97
|
modelId: string | undefined,
|
|
91
98
|
cellManager: EvalDetachedCellManager,
|
|
92
99
|
): void => {
|
|
100
|
+
const onCellSettled = (payload: EvalExecutionEventPayload): void => {
|
|
101
|
+
if (activeCells !== cellManager) return;
|
|
102
|
+
pi.rpc?.emit(EVAL_EXECUTION_EVENT, toEvalExecutionRpcPayload(payload));
|
|
103
|
+
pi.events?.emit(EVAL_EXECUTION_EVENT, payload);
|
|
104
|
+
};
|
|
93
105
|
pi.registerTool(
|
|
94
106
|
createEvalTool({
|
|
95
107
|
enabledLanguages: runtime.enabledLanguages,
|
|
@@ -102,6 +114,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
102
114
|
artifactsDir: runtime.artifactsDir,
|
|
103
115
|
cellManager,
|
|
104
116
|
executionTracker: manager,
|
|
117
|
+
onCellSettled,
|
|
105
118
|
renderers,
|
|
106
119
|
spawns: runtime.spawns,
|
|
107
120
|
spawnDefaultAgent: runtime.settings.taskTools.task,
|
package/src/tool/call-capture.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type AgentToolResult, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { EvalToolCallSummary } from "./types.ts";
|
|
2
3
|
|
|
3
4
|
export const MAX_ENRICHED_TOOL_CALLS = 30;
|
|
5
|
+
export const MAX_AGGREGATED_TOOL_NAMES = 64;
|
|
6
|
+
export const MAX_CAPTURED_IDENTIFIER_CODE_POINTS = 128;
|
|
7
|
+
export const MAX_CAPTURED_TOOL_NAME_CODE_POINTS = 128;
|
|
8
|
+
export const MAX_RPC_EVENT_BYTES = 32 * 1024;
|
|
4
9
|
|
|
5
10
|
const MAX_ARGUMENT_STRING_CODE_POINTS = 512;
|
|
6
11
|
const MAX_ARGUMENT_ENTRIES = 32;
|
|
@@ -13,6 +18,22 @@ type BoundedValue = {
|
|
|
13
18
|
readonly truncated: boolean;
|
|
14
19
|
};
|
|
15
20
|
|
|
21
|
+
export interface EvalToolCallMetric {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly startedAt: number;
|
|
24
|
+
ok: boolean | undefined;
|
|
25
|
+
durationMs: number | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolCallCapture {
|
|
29
|
+
readonly callId: string;
|
|
30
|
+
readonly args: unknown;
|
|
31
|
+
readonly startedAt: number;
|
|
32
|
+
readonly metric: EvalToolCallMetric;
|
|
33
|
+
readonly includeDetails: boolean;
|
|
34
|
+
readonly argsTruncated?: true;
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
export function capCodePoints(text: string, max: number): string {
|
|
17
38
|
let end = 0;
|
|
18
39
|
let count = 0;
|
|
@@ -83,6 +104,49 @@ export function boundToolCallArgs(args: unknown): { args: unknown; truncated: bo
|
|
|
83
104
|
}
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
export function createToolCallMetric(name: string, startedAt: number): EvalToolCallMetric {
|
|
108
|
+
return {
|
|
109
|
+
name: capCodePoints(name, MAX_CAPTURED_TOOL_NAME_CODE_POINTS),
|
|
110
|
+
startedAt,
|
|
111
|
+
ok: undefined,
|
|
112
|
+
durationMs: undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function settleToolCallMetric(metric: EvalToolCallMetric, ok: boolean, completedAt: number): void {
|
|
117
|
+
metric.ok = ok;
|
|
118
|
+
metric.durationMs = Math.max(0, completedAt - metric.startedAt);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function recordToolCall(
|
|
122
|
+
toolCalls: EvalToolCallSummary[],
|
|
123
|
+
ok: boolean,
|
|
124
|
+
capture: ToolCallCapture,
|
|
125
|
+
resultPreview: string | undefined,
|
|
126
|
+
error: string | undefined,
|
|
127
|
+
): void {
|
|
128
|
+
const completedAt = Date.now();
|
|
129
|
+
settleToolCallMetric(capture.metric, ok, completedAt);
|
|
130
|
+
const summary = {
|
|
131
|
+
name: capture.metric.name,
|
|
132
|
+
ok,
|
|
133
|
+
...(error === undefined ? {} : { error: capCodePoints(error, 512) }),
|
|
134
|
+
...(capture.includeDetails ? { durationMs: completedAt - capture.startedAt } : {}),
|
|
135
|
+
};
|
|
136
|
+
const enrichedCount = toolCalls.filter((toolCall) => toolCall.callId !== undefined).length;
|
|
137
|
+
if (!capture.includeDetails || enrichedCount >= MAX_ENRICHED_TOOL_CALLS) {
|
|
138
|
+
toolCalls.push(summary);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
toolCalls.push({
|
|
142
|
+
...summary,
|
|
143
|
+
callId: capture.callId,
|
|
144
|
+
args: capture.args,
|
|
145
|
+
...(capture.argsTruncated === true ? { argsTruncated: true } : {}),
|
|
146
|
+
...(resultPreview === undefined ? {} : { resultPreview }),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
86
150
|
export function toolCallResultPreview(result: AgentToolResult<unknown>): string | undefined {
|
|
87
151
|
for (const part of result.content) {
|
|
88
152
|
if (part.type !== "text") continue;
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentToolResult, type ExtensionContext, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import { RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
|
|
3
4
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
4
5
|
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
5
6
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
@@ -7,7 +8,15 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
7
8
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
8
9
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
9
10
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
boundToolCallArgs,
|
|
13
|
+
capCodePoints,
|
|
14
|
+
createToolCallMetric,
|
|
15
|
+
MAX_CAPTURED_IDENTIFIER_CODE_POINTS,
|
|
16
|
+
recordToolCall,
|
|
17
|
+
type ToolCallCapture,
|
|
18
|
+
toolCallResultPreview,
|
|
19
|
+
} from "./call-capture.ts";
|
|
11
20
|
import { CellResultBuilder, type CellState } from "./cell-runtime.ts";
|
|
12
21
|
import { type EvalImageResizer, marshalToolResult, toolResultIsError } from "./image.ts";
|
|
13
22
|
import { upsertStatusEvent } from "./status-events.ts";
|
|
@@ -22,13 +31,6 @@ type ResolvedToolReply = {
|
|
|
22
31
|
readonly errorText?: string;
|
|
23
32
|
};
|
|
24
33
|
|
|
25
|
-
type ToolCallEnrichment = {
|
|
26
|
-
readonly callId: string;
|
|
27
|
-
readonly args: unknown;
|
|
28
|
-
readonly startedAt: number;
|
|
29
|
-
readonly argsTruncated?: true;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
34
|
export interface CellBridgeRuntime {
|
|
33
35
|
readonly executeTool: AgentExecuteTool;
|
|
34
36
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
@@ -111,9 +113,21 @@ export class CellHandler {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const metric = createToolCallMetric(message.toolName, startedAt);
|
|
118
|
+
this.#state.toolCallMetrics.push(metric);
|
|
119
|
+
const capturedArgs = boundToolCallArgs(message.args);
|
|
120
|
+
const capture: ToolCallCapture = {
|
|
121
|
+
callId: capCodePoints(message.callId, MAX_CAPTURED_IDENTIFIER_CODE_POINTS),
|
|
122
|
+
args: capturedArgs.args,
|
|
123
|
+
startedAt,
|
|
124
|
+
metric,
|
|
125
|
+
includeDetails: message.toolName !== RESERVED_SCHEMA_TOOL,
|
|
126
|
+
...(capturedArgs.truncated ? { argsTruncated: true } : {}),
|
|
127
|
+
};
|
|
114
128
|
if (message.toolName === "eval") {
|
|
115
129
|
const error = "recursive eval is not allowed";
|
|
116
|
-
this.#state.toolCalls
|
|
130
|
+
recordToolCall(this.#state.toolCalls, false, capture, undefined, error);
|
|
117
131
|
this.#kernel.deliverToolReply({
|
|
118
132
|
type: "tool-reply",
|
|
119
133
|
callId: message.callId,
|
|
@@ -123,20 +137,24 @@ export class CellHandler {
|
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
125
139
|
if (isReservedToolName(message.toolName)) {
|
|
126
|
-
await this.#deliverToolReply(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
await this.#deliverToolReply(
|
|
141
|
+
message,
|
|
142
|
+
async () => ({
|
|
143
|
+
value: await runReservedTool(message.toolName, {
|
|
144
|
+
callId: message.callId,
|
|
145
|
+
args: message.args,
|
|
146
|
+
executeTool: this.#runtime.executeTool,
|
|
147
|
+
taskToolName: this.#runtime.settings.taskTools.task,
|
|
148
|
+
taskOutputToolName: this.#runtime.settings.taskTools.output,
|
|
149
|
+
listTools: this.#runtime.listTools,
|
|
150
|
+
signal: this.#state.signal,
|
|
151
|
+
emitStatus: (event) => this.#recordStatus(event),
|
|
152
|
+
marshalToolResult,
|
|
153
|
+
}),
|
|
154
|
+
toolCallOk: true,
|
|
137
155
|
}),
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
capture,
|
|
157
|
+
);
|
|
140
158
|
return;
|
|
141
159
|
}
|
|
142
160
|
if (message.toolName === "completion" && this.#runtime.complete) {
|
|
@@ -148,16 +166,10 @@ export class CellHandler {
|
|
|
148
166
|
isActive: () => this.#state.active,
|
|
149
167
|
});
|
|
150
168
|
if (!this.#state.active) return;
|
|
151
|
-
this.#state.toolCalls.
|
|
152
|
-
result.ok
|
|
153
|
-
? { name: message.toolName, ok: true }
|
|
154
|
-
: { name: message.toolName, ok: false, error: result.error },
|
|
155
|
-
);
|
|
169
|
+
recordToolCall(this.#state.toolCalls, result.ok, capture, undefined, result.ok ? undefined : result.error);
|
|
156
170
|
this.#resultBuilder.emitUpdate(false);
|
|
157
171
|
return;
|
|
158
172
|
}
|
|
159
|
-
const capturedArgs = boundToolCallArgs(message.args);
|
|
160
|
-
const startedAt = Date.now();
|
|
161
173
|
await this.#deliverToolReply(
|
|
162
174
|
message,
|
|
163
175
|
async () => {
|
|
@@ -185,24 +197,19 @@ export class CellHandler {
|
|
|
185
197
|
...(errorText === undefined ? {} : { errorText }),
|
|
186
198
|
};
|
|
187
199
|
},
|
|
188
|
-
|
|
189
|
-
callId: message.callId,
|
|
190
|
-
args: capturedArgs.args,
|
|
191
|
-
startedAt,
|
|
192
|
-
...(capturedArgs.truncated ? { argsTruncated: true } : {}),
|
|
193
|
-
},
|
|
200
|
+
capture,
|
|
194
201
|
);
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
async #deliverToolReply(
|
|
198
205
|
message: Extract<KernelToHostMessage, { type: "tool-call" }>,
|
|
199
206
|
resolve: () => Promise<ResolvedToolReply>,
|
|
200
|
-
|
|
207
|
+
capture: ToolCallCapture,
|
|
201
208
|
): Promise<void> {
|
|
202
209
|
try {
|
|
203
210
|
const reply = await resolve();
|
|
204
211
|
if (!this.#state.active) return;
|
|
205
|
-
this.#
|
|
212
|
+
recordToolCall(this.#state.toolCalls, reply.toolCallOk, capture, reply.resultPreview, reply.errorText);
|
|
206
213
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
207
214
|
} catch (error) {
|
|
208
215
|
if (!this.#state.active) return;
|
|
@@ -211,7 +218,7 @@ export class CellHandler {
|
|
|
211
218
|
message.toolName,
|
|
212
219
|
this.#toolParameters(message.toolName),
|
|
213
220
|
);
|
|
214
|
-
this.#
|
|
221
|
+
recordToolCall(this.#state.toolCalls, false, capture, undefined, text);
|
|
215
222
|
this.#kernel.deliverToolReply({
|
|
216
223
|
type: "tool-reply",
|
|
217
224
|
callId: message.callId,
|
|
@@ -222,29 +229,6 @@ export class CellHandler {
|
|
|
222
229
|
this.#resultBuilder.emitUpdate(false);
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
#pushToolCall(
|
|
226
|
-
name: string,
|
|
227
|
-
ok: boolean,
|
|
228
|
-
enrich: ToolCallEnrichment | undefined,
|
|
229
|
-
resultPreview: string | undefined,
|
|
230
|
-
error: string | undefined,
|
|
231
|
-
): void {
|
|
232
|
-
const summary = { name, ok, ...(error === undefined ? {} : { error }) };
|
|
233
|
-
const enrichedCount = this.#state.toolCalls.filter((toolCall) => toolCall.callId !== undefined).length;
|
|
234
|
-
if (enrich === undefined || enrichedCount >= MAX_ENRICHED_TOOL_CALLS) {
|
|
235
|
-
this.#state.toolCalls.push(summary);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
this.#state.toolCalls.push({
|
|
239
|
-
...summary,
|
|
240
|
-
callId: enrich.callId,
|
|
241
|
-
args: enrich.args,
|
|
242
|
-
durationMs: Date.now() - enrich.startedAt,
|
|
243
|
-
...(enrich.argsTruncated === true ? { argsTruncated: true } : {}),
|
|
244
|
-
...(resultPreview === undefined ? {} : { resultPreview }),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
232
|
#toolParameters(toolName: string): unknown {
|
|
249
233
|
return this.#runtime.listTools?.().find((tool) => tool.name === toolName)?.parameters;
|
|
250
234
|
}
|
package/src/tool/cell-runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import type { EvalToolCallMetric } from "./call-capture.ts";
|
|
3
4
|
import { type EvalImageResizer, EvalOutputCollector, type EvalOutputResult } from "./image.ts";
|
|
4
5
|
import type { EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
5
6
|
|
|
@@ -9,14 +10,17 @@ type ToolCall = EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? I
|
|
|
9
10
|
|
|
10
11
|
export interface CellState {
|
|
11
12
|
readonly input: EvalToolInput;
|
|
13
|
+
readonly startedAt: number;
|
|
12
14
|
readonly signal: AbortSignal;
|
|
13
15
|
readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
|
|
14
16
|
readonly toolCalls: ToolCall[];
|
|
17
|
+
readonly toolCallMetrics: EvalToolCallMetric[];
|
|
15
18
|
readonly pendingBridgeCalls: Promise<void>[];
|
|
16
19
|
readonly statusEvents: EvalStatusEvent[];
|
|
17
20
|
active: boolean;
|
|
18
21
|
output: string;
|
|
19
22
|
phase: string | undefined;
|
|
23
|
+
error: string | undefined;
|
|
20
24
|
durationMs: number;
|
|
21
25
|
status: "pending" | "running" | "complete" | "error";
|
|
22
26
|
}
|
|
@@ -70,6 +74,7 @@ export class CellResultBuilder {
|
|
|
70
74
|
if (result.valueRepr) this.#output.push(`${result.valueRepr}\n`);
|
|
71
75
|
this.#state.status = "complete";
|
|
72
76
|
} else {
|
|
77
|
+
this.#state.error = result.error.message;
|
|
73
78
|
this.#output.push(`${result.error.message}\n`);
|
|
74
79
|
this.#state.status = "error";
|
|
75
80
|
}
|
|
@@ -77,6 +82,7 @@ export class CellResultBuilder {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
async finalizeCancellation(error: Error): Promise<AgentToolResult<EvalToolDetails>> {
|
|
85
|
+
this.#state.error = error.message;
|
|
80
86
|
this.#output.push(`${error.message}\n`);
|
|
81
87
|
this.#state.status = "error";
|
|
82
88
|
return await this.#finish(true);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
|
+
import {
|
|
3
|
+
capCodePoints,
|
|
4
|
+
MAX_AGGREGATED_TOOL_NAMES,
|
|
5
|
+
MAX_CAPTURED_IDENTIFIER_CODE_POINTS,
|
|
6
|
+
MAX_ENRICHED_TOOL_CALLS,
|
|
7
|
+
MAX_RPC_EVENT_BYTES,
|
|
8
|
+
} from "./call-capture.ts";
|
|
9
|
+
import type { CellState } from "./cell-runtime.ts";
|
|
10
|
+
import type { EvalLanguage, EvalToolCallSummary, EvalToolDetails } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export const EVAL_EXECUTION_EVENT = "senpi.eval.execution";
|
|
13
|
+
|
|
14
|
+
export interface EvalToolAggregate {
|
|
15
|
+
readonly count: number;
|
|
16
|
+
readonly totalDurationMs: number;
|
|
17
|
+
readonly okCount: number;
|
|
18
|
+
readonly errorCount: number;
|
|
19
|
+
readonly pendingCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type EvalToolAggregates = Record<string, EvalToolAggregate>;
|
|
23
|
+
|
|
24
|
+
interface EvalExecutionBasePayload {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly cellId: string;
|
|
27
|
+
readonly language: EvalLanguage;
|
|
28
|
+
readonly ok: boolean;
|
|
29
|
+
readonly startedAt: number;
|
|
30
|
+
readonly completedAt: number;
|
|
31
|
+
readonly durationMs: number;
|
|
32
|
+
readonly kernelDurationMs?: number;
|
|
33
|
+
readonly detached: boolean;
|
|
34
|
+
readonly toolCallCount: number;
|
|
35
|
+
readonly pendingToolCallCount: number;
|
|
36
|
+
readonly distinctToolsCalled: readonly string[];
|
|
37
|
+
readonly toolAggregates: EvalToolAggregates;
|
|
38
|
+
readonly toolAggregatesTruncated: boolean;
|
|
39
|
+
readonly toolAggregateOverflow?: EvalToolAggregate;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EvalExecutionEventPayload extends EvalExecutionBasePayload {
|
|
43
|
+
readonly detailLevel: "full";
|
|
44
|
+
readonly error?: string;
|
|
45
|
+
readonly toolCalls: readonly EvalToolCallSummary[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EvalExecutionRpcToolCallSummary {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly ok: boolean;
|
|
51
|
+
readonly durationMs?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EvalExecutionRpcPayload extends EvalExecutionBasePayload {
|
|
55
|
+
readonly detailLevel: "metadata";
|
|
56
|
+
readonly rpcTruncated: boolean;
|
|
57
|
+
readonly toolCalls: readonly EvalExecutionRpcToolCallSummary[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type EvalExecutionSettleOutcome =
|
|
61
|
+
| { readonly result: AgentToolResult<EvalToolDetails> }
|
|
62
|
+
| { readonly error: unknown };
|
|
63
|
+
|
|
64
|
+
export interface BuildEvalExecutionEventPayloadOptions {
|
|
65
|
+
readonly cellId: string;
|
|
66
|
+
readonly state: CellState;
|
|
67
|
+
readonly outcome: EvalExecutionSettleOutcome;
|
|
68
|
+
readonly completedAt: number;
|
|
69
|
+
readonly detached: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildEvalExecutionEventPayload(
|
|
73
|
+
options: BuildEvalExecutionEventPayloadOptions,
|
|
74
|
+
): EvalExecutionEventPayload {
|
|
75
|
+
const { cellId, state, outcome, completedAt, detached } = options;
|
|
76
|
+
const result = "result" in outcome ? outcome.result : undefined;
|
|
77
|
+
const ok = result?.details.isError !== true && !("error" in outcome);
|
|
78
|
+
const aggregateState = aggregateToolCalls(state, completedAt);
|
|
79
|
+
const error = ok ? undefined : settleError(state, outcome);
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
detailLevel: "full",
|
|
83
|
+
cellId: capCodePoints(cellId, MAX_CAPTURED_IDENTIFIER_CODE_POINTS),
|
|
84
|
+
language: state.input.language,
|
|
85
|
+
ok,
|
|
86
|
+
...(error === undefined ? {} : { error: capCodePoints(error, 512) }),
|
|
87
|
+
startedAt: state.startedAt,
|
|
88
|
+
completedAt,
|
|
89
|
+
durationMs: Math.max(0, completedAt - state.startedAt),
|
|
90
|
+
...(result === undefined ? {} : { kernelDurationMs: result.details.durationMs }),
|
|
91
|
+
detached,
|
|
92
|
+
toolCallCount: state.toolCallMetrics.length,
|
|
93
|
+
pendingToolCallCount: aggregateState.pendingCount,
|
|
94
|
+
toolCalls: state.toolCalls.slice(0, MAX_ENRICHED_TOOL_CALLS),
|
|
95
|
+
distinctToolsCalled: [...aggregateState.aggregates.keys()],
|
|
96
|
+
toolAggregates: Object.fromEntries(aggregateState.aggregates),
|
|
97
|
+
toolAggregatesTruncated: aggregateState.overflow !== undefined,
|
|
98
|
+
...(aggregateState.overflow === undefined ? {} : { toolAggregateOverflow: aggregateState.overflow }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function toEvalExecutionRpcPayload(payload: EvalExecutionEventPayload): EvalExecutionRpcPayload {
|
|
103
|
+
const candidate: EvalExecutionRpcPayload = {
|
|
104
|
+
version: payload.version,
|
|
105
|
+
detailLevel: "metadata",
|
|
106
|
+
rpcTruncated: false,
|
|
107
|
+
cellId: payload.cellId,
|
|
108
|
+
language: payload.language,
|
|
109
|
+
ok: payload.ok,
|
|
110
|
+
startedAt: payload.startedAt,
|
|
111
|
+
completedAt: payload.completedAt,
|
|
112
|
+
durationMs: payload.durationMs,
|
|
113
|
+
...(payload.kernelDurationMs === undefined ? {} : { kernelDurationMs: payload.kernelDurationMs }),
|
|
114
|
+
detached: payload.detached,
|
|
115
|
+
toolCallCount: payload.toolCallCount,
|
|
116
|
+
pendingToolCallCount: payload.pendingToolCallCount,
|
|
117
|
+
toolCalls: payload.toolCalls.map((call) => ({
|
|
118
|
+
name: call.name,
|
|
119
|
+
ok: call.ok,
|
|
120
|
+
...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
|
|
121
|
+
})),
|
|
122
|
+
distinctToolsCalled: payload.distinctToolsCalled,
|
|
123
|
+
toolAggregates: payload.toolAggregates,
|
|
124
|
+
toolAggregatesTruncated: payload.toolAggregatesTruncated,
|
|
125
|
+
...(payload.toolAggregateOverflow === undefined ? {} : { toolAggregateOverflow: payload.toolAggregateOverflow }),
|
|
126
|
+
};
|
|
127
|
+
if (serializedBytes(candidate) <= MAX_RPC_EVENT_BYTES) return candidate;
|
|
128
|
+
return {
|
|
129
|
+
...candidate,
|
|
130
|
+
rpcTruncated: true,
|
|
131
|
+
toolCalls: [],
|
|
132
|
+
distinctToolsCalled: [],
|
|
133
|
+
toolAggregates: {},
|
|
134
|
+
toolAggregatesTruncated: true,
|
|
135
|
+
toolAggregateOverflow: totalAggregate(payload),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function aggregateToolCalls(
|
|
140
|
+
state: CellState,
|
|
141
|
+
completedAt: number,
|
|
142
|
+
): {
|
|
143
|
+
readonly aggregates: Map<string, EvalToolAggregate>;
|
|
144
|
+
readonly overflow: EvalToolAggregate | undefined;
|
|
145
|
+
readonly pendingCount: number;
|
|
146
|
+
} {
|
|
147
|
+
const aggregates = new Map<string, EvalToolAggregate>();
|
|
148
|
+
let overflow: EvalToolAggregate | undefined;
|
|
149
|
+
let pendingCount = 0;
|
|
150
|
+
for (const metric of state.toolCallMetrics) {
|
|
151
|
+
const item: EvalToolAggregate = {
|
|
152
|
+
count: 1,
|
|
153
|
+
totalDurationMs: metric.durationMs ?? Math.max(0, completedAt - metric.startedAt),
|
|
154
|
+
okCount: metric.ok === true ? 1 : 0,
|
|
155
|
+
errorCount: metric.ok === false ? 1 : 0,
|
|
156
|
+
pendingCount: metric.ok === undefined ? 1 : 0,
|
|
157
|
+
};
|
|
158
|
+
pendingCount += item.pendingCount;
|
|
159
|
+
const existing = aggregates.get(metric.name);
|
|
160
|
+
if (existing !== undefined) {
|
|
161
|
+
aggregates.set(metric.name, addAggregate(existing, item));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (aggregates.size < MAX_AGGREGATED_TOOL_NAMES) {
|
|
165
|
+
aggregates.set(metric.name, item);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
overflow = overflow === undefined ? item : addAggregate(overflow, item);
|
|
169
|
+
}
|
|
170
|
+
return { aggregates, overflow, pendingCount };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function addAggregate(left: EvalToolAggregate, right: EvalToolAggregate): EvalToolAggregate {
|
|
174
|
+
return {
|
|
175
|
+
count: left.count + right.count,
|
|
176
|
+
totalDurationMs: left.totalDurationMs + right.totalDurationMs,
|
|
177
|
+
okCount: left.okCount + right.okCount,
|
|
178
|
+
errorCount: left.errorCount + right.errorCount,
|
|
179
|
+
pendingCount: left.pendingCount + right.pendingCount,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function totalAggregate(payload: EvalExecutionEventPayload): EvalToolAggregate {
|
|
184
|
+
let total: EvalToolAggregate = { count: 0, totalDurationMs: 0, okCount: 0, errorCount: 0, pendingCount: 0 };
|
|
185
|
+
for (const aggregate of Object.values(payload.toolAggregates)) total = addAggregate(total, aggregate);
|
|
186
|
+
if (payload.toolAggregateOverflow !== undefined) total = addAggregate(total, payload.toolAggregateOverflow);
|
|
187
|
+
return total;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function serializedBytes(value: unknown): number {
|
|
191
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function settleError(state: CellState, outcome: EvalExecutionSettleOutcome): string | undefined {
|
|
195
|
+
if (state.error !== undefined) return state.error;
|
|
196
|
+
if ("error" in outcome) return outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
197
|
+
for (const part of outcome.result.content) {
|
|
198
|
+
if (part.type === "text" && part.text.length > 0) return part.text.trimEnd();
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
|
5
5
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
6
6
|
import type { EvalTimeoutFactory } from "./cell-execution.ts";
|
|
7
7
|
import type { EvalDetachedCellManager } from "./detached-cell-manager.ts";
|
|
8
|
+
import type { EvalExecutionEventPayload } from "./eval-execution-event.ts";
|
|
8
9
|
import type { EvalImageResizer } from "./image.ts";
|
|
9
10
|
import type {
|
|
10
11
|
EnabledEvalLanguages,
|
|
@@ -29,6 +30,7 @@ export interface CreateEvalToolOptions {
|
|
|
29
30
|
readonly imageResizer?: EvalImageResizer;
|
|
30
31
|
readonly executionTracker?: EvalExecutionTracker;
|
|
31
32
|
readonly cellManager?: EvalDetachedCellManager;
|
|
33
|
+
readonly onCellSettled?: (payload: EvalExecutionEventPayload) => void;
|
|
32
34
|
readonly timeoutFactory?: EvalTimeoutFactory;
|
|
33
35
|
readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
|
|
34
36
|
readonly renderers?: Pick<ToolDefinition<EvalInputSchema, EvalToolDetails>, "renderCall" | "renderResult">;
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-executi
|
|
|
8
8
|
import { CellHandler, type CellState } from "./cell-handler.ts";
|
|
9
9
|
import { EvalDetachedCellManager } from "./detached-cell-manager.ts";
|
|
10
10
|
import { detachedKernelBusyError, executeEvalControl, resultAfterDetach } from "./detached-eval-result.ts";
|
|
11
|
+
import { buildEvalExecutionEventPayload, type EvalExecutionSettleOutcome } from "./eval-execution-event.ts";
|
|
11
12
|
import { clampEvalSummary, evalTimeoutBehavior, isEvalControlRequest, parseEvalRequest } from "./eval-request.ts";
|
|
12
13
|
import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
|
|
13
14
|
import { describeTimeoutState } from "./interrupt-note.ts";
|
|
@@ -99,18 +100,22 @@ async function runEvalCell(
|
|
|
99
100
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
100
101
|
const state: CellState = {
|
|
101
102
|
input: invocation.input,
|
|
103
|
+
startedAt: Date.now(),
|
|
102
104
|
signal: cellSignal,
|
|
103
105
|
onUpdate: invocation.onUpdate,
|
|
104
106
|
toolCalls: [],
|
|
107
|
+
toolCallMetrics: [],
|
|
105
108
|
pendingBridgeCalls: [],
|
|
106
109
|
statusEvents: [],
|
|
107
110
|
active: true,
|
|
108
111
|
output: "",
|
|
109
112
|
phase: undefined,
|
|
113
|
+
error: undefined,
|
|
110
114
|
durationMs: 0,
|
|
111
115
|
status: "pending",
|
|
112
116
|
};
|
|
113
117
|
const cell = cellManager.create(invocation.cellId, invocation.input);
|
|
118
|
+
let detached = false;
|
|
114
119
|
let execution: CellExecution;
|
|
115
120
|
execution = new CellExecution({
|
|
116
121
|
callerSignal: invocation.signal,
|
|
@@ -119,6 +124,7 @@ async function runEvalCell(
|
|
|
119
124
|
timeoutFactory: options.timeoutFactory ?? defaultTimeoutFactory,
|
|
120
125
|
onTimeout: (error) => {
|
|
121
126
|
if (timeoutBehavior === "detach" && cellManager.detach(cell)) {
|
|
127
|
+
detached = true;
|
|
122
128
|
execution.detach();
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
@@ -139,13 +145,29 @@ async function runEvalCell(
|
|
|
139
145
|
bridgeContext,
|
|
140
146
|
bridgeAbortController,
|
|
141
147
|
);
|
|
148
|
+
let settleEventEmitted = false;
|
|
149
|
+
const emitSettled = (outcome: EvalExecutionSettleOutcome): void => {
|
|
150
|
+
if (settleEventEmitted) return;
|
|
151
|
+
settleEventEmitted = true;
|
|
152
|
+
options.onCellSettled?.(
|
|
153
|
+
buildEvalExecutionEventPayload({
|
|
154
|
+
cellId: invocation.cellId,
|
|
155
|
+
state,
|
|
156
|
+
outcome,
|
|
157
|
+
completedAt: Date.now(),
|
|
158
|
+
detached,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
};
|
|
142
162
|
const finalized = running.then(
|
|
143
163
|
(result) => {
|
|
144
164
|
cellManager.complete(cell, result);
|
|
165
|
+
emitSettled({ result });
|
|
145
166
|
return result;
|
|
146
167
|
},
|
|
147
168
|
(error: unknown) => {
|
|
148
169
|
cellManager.fail(cell, error instanceof Error ? error : new Error(String(error)));
|
|
170
|
+
emitSettled({ error });
|
|
149
171
|
throw error;
|
|
150
172
|
},
|
|
151
173
|
);
|