@code-yeongyu/senpi-codemode 2026.10.6 → 2026.10.8
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 +37 -0
- package/README.md +20 -2
- package/package.json +5 -4
- package/src/bridge/kernel-tools-protocol.ts +8 -0
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +100 -0
- package/src/bridges/handle-bridge.ts +72 -0
- package/src/bridges/reserved-dispatch.ts +20 -2
- package/src/bridges/schema-bridge.ts +4 -0
- package/src/completion/handle.ts +69 -0
- package/src/completion/tool-bridge.ts +31 -12
- package/src/environments/editable-check.ts +116 -0
- package/src/environments/install-lock.ts +223 -0
- package/src/environments/py-environment.ts +98 -0
- package/src/environments/py-installer.ts +187 -0
- package/src/environments/revision-store.ts +95 -0
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/kernel-registration.ts +5 -0
- package/src/extension/removed-tool-hints.ts +15 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +32 -5
- package/src/extension/start-recovery.ts +111 -0
- package/src/handles/completion-handles.ts +202 -0
- package/src/handles/handle-args.ts +103 -0
- package/src/handles/handle-registry.ts +164 -0
- package/src/handles/wait.ts +214 -0
- package/src/handles/watch-queue.ts +102 -0
- package/src/index.ts +99 -94
- package/src/kernels/jl/handles.jl +124 -0
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +6 -1
- package/src/kernels/js/handles.js +93 -0
- package/src/kernels/js/kernel-tools-define.d.ts +6 -0
- package/src/kernels/js/kernel-tools-define.js +3 -1
- package/src/kernels/js/kernel-tools-errors.js +2 -0
- package/src/kernels/js/kernel-tools-errors.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.js +18 -9
- package/src/kernels/js/kernel-tools-schema.js +6 -2
- package/src/kernels/js/worker-runtime.js +10 -1
- package/src/kernels/py/host-cell.ts +42 -0
- package/src/kernels/py/kernel-contract.ts +13 -2
- package/src/kernels/py/kernel-tools-host.ts +97 -0
- package/src/kernels/py/kernel.ts +88 -12
- package/src/kernels/py/prelude.py +797 -11
- package/src/kernels/py/process.ts +15 -4
- package/src/kernels/py/startup.ts +58 -20
- package/src/kernels/py/transport.ts +36 -7
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +7 -7
- package/src/kernels/sandbox/vendor/pi-codemode/LICENSE +21 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +42 -0
- package/src/kernels/sandbox/vendor/pi-codemode/identifier.ts +14 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +514 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +431 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/protocol.ts +61 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +231 -0
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +185 -0
- package/src/kernels/sandbox/vendor/pi-codemode/wasm.ts +37 -0
- package/src/kernels/shared/kernel-tools-pump.ts +6 -0
- package/src/kernels/shared/process-cpu.ts +69 -0
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/tool/cell-handler.ts +21 -18
- package/src/tool/cell-reserved-dispatch.ts +51 -0
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/run-eval-cell.ts +2 -0
- package/src/tool/types.ts +15 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
2
|
import type { AgentExecuteTool } from "./bridges/agent-bridge.ts";
|
|
4
3
|
import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
@@ -14,18 +13,22 @@ import {
|
|
|
14
13
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
15
14
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
16
15
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
16
|
+
import { hostLine, modelIdFrom } from "./extension/host-facts.ts";
|
|
17
17
|
import { activeKernelPreludes, kernelPreludeDocsKey, promptKernelPreludes } from "./extension/kernel-preludes.ts";
|
|
18
|
+
import { registerRemovedToolHints } from "./extension/removed-tool-hints.ts";
|
|
18
19
|
import {
|
|
19
20
|
createExecuteTool,
|
|
20
21
|
createRuntime,
|
|
21
22
|
enabledLanguagesFrom,
|
|
22
23
|
type SessionRuntime,
|
|
23
24
|
} from "./extension/runtime-factory.ts";
|
|
24
|
-
import { jsRuntimeInfo
|
|
25
|
+
import { jsRuntimeInfo } from "./extension/runtime-info.ts";
|
|
25
26
|
import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
|
|
26
27
|
import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
|
|
27
28
|
import { activeBunSkillPath, registerBunSkillContribution } from "./extension/skill-contribution.ts";
|
|
29
|
+
import { StartRecovery, withStartRecovery } from "./extension/start-recovery.ts";
|
|
28
30
|
import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
|
|
31
|
+
import { HandleRegistry } from "./handles/handle-registry.ts";
|
|
29
32
|
import type { KernelToolsCapability } from "./kernels/js/kernel-tools-types.ts";
|
|
30
33
|
import { EvalDetachedCellManager, type EvalDetachedCellStatusEntry } from "./tool/detached-cell-manager.ts";
|
|
31
34
|
import {
|
|
@@ -36,12 +39,9 @@ import {
|
|
|
36
39
|
import { createEvalTool } from "./tool/eval-tool.ts";
|
|
37
40
|
import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"session_before_switch",
|
|
43
|
-
"session_before_fork",
|
|
44
|
-
] as const;
|
|
42
|
+
// session_before_switch / session_before_fork are veto points another extension can cancel, so they never
|
|
43
|
+
// tear the runtime down; a switch or fork that goes ahead emits session_shutdown before the old session ends.
|
|
44
|
+
const SESSION_LIFECYCLE_EVENTS = ["session_start", "session_shutdown"] as const;
|
|
45
45
|
|
|
46
46
|
type SessionLifecycleEvent = (typeof SESSION_LIFECYCLE_EVENTS)[number];
|
|
47
47
|
|
|
@@ -93,6 +93,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
93
93
|
let activeModelId: string | undefined;
|
|
94
94
|
let activeContext: ExtensionContext | undefined;
|
|
95
95
|
let activeCells: EvalDetachedCellManager | undefined;
|
|
96
|
+
let activeHandles: HandleRegistry | undefined;
|
|
96
97
|
let promptPreludeDocs = "";
|
|
97
98
|
const notifier = new EvalNotifier({
|
|
98
99
|
sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
|
|
@@ -122,10 +123,12 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
122
123
|
pi.rpc?.emit(WAKE_SOURCE_STATE_EVENT, state);
|
|
123
124
|
pi.events?.emit(WAKE_SOURCE_STATE_EVENT, state);
|
|
124
125
|
};
|
|
126
|
+
const recovery = new StartRecovery(async (event, ctx) => await startSession(event, ctx));
|
|
125
127
|
const registerEvalForRuntime = (
|
|
126
128
|
runtime: SessionRuntime,
|
|
127
129
|
modelId: string | undefined,
|
|
128
130
|
cellManager: EvalDetachedCellManager,
|
|
131
|
+
handles: HandleRegistry,
|
|
129
132
|
): void => {
|
|
130
133
|
const onCellSettled = (payload: EvalExecutionEventPayload): void => {
|
|
131
134
|
if (activeCells !== cellManager) return;
|
|
@@ -140,93 +143,104 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
140
143
|
const preludes = promptKernelPreludes(pi);
|
|
141
144
|
promptPreludeDocs = kernelPreludeDocsKey(preludes);
|
|
142
145
|
pi.registerTool(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
146
|
+
withStartRecovery(
|
|
147
|
+
recovery,
|
|
148
|
+
createEvalTool({
|
|
149
|
+
enabledLanguages: runtime.enabledLanguages,
|
|
150
|
+
kernelManager: manager,
|
|
151
|
+
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
152
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(runtime.settings),
|
|
153
|
+
runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
|
|
154
|
+
hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
|
|
155
|
+
executeTool: runtime.executeTool,
|
|
156
|
+
listTools: () => pi.getAllTools(),
|
|
157
|
+
complete,
|
|
158
|
+
settings: runtime.settings,
|
|
159
|
+
artifactsDir: runtime.artifactsDir,
|
|
160
|
+
cellManager,
|
|
161
|
+
handles,
|
|
162
|
+
executionTracker: manager,
|
|
163
|
+
onCellSettled,
|
|
164
|
+
renderers,
|
|
165
|
+
monitor,
|
|
166
|
+
spawns: runtime.spawns,
|
|
167
|
+
spawnDefaultAgent: runtime.settings.taskTools.task,
|
|
168
|
+
hostLine: hostLine(),
|
|
169
|
+
runtimes: runtime.runtimes,
|
|
170
|
+
kernelPreludes: () => activeKernelPreludes(pi),
|
|
171
|
+
promptKernelPreludes: preludes,
|
|
172
|
+
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
173
|
+
...(modelId === undefined ? {} : { modelId }),
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
169
176
|
);
|
|
170
177
|
};
|
|
171
178
|
const dropRuntime = async (): Promise<void> => {
|
|
172
179
|
const cells = activeCells;
|
|
180
|
+
const handles = activeHandles;
|
|
173
181
|
activeRuntime = undefined;
|
|
174
182
|
activeModelId = undefined;
|
|
175
183
|
activeCells = undefined;
|
|
184
|
+
activeHandles = undefined;
|
|
176
185
|
statusTicker.stop();
|
|
186
|
+
// Fail every saved handle closed before the kernels go: a late wait() must never reach a successor generation.
|
|
187
|
+
handles?.dispose();
|
|
177
188
|
await cells?.dispose();
|
|
178
189
|
activeContext = undefined;
|
|
179
190
|
await manager.dispose();
|
|
180
191
|
};
|
|
181
192
|
pi.registerTool(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
executeTool: createExecuteTool(pi),
|
|
190
|
-
listTools: () => pi.getAllTools(),
|
|
191
|
-
complete,
|
|
192
|
-
settings: defaultCodemodeSettings,
|
|
193
|
-
cellManager: new EvalDetachedCellManager({
|
|
194
|
-
notifier,
|
|
195
|
-
maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
|
|
196
|
-
retainedResultsBytes: resolveRetainedResultsBytes(defaultCodemodeSettings),
|
|
197
|
-
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
193
|
+
withStartRecovery(
|
|
194
|
+
recovery,
|
|
195
|
+
createEvalTool({
|
|
196
|
+
enabledLanguages: { py: true, js: true, rb: true, jl: true },
|
|
197
|
+
kernelManager: manager,
|
|
198
|
+
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
199
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(defaultCodemodeSettings),
|
|
198
200
|
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
202
|
+
executeTool: createExecuteTool(pi),
|
|
203
|
+
listTools: () => pi.getAllTools(),
|
|
204
|
+
complete,
|
|
205
|
+
settings: defaultCodemodeSettings,
|
|
206
|
+
cellManager: new EvalDetachedCellManager({
|
|
207
|
+
notifier,
|
|
208
|
+
maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
|
|
209
|
+
retainedResultsBytes: resolveRetainedResultsBytes(defaultCodemodeSettings),
|
|
210
|
+
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
211
|
+
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
212
|
+
onStatusChange: showDetachedCells,
|
|
213
|
+
onWakeSourceState: emitWakeSourceState,
|
|
214
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
215
|
+
}),
|
|
216
|
+
executionTracker: manager,
|
|
217
|
+
renderers,
|
|
218
|
+
// The baseline tool is registered before extensions such as monitor load.
|
|
219
|
+
monitor: false,
|
|
220
|
+
hostLine: hostLine(),
|
|
221
|
+
runtimes: { js: jsRuntimeInfo() },
|
|
222
|
+
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
202
223
|
}),
|
|
203
|
-
|
|
204
|
-
renderers,
|
|
205
|
-
// The baseline tool is registered before extensions such as monitor load.
|
|
206
|
-
monitor: false,
|
|
207
|
-
hostLine: hostLine(),
|
|
208
|
-
runtimes: { js: jsRuntimeInfo() },
|
|
209
|
-
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
210
|
-
}),
|
|
211
|
-
);
|
|
212
|
-
pi.registerRemovedToolHint(
|
|
213
|
-
"exec",
|
|
214
|
-
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on their own and notify when complete.',
|
|
215
|
-
);
|
|
216
|
-
pi.registerRemovedToolHint(
|
|
217
|
-
"wait",
|
|
218
|
-
'wait was removed; detached eval cells notify when complete. Use eval({ action: "peek"|"stop", cell_id }) to inspect or stop one.',
|
|
224
|
+
),
|
|
219
225
|
);
|
|
226
|
+
registerRemovedToolHints(pi);
|
|
220
227
|
registerBunSkillContribution(pi);
|
|
221
228
|
|
|
222
|
-
|
|
229
|
+
const startSession = async (event: unknown, ctx: ExtensionContext): Promise<void> => {
|
|
223
230
|
const previousCells = activeCells;
|
|
231
|
+
const previousHandles = activeHandles;
|
|
224
232
|
activeCells = undefined;
|
|
233
|
+
activeHandles = undefined;
|
|
234
|
+
previousHandles?.dispose();
|
|
225
235
|
await previousCells?.dispose();
|
|
226
236
|
const generation = manager.beginReplacement();
|
|
227
|
-
const
|
|
237
|
+
const handles = new HandleRegistry({ ownerSessionId: ctx.sessionManager.getSessionId() });
|
|
238
|
+
const runtime = await createRuntime(pi, ctx, event, complete, options, handles);
|
|
228
239
|
const replaced = await manager.replace(generation, runtime.manager);
|
|
229
|
-
if (!replaced)
|
|
240
|
+
if (!replaced) {
|
|
241
|
+
handles.dispose();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
230
244
|
notifier.reset();
|
|
231
245
|
activeContext = ctx;
|
|
232
246
|
const cellManager = new EvalDetachedCellManager({
|
|
@@ -242,15 +256,18 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
242
256
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
243
257
|
});
|
|
244
258
|
activeCells = cellManager;
|
|
259
|
+
activeHandles = handles;
|
|
245
260
|
// The goal builtin clears its per-session counts at session_start; re-publish our snapshot.
|
|
246
261
|
cellManager.publishWakeSourceState();
|
|
247
262
|
activeRuntime = runtime;
|
|
248
263
|
activeModelId = ctx.model?.id;
|
|
249
|
-
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
264
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager, handles);
|
|
265
|
+
};
|
|
266
|
+
pi.on("session_start", async (event, ctx) => await recovery.runStart(event, ctx));
|
|
267
|
+
pi.on("session_shutdown", async () => {
|
|
268
|
+
recovery.sessionEnded();
|
|
269
|
+
await dropRuntime();
|
|
250
270
|
});
|
|
251
|
-
pi.on("session_shutdown", async () => dropRuntime());
|
|
252
|
-
pi.on("session_before_switch", async () => dropRuntime());
|
|
253
|
-
pi.on("session_before_fork", async () => dropRuntime());
|
|
254
271
|
pi.on("model_select", async (event, ctx) => {
|
|
255
272
|
activeContext = ctx;
|
|
256
273
|
const runtime = activeRuntime;
|
|
@@ -259,33 +276,21 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
259
276
|
if (modelId === undefined || modelId === activeModelId) return;
|
|
260
277
|
activeModelId = modelId;
|
|
261
278
|
const cellManager = activeCells;
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
const handles = activeHandles;
|
|
280
|
+
if (cellManager === undefined || handles === undefined) return;
|
|
281
|
+
registerEvalForRuntime(runtime, modelId, cellManager, handles);
|
|
264
282
|
});
|
|
265
283
|
// Tool activation has no event of its own; the next turn re-documents a changed contribution set.
|
|
266
284
|
pi.on("turn_start", async () => {
|
|
267
285
|
const runtime = activeRuntime;
|
|
268
286
|
const cellManager = activeCells;
|
|
269
|
-
|
|
287
|
+
const handles = activeHandles;
|
|
288
|
+
if (runtime === undefined || cellManager === undefined || handles === undefined) return;
|
|
270
289
|
if (kernelPreludeDocsKey(promptKernelPreludes(pi)) === promptPreludeDocs) return;
|
|
271
|
-
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
290
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager, handles);
|
|
272
291
|
});
|
|
273
292
|
}
|
|
274
293
|
|
|
275
|
-
function hostLine(): string {
|
|
276
|
-
const cpu = os.cpus()[0]?.model?.trim();
|
|
277
|
-
return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`, jsRuntimeLabel()]
|
|
278
|
-
.filter((part): part is string => !!part)
|
|
279
|
-
.join(" \u00b7 ");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function modelIdFrom(event: unknown): string | undefined {
|
|
283
|
-
if (typeof event !== "object" || event === null || !("model" in event)) return undefined;
|
|
284
|
-
const model = event.model;
|
|
285
|
-
if (typeof model !== "object" || model === null || !("id" in model)) return undefined;
|
|
286
|
-
return typeof model.id === "string" ? model.id : undefined;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
294
|
export {
|
|
290
295
|
KERNEL_TOOLS_CAPABILITIES,
|
|
291
296
|
KERNEL_TOOLS_UNSUPPORTED,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# In-cell wait()/handle() over the host's handle capability. Mirrors src/bridge/reserved.ts.
|
|
2
|
+
# `Main.wait` is never defined here: `Base.wait` gains methods for the handle types, so `wait(::Task)` for
|
|
3
|
+
# `@async` users keeps working.
|
|
4
|
+
const SENPI_RESERVED_WAIT_TOOL = "__wait__"
|
|
5
|
+
const SENPI_RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
6
|
+
const SENPI_RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
7
|
+
const SENPI_RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
8
|
+
const SENPI_RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
9
|
+
const SENPI_HANDLE_KINDS = ("agent", "completion", "workpool")
|
|
10
|
+
const SENPI_WAIT_MODES = ("all", "any", "settled")
|
|
11
|
+
const SENPI_WAIT_SOCKET_GRACE_SECONDS = 30
|
|
12
|
+
const SENPI_HANDLE_USAGE = "handle() expects an agent(...; handle=true) record, a workpool, a completion handle, or a saved {kind, id, run_epoch} reference"
|
|
13
|
+
|
|
14
|
+
struct SenpiHandleControl
|
|
15
|
+
ref::Dict{String, Any}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The legacy record's fields behave as a Dict; `control` and `ref` are properties, never keys.
|
|
19
|
+
struct SenpiHandle <: AbstractDict{String, Any}
|
|
20
|
+
fields::Dict{String, Any}
|
|
21
|
+
ref::Dict{String, Any}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Base.iterate(handle::SenpiHandle, state...) = iterate(getfield(handle, :fields), state...)
|
|
25
|
+
Base.length(handle::SenpiHandle) = length(getfield(handle, :fields))
|
|
26
|
+
Base.get(handle::SenpiHandle, key, default) = get(getfield(handle, :fields), key, default)
|
|
27
|
+
Base.getindex(handle::SenpiHandle, key) = getfield(handle, :fields)[key]
|
|
28
|
+
Base.haskey(handle::SenpiHandle, key) = haskey(getfield(handle, :fields), key)
|
|
29
|
+
Base.keys(handle::SenpiHandle) = keys(getfield(handle, :fields))
|
|
30
|
+
Base.values(handle::SenpiHandle) = values(getfield(handle, :fields))
|
|
31
|
+
|
|
32
|
+
function Base.getproperty(handle::SenpiHandle, name::Symbol)
|
|
33
|
+
name === :ref && return copy(getfield(handle, :ref))
|
|
34
|
+
name === :control && return SenpiHandleControl(copy(getfield(handle, :ref)))
|
|
35
|
+
fields = getfield(handle, :fields)
|
|
36
|
+
key = string(name)
|
|
37
|
+
haskey(fields, key) && return fields[key]
|
|
38
|
+
getfield(handle, name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Base.propertynames(handle::SenpiHandle) = (Symbol.(collect(keys(getfield(handle, :fields))))..., :control, :ref)
|
|
42
|
+
|
|
43
|
+
function senpi_handle_ref(value)
|
|
44
|
+
value isa SenpiHandle && return copy(getfield(value, :ref))
|
|
45
|
+
value isa SenpiWorkpool && return Dict{String, Any}("kind" => "workpool", "id" => getfield(value, :pool_id), "run_epoch" => 0)
|
|
46
|
+
value isa AbstractDict || error(SENPI_HANDLE_USAGE)
|
|
47
|
+
record = Dict{String, Any}(string(key) => item for (key, item) in value)
|
|
48
|
+
pool_id = get(record, "pool_id", nothing)
|
|
49
|
+
pool_id isa AbstractString && return Dict{String, Any}("kind" => "workpool", "id" => string(pool_id), "run_epoch" => 0)
|
|
50
|
+
kind = get(record, "kind", nothing)
|
|
51
|
+
if !(kind in SENPI_HANDLE_KINDS)
|
|
52
|
+
handle_uri = get(record, "handle", nothing)
|
|
53
|
+
scheme = handle_uri isa AbstractString ? first(split(handle_uri, "://"; limit=2)) : nothing
|
|
54
|
+
kind = scheme in SENPI_HANDLE_KINDS ? scheme : nothing
|
|
55
|
+
end
|
|
56
|
+
identity = get(record, "id", get(record, "task_id", nothing))
|
|
57
|
+
(kind === nothing || !(identity isa AbstractString) || isempty(identity)) && error(SENPI_HANDLE_USAGE)
|
|
58
|
+
run_epoch = get(record, "run_epoch", kind == "workpool" ? 0 : nothing)
|
|
59
|
+
(run_epoch isa Integer && !(run_epoch isa Bool) && run_epoch >= 0) || error(SENPI_HANDLE_USAGE * "; run_epoch must be a non-negative integer")
|
|
60
|
+
Dict{String, Any}("kind" => string(kind), "id" => string(identity), "run_epoch" => Int(run_epoch))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
function senpi_handle_call(tool_name::String, arguments)
|
|
64
|
+
senpi_with_bridge_timeout_pause(() -> senpi_call_tool(tool_name, arguments))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A long-lived request bounded by the explicit timeout (plus grace for the host's reply) or else only by the
|
|
68
|
+
# cell's own end; an interrupt from the host (cancel, the cell's hard limit) closes the socket and so the
|
|
69
|
+
# host-side subscription.
|
|
70
|
+
function senpi_wait_post(arguments, timeout)
|
|
71
|
+
read_timeout = timeout === nothing ? nothing : Float64(timeout) + SENPI_WAIT_SOCKET_GRACE_SECONDS
|
|
72
|
+
payload = Dict("callId" => "jl-" * string(time_ns()), "toolName" => SENPI_RESERVED_WAIT_TOOL, "args" => arguments)
|
|
73
|
+
senpi_with_bridge_timeout_pause(() -> senpi_bridge_request("/call", payload; read_timeout=read_timeout))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
function senpi_wait(handles; timeout=nothing, mode="all")
|
|
77
|
+
items = handles === nothing ? Any[] : handles isa AbstractVector || handles isa Tuple ? collect(handles) : Any[handles]
|
|
78
|
+
if timeout !== nothing
|
|
79
|
+
(timeout isa Real && !(timeout isa Bool) && isfinite(timeout) && timeout >= 0) || error("wait() timeout must be a finite number of seconds >= 0")
|
|
80
|
+
end
|
|
81
|
+
string(mode) in SENPI_WAIT_MODES || error("wait() mode must be \"all\", \"any\" or \"settled\"")
|
|
82
|
+
arguments = Dict{String, Any}("refs" => [senpi_handle_ref(item) for item in items], "mode" => string(mode))
|
|
83
|
+
timeout === nothing || (arguments["timeout"] = timeout)
|
|
84
|
+
senpi_wait_post(arguments, timeout)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
function Base.getproperty(control::SenpiHandleControl, name::Symbol)
|
|
88
|
+
ref = getfield(control, :ref)
|
|
89
|
+
name === :ref && return copy(ref)
|
|
90
|
+
name === :status && return () -> senpi_handle_call(SENPI_RESERVED_HANDLE_STATUS_TOOL, Dict("ref" => ref))
|
|
91
|
+
if name === :output
|
|
92
|
+
return (; format="raw", offset=nothing, limit=nothing) -> begin
|
|
93
|
+
arguments = Dict{String, Any}("ref" => ref, "format" => string(format))
|
|
94
|
+
offset === nothing || (arguments["offset"] = offset)
|
|
95
|
+
limit === nothing || (arguments["limit"] = limit)
|
|
96
|
+
senpi_handle_call(SENPI_RESERVED_HANDLE_OUTPUT_TOOL, arguments)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
name === :send && return message -> senpi_handle_call(SENPI_RESERVED_HANDLE_SEND_TOOL, Dict("ref" => ref, "message" => string(message)))
|
|
100
|
+
name === :cancel && return () -> senpi_handle_call(SENPI_RESERVED_HANDLE_CANCEL_TOOL, Dict("ref" => ref))
|
|
101
|
+
name === :wait && return (; timeout=nothing) -> senpi_wait([ref]; timeout=timeout, mode="all")[1]
|
|
102
|
+
getfield(control, name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Base.propertynames(::SenpiHandleControl) = (:status, :output, :send, :cancel, :wait, :ref)
|
|
106
|
+
Base.show(io::IO, control::SenpiHandleControl) = Base.print(io, "<handle.control ", getfield(control, :ref)["kind"], "://", getfield(control, :ref)["id"], "@", getfield(control, :ref)["run_epoch"], ">")
|
|
107
|
+
|
|
108
|
+
function senpi_handle_view(value)
|
|
109
|
+
ref = senpi_handle_ref(value)
|
|
110
|
+
fields = Dict{String, Any}()
|
|
111
|
+
if value isa AbstractDict
|
|
112
|
+
for (key, item) in value
|
|
113
|
+
item isa Function || (fields[string(key)] = item)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
get!(fields, "id", ref["id"])
|
|
117
|
+
get!(fields, "run_epoch", ref["run_epoch"])
|
|
118
|
+
get!(fields, "handle", ref["kind"] * "://" * ref["id"])
|
|
119
|
+
SenpiHandle(fields, ref)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Block until the handles settle; never cancels work. See tool_schema("eval:wait").
|
|
123
|
+
Base.wait(handle::SenpiHandle; timeout=nothing, mode="all") = senpi_wait([handle]; timeout=timeout, mode=mode)
|
|
124
|
+
Base.wait(handles::AbstractVector{<:SenpiHandle}; timeout=nothing, mode="all") = senpi_wait(handles; timeout=timeout, mode=mode)
|
|
@@ -226,6 +226,14 @@ function workpool(agent::AbstractDict, name::AbstractString; mode=nothing)
|
|
|
226
226
|
throw(SenpiBridgeError("Host did not return a workpool identity", "workpool_unavailable"))
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
+
include("handles.jl")
|
|
230
|
+
|
|
231
|
+
# Rich view of an agent record, workpool, completion handle or saved reference; see tool_schema("eval:helpers").
|
|
232
|
+
# The barrier is `Base.wait` on the view types (never a `Main.wait`): wait(handle(node)) or wait([handle(a), handle(b)]).
|
|
233
|
+
function handle(value)
|
|
234
|
+
senpi_handle_view(value)
|
|
235
|
+
end
|
|
236
|
+
|
|
229
237
|
function completion(prompt::AbstractString; model="default", system=nothing, schema=nothing, kwargs...)
|
|
230
238
|
options = Dict{String, Any}("model" => model)
|
|
231
239
|
system !== nothing && (options["system"] = system)
|
|
@@ -234,6 +242,7 @@ function completion(prompt::AbstractString; model="default", system=nothing, sch
|
|
|
234
242
|
options[string(key)] = value
|
|
235
243
|
end
|
|
236
244
|
response = senpi_with_bridge_timeout_pause(() -> senpi_completion(string(prompt), options))
|
|
245
|
+
get(options, "handle", false) === true && return handle(response)
|
|
237
246
|
response isa AbstractDict || return response
|
|
238
247
|
haskey(response, "value") && return response["value"]
|
|
239
248
|
get(response, "text", response)
|
package/src/kernels/jl/runner.jl
CHANGED
|
@@ -224,12 +224,15 @@ function senpi_http_body(response::AbstractString)
|
|
|
224
224
|
String(output)
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
# `read_timeout` (seconds) bounds only the long-lived `wait()` request; ordinary calls read until the host closes.
|
|
228
|
+
function senpi_bridge_request(path::String, payload; read_timeout=nothing)
|
|
228
229
|
port = get(senpi_connection, "port", nothing)
|
|
229
230
|
token = get(senpi_connection, "token", nothing)
|
|
230
231
|
port isa Integer && token isa AbstractString || error("Julia tool bridge is not initialized")
|
|
231
232
|
body = senpi_json(payload)
|
|
232
233
|
socket = connect(ip"127.0.0.1", port)
|
|
234
|
+
timed_out = Ref(false)
|
|
235
|
+
timer = read_timeout === nothing ? nothing : Timer(_ -> (timed_out[] = true; close(socket)), Float64(read_timeout))
|
|
233
236
|
try
|
|
234
237
|
request = join([
|
|
235
238
|
"POST " * path * " HTTP/1.1",
|
|
@@ -244,12 +247,14 @@ function senpi_bridge_request(path::String, payload)
|
|
|
244
247
|
Base.write(socket, request)
|
|
245
248
|
flush(socket)
|
|
246
249
|
response = Base.read(socket, String)
|
|
250
|
+
timed_out[] && throw(SenpiBridgeError("bridge request timed out after $(read_timeout)s", "bridge_timeout"))
|
|
247
251
|
parsed = senpi_json_parse(senpi_http_body(response))
|
|
248
252
|
parsed isa AbstractDict || error("Bridge returned invalid JSON")
|
|
249
253
|
get(parsed, "ok", false) === true && return get(parsed, "value", nothing)
|
|
250
254
|
failure = get(parsed, "error", parsed)
|
|
251
255
|
throw(SenpiBridgeError(failure isa AbstractDict ? string(get(failure, "message", failure)) : string(failure), failure isa AbstractDict ? get(failure, "code", nothing) : nothing))
|
|
252
256
|
finally
|
|
257
|
+
timer === nothing || close(timer)
|
|
253
258
|
close(socket)
|
|
254
259
|
end
|
|
255
260
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// In-cell `wait()` / `handle()` over the host's handle capability. Mirrors src/bridge/reserved.ts; this
|
|
2
|
+
// worker asset cannot import TypeScript. Every call rides the ordinary bridge-call path (`callTool`), so
|
|
3
|
+
// the run budget pauses while the cell is parked and the hard limit still applies.
|
|
4
|
+
const RESERVED_WAIT_TOOL = "__wait__";
|
|
5
|
+
const RESERVED_HANDLE_STATUS_TOOL = "__handle_status__";
|
|
6
|
+
const RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__";
|
|
7
|
+
const RESERVED_HANDLE_SEND_TOOL = "__handle_send__";
|
|
8
|
+
const RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__";
|
|
9
|
+
const KINDS = new Set(["agent", "completion", "workpool"]);
|
|
10
|
+
const WAIT_MODES = new Set(["all", "any", "settled"]);
|
|
11
|
+
const USAGE = "handle() expects an agent(..., {handle: true}) record, a workpool, a completion handle, or a saved {kind, id, run_epoch} reference";
|
|
12
|
+
|
|
13
|
+
export function createHandleHelpers(callTool) {
|
|
14
|
+
const wait = async (handles, options) => {
|
|
15
|
+
const list = Array.isArray(handles) ? handles : handles === undefined || handles === null ? [] : [handles];
|
|
16
|
+
const refs = list.map(toHandleRef);
|
|
17
|
+
const { timeout, mode } = waitOptions(options);
|
|
18
|
+
return await callTool(RESERVED_WAIT_TOOL, {
|
|
19
|
+
refs,
|
|
20
|
+
...(timeout === undefined ? {} : { timeout }),
|
|
21
|
+
...(mode === undefined ? {} : { mode }),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const handle = value => {
|
|
25
|
+
const ref = toHandleRef(value);
|
|
26
|
+
const view = Object.create(Object.prototype);
|
|
27
|
+
if (isPlainObject(value)) {
|
|
28
|
+
for (const [key, field] of Object.entries(value)) {
|
|
29
|
+
if (typeof field !== "function") view[key] = field;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!Object.hasOwn(view, "id")) view.id = ref.id;
|
|
33
|
+
if (!Object.hasOwn(view, "run_epoch")) view.run_epoch = ref.run_epoch;
|
|
34
|
+
if (!Object.hasOwn(view, "handle")) view.handle = `${ref.kind}://${ref.id}`;
|
|
35
|
+
const control = Object.freeze({
|
|
36
|
+
status: async () => await callTool(RESERVED_HANDLE_STATUS_TOOL, { ref }),
|
|
37
|
+
output: async (opts = {}) => await callTool(RESERVED_HANDLE_OUTPUT_TOOL, { ref, ...outputOptions(opts) }),
|
|
38
|
+
send: async message => await callTool(RESERVED_HANDLE_SEND_TOOL, { ref, message: String(message) }),
|
|
39
|
+
cancel: async () => await callTool(RESERVED_HANDLE_CANCEL_TOOL, { ref }),
|
|
40
|
+
wait: async (opts = {}) => (await wait([ref], { ...waitOptions(opts), mode: "all" }))[0],
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(view, "ref", { value: Object.freeze({ ...ref }), enumerable: false });
|
|
43
|
+
Object.defineProperty(view, "control", { value: control, enumerable: false });
|
|
44
|
+
return view;
|
|
45
|
+
};
|
|
46
|
+
return { wait, handle };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Accepts legacy agent records, handle() views, completion handles, workpools and saved refs. */
|
|
50
|
+
export function toHandleRef(value) {
|
|
51
|
+
if (!isPlainObject(value)) throw new TypeError(USAGE);
|
|
52
|
+
if (isPlainObject(value.ref) && KINDS.has(value.ref.kind)) return toHandleRef(value.ref);
|
|
53
|
+
if (typeof value.pool_id === "string") return { kind: "workpool", id: value.pool_id, run_epoch: 0 };
|
|
54
|
+
const kind = KINDS.has(value.kind) ? value.kind : kindFromHandle(value.handle);
|
|
55
|
+
const id = value.id ?? value.task_id;
|
|
56
|
+
if (kind === undefined || typeof id !== "string" || id.length === 0) throw new TypeError(USAGE);
|
|
57
|
+
const run_epoch = value.run_epoch ?? (kind === "workpool" ? 0 : undefined);
|
|
58
|
+
if (!Number.isInteger(run_epoch) || run_epoch < 0) throw new TypeError(`${USAGE}; run_epoch must be a non-negative integer`);
|
|
59
|
+
return { kind, id, run_epoch };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function kindFromHandle(handle) {
|
|
63
|
+
if (typeof handle !== "string") return undefined;
|
|
64
|
+
const scheme = handle.split("://")[0];
|
|
65
|
+
return KINDS.has(scheme) ? scheme : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function waitOptions(options) {
|
|
69
|
+
if (options === undefined || options === null) return {};
|
|
70
|
+
if (!isPlainObject(options)) throw new TypeError("wait() accepts { timeout, mode }");
|
|
71
|
+
for (const key of Object.keys(options)) {
|
|
72
|
+
if (key !== "timeout" && key !== "mode") throw new TypeError("wait() accepts { timeout, mode }");
|
|
73
|
+
}
|
|
74
|
+
const { timeout, mode } = options;
|
|
75
|
+
if (timeout !== undefined && timeout !== null && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout < 0)) {
|
|
76
|
+
throw new TypeError("wait() timeout must be a finite number of seconds >= 0");
|
|
77
|
+
}
|
|
78
|
+
if (mode !== undefined && mode !== null && !WAIT_MODES.has(mode)) throw new TypeError('wait() mode must be "all", "any" or "settled"');
|
|
79
|
+
return { ...(timeout === undefined || timeout === null ? {} : { timeout }), ...(mode === undefined || mode === null ? {} : { mode }) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function outputOptions(options) {
|
|
83
|
+
if (!isPlainObject(options)) throw new TypeError("control.output() accepts { format, offset, limit }");
|
|
84
|
+
const picked = {};
|
|
85
|
+
for (const key of ["format", "offset", "limit"]) {
|
|
86
|
+
if (options[key] !== undefined && options[key] !== null) picked[key] = options[key];
|
|
87
|
+
}
|
|
88
|
+
return picked;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
93
|
+
}
|
|
@@ -2,9 +2,15 @@ export type KernelToolFunction = {
|
|
|
2
2
|
readonly name: string;
|
|
3
3
|
};
|
|
4
4
|
|
|
5
|
+
export type KernelToolNamespaceRegistry = {
|
|
6
|
+
readonly defined?: () => string[];
|
|
7
|
+
readonly undefine?: (name: unknown) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
export function createToolNamespace(
|
|
6
11
|
define: (fn: KernelToolFunction, metadata?: unknown) => unknown,
|
|
7
12
|
callHost: (name: string, args: unknown) => Promise<unknown>,
|
|
13
|
+
registry?: KernelToolNamespaceRegistry,
|
|
8
14
|
): ((fn: KernelToolFunction, metadata?: unknown) => unknown) & {
|
|
9
15
|
readonly [name: string]: (args?: unknown) => Promise<unknown>;
|
|
10
16
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function createToolNamespace(define, callHost) {
|
|
1
|
+
export function createToolNamespace(define, callHost, registry = {}) {
|
|
2
2
|
const registrar = function tool(fn, metadata) {
|
|
3
3
|
return define(fn, metadata);
|
|
4
4
|
};
|
|
@@ -6,6 +6,8 @@ export function createToolNamespace(define, callHost) {
|
|
|
6
6
|
get(target, prop) {
|
|
7
7
|
if (typeof prop !== "string") return undefined;
|
|
8
8
|
if (prop in Function.prototype || prop === "arguments" || prop === "caller") return target[prop];
|
|
9
|
+
if (prop === "defined" && registry.defined) return () => registry.defined();
|
|
10
|
+
if (prop === "undefine" && registry.undefine) return (name) => registry.undefine(name);
|
|
9
11
|
return async (args) => await callHost(prop, args ?? {});
|
|
10
12
|
},
|
|
11
13
|
});
|
|
@@ -8,6 +8,8 @@ export const KERNEL_TOOL_ERROR_CODES = [
|
|
|
8
8
|
"kernel_tool_failed",
|
|
9
9
|
"kernel_tool_recursion",
|
|
10
10
|
"kernel_tool_host_denied",
|
|
11
|
+
"kernel_tool_cancelled",
|
|
12
|
+
"kernel_tool_loop_mismatch",
|
|
11
13
|
] as const;
|
|
12
14
|
|
|
13
15
|
export type KernelToolErrorCode = (typeof KERNEL_TOOL_ERROR_CODES)[number];
|
|
@@ -22,6 +22,8 @@ export function createKernelToolRegistry(options?: KernelToolRegistryOptions): {
|
|
|
22
22
|
},
|
|
23
23
|
signal?: AbortSignal,
|
|
24
24
|
): Promise<unknown>;
|
|
25
|
+
defined(): string[];
|
|
26
|
+
undefine(name: unknown): boolean;
|
|
25
27
|
bumpGeneration(): number;
|
|
26
28
|
setCollisionNames(hostToolNames?: readonly string[], foreignLanguageNames?: readonly string[]): void;
|
|
27
29
|
};
|