@code-yeongyu/senpi-codemode 2026.7.25-2 → 2026.7.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +45 -19
- package/package.json +3 -3
- package/src/bridge/http-server.ts +6 -1
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/output-bridge.ts +0 -1
- package/src/bridges/reserved-dispatch.ts +56 -0
- package/src/bridges/schema-bridge.ts +69 -0
- package/src/bridges/schema-hint.ts +140 -0
- package/src/extension/eval-notifier.ts +40 -0
- package/src/extension/runtime-factory.ts +3 -1
- package/src/index.ts +50 -66
- package/src/kernels/jl/prelude.jl +6 -0
- package/src/kernels/js/context-manager.ts +5 -2
- package/src/kernels/js/local-module-loader.ts +4 -0
- package/src/kernels/js/worker-runtime.js +6 -0
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +19 -4
- package/src/kernels/py/prelude.py +17 -0
- package/src/kernels/rb/prelude.rb +6 -0
- package/src/kernels/shared/subprocess-kernel.ts +8 -5
- package/src/prompt/eval-prompt.ts +28 -5
- package/src/tool/cell-handler.ts +20 -18
- package/src/tool/detached-cell-manager.ts +322 -0
- package/src/tool/eval-tool.ts +256 -18
- package/src/tool/interrupt-note.ts +58 -0
- package/src/tool/render.ts +28 -6
- package/src/tool/status-events.ts +20 -0
- package/src/tool/types.ts +66 -23
- package/src/codemode/runtime.ts +0 -258
- package/src/codemode/tools.ts +0 -106
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 {
|
|
5
|
-
import { type CodeModeTool, createCodeModeTools, isGptCodeModeModel } from "./codemode/tools.ts";
|
|
4
|
+
import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
6
5
|
import { type CompletionRequest, type CompletionResult, createCompletionHandler } from "./completion/handler.ts";
|
|
7
6
|
import { defaultCodemodeSettings } from "./config/settings.ts";
|
|
7
|
+
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
8
8
|
import {
|
|
9
9
|
createExecuteTool,
|
|
10
10
|
createRuntime,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./extension/runtime-factory.ts";
|
|
14
14
|
import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
|
|
15
15
|
import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
|
|
16
|
+
import { EvalDetachedCellManager } from "./tool/detached-cell-manager.ts";
|
|
16
17
|
import { createEvalTool } from "./tool/eval-tool.ts";
|
|
17
18
|
import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
|
|
18
19
|
|
|
@@ -29,11 +30,12 @@ type CodemodeEvent = SessionLifecycleEvent | "model_select";
|
|
|
29
30
|
|
|
30
31
|
export interface CodemodeExtensionAPI {
|
|
31
32
|
registerTool(tool: ReturnType<typeof createEvalTool>): void;
|
|
33
|
+
registerRemovedToolHint(name: string, hint: string): void;
|
|
32
34
|
on(event: CodemodeEvent, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
33
35
|
executeTool: AgentExecuteTool;
|
|
34
36
|
getActiveTools(): string[];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
getAllTools(): readonly EvalSchemaToolInfo[];
|
|
38
|
+
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface SenpiCodemodeOptions {
|
|
@@ -43,28 +45,35 @@ export interface SenpiCodemodeOptions {
|
|
|
43
45
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
type DynamicCodeModeExtensionAPI = CodemodeExtensionAPI & {
|
|
47
|
-
registerTool(tool: CodeModeTool): void;
|
|
48
|
-
setActiveTools(toolNames: string[]): void | Promise<void>;
|
|
49
|
-
getAllTools(): readonly { readonly name: string }[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
48
|
export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCodemodeOptions = {}): void {
|
|
53
49
|
const manager = new SessionManagerProxy();
|
|
54
50
|
const complete = options.complete ?? ((request, ctx) => createCompletionHandler()(ctx)(request));
|
|
55
51
|
const renderers = { renderCall: renderEvalCall, renderResult: renderEvalResult };
|
|
56
|
-
let activeRuntime:
|
|
52
|
+
let activeRuntime: SessionRuntime | undefined;
|
|
57
53
|
let activeModelId: string | undefined;
|
|
58
|
-
|
|
54
|
+
let activeContext: ExtensionContext | undefined;
|
|
55
|
+
let activeCells: EvalDetachedCellManager | undefined;
|
|
56
|
+
const notifier = new EvalNotifier({
|
|
57
|
+
sendUserMessage: (content, notifyOptions) => pi.sendUserMessage(content, notifyOptions),
|
|
58
|
+
getContext: () => activeContext,
|
|
59
|
+
getMode: () => "wake",
|
|
60
|
+
});
|
|
61
|
+
const registerEvalForRuntime = (
|
|
62
|
+
runtime: SessionRuntime,
|
|
63
|
+
modelId: string | undefined,
|
|
64
|
+
cellManager: EvalDetachedCellManager,
|
|
65
|
+
): void => {
|
|
59
66
|
pi.registerTool(
|
|
60
67
|
createEvalTool({
|
|
61
68
|
enabledLanguages: runtime.enabledLanguages,
|
|
62
69
|
kernelManager: manager,
|
|
63
70
|
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
64
71
|
executeTool: runtime.executeTool,
|
|
72
|
+
listTools: () => pi.getAllTools(),
|
|
65
73
|
complete,
|
|
66
74
|
settings: runtime.settings,
|
|
67
75
|
artifactsDir: runtime.artifactsDir,
|
|
76
|
+
cellManager,
|
|
68
77
|
executionTracker: manager,
|
|
69
78
|
renderers,
|
|
70
79
|
spawns: runtime.spawns,
|
|
@@ -75,21 +84,13 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
75
84
|
);
|
|
76
85
|
};
|
|
77
86
|
const dropRuntime = async (): Promise<void> => {
|
|
78
|
-
const
|
|
87
|
+
const cells = activeCells;
|
|
79
88
|
activeRuntime = undefined;
|
|
80
89
|
activeModelId = undefined;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const tools = createCodeModeTools({ runtime });
|
|
86
|
-
pi.registerTool(tools.exec);
|
|
87
|
-
pi.registerTool(tools.wait);
|
|
88
|
-
await pi.setActiveTools([...new Set([...pi.getActiveTools(), "exec", "wait"])]);
|
|
89
|
-
};
|
|
90
|
-
const deactivateCodeModeTools = async (): Promise<void> => {
|
|
91
|
-
if (!isDynamicCodeModeExtensionAPI(pi)) return;
|
|
92
|
-
await pi.setActiveTools(pi.getActiveTools().filter((name) => name !== "exec" && name !== "wait"));
|
|
90
|
+
activeCells = undefined;
|
|
91
|
+
await cells?.dispose();
|
|
92
|
+
activeContext = undefined;
|
|
93
|
+
await manager.dispose();
|
|
93
94
|
};
|
|
94
95
|
pi.registerTool(
|
|
95
96
|
createEvalTool({
|
|
@@ -97,73 +98,56 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
97
98
|
kernelManager: manager,
|
|
98
99
|
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
99
100
|
executeTool: createExecuteTool(pi),
|
|
101
|
+
listTools: () => pi.getAllTools(),
|
|
100
102
|
complete,
|
|
101
103
|
settings: defaultCodemodeSettings,
|
|
104
|
+
cellManager: new EvalDetachedCellManager({ notifier }),
|
|
102
105
|
executionTracker: manager,
|
|
103
106
|
renderers,
|
|
104
107
|
hostLine: hostLine(),
|
|
105
108
|
}),
|
|
106
109
|
);
|
|
110
|
+
pi.registerRemovedToolHint(
|
|
111
|
+
"exec",
|
|
112
|
+
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on timeout and notify when complete.',
|
|
113
|
+
);
|
|
114
|
+
pi.registerRemovedToolHint(
|
|
115
|
+
"wait",
|
|
116
|
+
'wait was removed; detached eval cells notify when complete. Use eval({ action: "peek"|"stop", cell_id }) to inspect or stop one.',
|
|
117
|
+
);
|
|
107
118
|
|
|
108
119
|
pi.on("session_start", async (event, ctx) => {
|
|
109
|
-
const
|
|
120
|
+
const previousCells = activeCells;
|
|
121
|
+
activeCells = undefined;
|
|
122
|
+
await previousCells?.dispose();
|
|
110
123
|
const generation = manager.beginReplacement();
|
|
111
124
|
const runtime = await createRuntime(pi, ctx, event, complete, options);
|
|
112
125
|
const replaced = await manager.replace(generation, runtime.manager);
|
|
113
126
|
if (!replaced) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
parallelPoolWidth: runtime.parallelPoolWidth,
|
|
120
|
-
executeTool: runtime.executeTool,
|
|
121
|
-
})
|
|
122
|
-
: undefined;
|
|
123
|
-
activeRuntime = { ...runtime, ...(codeMode === undefined ? {} : { codeMode }) };
|
|
127
|
+
notifier.reset();
|
|
128
|
+
activeContext = ctx;
|
|
129
|
+
const cellManager = new EvalDetachedCellManager({ artifactsDir: runtime.artifactsDir, notifier });
|
|
130
|
+
activeCells = cellManager;
|
|
131
|
+
activeRuntime = runtime;
|
|
124
132
|
activeModelId = ctx.model?.id;
|
|
125
|
-
registerEvalForRuntime(
|
|
126
|
-
if (codeMode) await activateCodeModeTools(codeMode);
|
|
127
|
-
else await deactivateCodeModeTools();
|
|
133
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
128
134
|
});
|
|
129
135
|
pi.on("session_shutdown", async () => dropRuntime());
|
|
130
136
|
pi.on("session_before_switch", async () => dropRuntime());
|
|
131
137
|
pi.on("session_before_fork", async () => dropRuntime());
|
|
132
|
-
pi.on("model_select", async (event) => {
|
|
138
|
+
pi.on("model_select", async (event, ctx) => {
|
|
139
|
+
activeContext = ctx;
|
|
133
140
|
const runtime = activeRuntime;
|
|
134
141
|
if (runtime === undefined) return;
|
|
135
142
|
const modelId = modelIdFrom(event);
|
|
136
143
|
if (modelId === undefined || modelId === activeModelId) return;
|
|
137
144
|
activeModelId = modelId;
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
await codeMode?.dispose();
|
|
142
|
-
activeRuntime = nextRuntime;
|
|
143
|
-
await deactivateCodeModeTools();
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (runtime.codeMode) {
|
|
147
|
-
if (isDynamicCodeModeExtensionAPI(pi)) {
|
|
148
|
-
await pi.setActiveTools([...new Set([...pi.getActiveTools(), "exec", "wait"])]);
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const codeMode = new CodeModeSessionRuntime({
|
|
153
|
-
sessionId: runtime.sessionId,
|
|
154
|
-
cwd: runtime.cwd,
|
|
155
|
-
parallelPoolWidth: runtime.parallelPoolWidth,
|
|
156
|
-
executeTool: runtime.executeTool,
|
|
157
|
-
});
|
|
158
|
-
activeRuntime = { ...runtime, codeMode };
|
|
159
|
-
await activateCodeModeTools(codeMode);
|
|
145
|
+
const cellManager = activeCells;
|
|
146
|
+
if (cellManager === undefined) return;
|
|
147
|
+
registerEvalForRuntime(runtime, modelId, cellManager);
|
|
160
148
|
});
|
|
161
149
|
}
|
|
162
150
|
|
|
163
|
-
function isDynamicCodeModeExtensionAPI(pi: CodemodeExtensionAPI): pi is DynamicCodeModeExtensionAPI {
|
|
164
|
-
return typeof pi.setActiveTools === "function" && typeof pi.getAllTools === "function";
|
|
165
|
-
}
|
|
166
|
-
|
|
167
151
|
function hostLine(): string {
|
|
168
152
|
const cpu = os.cpus()[0]?.model?.trim();
|
|
169
153
|
return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`]
|
|
@@ -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'")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
2
3
|
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
3
4
|
import {
|
|
4
5
|
assertJavaScriptKernelOpen,
|
|
@@ -48,14 +49,16 @@ export class JavaScriptKernel {
|
|
|
48
49
|
return await promise;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
async interrupt(reason = "interrupted"): Promise<
|
|
52
|
+
async interrupt(reason = "interrupted"): Promise<KernelInterruptHandle> {
|
|
52
53
|
assertJavaScriptKernelOpen(this.#lifecycle, "interrupt");
|
|
53
54
|
const active = this.#runs.active;
|
|
54
55
|
const target = this.#runs.takeInterruptTarget();
|
|
55
|
-
if (!target) return;
|
|
56
|
+
if (!target) return { stateRetained: Promise.resolve(true) };
|
|
56
57
|
if (target === active) this.#clearTimeout();
|
|
57
58
|
this.#runs.settle(target, stoppedResult(target.input.cellId, `JS cell interrupted: ${reason}`));
|
|
58
59
|
await this.#restartAfterStop();
|
|
60
|
+
// A restart always replaces the worker VM, so no user global survives.
|
|
61
|
+
return { stateRetained: Promise.resolve(false) };
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
async reset(): Promise<void> {
|
|
@@ -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");
|
|
@@ -29,4 +29,6 @@ export interface PendingRun {
|
|
|
29
29
|
timeoutTimer: NodeJS.Timeout | null;
|
|
30
30
|
escalationTimer?: NodeJS.Timeout;
|
|
31
31
|
interruptReason?: string;
|
|
32
|
+
/** Set while an interrupt outcome is pending; resolved once the kernel knows whether state survived. */
|
|
33
|
+
resolveStateRetained?: (retained: boolean) => void;
|
|
32
34
|
}
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
1
2
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
2
3
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
3
4
|
|
|
@@ -41,7 +42,7 @@ export class PythonKernel {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
async interrupt(reason = "interrupted"): Promise<
|
|
45
|
+
async interrupt(reason = "interrupted"): Promise<KernelInterruptHandle> {
|
|
45
46
|
if (this.#failure) throw this.#failure;
|
|
46
47
|
for (const pending of [...this.#queue]) {
|
|
47
48
|
pending.interruptReason = reason;
|
|
@@ -49,7 +50,8 @@ export class PythonKernel {
|
|
|
49
50
|
}
|
|
50
51
|
const active = this.#active;
|
|
51
52
|
const transport = this.#transport;
|
|
52
|
-
if (!active || !transport || active.interruptReason !== undefined)
|
|
53
|
+
if (!active || !transport || active.interruptReason !== undefined)
|
|
54
|
+
return { stateRetained: Promise.resolve(true) };
|
|
53
55
|
active.interruptReason = reason;
|
|
54
56
|
if (active.timeoutTimer) clearTimeout(active.timeoutTimer);
|
|
55
57
|
active.timeoutTimer = null;
|
|
@@ -57,7 +59,11 @@ export class PythonKernel {
|
|
|
57
59
|
() => void this.#escalateInterruptedRun(active).catch(() => undefined),
|
|
58
60
|
interruptEscalationMs,
|
|
59
61
|
);
|
|
62
|
+
const stateRetained = new Promise<boolean>((resolve) => {
|
|
63
|
+
active.resolveStateRetained = resolve;
|
|
64
|
+
});
|
|
60
65
|
transport.interrupt(reason);
|
|
66
|
+
return { stateRetained };
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
async reset(): Promise<void> {
|
|
@@ -187,13 +193,18 @@ export class PythonKernel {
|
|
|
187
193
|
if (this.#transport !== transport) return;
|
|
188
194
|
const pending = this.#pending.get(result.cellId);
|
|
189
195
|
if (pending) this.#settleRun(pending, result);
|
|
196
|
+
// A result frame from the live runner proves the process survived the interrupt.
|
|
197
|
+
if (pending?.resolveStateRetained) pending.resolveStateRetained(true);
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
#onExit(transport: PythonKernelTransport, error: Error): void {
|
|
193
201
|
if (this.#transport !== transport) return;
|
|
194
202
|
this.#transport = null;
|
|
195
203
|
const active = this.#active;
|
|
196
|
-
if (active)
|
|
204
|
+
if (active) {
|
|
205
|
+
if (active.resolveStateRetained) active.resolveStateRetained(false);
|
|
206
|
+
this.#settleRun(active, failedPythonResult(active.input.cellId, "Python kernel died", error.message));
|
|
207
|
+
}
|
|
197
208
|
this.#startNext();
|
|
198
209
|
}
|
|
199
210
|
|
|
@@ -209,7 +220,10 @@ export class PythonKernel {
|
|
|
209
220
|
},
|
|
210
221
|
);
|
|
211
222
|
const active = this.#active;
|
|
212
|
-
if (active)
|
|
223
|
+
if (active) {
|
|
224
|
+
if (active.resolveStateRetained) active.resolveStateRetained(false);
|
|
225
|
+
this.#settleRun(active, failedPythonResult(active.input.cellId, "Python kernel died", error.message));
|
|
226
|
+
}
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
#settleRun(pending: PendingRun, result: ResultMessage): void {
|
|
@@ -258,6 +272,7 @@ export class PythonKernel {
|
|
|
258
272
|
async #escalateInterruptedRun(pending: PendingRun): Promise<void> {
|
|
259
273
|
if (this.#active !== pending || pending.interruptReason === undefined) return;
|
|
260
274
|
const transport = this.#transport;
|
|
275
|
+
if (pending.resolveStateRetained) pending.resolveStateRetained(false);
|
|
261
276
|
if (transport) await this.#beginRetirement(transport);
|
|
262
277
|
if (this.#pending.has(pending.input.cellId))
|
|
263
278
|
this.#settleRun(pending, failedPythonResult(pending.input.cellId, "Eval interrupted"));
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import locale
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
|
+
import signal
|
|
15
16
|
import subprocess
|
|
16
17
|
import sys
|
|
17
18
|
import time
|
|
@@ -36,6 +37,7 @@ EMIT_LOCK = Lock()
|
|
|
36
37
|
# Mirrors src/bridge/reserved.ts; this standalone subprocess asset cannot import TypeScript.
|
|
37
38
|
RESERVED_AGENT_TOOL = "__agent__"
|
|
38
39
|
RESERVED_OUTPUT_TOOL = "__output__"
|
|
40
|
+
RESERVED_SCHEMA_TOOL = "__schema__"
|
|
39
41
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
40
42
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
41
43
|
|
|
@@ -373,6 +375,14 @@ def completion(
|
|
|
373
375
|
return response.get("text", response)
|
|
374
376
|
|
|
375
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
|
+
|
|
376
386
|
def output(
|
|
377
387
|
*ids: str,
|
|
378
388
|
format: str = "raw",
|
|
@@ -843,6 +853,7 @@ USER_NS.update(
|
|
|
843
853
|
"completion": completion,
|
|
844
854
|
"agent": agent,
|
|
845
855
|
"output": output,
|
|
856
|
+
"tool_schema": tool_schema,
|
|
846
857
|
"__senpi_magic": _magic,
|
|
847
858
|
"__senpi_magic_cell": _magic_cell,
|
|
848
859
|
"__senpi_shell": _shell,
|
|
@@ -886,6 +897,9 @@ def run_cell(cell_id: str, code: str) -> None:
|
|
|
886
897
|
start = time.monotonic()
|
|
887
898
|
stdout = io.StringIO()
|
|
888
899
|
stderr = io.StringIO()
|
|
900
|
+
# SIGINT must interrupt user code here; the idle baseline (set between
|
|
901
|
+
# cells) ignores it so a late signal cannot kill the stdin-read loop.
|
|
902
|
+
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
889
903
|
try:
|
|
890
904
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
891
905
|
body, expression = compile_cell(code)
|
|
@@ -914,6 +928,8 @@ def run_cell(cell_id: str, code: str) -> None:
|
|
|
914
928
|
"durationMs": elapsed(start),
|
|
915
929
|
}
|
|
916
930
|
)
|
|
931
|
+
finally:
|
|
932
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
917
933
|
|
|
918
934
|
|
|
919
935
|
def elapsed(start: float) -> int:
|
|
@@ -942,6 +958,7 @@ def handle(message: dict[str, Any]) -> bool:
|
|
|
942
958
|
|
|
943
959
|
|
|
944
960
|
def main() -> None:
|
|
961
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
945
962
|
for raw in sys.stdin:
|
|
946
963
|
try:
|
|
947
964
|
if not handle(json.loads(raw)):
|
|
@@ -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)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
+
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
3
4
|
import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
|
|
4
5
|
import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
|
|
5
6
|
import { SubprocessRunQueue } from "./subprocess-queue.ts";
|
|
@@ -44,24 +45,26 @@ export class SubprocessKernel {
|
|
|
44
45
|
return run;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
async interrupt(reason = "interrupted"): Promise<
|
|
48
|
-
if (this.closed) return;
|
|
48
|
+
async interrupt(reason = "interrupted"): Promise<KernelInterruptHandle> {
|
|
49
|
+
if (this.closed) return { stateRetained: Promise.resolve(true) };
|
|
49
50
|
if (!this.runs.active) {
|
|
50
|
-
if (!this.retirementPromise) return;
|
|
51
|
+
if (!this.retirementPromise) return { stateRetained: Promise.resolve(true) };
|
|
51
52
|
const queued = this.runs.takeWaiting();
|
|
52
53
|
if (queued) this.runs.settle(queued, failureResult(queued, new CellInterruptedError(reason)));
|
|
53
|
-
return;
|
|
54
|
+
return { stateRetained: Promise.resolve(true) };
|
|
54
55
|
}
|
|
55
56
|
const process = this.process;
|
|
56
57
|
process?.retire();
|
|
57
58
|
this.runs.clearToolCalls();
|
|
58
59
|
const run = this.runs.active;
|
|
59
|
-
if (!run) return;
|
|
60
|
+
if (!run) return { stateRetained: Promise.resolve(true) };
|
|
60
61
|
this.runs.releaseActive(run);
|
|
61
62
|
this.runs.settle(run, failureResult(run, new CellInterruptedError(reason)));
|
|
62
63
|
const signal = globalThis.process.platform === "win32" ? "SIGTERM" : "SIGINT";
|
|
63
64
|
await this.restartProcess(process, signal, 5_000);
|
|
64
65
|
if (this.failure) throw this.failure;
|
|
66
|
+
// Restart always spawns a fresh interpreter, so no user global survives.
|
|
67
|
+
return { stateRetained: Promise.resolve(false) };
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
nextToolCall(): Promise<ToolCallMessage> {
|
|
@@ -21,7 +21,7 @@ export interface EvalPromptOptions {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Prompt dialect for the eval-first batching emphasis. */
|
|
24
|
-
export type EvalEmphasisStyle = "default" | "claude" | "codex" | "kimi";
|
|
24
|
+
export type EvalEmphasisStyle = "default" | "claude" | "codex" | "gpt" | "kimi";
|
|
25
25
|
|
|
26
26
|
const CLAUDE_MODEL_RE = /(^|[/.:])claude[-.]/i;
|
|
27
27
|
const GLM_MODEL_RE = /(^|[/.:@-])glm[-.]?\d/i;
|
|
@@ -32,14 +32,22 @@ const OPENAI_MODEL_RE = /(^|[/.:])(gpt|chatgpt|codex)[-.]|(^|[/.:])o[134](?:[-.]
|
|
|
32
32
|
* Selects the eval-first batching dialect for a model id:
|
|
33
33
|
* - `claude`: Claude/GLM — direct imperatives; both are steered most reliably
|
|
34
34
|
* by explicit tagged directives (GLM prompting guidance routes to Claude's).
|
|
35
|
-
* - `
|
|
35
|
+
* - `gpt`: GPT models — terse composition-forward rules that direct detached
|
|
36
|
+
* cells to notify on completion instead of being polled.
|
|
37
|
+
* - `codex`: Other OpenAI reasoning families — terse bounded rules, no emphasis spam.
|
|
36
38
|
* - `kimi`: Kimi K-series — maximum-emphasis POSITIVE imperatives (uppercase/
|
|
37
39
|
* bold DO-framing); all-caps NEVER prohibitions stay out because they make
|
|
38
40
|
* K-series overthink instead of comply.
|
|
39
41
|
* - `default`: everything else (and no model) — maximum-emphasis fallback.
|
|
40
42
|
*/
|
|
43
|
+
/** True only for GPT model ids that receive the terse eval composition dialect. */
|
|
44
|
+
export function isGptCodeModeModel(modelId: string | undefined): boolean {
|
|
45
|
+
return modelId !== undefined && /(^|[/.:])gpt[-.]/iu.test(modelId);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyle {
|
|
42
49
|
if (!modelId) return "default";
|
|
50
|
+
if (isGptCodeModeModel(modelId)) return "gpt";
|
|
43
51
|
if (CLAUDE_MODEL_RE.test(modelId) || GLM_MODEL_RE.test(modelId)) return "claude";
|
|
44
52
|
if (KIMI_MODEL_RE.test(modelId)) return "kimi";
|
|
45
53
|
if (OPENAI_MODEL_RE.test(modelId)) return "codex";
|
|
@@ -92,7 +100,11 @@ Work incrementally: imports in one call, define in the next, test, then use —
|
|
|
92
100
|
- Enumerate every lookup the step needs, then run all independent ones simultaneously with \`parallel(thunks)\` inside the cell; keep calls sequential only when one result feeds the next.
|
|
93
101
|
- Write real code around the calls: loop or comprehend over file sets with \`read()\`/stdlib, branch per case, and wrap risky calls in try/except so one failure degrades only its item — recover or retry inside the cell, keep the batch alive.
|
|
94
102
|
- Post-process \`tool.<name>()\` results programmatically and return distilled facts, not raw dumps.
|
|
95
|
-
</eval_first_batching>{{/if}}{{#if
|
|
103
|
+
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
104
|
+
GPT eval: compose multi-tool work inside one cell with \`tool.<name>(args)\` and \`parallel(thunks)\`; do not split a planned step into serial tool calls.
|
|
105
|
+
- Long pure-compute cells detach on timeout and notify on completion. Do not poll or re-run them; use \`eval({ action: "peek"|"stop", cell_id })\` only to inspect or stop a detached cell.
|
|
106
|
+
- Reduce tool results in the cell and return only decision-relevant facts.
|
|
107
|
+
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route multi-call steps through eval: one cell per step, independent lookups dispatched together via \`parallel(thunks)\`; keep work sequential only when one result determines the next action.
|
|
96
108
|
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically.
|
|
97
109
|
- Wrap failable calls in try/except inside the cell; a failed item degrades only itself. After two distinct failed strategies for the same fact, fall back to direct tool calls.
|
|
98
110
|
- Reduce large results in-kernel to the facts the task needs before returning.{{/if}}{{#if styleKimi}}**EVAL IS YOUR SUPERPOWER — MAKE IT YOUR DEFAULT WAY TO ACT.** Before any step, think: "how do I execute this WHOLE step in ONE parallelized cell?" — then write that ONE cell.
|
|
@@ -112,13 +124,17 @@ Fields:
|
|
|
112
124
|
- \`code\` — cell body, verbatim. Newlines/quotes JSON-encoded; no fences, no headers.
|
|
113
125
|
- \`title\` (optional) — short transcript label (e.g. \`"imports"\`).
|
|
114
126
|
- \`timeout\` (optional) — seconds. Raise only for heavy compute or long{{#if spawns}} non-agent{{/if}} tool calls.
|
|
127
|
+
- \`on_timeout\` (optional) — \`"detach"\` keeps pure computation running in interactive sessions (the default); \`"error"\` interrupts for deadline-sensitive work and is the print/json default.
|
|
115
128
|
- \`reset\` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a \`py\` reset never touches the JS VM.{{/ifAll}}
|
|
129
|
+
- \`action\` (optional) — defaults to \`"run"\`. A detached cell returns its id: use \`eval({ action: "peek", cell_id })\` for buffered output/state or \`eval({ action: "stop", cell_id })\` to cancel it.
|
|
130
|
+
|
|
131
|
+
A detached cell keeps its language kernel busy while it finishes. Do not re-run a detached cell: the same-language busy error names its cell id and output tail; another language can continue. Completion arrives as one notification with the final value/error and buffered output. Stopping a cell interrupts its kernel; the stop result states whether kernel state survived or the kernel was restarted and its variables lost.
|
|
116
132
|
|
|
117
133
|
{{#if py}}Live event loop: use top-level \`await\` directly; \`asyncio.run(…)\` raises "cannot be called from a running event loop".{{/if}}
|
|
118
134
|
{{#if js}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}
|
|
119
135
|
{{#if rb}}Ruby: synchronous; helper options are keyword args{{#if spawns}} (e.g. \`output("id", limit: 2)\`){{/if}}; the last expression auto-displays unless it is \`nil\`, an assignment, or a definition (like IRB).{{/if}}
|
|
120
136
|
{{#if jl}}Julia: synchronous; helper options are standard keyword args{{#if spawns}} (e.g. \`output("id", limit=2)\`){{/if}}; the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
|
121
|
-
On error, fix and re-run only the failing step —
|
|
137
|
+
On error, fix and re-run only the failing step. State usually survives a normal error, but a timeout or stop may have restarted the kernel — its message says which. Before rebuilding state, check a sentinel (a variable you defined earlier); only re-establish what is actually gone, since blind re-runs duplicate side effects.
|
|
122
138
|
</instruction>
|
|
123
139
|
|
|
124
140
|
<prelude>
|
|
@@ -135,9 +151,14 @@ write(path, content) → str
|
|
|
135
151
|
env(key?=None, value?=None) → str | None | dict
|
|
136
152
|
No args → full env dict; one → value of \`key\`; two → set \`key=value\`, return value.
|
|
137
153
|
{{#if spawns}}output(*ids, format?="raw", offset?=None, limit?=None) → str | dict | list[dict]
|
|
138
|
-
Task/agent output by id. \`format\` selects full (\`"raw"\`) or trailing (\`"tail"\`) output.
|
|
154
|
+
Task/agent output by id. Reads immediately: running tasks return their status; \`format\` selects full (\`"raw"\`) or trailing (\`"tail"\`) output.
|
|
139
155
|
{{/if}}tool.<name>(args) → unknown
|
|
140
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.
|
|
141
162
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
142
163
|
Oneshot, stateless (no history/tools). \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → structured output, parsed object.
|
|
143
164
|
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
@@ -186,6 +207,7 @@ export function buildEvalPrompt(
|
|
|
186
207
|
spawnDefaultAgent,
|
|
187
208
|
styleClaude: style === "claude",
|
|
188
209
|
styleCodex: style === "codex",
|
|
210
|
+
styleGpt: style === "gpt",
|
|
189
211
|
styleKimi: style === "kimi",
|
|
190
212
|
styleDefault: style === "default",
|
|
191
213
|
hostLine: options.hostLine ?? "",
|
|
@@ -225,6 +247,7 @@ const BATCHING_GUIDELINES: Record<EvalEmphasisStyle, string> = {
|
|
|
225
247
|
claude:
|
|
226
248
|
"Prefer eval for any step needing more than one tool call: one cell that runs independent calls in parallel, handles per-call failures in code, and returns distilled facts.",
|
|
227
249
|
codex: "Route multi-call steps through eval: one cell per step, independent calls dispatched in parallel; fall back to direct tool calls when one call is sufficient or each result changes the next decision.",
|
|
250
|
+
gpt: "Use eval to compose tool work in one cell; long cells detach on timeout and notify on completion, so do not poll.",
|
|
228
251
|
kimi: "**EVAL IS YOUR SUPERPOWER — DEFAULT TO IT.** Execute EVERY multi-call step as ONE eval cell: run ALL independent calls simultaneously via parallel(thunks), handle failures per item in code, and return ONLY distilled facts.",
|
|
229
252
|
};
|
|
230
253
|
|