@code-yeongyu/senpi-codemode 2026.7.26 → 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 CHANGED
@@ -12,6 +12,22 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.7.28] - 2026-07-28
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - 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)).
22
+
23
+ ### Changed
24
+
25
+ - 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)).
26
+
27
+ ### Fixed
28
+
29
+ ### Removed
30
+
15
31
  ## [2026.7.26] - 2026-07-26
16
32
 
17
33
  ### 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.7.26",
3
+ "version": "2026.7.28",
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.26",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.28",
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.26"
40
+ "@code-yeongyu/senpi": "2026.7.28"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -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
@@ -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 { RESERVED_AGENT_TOOL, RESERVED_OUTPUT_TOOL } from "../bridge/reserved.ts";
4
- import { type AgentExecuteTool, runEvalAgent } from "../bridges/agent-bridge.ts";
5
- import { runEvalOutput } from "../bridges/output-bridge.ts";
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 === RESERVED_AGENT_TOOL) {
156
+ if (isReservedToolName(message.toolName)) {
155
157
  await this.#deliverToolReply(message, async () => ({
156
- value: await runEvalAgent(message.args, {
158
+ value: await runReservedTool(message.toolName, {
157
159
  callId: message.callId,
158
- taskToolName: this.#runtime.settings.taskTools.task,
160
+ args: message.args,
159
161
  executeTool: this.#runtime.executeTool,
160
- signal: this.#state.signal,
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
- executeTool: this.#runtime.executeTool,
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 = error instanceof Error ? error.message : String(error);
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);
@@ -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/types.ts CHANGED
@@ -110,7 +110,7 @@ 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 {