@code-yeongyu/senpi-codemode 2026.9.15-2 → 2026.9.16-2
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 +35 -0
- package/README.md +24 -0
- package/package.json +4 -4
- package/src/bridge/http-server.ts +7 -1
- package/src/bridge/kernel-tools-protocol.ts +85 -0
- package/src/bridge/protocol.ts +6 -0
- package/src/bridges/agent-bridge.ts +30 -11
- package/src/extension/session-manager.ts +11 -0
- package/src/index.ts +10 -0
- package/src/kernels/jl/prelude.jl +46 -0
- package/src/kernels/jl/runner.jl +1 -1
- package/src/kernels/js/context-manager.ts +29 -0
- package/src/kernels/js/kernel-contract.ts +12 -0
- package/src/kernels/js/kernel-tools-context.d.ts +13 -0
- package/src/kernels/js/kernel-tools-context.js +7 -0
- package/src/kernels/js/kernel-tools-define.d.ts +10 -0
- package/src/kernels/js/kernel-tools-define.js +12 -0
- package/src/kernels/js/kernel-tools-errors.js +22 -0
- package/src/kernels/js/kernel-tools-errors.ts +26 -0
- package/src/kernels/js/kernel-tools-host.ts +134 -0
- package/src/kernels/js/kernel-tools-naming.js +28 -0
- package/src/kernels/js/kernel-tools-parse.d.ts +5 -0
- package/src/kernels/js/kernel-tools-parse.js +255 -0
- package/src/kernels/js/kernel-tools-pump.js +105 -0
- package/src/kernels/js/kernel-tools-registry.d.ts +27 -0
- package/src/kernels/js/kernel-tools-registry.js +127 -0
- package/src/kernels/js/kernel-tools-schema.js +69 -0
- package/src/kernels/js/kernel-tools-types.ts +42 -0
- package/src/kernels/js/prelude.ts +2 -0
- package/src/kernels/js/worker-core.js +26 -3
- package/src/kernels/js/worker-runtime.js +20 -8
- package/src/kernels/js/worker-slot.ts +6 -1
- package/src/kernels/js/worker-startup.ts +9 -7
- package/src/kernels/js/workpool.js +37 -0
- package/src/kernels/kernel-tools-unavailable.ts +5 -0
- package/src/kernels/py/kernel.ts +13 -0
- package/src/kernels/py/prelude.py +57 -3
- package/src/kernels/rb/prelude.rb +13 -2
- package/src/kernels/rb/workpool.rb +38 -0
- package/src/kernels/shared/subprocess-kernel.ts +13 -0
- package/src/kernels/shared/subprocess-queue.ts +2 -0
- package/src/prompt/eval-prompt-template.ts +4 -2
- package/src/tool/cell-handler.ts +31 -3
- package/src/tool/run-eval-cell.ts +52 -30
- package/src/tool/types.ts +2 -0
|
@@ -140,13 +140,22 @@ def __senpi_bridge_request(path, payload)
|
|
|
140
140
|
return body["value"] if body.is_a?(Hash) && body["ok"] == true
|
|
141
141
|
|
|
142
142
|
error = body.is_a?(Hash) ? body["error"] : body
|
|
143
|
-
raise(error.is_a?(Hash) ? error["message"].to_s : error.to_s)
|
|
143
|
+
raise SenpiBridgeError.new(error.is_a?(Hash) ? error["message"].to_s : error.to_s, error.is_a?(Hash) ? error["code"] : nil)
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def __senpi_call_tool(name, args)
|
|
147
147
|
__senpi_bridge_request("/call", { "callId" => "rb-#{Process.pid}-#{rand(1_000_000)}", "toolName" => name, "args" => args })
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
class SenpiBridgeError < StandardError
|
|
151
|
+
attr_reader :code
|
|
152
|
+
|
|
153
|
+
def initialize(message, code = nil)
|
|
154
|
+
super(message)
|
|
155
|
+
@code = code
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
150
159
|
class SenpiToolCallable
|
|
151
160
|
def initialize(name)
|
|
152
161
|
@name = name
|
|
@@ -177,6 +186,8 @@ def tool
|
|
|
177
186
|
$__senpi_tool_proxy ||= SenpiToolProxy.new
|
|
178
187
|
end
|
|
179
188
|
|
|
189
|
+
require_relative "workpool"
|
|
190
|
+
|
|
180
191
|
def completion(prompt, model: "default", system: nil, schema: nil, **kwargs)
|
|
181
192
|
options = { "model" => model }.merge(kwargs.transform_keys(&:to_s))
|
|
182
193
|
options["system"] = system unless system.nil?
|
|
@@ -213,7 +224,7 @@ def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, isolated:
|
|
|
213
224
|
text_value = record.fetch("text", response)
|
|
214
225
|
result = schema.nil? ? text_value : record.key?("data") ? record["data"] : JSON.parse(text_value.to_s)
|
|
215
226
|
return result unless handle
|
|
216
|
-
{ "text" => text_value, "output" => text_value, "handle" => record["handle"] || (record["id"] && "agent://#{record["id"]}"), "id" => record["id"], "agent" => record.fetch("agent", agent) }.tap do |node|
|
|
227
|
+
{ "text" => text_value, "output" => text_value, "handle" => record["handle"] || (record["id"] && "agent://#{record["id"]}"), "id" => record["id"], "run_epoch" => record["run_epoch"], "agent" => record.fetch("agent", agent) }.tap do |node|
|
|
217
228
|
node["data"] = result unless schema.nil?
|
|
218
229
|
end
|
|
219
230
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Only an opaque host identity is retained; all work remains engine-owned.
|
|
2
|
+
def __senpi_workpool_call(args)
|
|
3
|
+
tool.workpool(args)
|
|
4
|
+
rescue SenpiBridgeError => error
|
|
5
|
+
raise SenpiBridgeError.new("No active host workpool tool", "workpool_unavailable") if ["unknown_tool", "inactive_tool"].include?(error.code)
|
|
6
|
+
raise
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class SenpiWorkpool
|
|
10
|
+
attr_reader :pool_id
|
|
11
|
+
|
|
12
|
+
def initialize(pool_id)
|
|
13
|
+
@pool_id = pool_id
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def push(items)
|
|
18
|
+
__senpi_workpool_call({ "op" => "push", "pool_id" => @pool_id, "items" => items })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[:close, :inspect, :cancel].each do |op|
|
|
22
|
+
define_method(op) { __senpi_workpool_call({ "op" => op.to_s, "pool_id" => @pool_id }) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def workpool(agent, name, mode: nil)
|
|
27
|
+
args = { "op" => "create", "agent" => agent, "name" => name }
|
|
28
|
+
args["mode"] = mode unless mode.nil?
|
|
29
|
+
result = __senpi_workpool_call(args)
|
|
30
|
+
details = result["details"]
|
|
31
|
+
if details.is_a?(Hash)
|
|
32
|
+
error = details["error"]
|
|
33
|
+
raise SenpiBridgeError.new(error["message"], error["code"]) if error.is_a?(Hash)
|
|
34
|
+
pool_id = details["pool_id"]
|
|
35
|
+
return SenpiWorkpool.new(pool_id) if !result["hasError"] && pool_id.is_a?(String) && /\Awp_[0-9a-f]{32}\z/.match?(pool_id)
|
|
36
|
+
end
|
|
37
|
+
raise SenpiBridgeError.new("Host did not return a workpool identity", "workpool_unavailable")
|
|
38
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
+
import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
|
|
4
5
|
import { applySessionEnvironment } from "../session-env.ts";
|
|
5
6
|
import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
|
|
6
7
|
import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
|
|
@@ -69,6 +70,18 @@ export class SubprocessKernel {
|
|
|
69
70
|
return { stateRetained: Promise.resolve(false) };
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
listKernelToolNames(): readonly string[] {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describeKernelTools(_names: readonly string[]): Promise<never> {
|
|
78
|
+
return rejectKernelToolsUnavailable();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
|
|
82
|
+
return rejectKernelToolsUnavailable();
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
nextToolCall(): Promise<ToolCallMessage> {
|
|
73
86
|
return this.runs.nextToolCall();
|
|
74
87
|
}
|
|
@@ -57,8 +57,10 @@ tool_schema(name?) → dict
|
|
|
57
57
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
58
58
|
Oneshot, stateless. \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → parsed structured output.
|
|
59
59
|
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
60
|
-
Run a subagent → final output. \`agent\` picks a discovered agent. \`schema\` as in completion(). \`handle\` → workflow node { text, output, handle: \`agent://<id>\`, id, agent } (parsed under \`data\` with \`schema\`).
|
|
61
|
-
{{/if}}
|
|
60
|
+
Run a subagent → final output. \`agent\` picks a discovered agent. \`schema\` as in completion(). \`handle\` → workflow node { text, output, handle: \`agent://<id>\`, id, run_epoch, agent } (parsed under \`data\` with \`schema\`).
|
|
61
|
+
{{/if}}workpool(agent, name, mode?) → { pool_id, push(items), close(), inspect(), cancel() }
|
|
62
|
+
Thin host-tool adapter; agent is a plain-data spec. The host owns scheduling and keyed yields; explicitly close for automatic aggregate delivery. Requires the host workpool tool; omitted mode uses its default.
|
|
63
|
+
parallel(thunks) → list
|
|
62
64
|
Thunks through a bounded pool (as wide as a \`task\` batch), input order kept; a throwing thunk propagates.
|
|
63
65
|
pipeline(items, ...stages) → list
|
|
64
66
|
Map items through one-arg stages with a barrier between stages; each stage receives the previous stage's result.
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AgentToolResult,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
kernelToolsStorage,
|
|
5
|
+
sanitizeTerminalLabel,
|
|
6
|
+
} from "@code-yeongyu/senpi";
|
|
2
7
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
8
|
import { RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
|
|
4
9
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
@@ -8,6 +13,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
8
13
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
9
14
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
10
15
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
16
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
11
17
|
import {
|
|
12
18
|
boundToolCallArgs,
|
|
13
19
|
capCodePoints,
|
|
@@ -39,6 +45,8 @@ export interface CellBridgeRuntime {
|
|
|
39
45
|
readonly ctx: ExtensionContext;
|
|
40
46
|
readonly artifactPath?: string;
|
|
41
47
|
readonly imageResizer?: EvalImageResizer;
|
|
48
|
+
/** This cell's live kernel-tool capability; only a JS kernel has one (#1754). */
|
|
49
|
+
readonly kernelTools?: KernelToolsCapability;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export class CellHandler {
|
|
@@ -81,7 +89,7 @@ export class CellHandler {
|
|
|
81
89
|
this.#resultBuilder.display(message);
|
|
82
90
|
return;
|
|
83
91
|
case "tool-call": {
|
|
84
|
-
const pending = this.#
|
|
92
|
+
const pending = this.#dispatchToolCall(message);
|
|
85
93
|
this.#state.pendingBridgeCalls.push(pending);
|
|
86
94
|
await pending;
|
|
87
95
|
return;
|
|
@@ -90,6 +98,8 @@ export class CellHandler {
|
|
|
90
98
|
case "init-failed":
|
|
91
99
|
case "result":
|
|
92
100
|
case "closed":
|
|
101
|
+
case "kernel-tool-describe-reply":
|
|
102
|
+
case "kernel-tool-invoke-reply":
|
|
93
103
|
return;
|
|
94
104
|
default:
|
|
95
105
|
throw new TypeError(`Unhandled kernel message: ${String(message)}`);
|
|
@@ -112,6 +122,19 @@ export class CellHandler {
|
|
|
112
122
|
return this.#resultBuilder.liveResult();
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The worker's message loop fires outside the async context `run-eval-cell.ts` enters around the
|
|
127
|
+
* awaited run chain, so the capability has to be entered here — around the whole dispatch, including
|
|
128
|
+
* the reserved agent()/output() bridges where a host task tool resolves the parent's kernel tools —
|
|
129
|
+
* for exactly the duration of each host tool call this cell makes (#1754). Without a capability
|
|
130
|
+
* (py/rb/jl) the store stays empty and `ctx.kernelTools` remains undefined.
|
|
131
|
+
*/
|
|
132
|
+
async #dispatchToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
133
|
+
const kernelTools = this.#runtime.kernelTools;
|
|
134
|
+
if (kernelTools === undefined) return await this.#handleToolCall(message);
|
|
135
|
+
return await kernelToolsStorage.run(kernelTools, async () => await this.#handleToolCall(message));
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
116
139
|
const startedAt = Date.now();
|
|
117
140
|
const metric = createToolCallMetric(message.toolName, startedAt);
|
|
@@ -223,7 +246,12 @@ export class CellHandler {
|
|
|
223
246
|
type: "tool-reply",
|
|
224
247
|
callId: message.callId,
|
|
225
248
|
ok: false,
|
|
226
|
-
error: {
|
|
249
|
+
error: {
|
|
250
|
+
message: text,
|
|
251
|
+
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
252
|
+
? { code: error.code }
|
|
253
|
+
: {}),
|
|
254
|
+
},
|
|
227
255
|
});
|
|
228
256
|
}
|
|
229
257
|
this.#resultBuilder.emitUpdate(false);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { type AgentToolResult, type ExtensionContext, kernelToolsStorage } from "@code-yeongyu/senpi";
|
|
4
4
|
import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
|
|
5
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
5
6
|
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
6
7
|
import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
|
|
7
8
|
import { CellHandler, type CellState } from "./cell-handler.ts";
|
|
@@ -11,7 +12,7 @@ import { buildEvalExecutionEventPayload, type EvalExecutionSettleOutcome } from
|
|
|
11
12
|
import { evalTimeoutBehavior } from "./eval-request.ts";
|
|
12
13
|
import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
|
|
13
14
|
import { describeTimeoutState } from "./interrupt-note.ts";
|
|
14
|
-
import type { EvalToolDetails } from "./types.ts";
|
|
15
|
+
import type { EvalKernel, EvalToolDetails } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
export async function runEvalCell(
|
|
17
18
|
options: CreateEvalToolOptions,
|
|
@@ -166,34 +167,42 @@ async function executeCell(
|
|
|
166
167
|
void pending.catch((error: unknown) => execution.cancel(error));
|
|
167
168
|
}),
|
|
168
169
|
);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
170
|
+
// Computed before the handler so the cell's capability can be entered per host tool call from the
|
|
171
|
+
// worker's message loop, which runs outside the `kernelToolsStorage.run` context below (#1754).
|
|
172
|
+
const kernelTools = jsKernelTools(kernel, invocation.input.language);
|
|
173
|
+
const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
|
|
174
|
+
execution.setKernel(kernel);
|
|
175
|
+
const activeHandler = new CellHandler(kernel, state, {
|
|
176
|
+
executeTool: options.executeTool,
|
|
177
|
+
...(options.listTools === undefined ? {} : { listTools: options.listTools }),
|
|
178
|
+
settings: options.settings ?? defaultCodemodeSettings,
|
|
179
|
+
...(options.complete === undefined ? {} : { complete: options.complete }),
|
|
180
|
+
ctx: bridgeContext,
|
|
181
|
+
...(options.artifactsDir === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
|
|
184
|
+
...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
|
|
185
|
+
...(kernelTools === undefined ? {} : { kernelTools }),
|
|
186
|
+
});
|
|
187
|
+
handler = activeHandler;
|
|
188
|
+
cellManager.markRunning(
|
|
189
|
+
cell,
|
|
190
|
+
kernel,
|
|
191
|
+
() => activeHandler.liveResult(),
|
|
192
|
+
(error) => execution.cancel(error),
|
|
193
|
+
);
|
|
194
|
+
// Includes a steer already queued at execute start or received while acquiring the kernel.
|
|
195
|
+
onReady();
|
|
196
|
+
if ("setContext" in options.kernelManager && typeof options.kernelManager.setContext === "function") {
|
|
197
|
+
options.kernelManager.setContext(bridgeContext);
|
|
198
|
+
}
|
|
199
|
+
if (invocation.input.reset) await execution.wait(kernel.reset());
|
|
200
|
+
const result = await execution.wait(kernel.run({ cellId: invocation.cellId, code: invocation.input.code }));
|
|
201
|
+
if (result.ok && state.pendingBridgeCalls.length > 0)
|
|
202
|
+
await execution.wait(Promise.all(state.pendingBridgeCalls));
|
|
203
|
+
return await handler.finalize(result);
|
|
204
|
+
};
|
|
205
|
+
return kernelTools ? await kernelToolsStorage.run(kernelTools, runBound) : await runBound();
|
|
197
206
|
} catch (error) {
|
|
198
207
|
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
199
208
|
return await handler.finalizeCancellation(error);
|
|
@@ -206,3 +215,16 @@ async function executeCell(
|
|
|
206
215
|
if (handler) await handler.flushOutput();
|
|
207
216
|
}
|
|
208
217
|
}
|
|
218
|
+
|
|
219
|
+
function jsKernelTools(kernel: EvalKernel, language: string): KernelToolsCapability | undefined {
|
|
220
|
+
if (language !== "js") return undefined;
|
|
221
|
+
if (!("describeKernelTools" in kernel) || typeof kernel.describeKernelTools !== "function") return undefined;
|
|
222
|
+
const js = kernel as EvalKernel & {
|
|
223
|
+
describeKernelTools: KernelToolsCapability["describe"];
|
|
224
|
+
invokeKernelTool: KernelToolsCapability["invoke"];
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
describe: (names) => js.describeKernelTools(names),
|
|
228
|
+
invoke: (request, signal) => js.invokeKernelTool(request, signal),
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/tool/types.ts
CHANGED
|
@@ -132,6 +132,8 @@ export interface EvalKernel {
|
|
|
132
132
|
deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void;
|
|
133
133
|
reset(): Promise<void>;
|
|
134
134
|
close(): Promise<void>;
|
|
135
|
+
/** Names this kernel has registered; JS collides with other languages in the same session. */
|
|
136
|
+
listKernelToolNames?(): readonly string[];
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
export interface EvalKernelManager {
|