@code-yeongyu/senpi-codemode 2026.7.26 → 2026.7.28-3
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 +45 -0
- package/README.md +21 -0
- package/package.json +3 -3
- package/src/bridge/reserved.ts +2 -0
- 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/runtime-factory.ts +3 -1
- package/src/index.ts +4 -0
- package/src/kernels/jl/prelude.jl +6 -0
- package/src/kernels/js/local-module-loader.ts +4 -0
- package/src/kernels/js/worker-runtime.js +6 -0
- package/src/kernels/py/prelude.py +10 -0
- package/src/kernels/rb/prelude.rb +6 -0
- package/src/prompt/eval-prompt.ts +5 -0
- package/src/tool/call-capture.ts +93 -0
- package/src/tool/cell-handler.ts +102 -26
- package/src/tool/detached-cell-manager.ts +10 -1
- package/src/tool/eval-tool.ts +3 -0
- package/src/tool/render.ts +47 -27
- package/src/tool/tool-widgets.ts +259 -0
- package/src/tool/types.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,51 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.7.28-3] - 2026-07-28
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Add nested tool-call widgets that render the real call shape of tools invoked from eval cells, with truthful status, duration, and sanitized previews ([#444](https://github.com/code-yeongyu/senpi/pull/444)).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
## [2026.7.28-2] - 2026-07-28
|
|
30
|
+
|
|
31
|
+
### Breaking Changes
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Start a fresh eval cell when a caller reuses the ID of a terminal cell, preventing completed or failed results from being replayed as though new code had executed ([#439](https://github.com/code-yeongyu/senpi/pull/439)).
|
|
40
|
+
- Omit the eval `took` duration when timing metadata is unavailable, avoiding misleading zero-duration status output for detached or restored cell results ([#439](https://github.com/code-yeongyu/senpi/pull/439)).
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
## [2026.7.28] - 2026-07-28
|
|
45
|
+
|
|
46
|
+
### Breaking Changes
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- Add `tool_schema()` and return parameter schemas from failed eval tool calls so cells can inspect and self-correct tool invocations ([#407](https://github.com/code-yeongyu/senpi/pull/407)).
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- Allow eval cells and extensions to activate named searchable tools lazily on the calling surface without globally widening the active tool set ([#408](https://github.com/code-yeongyu/senpi/pull/408)).
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
### Removed
|
|
59
|
+
|
|
15
60
|
## [2026.7.26] - 2026-07-26
|
|
16
61
|
|
|
17
62
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ options object and asynchronous helpers are `await`-able.
|
|
|
101
101
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
102
102
|
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. |
|
|
103
103
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline. |
|
|
104
|
+
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
|
|
104
105
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
|
105
106
|
| `agent(prompt, ...)` | Delegates to the configured active `taskTools.task` tool. Supports background handles and structured JSON results. |
|
|
106
107
|
| `output(ids, format?, offset?, limit?)` | Delegates transcript retrieval to the configured active `taskTools.output` tool. |
|
|
@@ -108,6 +109,11 @@ options object and asynchronous helpers are `await`-able.
|
|
|
108
109
|
| `pipeline(items, ...stages)` | Applies stages left to right with a barrier between stages. |
|
|
109
110
|
| `log(message)` / `phase(title)` | Emits progress text and starts a status phase. |
|
|
110
111
|
|
|
112
|
+
When a `tool.<name>()` call fails argument validation, the error delivered back
|
|
113
|
+
into the cell carries the tool's expected parameters, so the cell can correct the
|
|
114
|
+
arguments and retry instead of falling back to one-at-a-time tool calls.
|
|
115
|
+
`tool_schema()` exposes the same catalog up front.
|
|
116
|
+
|
|
111
117
|
`agent()` is available only when the configured task tool is active in the
|
|
112
118
|
session. `output()` similarly requires the configured task-output tool and
|
|
113
119
|
returns immediately: a running task reports its current status, while completed
|
|
@@ -179,3 +185,18 @@ Direct real-surface QA drivers live in `scripts/qa-*.ts`: kernel cells
|
|
|
179
185
|
(`qa-py-cell.ts`, `qa-js-cell.ts`, `qa-rb-cell.ts`, `qa-jl-cell.ts`), end-to-end
|
|
180
186
|
extension execution (`qa-e2e-eval.ts`), and renderer output
|
|
181
187
|
(`qa-render-dump.ts`).
|
|
188
|
+
|
|
189
|
+
### Nested tool-call widgets
|
|
190
|
+
|
|
191
|
+
When an eval cell invokes `tool.<name>(...)`, the result panel can render a
|
|
192
|
+
nested widget for the invoked tool. The widget captures bounded args, duration,
|
|
193
|
+
and a sanitized 160 code points result preview; the rendering path is
|
|
194
|
+
always-on and does not depend on any toggle or session flag.
|
|
195
|
+
|
|
196
|
+
The capture budget is fixed at 30 enriched calls per cell, with a 4096-character
|
|
197
|
+
serialized args budget. Previews are capped at 160 code points, and collapsed
|
|
198
|
+
widgets stay within the 8 lines collapsed widget budget.
|
|
199
|
+
|
|
200
|
+
Entries without args — including old sessions, reserved/completion rows, and
|
|
201
|
+
calls past the cap — render as plain rows. Edit renders a fallback row by
|
|
202
|
+
design, even when its args are present.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.28-3",
|
|
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": "7.29.7",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.28-3",
|
|
34
34
|
"typebox": "1.1.38"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@code-yeongyu/senpi": "*"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.7.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.7.28-3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/bridge/reserved.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
export const RESERVED_AGENT_TOOL = "__agent__" as const;
|
|
4
4
|
/** ADAPTATION: senpi delegates output through a reserved kernel-side tool name. */
|
|
5
5
|
export const RESERVED_OUTPUT_TOOL = "__output__" as const;
|
|
6
|
+
/** ADAPTATION: senpi resolves tool parameter schemas through a reserved kernel-side tool name. */
|
|
7
|
+
export const RESERVED_SCHEMA_TOOL = "__schema__" as const;
|
|
6
8
|
/** Canonical oh-my-pi eval-timeout pause operation. */
|
|
7
9
|
export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
|
|
8
10
|
/** Canonical oh-my-pi eval-timeout resume operation. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
|
+
import { RESERVED_AGENT_TOOL, RESERVED_OUTPUT_TOOL, RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
|
|
3
|
+
import type { EvalStatusEvent, ExecuteTool } from "../tool/types.ts";
|
|
4
|
+
import { type AgentExecuteTool, runEvalAgent } from "./agent-bridge.ts";
|
|
5
|
+
import { type MarshalledToolResult, type OutputExecuteTool, runEvalOutput } from "./output-bridge.ts";
|
|
6
|
+
import { type EvalSchemaToolInfo, runEvalSchema } from "./schema-bridge.ts";
|
|
7
|
+
|
|
8
|
+
export interface ReservedDispatchContext {
|
|
9
|
+
readonly callId: string;
|
|
10
|
+
readonly args: unknown;
|
|
11
|
+
readonly executeTool: AgentExecuteTool & OutputExecuteTool & ExecuteTool;
|
|
12
|
+
readonly taskToolName: string;
|
|
13
|
+
readonly taskOutputToolName: string;
|
|
14
|
+
readonly listTools: (() => readonly EvalSchemaToolInfo[]) | undefined;
|
|
15
|
+
readonly signal: AbortSignal | undefined;
|
|
16
|
+
readonly emitStatus: (event: EvalStatusEvent) => void;
|
|
17
|
+
readonly marshalToolResult: (result: AgentToolResult<unknown>) => MarshalledToolResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class SchemaUnavailableError extends Error {
|
|
21
|
+
readonly name = "SchemaUnavailableError";
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super("tool_schema() unavailable: this session does not expose a tool catalog");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isReservedToolName(toolName: string): boolean {
|
|
29
|
+
return toolName === RESERVED_AGENT_TOOL || toolName === RESERVED_OUTPUT_TOOL || toolName === RESERVED_SCHEMA_TOOL;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runReservedTool(toolName: string, context: ReservedDispatchContext): Promise<unknown> {
|
|
33
|
+
if (toolName === RESERVED_AGENT_TOOL) {
|
|
34
|
+
return await runEvalAgent(context.args, {
|
|
35
|
+
callId: context.callId,
|
|
36
|
+
taskToolName: context.taskToolName,
|
|
37
|
+
executeTool: context.executeTool,
|
|
38
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
39
|
+
emitStatus: context.emitStatus,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (toolName === RESERVED_OUTPUT_TOOL) {
|
|
43
|
+
return await runEvalOutput(context.args, {
|
|
44
|
+
taskOutputToolName: context.taskOutputToolName,
|
|
45
|
+
executeTool: context.executeTool,
|
|
46
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
47
|
+
marshalToolResult: context.marshalToolResult,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (toolName === RESERVED_SCHEMA_TOOL) {
|
|
51
|
+
const listTools = context.listTools;
|
|
52
|
+
if (listTools === undefined) throw new SchemaUnavailableError();
|
|
53
|
+
return runEvalSchema(context.args, { listTools });
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`runReservedTool received a non-reserved tool name: ${toolName}`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { Check, Errors } from "typebox/value";
|
|
3
|
+
|
|
4
|
+
const schemaArgsSchema = Type.Object(
|
|
5
|
+
{ name: Type.Optional(Type.String({ minLength: 1 })) },
|
|
6
|
+
{ additionalProperties: false },
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
type SchemaArgs = Static<typeof schemaArgsSchema>;
|
|
10
|
+
|
|
11
|
+
export interface EvalSchemaToolInfo {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description?: string | undefined;
|
|
14
|
+
readonly parameters?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunEvalSchemaOptions {
|
|
18
|
+
readonly listTools: () => readonly EvalSchemaToolInfo[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type EvalSchemaResult =
|
|
22
|
+
| { readonly tools: readonly string[] }
|
|
23
|
+
| { readonly name: string; readonly description: string | undefined; readonly parameters: unknown };
|
|
24
|
+
|
|
25
|
+
class SchemaArgumentsError extends Error {
|
|
26
|
+
readonly name = "SchemaArgumentsError";
|
|
27
|
+
|
|
28
|
+
constructor(summary: string) {
|
|
29
|
+
super(`schema() received invalid arguments: ${summary}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class SchemaUnknownToolError extends Error {
|
|
34
|
+
readonly name = "SchemaUnknownToolError";
|
|
35
|
+
|
|
36
|
+
constructor(requested: string, suggestions: readonly string[]) {
|
|
37
|
+
const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
38
|
+
super(`schema() found no tool named "${requested}".${hint}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runEvalSchema(args: unknown, options: RunEvalSchemaOptions): EvalSchemaResult {
|
|
43
|
+
const parsed = parseSchemaArgs(args);
|
|
44
|
+
const tools = options.listTools();
|
|
45
|
+
if (parsed.name === undefined) return { tools: tools.map((tool) => tool.name) };
|
|
46
|
+
|
|
47
|
+
const match = tools.find((tool) => tool.name === parsed.name);
|
|
48
|
+
if (match) return { name: match.name, description: match.description, parameters: match.parameters };
|
|
49
|
+
throw new SchemaUnknownToolError(parsed.name, nearestNames(parsed.name, tools));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseSchemaArgs(value: unknown): SchemaArgs {
|
|
53
|
+
if (Check(schemaArgsSchema, value)) return value;
|
|
54
|
+
const summary = Errors(schemaArgsSchema, value)
|
|
55
|
+
.map((error) => `${error.instancePath || "/"} ${error.message}`)
|
|
56
|
+
.join("; ");
|
|
57
|
+
throw new SchemaArgumentsError(summary || "invalid value");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nearestNames(requested: string, tools: readonly EvalSchemaToolInfo[]): readonly string[] {
|
|
61
|
+
const needle = requested.toLowerCase();
|
|
62
|
+
return tools
|
|
63
|
+
.map((tool) => tool.name)
|
|
64
|
+
.filter((name) => {
|
|
65
|
+
const candidate = name.toLowerCase();
|
|
66
|
+
return candidate.includes(needle) || needle.includes(candidate);
|
|
67
|
+
})
|
|
68
|
+
.slice(0, 5);
|
|
69
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const SCHEMA_HINT_MAX_CHARS = 1_200;
|
|
2
|
+
const SCHEMA_HINT_MAX_PROPERTIES = 30;
|
|
3
|
+
const DESCRIPTION_MAX_CHARS = 80;
|
|
4
|
+
const ENUM_MAX_VALUES = 8;
|
|
5
|
+
const NESTED_MAX_DEPTH = 2;
|
|
6
|
+
const HINT_HEADER = "Expected parameters:";
|
|
7
|
+
|
|
8
|
+
interface SchemaLike {
|
|
9
|
+
readonly type?: unknown;
|
|
10
|
+
readonly properties?: unknown;
|
|
11
|
+
readonly required?: unknown;
|
|
12
|
+
readonly items?: unknown;
|
|
13
|
+
readonly description?: unknown;
|
|
14
|
+
readonly enum?: unknown;
|
|
15
|
+
readonly const?: unknown;
|
|
16
|
+
readonly oneOf?: unknown;
|
|
17
|
+
readonly anyOf?: unknown;
|
|
18
|
+
readonly allOf?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function appendSchemaHint(message: string, toolName: string, schema: unknown): string {
|
|
22
|
+
const rendered = renderSchemaHint(toolName, schema);
|
|
23
|
+
if (rendered === undefined) return message;
|
|
24
|
+
return `${message}\n\n${HINT_HEADER}\n${rendered}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderSchemaHint(toolName: string, schema: unknown): string | undefined {
|
|
28
|
+
if (!isSchemaLike(schema)) return undefined;
|
|
29
|
+
const lines = renderObjectLines(schema, 0, 1);
|
|
30
|
+
if (lines.length === 0) return undefined;
|
|
31
|
+
return fitLines(lines, `[truncated; call tool_schema(${JSON.stringify(toolName)}) for the full schema]`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderObjectLines(schema: SchemaLike, indent: number, depth: number): readonly string[] {
|
|
35
|
+
const required = stringArray(schema.required);
|
|
36
|
+
const entries = propertyEntries(schema.properties);
|
|
37
|
+
if (entries.length === 0 && required.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
const ordered = [
|
|
40
|
+
...entries.filter(([name]) => required.includes(name)),
|
|
41
|
+
...entries.filter(([name]) => !required.includes(name)),
|
|
42
|
+
];
|
|
43
|
+
const pad = " ".repeat(indent + 1);
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
if (indent === 0 && required.length > 0) lines.push(`required: ${required.join(", ")}`);
|
|
46
|
+
|
|
47
|
+
const shown = ordered.slice(0, SCHEMA_HINT_MAX_PROPERTIES);
|
|
48
|
+
for (const [name, value] of shown) {
|
|
49
|
+
lines.push(`${pad}${name}${required.includes(name) ? "" : "?"}: ${describeProperty(value)}`);
|
|
50
|
+
lines.push(...nestedLines(value, indent, depth));
|
|
51
|
+
}
|
|
52
|
+
const hidden = ordered.length - shown.length;
|
|
53
|
+
if (hidden > 0) lines.push(`${pad}… ${hidden} more propert${hidden === 1 ? "y" : "ies"}`);
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nestedLines(value: unknown, indent: number, depth: number): readonly string[] {
|
|
58
|
+
if (depth >= NESTED_MAX_DEPTH || !isSchemaLike(value)) return [];
|
|
59
|
+
const nested = isSchemaLike(value.items) ? value.items : value;
|
|
60
|
+
if (propertyEntries(nested.properties).length === 0) return [];
|
|
61
|
+
return renderObjectLines(nested, indent + 1, depth + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function describeProperty(value: unknown): string {
|
|
65
|
+
if (!isSchemaLike(value)) return "unknown";
|
|
66
|
+
const parts = [typeLabel(value)];
|
|
67
|
+
const description = firstLine(value.description);
|
|
68
|
+
if (description !== undefined) parts.push(`— ${truncate(description, DESCRIPTION_MAX_CHARS)}`);
|
|
69
|
+
return parts.join(" ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function typeLabel(schema: SchemaLike): string {
|
|
73
|
+
if (Object.hasOwn(schema, "const")) return JSON.stringify(schema.const);
|
|
74
|
+
const enumValues = schema.enum;
|
|
75
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) return unionLabel(enumValues.map(literal));
|
|
76
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
77
|
+
if (Array.isArray(branches) && branches.length > 0) return unionLabel(branches.map(branchLabel));
|
|
78
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) return branchLabel(schema.allOf[0]);
|
|
79
|
+
|
|
80
|
+
const type = schema.type;
|
|
81
|
+
const base = typeof type === "string" ? type : Array.isArray(type) ? type.filter(isString).join("|") : "unknown";
|
|
82
|
+
if (base !== "array") return base;
|
|
83
|
+
return isSchemaLike(schema.items) ? `array<${typeLabel(schema.items)}>` : "array";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function unionLabel(values: readonly string[]): string {
|
|
87
|
+
const shown = values.slice(0, ENUM_MAX_VALUES).join(" | ");
|
|
88
|
+
return values.length > ENUM_MAX_VALUES ? `${shown} | … (${values.length - ENUM_MAX_VALUES} more)` : shown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function branchLabel(value: unknown): string {
|
|
92
|
+
return isSchemaLike(value) ? typeLabel(value) : "unknown";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function literal(value: unknown): string {
|
|
96
|
+
return JSON.stringify(value) ?? String(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fitLines(lines: readonly string[], marker: string): string {
|
|
100
|
+
const budget = SCHEMA_HINT_MAX_CHARS - HINT_HEADER.length - 1;
|
|
101
|
+
const kept: string[] = [];
|
|
102
|
+
let used = 0;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const next = used + line.length + (kept.length === 0 ? 0 : 1);
|
|
105
|
+
if (next > budget - marker.length - 1) {
|
|
106
|
+
kept.push(marker);
|
|
107
|
+
return kept.join("\n");
|
|
108
|
+
}
|
|
109
|
+
kept.push(line);
|
|
110
|
+
used = next;
|
|
111
|
+
}
|
|
112
|
+
return kept.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function propertyEntries(properties: unknown): readonly (readonly [string, unknown])[] {
|
|
116
|
+
if (typeof properties !== "object" || properties === null || Array.isArray(properties)) return [];
|
|
117
|
+
return Object.entries(properties);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stringArray(value: unknown): readonly string[] {
|
|
121
|
+
return Array.isArray(value) ? value.filter(isString) : [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function firstLine(value: unknown): string | undefined {
|
|
125
|
+
if (typeof value !== "string") return undefined;
|
|
126
|
+
const line = value.split("\n", 1)[0]?.trim();
|
|
127
|
+
return line === undefined || line.length === 0 ? undefined : line;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function truncate(value: string, maxChars: number): string {
|
|
131
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}…`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isSchemaLike(value: unknown): value is SchemaLike {
|
|
135
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isString(value: unknown): value is string {
|
|
139
|
+
return typeof value === "string";
|
|
140
|
+
}
|
|
@@ -87,8 +87,10 @@ export async function createRuntime(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
90
|
+
// A cell names tools directly and cannot run tool_search first, so eval opts in to
|
|
91
|
+
// lazy activation. Eligibility still belongs to the extension that registered the tool.
|
|
90
92
|
const executeTool: AgentExecuteTool = (toolName, params, executeOptions) =>
|
|
91
|
-
pi.executeTool(toolName, params, executeOptions);
|
|
93
|
+
pi.executeTool(toolName, params, { ...executeOptions, activateInactiveTool: true });
|
|
92
94
|
return Object.assign(executeTool, {
|
|
93
95
|
isToolAvailable: (name: string): boolean => activeTools?.has(name) ?? pi.getActiveTools().includes(name),
|
|
94
96
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
3
|
import type { AgentExecuteTool } from "./bridges/agent-bridge.ts";
|
|
4
|
+
import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
4
5
|
import { type CompletionRequest, type CompletionResult, createCompletionHandler } from "./completion/handler.ts";
|
|
5
6
|
import { defaultCodemodeSettings } from "./config/settings.ts";
|
|
6
7
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
@@ -33,6 +34,7 @@ export interface CodemodeExtensionAPI {
|
|
|
33
34
|
on(event: CodemodeEvent, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
34
35
|
executeTool: AgentExecuteTool;
|
|
35
36
|
getActiveTools(): string[];
|
|
37
|
+
getAllTools(): readonly EvalSchemaToolInfo[];
|
|
36
38
|
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -67,6 +69,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
67
69
|
kernelManager: manager,
|
|
68
70
|
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
69
71
|
executeTool: runtime.executeTool,
|
|
72
|
+
listTools: () => pi.getAllTools(),
|
|
70
73
|
complete,
|
|
71
74
|
settings: runtime.settings,
|
|
72
75
|
artifactsDir: runtime.artifactsDir,
|
|
@@ -95,6 +98,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
95
98
|
kernelManager: manager,
|
|
96
99
|
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
97
100
|
executeTool: createExecuteTool(pi),
|
|
101
|
+
listTools: () => pi.getAllTools(),
|
|
98
102
|
complete,
|
|
99
103
|
settings: defaultCodemodeSettings,
|
|
100
104
|
cellManager: new EvalDetachedCellManager({ notifier }),
|
|
@@ -194,6 +194,12 @@ function completion(prompt::AbstractString; model="default", system=nothing, sch
|
|
|
194
194
|
get(response, "text", response)
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
function tool_schema(name=nothing)
|
|
198
|
+
arguments = Dict{String, Any}()
|
|
199
|
+
name !== nothing && (arguments["name"] = string(name))
|
|
200
|
+
senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__schema__", arguments))
|
|
201
|
+
end
|
|
202
|
+
|
|
197
203
|
function output(ids...; format="raw", offset=nothing, limit=nothing)
|
|
198
204
|
isempty(ids) && error("At least one output ID is required")
|
|
199
205
|
format in ("raw", "tail") || error("output() format must be 'raw' or 'tail'")
|
|
@@ -4,6 +4,7 @@ import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
|
|
|
4
4
|
import {
|
|
5
5
|
RESERVED_AGENT_TOOL,
|
|
6
6
|
RESERVED_OUTPUT_TOOL,
|
|
7
|
+
RESERVED_SCHEMA_TOOL,
|
|
7
8
|
TIMEOUT_PAUSE_OP,
|
|
8
9
|
TIMEOUT_RESUME_OP,
|
|
9
10
|
} from "../../bridge/reserved.ts";
|
|
@@ -33,6 +34,7 @@ type RuntimeModuleContext = {
|
|
|
33
34
|
readonly cwdUrl: string;
|
|
34
35
|
readonly localRootUrls: Readonly<Record<string, string>>;
|
|
35
36
|
readonly reservedAgentTool: string;
|
|
37
|
+
readonly reservedSchemaTool: string;
|
|
36
38
|
readonly reservedOutputTool: string;
|
|
37
39
|
readonly timeoutPauseOp: string;
|
|
38
40
|
readonly timeoutResumeOp: string;
|
|
@@ -55,6 +57,7 @@ function runtimeContext(options: LocalModuleLoaderOptions): RuntimeModuleContext
|
|
|
55
57
|
localRootUrls: roots,
|
|
56
58
|
reservedAgentTool: RESERVED_AGENT_TOOL,
|
|
57
59
|
reservedOutputTool: RESERVED_OUTPUT_TOOL,
|
|
60
|
+
reservedSchemaTool: RESERVED_SCHEMA_TOOL,
|
|
58
61
|
timeoutPauseOp: TIMEOUT_PAUSE_OP,
|
|
59
62
|
timeoutResumeOp: TIMEOUT_RESUME_OP,
|
|
60
63
|
};
|
|
@@ -66,6 +69,7 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
66
69
|
`globalThis.__senpi_module_context__ = ${serialized};`,
|
|
67
70
|
"globalThis.__senpi_reserved_agent_tool__ = globalThis.__senpi_module_context__.reservedAgentTool;",
|
|
68
71
|
"globalThis.__senpi_reserved_output_tool__ = globalThis.__senpi_module_context__.reservedOutputTool;",
|
|
72
|
+
"globalThis.__senpi_reserved_schema_tool__ = globalThis.__senpi_module_context__.reservedSchemaTool;",
|
|
69
73
|
"globalThis.__senpi_timeout_pause_op__ = globalThis.__senpi_module_context__.timeoutPauseOp;",
|
|
70
74
|
"globalThis.__senpi_timeout_resume_op__ = globalThis.__senpi_module_context__.timeoutResumeOp;",
|
|
71
75
|
"globalThis.__senpi_import__ = async (source, options) => {",
|
|
@@ -50,6 +50,7 @@ export class JsWorkerRuntime {
|
|
|
50
50
|
globalThis.read = async (path, options, ...rest) => await this.#read(path, helperOptions("read", options, rest));
|
|
51
51
|
globalThis.write = async (path, content) => await this.#write(path, content);
|
|
52
52
|
globalThis.output = async (...args) => await this.#output(args);
|
|
53
|
+
globalThis.tool_schema = async name => await this.#toolSchema(name);
|
|
53
54
|
globalThis.agent = async (prompt, options, ...rest) => await this.#agent(prompt, options, rest);
|
|
54
55
|
globalThis.parallel = async thunks => await this.#parallel(thunks);
|
|
55
56
|
globalThis.pipeline = async (items, ...stages) => await this.#pipeline(items, stages);
|
|
@@ -250,6 +251,11 @@ export class JsWorkerRuntime {
|
|
|
250
251
|
return node;
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
async #toolSchema(name) {
|
|
255
|
+
const args = name === undefined || name === null ? {} : { name: String(name) };
|
|
256
|
+
return await this.#callTool(reservedTool("__senpi_reserved_schema_tool__", "tool_schema"), args);
|
|
257
|
+
}
|
|
258
|
+
|
|
253
259
|
async #callTool(toolName, args) {
|
|
254
260
|
const hooks = this.#hooks;
|
|
255
261
|
if (!hooks) throw new Error("tool call outside active JS cell");
|
|
@@ -37,6 +37,7 @@ EMIT_LOCK = Lock()
|
|
|
37
37
|
# Mirrors src/bridge/reserved.ts; this standalone subprocess asset cannot import TypeScript.
|
|
38
38
|
RESERVED_AGENT_TOOL = "__agent__"
|
|
39
39
|
RESERVED_OUTPUT_TOOL = "__output__"
|
|
40
|
+
RESERVED_SCHEMA_TOOL = "__schema__"
|
|
40
41
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
41
42
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
42
43
|
|
|
@@ -374,6 +375,14 @@ def completion(
|
|
|
374
375
|
return response.get("text", response)
|
|
375
376
|
|
|
376
377
|
|
|
378
|
+
def tool_schema(name: str | None = None) -> Any:
|
|
379
|
+
args: dict[str, Any] = {} if name is None else {"name": name}
|
|
380
|
+
return bridge_post(
|
|
381
|
+
"/call",
|
|
382
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_SCHEMA_TOOL, "args": args},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
377
386
|
def output(
|
|
378
387
|
*ids: str,
|
|
379
388
|
format: str = "raw",
|
|
@@ -844,6 +853,7 @@ USER_NS.update(
|
|
|
844
853
|
"completion": completion,
|
|
845
854
|
"agent": agent,
|
|
846
855
|
"output": output,
|
|
856
|
+
"tool_schema": tool_schema,
|
|
847
857
|
"__senpi_magic": _magic,
|
|
848
858
|
"__senpi_magic_cell": _magic_cell,
|
|
849
859
|
"__senpi_shell": _shell,
|
|
@@ -5,6 +5,7 @@ require "uri"
|
|
|
5
5
|
|
|
6
6
|
SENPI_RESERVED_AGENT_TOOL = "__agent__"
|
|
7
7
|
SENPI_RESERVED_OUTPUT_TOOL = "__output__"
|
|
8
|
+
SENPI_RESERVED_SCHEMA_TOOL = "__schema__"
|
|
8
9
|
SENPI_INTERNAL_URL = Regexp.new("\\A([a-z][a-z0-9+.\\-]*)://(.*)\\z", Regexp::IGNORECASE)
|
|
9
10
|
|
|
10
11
|
def __senpi_status_enabled?
|
|
@@ -187,6 +188,11 @@ def completion(prompt, model: "default", system: nil, schema: nil, **kwargs)
|
|
|
187
188
|
result.fetch("text", result)
|
|
188
189
|
end
|
|
189
190
|
|
|
191
|
+
def tool_schema(name = nil)
|
|
192
|
+
args = name.nil? ? {} : { "name" => name.to_s }
|
|
193
|
+
__senpi_call_tool(SENPI_RESERVED_SCHEMA_TOOL, args)
|
|
194
|
+
end
|
|
195
|
+
|
|
190
196
|
def output(*ids, format: "raw", offset: nil, limit: nil)
|
|
191
197
|
raise ArgumentError, "At least one output ID is required" if ids.empty?
|
|
192
198
|
raise ArgumentError, "output() format must be 'raw' or 'tail'" unless ["raw", "tail"].include?(format)
|
|
@@ -154,6 +154,11 @@ env(key?=None, value?=None) → str | None | dict
|
|
|
154
154
|
Task/agent output by id. Reads immediately: running tasks return their status; \`format\` selects full (\`"raw"\`) or trailing (\`"tail"\`) output.
|
|
155
155
|
{{/if}}tool.<name>(args) → unknown
|
|
156
156
|
Invoke any session tool; \`args\` = its parameter object.
|
|
157
|
+
tool_schema(name?) → dict
|
|
158
|
+
Parameter schema of a tool without calling it; omit \`name\` to list tool names.
|
|
159
|
+
Use it before calling a tool you have not called before — a failed call also
|
|
160
|
+
returns the expected parameters, so fix the args and retry in the next cell
|
|
161
|
+
instead of abandoning eval.
|
|
157
162
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
158
163
|
Oneshot, stateless (no history/tools). \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → structured output, parsed object.
|
|
159
164
|
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type AgentToolResult, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
|
|
2
|
+
|
|
3
|
+
export const MAX_ENRICHED_TOOL_CALLS = 30;
|
|
4
|
+
|
|
5
|
+
const MAX_ARGUMENT_STRING_CODE_POINTS = 512;
|
|
6
|
+
const MAX_ARGUMENT_ENTRIES = 32;
|
|
7
|
+
const MAX_ARGUMENT_DEPTH = 6;
|
|
8
|
+
const MAX_SERIALIZED_ARGUMENT_LENGTH = 4096;
|
|
9
|
+
const MAX_RESULT_PREVIEW_CODE_POINTS = 160;
|
|
10
|
+
|
|
11
|
+
type BoundedValue = {
|
|
12
|
+
readonly value: unknown;
|
|
13
|
+
readonly truncated: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function capCodePoints(text: string, max: number): string {
|
|
17
|
+
let end = 0;
|
|
18
|
+
let count = 0;
|
|
19
|
+
while (count < max && end < text.length) {
|
|
20
|
+
const firstCodeUnit = text.charCodeAt(end);
|
|
21
|
+
const secondCodeUnit = text.charCodeAt(end + 1);
|
|
22
|
+
const isSurrogatePair =
|
|
23
|
+
firstCodeUnit >= 0xd800 && firstCodeUnit <= 0xdbff && secondCodeUnit >= 0xdc00 && secondCodeUnit <= 0xdfff;
|
|
24
|
+
end += isSurrogatePair ? 2 : 1;
|
|
25
|
+
count += 1;
|
|
26
|
+
}
|
|
27
|
+
return end === text.length ? text : `${text.slice(0, end)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function boundValue(value: unknown, depth: number, ancestors: WeakSet<object>): BoundedValue {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
const capped = capCodePoints(value, MAX_ARGUMENT_STRING_CODE_POINTS);
|
|
33
|
+
return { value: capped, truncated: capped !== value };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (value === null || typeof value !== "object") return { value, truncated: false };
|
|
37
|
+
if (depth >= MAX_ARGUMENT_DEPTH) return { value: "…", truncated: true };
|
|
38
|
+
if (ancestors.has(value)) throw new Error("cyclic tool-call arguments");
|
|
39
|
+
|
|
40
|
+
ancestors.add(value);
|
|
41
|
+
try {
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
const retainedLength = Math.min(value.length, MAX_ARGUMENT_ENTRIES);
|
|
44
|
+
const clone: unknown[] = [];
|
|
45
|
+
let truncated = value.length > MAX_ARGUMENT_ENTRIES;
|
|
46
|
+
for (let index = 0; index < retainedLength; index += 1) {
|
|
47
|
+
const bounded = boundValue(value[index], depth + 1, ancestors);
|
|
48
|
+
clone.push(bounded.value);
|
|
49
|
+
truncated ||= bounded.truncated;
|
|
50
|
+
}
|
|
51
|
+
return { value: clone, truncated };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries: [string, unknown][] = [];
|
|
55
|
+
let truncated = false;
|
|
56
|
+
let count = 0;
|
|
57
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
58
|
+
if (count >= MAX_ARGUMENT_ENTRIES) {
|
|
59
|
+
truncated = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const bounded = boundValue(nestedValue, depth + 1, ancestors);
|
|
63
|
+
entries.push([key, bounded.value]);
|
|
64
|
+
truncated ||= bounded.truncated;
|
|
65
|
+
count += 1;
|
|
66
|
+
}
|
|
67
|
+
return { value: Object.fromEntries(entries), truncated };
|
|
68
|
+
} finally {
|
|
69
|
+
ancestors.delete(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function boundToolCallArgs(args: unknown): { args: unknown; truncated: boolean } {
|
|
74
|
+
try {
|
|
75
|
+
const bounded = boundValue(args, 0, new WeakSet<object>());
|
|
76
|
+
const serialized = JSON.stringify(bounded.value);
|
|
77
|
+
if (typeof serialized !== "string" || serialized.length > MAX_SERIALIZED_ARGUMENT_LENGTH) {
|
|
78
|
+
return { args: undefined, truncated: true };
|
|
79
|
+
}
|
|
80
|
+
return { args: bounded.value, truncated: bounded.truncated };
|
|
81
|
+
} catch {
|
|
82
|
+
return { args: undefined, truncated: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function toolCallResultPreview(result: AgentToolResult<unknown>): string | undefined {
|
|
87
|
+
for (const part of result.content) {
|
|
88
|
+
if (part.type !== "text") continue;
|
|
89
|
+
const preview = sanitizeTerminalLabel(part.text).replace(/\s+/gu, " ").trim();
|
|
90
|
+
return preview.length === 0 ? undefined : capCodePoints(preview, MAX_RESULT_PREVIEW_CODE_POINTS);
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type AgentToolResult,
|
|
3
|
+
type AgentToolUpdateCallback,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
sanitizeTerminalLabel,
|
|
6
|
+
} from "@code-yeongyu/senpi";
|
|
2
7
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
9
|
+
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
10
|
+
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
11
|
+
import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
6
12
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
7
13
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
8
14
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
15
|
+
import { boundToolCallArgs, capCodePoints, MAX_ENRICHED_TOOL_CALLS, toolCallResultPreview } from "./call-capture.ts";
|
|
9
16
|
import {
|
|
10
17
|
type EvalImageResizer,
|
|
11
18
|
EvalOutputCollector,
|
|
@@ -16,7 +23,19 @@ import {
|
|
|
16
23
|
import { upsertStatusEvent } from "./status-events.ts";
|
|
17
24
|
import type { EvalKernel, EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
18
25
|
|
|
19
|
-
type ResolvedToolReply = {
|
|
26
|
+
type ResolvedToolReply = {
|
|
27
|
+
readonly value: unknown;
|
|
28
|
+
readonly toolCallOk: boolean;
|
|
29
|
+
readonly resultPreview?: string;
|
|
30
|
+
readonly errorText?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ToolCallEnrichment = {
|
|
34
|
+
readonly callId: string;
|
|
35
|
+
readonly args: unknown;
|
|
36
|
+
readonly startedAt: number;
|
|
37
|
+
readonly argsTruncated?: true;
|
|
38
|
+
};
|
|
20
39
|
|
|
21
40
|
const LIVE_OUTPUT_PREVIEW_LINES = 8;
|
|
22
41
|
|
|
@@ -36,6 +55,7 @@ export interface CellState {
|
|
|
36
55
|
|
|
37
56
|
export interface CellBridgeRuntime {
|
|
38
57
|
readonly executeTool: AgentExecuteTool;
|
|
58
|
+
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
39
59
|
readonly settings: ResolvedCodemodeSettings;
|
|
40
60
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
41
61
|
readonly ctx: ExtensionContext;
|
|
@@ -151,25 +171,17 @@ export class CellHandler {
|
|
|
151
171
|
});
|
|
152
172
|
return;
|
|
153
173
|
}
|
|
154
|
-
if (message.toolName
|
|
174
|
+
if (isReservedToolName(message.toolName)) {
|
|
155
175
|
await this.#deliverToolReply(message, async () => ({
|
|
156
|
-
value: await
|
|
176
|
+
value: await runReservedTool(message.toolName, {
|
|
157
177
|
callId: message.callId,
|
|
158
|
-
|
|
178
|
+
args: message.args,
|
|
159
179
|
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, {
|
|
180
|
+
taskToolName: this.#runtime.settings.taskTools.task,
|
|
170
181
|
taskOutputToolName: this.#runtime.settings.taskTools.output,
|
|
171
|
-
|
|
182
|
+
listTools: this.#runtime.listTools,
|
|
172
183
|
signal: this.#state.signal,
|
|
184
|
+
emitStatus: (event) => this.#recordStatus(event),
|
|
173
185
|
marshalToolResult,
|
|
174
186
|
}),
|
|
175
187
|
toolCallOk: true,
|
|
@@ -193,25 +205,62 @@ export class CellHandler {
|
|
|
193
205
|
this.#emitUpdate(false);
|
|
194
206
|
return;
|
|
195
207
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
const capturedArgs = boundToolCallArgs(message.args);
|
|
209
|
+
const startedAt = Date.now();
|
|
210
|
+
await this.#deliverToolReply(
|
|
211
|
+
message,
|
|
212
|
+
async () => {
|
|
213
|
+
const result = await this.#runtime.executeTool(message.toolName, message.args, {
|
|
214
|
+
signal: this.#state.signal,
|
|
215
|
+
});
|
|
216
|
+
const toolCallOk = !toolResultIsError(result);
|
|
217
|
+
if (toolCallOk) {
|
|
218
|
+
const resultPreview = toolCallResultPreview(result);
|
|
219
|
+
return {
|
|
220
|
+
value: marshalToolResult(result),
|
|
221
|
+
toolCallOk,
|
|
222
|
+
...(resultPreview === undefined ? {} : { resultPreview }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
let errorText: string | undefined;
|
|
226
|
+
for (const part of result.content) {
|
|
227
|
+
if (part.type !== "text") continue;
|
|
228
|
+
errorText = capCodePoints(sanitizeTerminalLabel(part.text), 512);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
value: marshalToolResult(result),
|
|
233
|
+
toolCallOk,
|
|
234
|
+
...(errorText === undefined ? {} : { errorText }),
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
callId: message.callId,
|
|
239
|
+
args: capturedArgs.args,
|
|
240
|
+
startedAt,
|
|
241
|
+
...(capturedArgs.truncated ? { argsTruncated: true } : {}),
|
|
242
|
+
},
|
|
243
|
+
);
|
|
200
244
|
}
|
|
201
245
|
|
|
202
246
|
async #deliverToolReply(
|
|
203
247
|
message: Extract<KernelToHostMessage, { type: "tool-call" }>,
|
|
204
248
|
resolve: () => Promise<ResolvedToolReply>,
|
|
249
|
+
enrich?: ToolCallEnrichment,
|
|
205
250
|
): Promise<void> {
|
|
206
251
|
try {
|
|
207
252
|
const reply = await resolve();
|
|
208
253
|
if (!this.#state.active) return;
|
|
209
|
-
this.#
|
|
254
|
+
this.#pushToolCall(message.toolName, reply.toolCallOk, enrich, reply.resultPreview, reply.errorText);
|
|
210
255
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
211
256
|
} catch (error) {
|
|
212
257
|
if (!this.#state.active) return;
|
|
213
|
-
const text =
|
|
214
|
-
|
|
258
|
+
const text = appendSchemaHint(
|
|
259
|
+
error instanceof Error ? error.message : String(error),
|
|
260
|
+
message.toolName,
|
|
261
|
+
this.#toolParameters(message.toolName),
|
|
262
|
+
);
|
|
263
|
+
this.#pushToolCall(message.toolName, false, enrich, undefined, text);
|
|
215
264
|
this.#kernel.deliverToolReply({
|
|
216
265
|
type: "tool-reply",
|
|
217
266
|
callId: message.callId,
|
|
@@ -222,6 +271,33 @@ export class CellHandler {
|
|
|
222
271
|
this.#emitUpdate(false);
|
|
223
272
|
}
|
|
224
273
|
|
|
274
|
+
#pushToolCall(
|
|
275
|
+
name: string,
|
|
276
|
+
ok: boolean,
|
|
277
|
+
enrich: ToolCallEnrichment | undefined,
|
|
278
|
+
resultPreview: string | undefined,
|
|
279
|
+
error: string | undefined,
|
|
280
|
+
): void {
|
|
281
|
+
const summary = { name, ok, ...(error === undefined ? {} : { error }) };
|
|
282
|
+
const enrichedCount = this.#state.toolCalls.filter((toolCall) => toolCall.callId !== undefined).length;
|
|
283
|
+
if (enrich === undefined || enrichedCount >= MAX_ENRICHED_TOOL_CALLS) {
|
|
284
|
+
this.#state.toolCalls.push(summary);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.#state.toolCalls.push({
|
|
288
|
+
...summary,
|
|
289
|
+
callId: enrich.callId,
|
|
290
|
+
args: enrich.args,
|
|
291
|
+
durationMs: Date.now() - enrich.startedAt,
|
|
292
|
+
...(enrich.argsTruncated === true ? { argsTruncated: true } : {}),
|
|
293
|
+
...(resultPreview === undefined ? {} : { resultPreview }),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#toolParameters(toolName: string): unknown {
|
|
298
|
+
return this.#runtime.listTools?.().find((tool) => tool.name === toolName)?.parameters;
|
|
299
|
+
}
|
|
300
|
+
|
|
225
301
|
#recordStatus(event: EvalStatusEvent): void {
|
|
226
302
|
if (!this.#runtime.settings.statusEvents) return;
|
|
227
303
|
upsertStatusEvent(this.#state.statusEvents, event);
|
|
@@ -68,7 +68,10 @@ export class EvalDetachedCellManager {
|
|
|
68
68
|
|
|
69
69
|
create(cellId: string, input: EvalToolInput): ManagedCell {
|
|
70
70
|
const existing = this.#cells.get(cellId);
|
|
71
|
-
if (existing !== undefined)
|
|
71
|
+
if (existing !== undefined) {
|
|
72
|
+
if (existing.state === "running" || existing.state === "detached") throw activeCellReuseError(existing);
|
|
73
|
+
this.#cells.delete(cellId);
|
|
74
|
+
}
|
|
72
75
|
const spillPath =
|
|
73
76
|
this.#artifactsDir === undefined
|
|
74
77
|
? undefined
|
|
@@ -232,6 +235,12 @@ export class EvalDetachedCellManager {
|
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
function activeCellReuseError(cell: ManagedCell): Error {
|
|
239
|
+
return new Error(
|
|
240
|
+
`Eval cell ${cell.cellId} from a previous call is still ${cell.state} in the ${cell.input.language} kernel. Use eval({ action: "peek", cell_id: "${cell.cellId}" }) to read it or eval({ action: "stop", cell_id: "${cell.cellId}" }) to end it before its id can be reused.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
function allowsTransition(from: EvalDetachedCellState, to: EvalDetachedCellState): boolean {
|
|
236
245
|
if (from === "running") return to === "detached" || to === "completed" || to === "failed" || to === "cancelled";
|
|
237
246
|
if (from === "detached") return to === "completed" || to === "failed" || to === "cancelled";
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
|
|
4
|
+
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
4
5
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
5
6
|
import { defaultCodemodeSettings, type ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
6
7
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
@@ -37,6 +38,7 @@ export interface CreateEvalToolOptions {
|
|
|
37
38
|
readonly kernelManager: EvalKernelManager;
|
|
38
39
|
readonly cellTimeoutSeconds: number;
|
|
39
40
|
readonly executeTool: ExecuteTool;
|
|
41
|
+
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
40
42
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
41
43
|
readonly settings?: ResolvedCodemodeSettings;
|
|
42
44
|
readonly artifactsDir?: string;
|
|
@@ -351,6 +353,7 @@ async function executeCell(
|
|
|
351
353
|
execution.setKernel(kernel);
|
|
352
354
|
handler = new CellHandler(kernel, state, {
|
|
353
355
|
executeTool: options.executeTool,
|
|
356
|
+
...(options.listTools === undefined ? {} : { listTools: options.listTools }),
|
|
354
357
|
settings: options.settings ?? defaultCodemodeSettings,
|
|
355
358
|
...(options.complete === undefined ? {} : { complete: options.complete }),
|
|
356
359
|
ctx: bridgeContext,
|
package/src/tool/render.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
20
20
|
renderJsonTreeLines,
|
|
21
21
|
} from "./json-tree.ts";
|
|
22
|
+
import { codePointPrefix, formatDuration, renderToolCallWidget } from "./tool-widgets.ts";
|
|
22
23
|
import type {
|
|
23
24
|
EvalCellResult,
|
|
24
25
|
EvalInputSchema,
|
|
@@ -116,18 +117,6 @@ function appendLines(target: string[], source: readonly string[]): void {
|
|
|
116
117
|
for (const line of source) target.push(line);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function codePointPrefix(text: string, maxCodePoints: number): string {
|
|
120
|
-
let end = 0;
|
|
121
|
-
for (let count = 0; count < maxCodePoints && end < text.length; count += 1) {
|
|
122
|
-
const firstCodeUnit = text.charCodeAt(end);
|
|
123
|
-
const secondCodeUnit = text.charCodeAt(end + 1);
|
|
124
|
-
const isSurrogatePair =
|
|
125
|
-
firstCodeUnit >= 0xd800 && firstCodeUnit <= 0xdbff && secondCodeUnit >= 0xdc00 && secondCodeUnit <= 0xdfff;
|
|
126
|
-
end += isSurrogatePair ? 2 : 1;
|
|
127
|
-
}
|
|
128
|
-
return text.slice(0, end);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
120
|
function renderAllVisualLines(text: string, width: number): string[] {
|
|
132
121
|
return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
|
|
133
122
|
}
|
|
@@ -232,18 +221,6 @@ function highlightedCode(code: string, language: EvalLanguage, theme: Theme | un
|
|
|
232
221
|
return (theme === undefined ? lines.map((line) => line.replace(/\u001b\[[0-9;]*m/gu, "")) : lines).join("\n");
|
|
233
222
|
}
|
|
234
223
|
|
|
235
|
-
function formatDuration(milliseconds: number): string {
|
|
236
|
-
const totalSeconds = Math.floor(Math.max(0, milliseconds) / 1_000);
|
|
237
|
-
if (totalSeconds < 1) return "<1s";
|
|
238
|
-
const seconds = totalSeconds % 60;
|
|
239
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
240
|
-
if (totalMinutes < 1) return `${seconds}s`;
|
|
241
|
-
const minutes = totalMinutes % 60;
|
|
242
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
243
|
-
if (hours < 1) return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
244
|
-
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
224
|
function spinner(frame: number | undefined): string {
|
|
248
225
|
return SPINNER_FRAMES.at((frame ?? 0) % SPINNER_FRAMES.length) ?? SPINNER_FRAMES[0];
|
|
249
226
|
}
|
|
@@ -695,6 +672,46 @@ function toolCallRows(details: EvalToolDetails | undefined): ToolCallRow[] {
|
|
|
695
672
|
});
|
|
696
673
|
}
|
|
697
674
|
|
|
675
|
+
function nestedToolCallBlock(
|
|
676
|
+
details: EvalToolDetails | undefined,
|
|
677
|
+
theme: Theme | undefined,
|
|
678
|
+
cwd: string,
|
|
679
|
+
expanded: boolean,
|
|
680
|
+
): RenderBlock | undefined {
|
|
681
|
+
const toolCalls = details?.toolCalls;
|
|
682
|
+
if (theme === undefined || toolCalls === undefined || !toolCalls.some((call) => call.args !== undefined)) {
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
const legacyRows = toolCallRows(details);
|
|
686
|
+
return {
|
|
687
|
+
kind: "dynamic",
|
|
688
|
+
render: (width) => {
|
|
689
|
+
const retainedCalls = expanded ? toolCalls : toolCalls.slice(-TOOL_CALL_PREVIEW_COUNT);
|
|
690
|
+
const retainedRows = expanded ? legacyRows : legacyRows.slice(-TOOL_CALL_PREVIEW_COUNT);
|
|
691
|
+
const skippedCount = toolCalls.length - retainedCalls.length;
|
|
692
|
+
const toolCallNoun = skippedCount === 1 ? "call" : "calls";
|
|
693
|
+
const lines =
|
|
694
|
+
expanded || skippedCount === 0
|
|
695
|
+
? []
|
|
696
|
+
: renderAllVisualLines(style(theme, "muted", `${skippedCount} earlier tool ${toolCallNoun}`), width);
|
|
697
|
+
const legacyBlock: Extract<RenderBlock, { kind: "toolCalls" }> = {
|
|
698
|
+
kind: "toolCalls",
|
|
699
|
+
calls: retainedRows,
|
|
700
|
+
expanded,
|
|
701
|
+
theme,
|
|
702
|
+
};
|
|
703
|
+
for (const [index, call] of retainedCalls.entries()) {
|
|
704
|
+
if (call.args !== undefined) {
|
|
705
|
+
appendLines(lines, renderToolCallWidget(call, { cwd, theme, expanded, width }));
|
|
706
|
+
} else {
|
|
707
|
+
appendLines(lines, renderToolCall(retainedRows[index], legacyBlock, width));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return lines;
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
698
715
|
function resultStatus(
|
|
699
716
|
details: EvalToolDetails | undefined,
|
|
700
717
|
options: ToolRenderResultOptions,
|
|
@@ -732,7 +749,7 @@ function resultMetadata(
|
|
|
732
749
|
): RenderBlock[] {
|
|
733
750
|
const metadata: string[] = [];
|
|
734
751
|
if (details?.phase) metadata.push(`phase ${details.phase}`);
|
|
735
|
-
if (!options.isPartial && details) metadata.push(`took ${details.durationMs}ms`);
|
|
752
|
+
if (!options.isPartial && typeof details?.durationMs === "number") metadata.push(`took ${details.durationMs}ms`);
|
|
736
753
|
if (metadata.length === 0) return [];
|
|
737
754
|
return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
|
|
738
755
|
}
|
|
@@ -824,7 +841,9 @@ export function renderEvalResult(
|
|
|
824
841
|
},
|
|
825
842
|
];
|
|
826
843
|
const calls = toolCallRows(details);
|
|
827
|
-
|
|
844
|
+
const nestedCalls = nestedToolCallBlock(details, theme, context.cwd, expanded);
|
|
845
|
+
if (calls.length > 0)
|
|
846
|
+
blocks.push({ kind: "blank" }, nestedCalls ?? { kind: "toolCalls", calls, expanded, theme });
|
|
828
847
|
component.setBlocks(blocks);
|
|
829
848
|
return component;
|
|
830
849
|
}
|
|
@@ -888,7 +907,8 @@ export function renderEvalResult(
|
|
|
888
907
|
);
|
|
889
908
|
}
|
|
890
909
|
const calls = toolCallRows(details);
|
|
891
|
-
|
|
910
|
+
const nestedCalls = nestedToolCallBlock(details, theme, context.cwd, expanded);
|
|
911
|
+
if (calls.length > 0) blocks.push({ kind: "blank" }, nestedCalls ?? { kind: "toolCalls", calls, expanded, theme });
|
|
892
912
|
if (details?.notice !== undefined)
|
|
893
913
|
blocks.push({ kind: "blank" }, { kind: "text", text: style(theme, "dim", details.notice) });
|
|
894
914
|
const warning = formatTruncationWarning(details?.meta) ?? (details?.truncated ? "[eval output truncated]" : null);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashToolDefinition,
|
|
3
|
+
createFindToolDefinition,
|
|
4
|
+
createGrepToolDefinition,
|
|
5
|
+
createLsToolDefinition,
|
|
6
|
+
createReadToolDefinition,
|
|
7
|
+
createWriteToolDefinition,
|
|
8
|
+
sanitizeTerminalLabel,
|
|
9
|
+
type Theme,
|
|
10
|
+
type ThemeColor,
|
|
11
|
+
type ToolDefinition,
|
|
12
|
+
truncateToVisualLines,
|
|
13
|
+
} from "@code-yeongyu/senpi";
|
|
14
|
+
import type { TSchema } from "typebox";
|
|
15
|
+
import { Check } from "typebox/value";
|
|
16
|
+
import type { EvalToolCallSummary } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export interface WidgetOptions {
|
|
19
|
+
readonly cwd: string;
|
|
20
|
+
readonly theme: Theme | undefined;
|
|
21
|
+
readonly expanded: boolean;
|
|
22
|
+
readonly width: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type AnyToolDef<TParams extends TSchema, TDetails, TState> = ToolDefinition<TParams, TDetails, TState>;
|
|
26
|
+
type RenderCallOf<TParams extends TSchema, TDetails, TState> = NonNullable<
|
|
27
|
+
AnyToolDef<TParams, TDetails, TState>["renderCall"]
|
|
28
|
+
>;
|
|
29
|
+
type ToolRenderContext<TParams extends TSchema, TDetails, TState> = Parameters<
|
|
30
|
+
RenderCallOf<TParams, TDetails, TState>
|
|
31
|
+
>[2];
|
|
32
|
+
type Component<TParams extends TSchema, TDetails, TState> = ReturnType<RenderCallOf<TParams, TDetails, TState>>;
|
|
33
|
+
|
|
34
|
+
type CoreWidgetName = "bash" | "read" | "write" | "grep" | "find" | "ls";
|
|
35
|
+
type CoreWidgetRenderer = (summary: EvalToolCallSummary, options: WidgetOptions) => string[] | undefined;
|
|
36
|
+
type CoreWidgets = Readonly<Record<CoreWidgetName, CoreWidgetRenderer>>;
|
|
37
|
+
|
|
38
|
+
const MIN_RENDERER_WIDTH = 20;
|
|
39
|
+
const MAX_RENDERER_LINES = 5;
|
|
40
|
+
const MAX_FALLBACK_ARGUMENT_CODE_POINTS = 120;
|
|
41
|
+
const MAX_COLLAPSED_ERROR_CODE_POINTS = 512;
|
|
42
|
+
const MAX_COLLAPSED_ERROR_LINES = 4;
|
|
43
|
+
const MAX_PREVIEW_LINES = 2;
|
|
44
|
+
const MAX_COLLAPSED_WIDGET_LINES = 8;
|
|
45
|
+
const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
|
|
46
|
+
const WIDGET_TRUNCATION_MARKER = "… (widget truncated)";
|
|
47
|
+
const TERMINAL_ESCAPE_PATTERN =
|
|
48
|
+
/(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\|\u009C))|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]/g;
|
|
49
|
+
const TERMINAL_CONTROL_PATTERN = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]+/g;
|
|
50
|
+
const coreWidgetsByCwd = new Map<string, CoreWidgets>();
|
|
51
|
+
|
|
52
|
+
export function codePointPrefix(text: string, maxCodePoints: number): string {
|
|
53
|
+
let end = 0;
|
|
54
|
+
for (let count = 0; count < maxCodePoints && end < text.length; count += 1) {
|
|
55
|
+
const firstCodeUnit = text.charCodeAt(end);
|
|
56
|
+
const secondCodeUnit = text.charCodeAt(end + 1);
|
|
57
|
+
const isSurrogatePair =
|
|
58
|
+
firstCodeUnit >= 0xd800 && firstCodeUnit <= 0xdbff && secondCodeUnit >= 0xdc00 && secondCodeUnit <= 0xdfff;
|
|
59
|
+
end += isSurrogatePair ? 2 : 1;
|
|
60
|
+
}
|
|
61
|
+
return text.slice(0, end);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatDuration(milliseconds: number): string {
|
|
65
|
+
const totalSeconds = Math.floor(Math.max(0, milliseconds) / 1_000);
|
|
66
|
+
if (totalSeconds < 1) return "<1s";
|
|
67
|
+
const seconds = totalSeconds % 60;
|
|
68
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
69
|
+
if (totalMinutes < 1) return `${seconds}s`;
|
|
70
|
+
const minutes = totalMinutes % 60;
|
|
71
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
72
|
+
if (hours < 1) return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
73
|
+
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function style(theme: Theme | undefined, color: ThemeColor, text: string): string {
|
|
77
|
+
return theme === undefined ? text : theme.fg(color, text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderAllVisualLines(text: string, width: number): string[] {
|
|
81
|
+
return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderFirstVisualLines(text: string, maxLines: number, width: number): string[] {
|
|
85
|
+
const truncated = truncateToVisualLines(text, maxLines, width);
|
|
86
|
+
if (truncated.skippedCount === 0) return truncated.visualLines.map((line) => line.trimEnd());
|
|
87
|
+
return renderAllVisualLines(text, width).slice(0, maxLines);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function indent(lines: readonly string[]): string[] {
|
|
91
|
+
return lines.map((line) => ` ${line}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderWith<TParams extends TSchema, TDetails, TState>(
|
|
95
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
96
|
+
summary: EvalToolCallSummary,
|
|
97
|
+
options: WidgetOptions,
|
|
98
|
+
initialState: TState,
|
|
99
|
+
): string[] | undefined {
|
|
100
|
+
let preparedArgs: unknown = summary.args;
|
|
101
|
+
try {
|
|
102
|
+
if (definition.prepareArguments !== undefined) preparedArgs = definition.prepareArguments(preparedArgs);
|
|
103
|
+
if (!Check(definition.parameters, preparedArgs)) return undefined;
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const theme = options.theme;
|
|
109
|
+
const innerWidth = options.width - 2;
|
|
110
|
+
const renderCall = definition.renderCall;
|
|
111
|
+
if (theme === undefined || innerWidth < MIN_RENDERER_WIDTH || renderCall === undefined) return undefined;
|
|
112
|
+
|
|
113
|
+
const context: ToolRenderContext<TParams, TDetails, TState> = {
|
|
114
|
+
args: preparedArgs,
|
|
115
|
+
toolCallId: summary.callId ?? summary.name,
|
|
116
|
+
invalidate: () => {},
|
|
117
|
+
lastComponent: undefined,
|
|
118
|
+
state: initialState,
|
|
119
|
+
cwd: options.cwd,
|
|
120
|
+
executionStarted: true,
|
|
121
|
+
argsComplete: true,
|
|
122
|
+
isPartial: false,
|
|
123
|
+
expanded: options.expanded,
|
|
124
|
+
showImages: false,
|
|
125
|
+
isError: !summary.ok,
|
|
126
|
+
hasResult: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const component: Component<TParams, TDetails, TState> = renderCall(preparedArgs, theme, context);
|
|
131
|
+
const renderedLines = component.render(innerWidth).map((line) => line.trimEnd());
|
|
132
|
+
const retainedLines = renderedLines.slice(0, MAX_RENDERER_LINES);
|
|
133
|
+
if (renderedLines.length > retainedLines.length) {
|
|
134
|
+
retainedLines.push(style(theme, "muted", WIDGET_TRUNCATION_MARKER));
|
|
135
|
+
}
|
|
136
|
+
return indent(retainedLines);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createCoreWidgets(cwd: string): CoreWidgets {
|
|
143
|
+
const bashDefinition = createBashToolDefinition(cwd);
|
|
144
|
+
const readDefinition = createReadToolDefinition(cwd);
|
|
145
|
+
const writeDefinition = createWriteToolDefinition(cwd);
|
|
146
|
+
const grepDefinition = createGrepToolDefinition(cwd);
|
|
147
|
+
const findDefinition = createFindToolDefinition(cwd);
|
|
148
|
+
const lsDefinition = createLsToolDefinition(cwd);
|
|
149
|
+
const widgets: CoreWidgets = {
|
|
150
|
+
bash: (summary, options) =>
|
|
151
|
+
renderWith(bashDefinition, summary, options, {
|
|
152
|
+
startedAt: undefined,
|
|
153
|
+
endedAt: undefined,
|
|
154
|
+
interval: undefined,
|
|
155
|
+
}),
|
|
156
|
+
read: (summary, options) => renderWith(readDefinition, summary, options, {}),
|
|
157
|
+
write: (summary, options) => renderWith(writeDefinition, summary, options, {}),
|
|
158
|
+
grep: (summary, options) => renderWith(grepDefinition, summary, options, {}),
|
|
159
|
+
find: (summary, options) => renderWith(findDefinition, summary, options, {}),
|
|
160
|
+
ls: (summary, options) => renderWith(lsDefinition, summary, options, {}),
|
|
161
|
+
};
|
|
162
|
+
return widgets;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function coreWidgetsFor(cwd: string): CoreWidgets {
|
|
166
|
+
const cached = coreWidgetsByCwd.get(cwd);
|
|
167
|
+
if (cached !== undefined) return cached;
|
|
168
|
+
const widgets = createCoreWidgets(cwd);
|
|
169
|
+
coreWidgetsByCwd.set(cwd, widgets);
|
|
170
|
+
return widgets;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isCoreWidgetName(name: string): name is CoreWidgetName {
|
|
174
|
+
switch (name) {
|
|
175
|
+
case "bash":
|
|
176
|
+
case "read":
|
|
177
|
+
case "write":
|
|
178
|
+
case "grep":
|
|
179
|
+
case "find":
|
|
180
|
+
case "ls":
|
|
181
|
+
return true;
|
|
182
|
+
default:
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function compactArguments(args: unknown): string {
|
|
188
|
+
if (args === undefined) return "";
|
|
189
|
+
try {
|
|
190
|
+
const serialized = JSON.stringify(args);
|
|
191
|
+
if (serialized === undefined) return "";
|
|
192
|
+
return codePointPrefix(sanitizeTerminalLabel(serialized), MAX_FALLBACK_ARGUMENT_CODE_POINTS);
|
|
193
|
+
} catch {
|
|
194
|
+
return "[unserializable args]";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderFallback(summary: EvalToolCallSummary, width: number): string[] {
|
|
199
|
+
const name = sanitizeTerminalLabel(summary.name);
|
|
200
|
+
const fallback = `tool.${name}(${compactArguments(summary.args)})`;
|
|
201
|
+
return indent(renderAllVisualLines(fallback, width));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderStatus(summary: EvalToolCallSummary, options: WidgetOptions, width: number): string[] {
|
|
205
|
+
const icon = style(options.theme, summary.ok ? "success" : "error", summary.ok ? "✓" : "✗");
|
|
206
|
+
const parts = [icon];
|
|
207
|
+
if (summary.durationMs !== undefined) parts.push(formatDuration(summary.durationMs));
|
|
208
|
+
if (summary.argsTruncated) parts.push("(args truncated)");
|
|
209
|
+
return indent(renderFirstVisualLines(parts.join(" "), 1, width));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sanitizeTerminalControl(text: string): string {
|
|
213
|
+
return text.replace(TERMINAL_ESCAPE_PATTERN, "").replace(TERMINAL_CONTROL_PATTERN, " ");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderPreview(preview: string, options: WidgetOptions, width: number): string[] {
|
|
217
|
+
const sanitized = sanitizeTerminalControl(preview);
|
|
218
|
+
if (sanitized.length === 0) return [];
|
|
219
|
+
return indent(renderFirstVisualLines(style(options.theme, "muted", sanitized), MAX_PREVIEW_LINES, width));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderCollapsedError(error: string, options: WidgetOptions, width: number): string[] {
|
|
223
|
+
const guardedError = codePointPrefix(error, MAX_COLLAPSED_ERROR_CODE_POINTS);
|
|
224
|
+
const guardedLines = renderAllVisualLines(style(options.theme, "error", guardedError), width);
|
|
225
|
+
if (guardedError.length === error.length && guardedLines.length <= MAX_COLLAPSED_ERROR_LINES) {
|
|
226
|
+
return indent(guardedLines);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const markerLines = renderAllVisualLines(style(options.theme, "muted", TOOL_ERROR_OMISSION_MARKER), width);
|
|
230
|
+
const errorBudget = Math.max(0, MAX_COLLAPSED_ERROR_LINES - markerLines.length);
|
|
231
|
+
const lines = guardedLines.slice(0, errorBudget);
|
|
232
|
+
lines.push(...markerLines.slice(0, MAX_COLLAPSED_ERROR_LINES - lines.length));
|
|
233
|
+
return indent(lines);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderError(error: string, options: WidgetOptions, width: number): string[] {
|
|
237
|
+
const sanitized = sanitizeTerminalControl(error);
|
|
238
|
+
if (options.expanded) return indent(renderAllVisualLines(style(options.theme, "error", sanitized), width));
|
|
239
|
+
return renderCollapsedError(sanitized, options, width);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The theme-less fallback is QA-only. Production's gate lives in render.ts:
|
|
244
|
+
* nestedToolCallBlock routes theme-less renders to legacy rows before this function is called.
|
|
245
|
+
*/
|
|
246
|
+
export function renderToolCallWidget(summary: EvalToolCallSummary, options: WidgetOptions): string[] {
|
|
247
|
+
const innerWidth = Math.max(1, options.width - 2);
|
|
248
|
+
const coreLines = isCoreWidgetName(summary.name)
|
|
249
|
+
? coreWidgetsFor(options.cwd)[summary.name](summary, options)
|
|
250
|
+
: undefined;
|
|
251
|
+
const lines = coreLines ?? renderFallback(summary, innerWidth);
|
|
252
|
+
lines.push(...renderStatus(summary, options, innerWidth));
|
|
253
|
+
if (summary.ok && summary.resultPreview !== undefined) {
|
|
254
|
+
lines.push(...renderPreview(summary.resultPreview, options, innerWidth));
|
|
255
|
+
} else if (!summary.ok && summary.error !== undefined) {
|
|
256
|
+
lines.push(...renderError(summary.error, options, innerWidth));
|
|
257
|
+
}
|
|
258
|
+
return options.expanded ? lines : lines.slice(0, MAX_COLLAPSED_WIDGET_LINES);
|
|
259
|
+
}
|
package/src/tool/types.ts
CHANGED
|
@@ -110,13 +110,18 @@ export interface EvalKernelManager {
|
|
|
110
110
|
export type ExecuteTool = (
|
|
111
111
|
toolName: string,
|
|
112
112
|
params: unknown,
|
|
113
|
-
options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback<unknown
|
|
113
|
+
options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback<unknown>; activateInactiveTool?: boolean },
|
|
114
114
|
) => Promise<AgentToolResult<unknown>>;
|
|
115
115
|
|
|
116
116
|
export interface EvalToolCallSummary {
|
|
117
117
|
readonly name: string;
|
|
118
118
|
readonly ok: boolean;
|
|
119
119
|
readonly error?: string;
|
|
120
|
+
readonly callId?: string;
|
|
121
|
+
readonly args?: unknown;
|
|
122
|
+
readonly argsTruncated?: boolean;
|
|
123
|
+
readonly durationMs?: number;
|
|
124
|
+
readonly resultPreview?: string;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
export type EvalStatusEvent = { readonly op: string } & Readonly<Record<string, unknown>>;
|