@code-yeongyu/senpi-codemode 2026.7.25-2 → 2026.7.28
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 +39 -0
- package/README.md +45 -19
- package/package.json +3 -3
- package/src/bridge/http-server.ts +6 -1
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/output-bridge.ts +0 -1
- package/src/bridges/reserved-dispatch.ts +56 -0
- package/src/bridges/schema-bridge.ts +69 -0
- package/src/bridges/schema-hint.ts +140 -0
- package/src/extension/eval-notifier.ts +40 -0
- package/src/extension/runtime-factory.ts +3 -1
- package/src/index.ts +50 -66
- package/src/kernels/jl/prelude.jl +6 -0
- package/src/kernels/js/context-manager.ts +5 -2
- package/src/kernels/js/local-module-loader.ts +4 -0
- package/src/kernels/js/worker-runtime.js +6 -0
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +19 -4
- package/src/kernels/py/prelude.py +17 -0
- package/src/kernels/rb/prelude.rb +6 -0
- package/src/kernels/shared/subprocess-kernel.ts +8 -5
- package/src/prompt/eval-prompt.ts +28 -5
- package/src/tool/cell-handler.ts +20 -18
- package/src/tool/detached-cell-manager.ts +322 -0
- package/src/tool/eval-tool.ts +256 -18
- package/src/tool/interrupt-note.ts +58 -0
- package/src/tool/render.ts +28 -6
- package/src/tool/status-events.ts +20 -0
- package/src/tool/types.ts +66 -23
- package/src/codemode/runtime.ts +0 -258
- package/src/codemode/tools.ts +0 -106
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
4
|
+
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
5
|
+
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
6
|
+
import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
6
7
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
7
8
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
8
9
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
@@ -36,6 +37,7 @@ export interface CellState {
|
|
|
36
37
|
|
|
37
38
|
export interface CellBridgeRuntime {
|
|
38
39
|
readonly executeTool: AgentExecuteTool;
|
|
40
|
+
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
39
41
|
readonly settings: ResolvedCodemodeSettings;
|
|
40
42
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
41
43
|
readonly ctx: ExtensionContext;
|
|
@@ -151,25 +153,17 @@ export class CellHandler {
|
|
|
151
153
|
});
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
|
-
if (message.toolName
|
|
156
|
+
if (isReservedToolName(message.toolName)) {
|
|
155
157
|
await this.#deliverToolReply(message, async () => ({
|
|
156
|
-
value: await
|
|
158
|
+
value: await runReservedTool(message.toolName, {
|
|
157
159
|
callId: message.callId,
|
|
158
|
-
|
|
160
|
+
args: message.args,
|
|
159
161
|
executeTool: this.#runtime.executeTool,
|
|
160
|
-
|
|
161
|
-
emitStatus: (event) => this.#recordStatus(event),
|
|
162
|
-
}),
|
|
163
|
-
toolCallOk: true,
|
|
164
|
-
}));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (message.toolName === RESERVED_OUTPUT_TOOL) {
|
|
168
|
-
await this.#deliverToolReply(message, async () => ({
|
|
169
|
-
value: await runEvalOutput(message.args, {
|
|
162
|
+
taskToolName: this.#runtime.settings.taskTools.task,
|
|
170
163
|
taskOutputToolName: this.#runtime.settings.taskTools.output,
|
|
171
|
-
|
|
164
|
+
listTools: this.#runtime.listTools,
|
|
172
165
|
signal: this.#state.signal,
|
|
166
|
+
emitStatus: (event) => this.#recordStatus(event),
|
|
173
167
|
marshalToolResult,
|
|
174
168
|
}),
|
|
175
169
|
toolCallOk: true,
|
|
@@ -210,7 +204,11 @@ export class CellHandler {
|
|
|
210
204
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
211
205
|
} catch (error) {
|
|
212
206
|
if (!this.#state.active) return;
|
|
213
|
-
const text =
|
|
207
|
+
const text = appendSchemaHint(
|
|
208
|
+
error instanceof Error ? error.message : String(error),
|
|
209
|
+
message.toolName,
|
|
210
|
+
this.#toolParameters(message.toolName),
|
|
211
|
+
);
|
|
214
212
|
this.#state.toolCalls.push({ name: message.toolName, ok: false, error: text });
|
|
215
213
|
this.#kernel.deliverToolReply({
|
|
216
214
|
type: "tool-reply",
|
|
@@ -222,6 +220,10 @@ export class CellHandler {
|
|
|
222
220
|
this.#emitUpdate(false);
|
|
223
221
|
}
|
|
224
222
|
|
|
223
|
+
#toolParameters(toolName: string): unknown {
|
|
224
|
+
return this.#runtime.listTools?.().find((tool) => tool.name === toolName)?.parameters;
|
|
225
|
+
}
|
|
226
|
+
|
|
225
227
|
#recordStatus(event: EvalStatusEvent): void {
|
|
226
228
|
if (!this.#runtime.settings.statusEvents) return;
|
|
227
229
|
upsertStatusEvent(this.#state.statusEvents, event);
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
4
|
+
import type { EvalKernel, EvalLanguage, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const NOTIFICATION_TAIL_BYTES = 512;
|
|
7
|
+
|
|
8
|
+
export type EvalDetachedCellState = "running" | "detached" | "completed" | "failed" | "cancelled";
|
|
9
|
+
|
|
10
|
+
type ManagedCell = {
|
|
11
|
+
readonly cellId: string;
|
|
12
|
+
readonly input: EvalToolInput;
|
|
13
|
+
readonly spillPath: string | undefined;
|
|
14
|
+
state: EvalDetachedCellState;
|
|
15
|
+
canDetach: boolean;
|
|
16
|
+
wasDetached: boolean;
|
|
17
|
+
kernel: EvalKernel | undefined;
|
|
18
|
+
/** Set after an interrupt-driven stop; undefined until the kernel reports its fate. */
|
|
19
|
+
stateRetained: boolean | undefined;
|
|
20
|
+
outputTail: (() => string) | undefined;
|
|
21
|
+
result: AgentToolResult<EvalToolDetails> | undefined;
|
|
22
|
+
notificationQueued: boolean;
|
|
23
|
+
readonly terminal: PromiseWithResolvers<EvalDetachedCellSnapshot>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface EvalDetachedCellSnapshot {
|
|
27
|
+
readonly cellId: string;
|
|
28
|
+
readonly language: EvalLanguage;
|
|
29
|
+
readonly state: EvalDetachedCellState;
|
|
30
|
+
readonly outputTail: string;
|
|
31
|
+
readonly result: AgentToolResult<EvalToolDetails> | undefined;
|
|
32
|
+
readonly stateRetained: boolean | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EvalDetachedCellNotification {
|
|
36
|
+
readonly cellId: string;
|
|
37
|
+
readonly content: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EvalDetachedCellNotifier {
|
|
41
|
+
notify(cells: readonly EvalDetachedCellNotification[]): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EvalDetachedCellManagerOptions {
|
|
45
|
+
readonly artifactsDir?: string;
|
|
46
|
+
readonly notifier?: EvalDetachedCellNotifier;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Session-owned lifecycle owner for eval cells that outlive their tool call.
|
|
51
|
+
*
|
|
52
|
+
* All state changes are synchronous and funnel through #transition. That is the
|
|
53
|
+
* atomic boundary for timeout, kernel completion, explicit stop, and session
|
|
54
|
+
* disposal races; exactly one path can release a language's detached busy mark.
|
|
55
|
+
*/
|
|
56
|
+
export class EvalDetachedCellManager {
|
|
57
|
+
readonly #artifactsDir: string | undefined;
|
|
58
|
+
readonly #notifier: EvalDetachedCellNotifier | undefined;
|
|
59
|
+
readonly #cells = new Map<string, ManagedCell>();
|
|
60
|
+
readonly #detachedByLanguage = new Map<EvalLanguage, ManagedCell>();
|
|
61
|
+
#notificationQueue: ManagedCell[] = [];
|
|
62
|
+
#notificationFlush: Promise<void> | undefined;
|
|
63
|
+
|
|
64
|
+
constructor(options: EvalDetachedCellManagerOptions = {}) {
|
|
65
|
+
this.#artifactsDir = options.artifactsDir;
|
|
66
|
+
this.#notifier = options.notifier;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
create(cellId: string, input: EvalToolInput): ManagedCell {
|
|
70
|
+
const existing = this.#cells.get(cellId);
|
|
71
|
+
if (existing !== undefined) throw new Error(`Eval cell ${cellId} is already managed`);
|
|
72
|
+
const spillPath =
|
|
73
|
+
this.#artifactsDir === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: join(this.#artifactsDir, "local", `detached-eval-${safeCellId(cellId)}.log`);
|
|
76
|
+
const cell: ManagedCell = {
|
|
77
|
+
cellId,
|
|
78
|
+
input,
|
|
79
|
+
spillPath,
|
|
80
|
+
state: "running",
|
|
81
|
+
canDetach: false,
|
|
82
|
+
wasDetached: false,
|
|
83
|
+
kernel: undefined,
|
|
84
|
+
stateRetained: undefined,
|
|
85
|
+
outputTail: undefined,
|
|
86
|
+
result: undefined,
|
|
87
|
+
notificationQueued: false,
|
|
88
|
+
terminal: Promise.withResolvers<EvalDetachedCellSnapshot>(),
|
|
89
|
+
};
|
|
90
|
+
this.#cells.set(cellId, cell);
|
|
91
|
+
return cell;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
markRunning(cell: ManagedCell, kernel: EvalKernel, outputTail: () => string): void {
|
|
95
|
+
if (cell.state !== "running") return;
|
|
96
|
+
cell.kernel = kernel;
|
|
97
|
+
cell.outputTail = outputTail;
|
|
98
|
+
cell.canDetach = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
detach(cell: ManagedCell): boolean {
|
|
102
|
+
if (!cell.canDetach || !this.#transition(cell, "detached")) return false;
|
|
103
|
+
cell.wasDetached = true;
|
|
104
|
+
this.#detachedByLanguage.set(cell.input.language, cell);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
complete(cell: ManagedCell, result: AgentToolResult<EvalToolDetails>): boolean {
|
|
109
|
+
cell.result = result;
|
|
110
|
+
return this.#transition(cell, result.details.isError === true ? "failed" : "completed");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fail(cell: ManagedCell, error: Error): boolean {
|
|
114
|
+
const result = errorResult(cell, error);
|
|
115
|
+
cell.result = result;
|
|
116
|
+
return this.#transition(cell, "failed");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async stop(cellId: string, reason = "Stopped detached eval cell"): Promise<EvalDetachedCellSnapshot> {
|
|
120
|
+
const cell = this.#get(cellId);
|
|
121
|
+
if (cell.state === "detached" && this.#transition(cell, "cancelled")) {
|
|
122
|
+
const kernel = cell.kernel;
|
|
123
|
+
if (kernel !== undefined) {
|
|
124
|
+
const handle = await kernel.interrupt(reason);
|
|
125
|
+
cell.stateRetained = await handle.stateRetained;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return this.#snapshot(cell);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
peek(cellId: string): EvalDetachedCellSnapshot {
|
|
132
|
+
return this.#snapshot(this.#get(cellId));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
busyFor(language: EvalLanguage): EvalDetachedCellSnapshot | undefined {
|
|
136
|
+
const cell = this.#detachedByLanguage.get(language);
|
|
137
|
+
return cell === undefined ? undefined : this.#snapshot(cell);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async waitForTerminal(cellId: string): Promise<EvalDetachedCellSnapshot> {
|
|
141
|
+
return await this.#get(cellId).terminal.promise;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async dispose(): Promise<void> {
|
|
145
|
+
const detached = [...this.#detachedByLanguage.values()];
|
|
146
|
+
await Promise.allSettled(
|
|
147
|
+
detached.map(async (cell) => await this.stop(cell.cellId, "Session ended; detached eval cell cancelled")),
|
|
148
|
+
);
|
|
149
|
+
await this.flushNotifications();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async flushNotifications(): Promise<void> {
|
|
153
|
+
const flush = this.#notificationFlush;
|
|
154
|
+
if (flush !== undefined) await flush;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#transition(cell: ManagedCell, next: EvalDetachedCellState): boolean {
|
|
158
|
+
if (!allowsTransition(cell.state, next)) return false;
|
|
159
|
+
cell.state = next;
|
|
160
|
+
if (next !== "running" && next !== "detached") cell.terminal.resolve(this.#snapshot(cell));
|
|
161
|
+
if (cell.wasDetached && next !== "detached") {
|
|
162
|
+
if (this.#detachedByLanguage.get(cell.input.language) === cell)
|
|
163
|
+
this.#detachedByLanguage.delete(cell.input.language);
|
|
164
|
+
this.#queueNotification(cell);
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#queueNotification(cell: ManagedCell): void {
|
|
170
|
+
if (cell.notificationQueued) return;
|
|
171
|
+
cell.notificationQueued = true;
|
|
172
|
+
this.#notificationQueue.push(cell);
|
|
173
|
+
this.#scheduleNotificationFlush();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#scheduleNotificationFlush(): void {
|
|
177
|
+
if (this.#notificationFlush !== undefined) return;
|
|
178
|
+
const flush = Promise.resolve().then(async () => {
|
|
179
|
+
const cells = this.#notificationQueue.splice(0);
|
|
180
|
+
const notifications = await Promise.all(cells.map(async (candidate) => await this.#notification(candidate)));
|
|
181
|
+
this.#notifier?.notify(notifications);
|
|
182
|
+
});
|
|
183
|
+
this.#notificationFlush = flush;
|
|
184
|
+
void flush.then(
|
|
185
|
+
() => this.#finishNotificationFlush(flush),
|
|
186
|
+
() => this.#finishNotificationFlush(flush),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#finishNotificationFlush(flush: Promise<void>): void {
|
|
191
|
+
if (this.#notificationFlush !== flush) return;
|
|
192
|
+
this.#notificationFlush = undefined;
|
|
193
|
+
if (this.#notificationQueue.length > 0) this.#scheduleNotificationFlush();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #notification(cell: ManagedCell): Promise<EvalDetachedCellNotification> {
|
|
197
|
+
const snapshot = this.#snapshot(cell);
|
|
198
|
+
const body = notificationBody(snapshot);
|
|
199
|
+
const overflow = Buffer.byteLength(body, "utf8") > NOTIFICATION_TAIL_BYTES;
|
|
200
|
+
let spillNotice = "";
|
|
201
|
+
if (overflow && cell.spillPath !== undefined) {
|
|
202
|
+
try {
|
|
203
|
+
await mkdir(dirname(cell.spillPath), { recursive: true });
|
|
204
|
+
await writeFile(cell.spillPath, body, "utf8");
|
|
205
|
+
spillNotice = `\nBuffered output overflowed; full output: ${localUri(cell.spillPath, this.#artifactsDir)}`;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
spillNotice = `\nBuffered output overflow could not be spilled: ${message}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
cellId: cell.cellId,
|
|
213
|
+
content: `${overflow ? notificationPreview(snapshot) : body}${overflow ? "\n[…notification tail capped…]" : ""}${spillNotice}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#get(cellId: string): ManagedCell {
|
|
218
|
+
const cell = this.#cells.get(cellId);
|
|
219
|
+
if (cell === undefined) throw new Error(`Unknown detached eval cell "${cellId}"`);
|
|
220
|
+
return cell;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#snapshot(cell: ManagedCell): EvalDetachedCellSnapshot {
|
|
224
|
+
return {
|
|
225
|
+
cellId: cell.cellId,
|
|
226
|
+
language: cell.input.language,
|
|
227
|
+
state: cell.state,
|
|
228
|
+
outputTail: cell.outputTail?.() ?? "",
|
|
229
|
+
result: cell.result,
|
|
230
|
+
stateRetained: cell.stateRetained,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function allowsTransition(from: EvalDetachedCellState, to: EvalDetachedCellState): boolean {
|
|
236
|
+
if (from === "running") return to === "detached" || to === "completed" || to === "failed" || to === "cancelled";
|
|
237
|
+
if (from === "detached") return to === "completed" || to === "failed" || to === "cancelled";
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function errorResult(cell: ManagedCell, error: Error): AgentToolResult<EvalToolDetails> {
|
|
242
|
+
const output = cell.outputTail?.() ?? "";
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: output.length > 0 ? `${output}\n${error.message}` : error.message }],
|
|
245
|
+
details: {
|
|
246
|
+
language: cell.input.language,
|
|
247
|
+
languages: [cell.input.language],
|
|
248
|
+
durationMs: 0,
|
|
249
|
+
toolCalls: [],
|
|
250
|
+
truncated: false,
|
|
251
|
+
isError: true,
|
|
252
|
+
cells: [
|
|
253
|
+
{
|
|
254
|
+
index: 0,
|
|
255
|
+
code: cell.input.code,
|
|
256
|
+
language: cell.input.language,
|
|
257
|
+
output,
|
|
258
|
+
status: "error",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function notificationBody(cell: EvalDetachedCellSnapshot): string {
|
|
266
|
+
const outcome = cell.state === "completed" ? "completed" : cell.state === "cancelled" ? "cancelled" : "failed";
|
|
267
|
+
const resultText =
|
|
268
|
+
cell.result?.content
|
|
269
|
+
.filter((part) => part.type === "text")
|
|
270
|
+
.map((part) => part.text)
|
|
271
|
+
.join("\n") ?? cell.outputTail;
|
|
272
|
+
const stateNote =
|
|
273
|
+
cell.state === "cancelled" && cell.language === "js"
|
|
274
|
+
? "JavaScript worker was restarted; VM state was lost."
|
|
275
|
+
: cell.state === "cancelled" && cell.language === "py"
|
|
276
|
+
? "Python kernel was interrupted; its existing variables are preserved."
|
|
277
|
+
: "Kernel state updated - variables are available to the next eval cell.";
|
|
278
|
+
return [
|
|
279
|
+
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
|
|
280
|
+
resultText.length === 0 ? "(no output)" : resultText,
|
|
281
|
+
`${stateNote}</system-reminder>`,
|
|
282
|
+
].join("\n");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function notificationPreview(cell: EvalDetachedCellSnapshot): string {
|
|
286
|
+
const outcome = cell.state === "completed" ? "completed" : cell.state === "cancelled" ? "cancelled" : "failed";
|
|
287
|
+
const resultText =
|
|
288
|
+
cell.result?.content
|
|
289
|
+
.filter((part) => part.type === "text")
|
|
290
|
+
.map((part) => part.text)
|
|
291
|
+
.join("\n") ?? cell.outputTail;
|
|
292
|
+
const stateNote =
|
|
293
|
+
cell.state === "cancelled" && cell.language === "js"
|
|
294
|
+
? "JavaScript worker was restarted; VM state was lost."
|
|
295
|
+
: cell.state === "cancelled" && cell.language === "py"
|
|
296
|
+
? "Python kernel was interrupted; its existing variables are preserved."
|
|
297
|
+
: "Kernel state updated - variables are available to the next eval cell.";
|
|
298
|
+
return [
|
|
299
|
+
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
|
|
300
|
+
"Buffered output tail:",
|
|
301
|
+
truncateTailUtf8(resultText, NOTIFICATION_TAIL_BYTES),
|
|
302
|
+
`${stateNote}</system-reminder>`,
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function safeCellId(cellId: string): string {
|
|
307
|
+
return cellId.replace(/[^a-zA-Z0-9_-]/gu, "_");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function localUri(path: string, artifactsDir: string | undefined): string {
|
|
311
|
+
if (artifactsDir === undefined) return `local://${path}`;
|
|
312
|
+
const root = join(artifactsDir, "local");
|
|
313
|
+
return path.startsWith(`${root}/`) ? `local://${path.slice(root.length + 1)}` : `local://${path}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function truncateTailUtf8(text: string, maxBytes: number): string {
|
|
317
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
318
|
+
const bytes = Buffer.from(text, "utf8");
|
|
319
|
+
let start = bytes.length - maxBytes;
|
|
320
|
+
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
|
|
321
|
+
return bytes.subarray(start).toString("utf8");
|
|
322
|
+
}
|