@code-yeongyu/senpi-codemode 2026.10.5 → 2026.10.7

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -2
  3. package/package.json +4 -4
  4. package/src/bridge/kernel-tools-protocol.ts +1 -1
  5. package/src/bridge/reserved.ts +10 -0
  6. package/src/bridges/eval-virtual-schemas.ts +79 -0
  7. package/src/bridges/handle-bridge.ts +72 -0
  8. package/src/bridges/reserved-dispatch.ts +20 -2
  9. package/src/bridges/schema-bridge.ts +4 -0
  10. package/src/completion/handle.ts +69 -0
  11. package/src/completion/tool-bridge.ts +31 -12
  12. package/src/config/feature-settings.ts +110 -0
  13. package/src/config/memory-settings.ts +8 -0
  14. package/src/config/settings-constants.ts +7 -0
  15. package/src/config/settings-overrides.ts +72 -0
  16. package/src/config/settings.ts +35 -78
  17. package/src/config/unknown-settings.ts +19 -0
  18. package/src/extension/bridge-dispatch.ts +41 -0
  19. package/src/extension/bridge-tool-call.ts +41 -0
  20. package/src/extension/host-facts.ts +18 -0
  21. package/src/extension/idle-parking-kernel.ts +272 -0
  22. package/src/extension/kernel-registration.ts +25 -0
  23. package/src/extension/kernel-tools-probe.ts +24 -0
  24. package/src/extension/removed-tool-hints.ts +15 -0
  25. package/src/extension/runtime-factory.ts +3 -0
  26. package/src/extension/session-manager-contract.ts +3 -0
  27. package/src/extension/session-manager.ts +58 -58
  28. package/src/extension/start-recovery.ts +111 -0
  29. package/src/handles/completion-handles.ts +202 -0
  30. package/src/handles/handle-args.ts +103 -0
  31. package/src/handles/handle-registry.ts +164 -0
  32. package/src/handles/wait.ts +214 -0
  33. package/src/handles/watch-queue.ts +102 -0
  34. package/src/index.ts +99 -94
  35. package/src/kernels/jl/handles.jl +124 -0
  36. package/src/kernels/jl/kernel.ts +3 -1
  37. package/src/kernels/jl/prelude.jl +9 -0
  38. package/src/kernels/jl/runner.jl +160 -1
  39. package/src/kernels/js/context-manager.ts +1 -1
  40. package/src/kernels/js/handles.js +93 -0
  41. package/src/kernels/js/kernel-tools-define.d.ts +6 -0
  42. package/src/kernels/js/kernel-tools-define.js +3 -1
  43. package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
  44. package/src/kernels/js/kernel-tools-registry.js +18 -9
  45. package/src/kernels/js/kernel-tools-schema.js +6 -2
  46. package/src/kernels/js/kernel-tools-types.ts +2 -1
  47. package/src/kernels/js/worker-global-sizes.js +84 -14
  48. package/src/kernels/js/worker-runtime.js +10 -1
  49. package/src/kernels/py/host-cell.ts +42 -0
  50. package/src/kernels/py/kernel-contract.ts +2 -0
  51. package/src/kernels/py/kernel.ts +48 -4
  52. package/src/kernels/py/prelude.py +145 -4
  53. package/src/kernels/rb/handles.rb +97 -0
  54. package/src/kernels/rb/kernel.ts +3 -1
  55. package/src/kernels/rb/paths.rb +37 -0
  56. package/src/kernels/rb/prelude.rb +15 -38
  57. package/src/kernels/rb/runner.rb +141 -0
  58. package/src/kernels/shared/kernel-memory-host.ts +14 -6
  59. package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
  60. package/src/kernels/shared/process-group-cpu.ts +139 -0
  61. package/src/kernels/shared/subprocess-contract.ts +3 -0
  62. package/src/kernels/shared/subprocess-kernel.ts +33 -1
  63. package/src/kernels/shared/subprocess-startup.ts +90 -0
  64. package/src/tool/cell-handler.ts +21 -18
  65. package/src/tool/cell-reserved-dispatch.ts +51 -0
  66. package/src/tool/eval-tool-options.ts +3 -0
  67. package/src/tool/run-eval-cell.ts +7 -6
  68. package/src/tool/types.ts +15 -0
@@ -0,0 +1,102 @@
1
+ import type { HandleSnapshot, HandleWatch } from "@code-yeongyu/senpi";
2
+ import { refKey } from "./handle-args.ts";
3
+
4
+ /**
5
+ * A pull-side buffer for handle snapshots. Pushes queue up until the consumer iterates, so a change
6
+ * that lands between subscription and `initial` is delivered exactly once; `close()` is idempotent
7
+ * and ends the iterator.
8
+ */
9
+ export class WatchQueue {
10
+ readonly #keys: ReadonlySet<string> | undefined;
11
+ readonly #queue: HandleSnapshot[] = [];
12
+ #wake: (() => void) | undefined;
13
+ #closed = false;
14
+ #failure: unknown;
15
+ #failed = false;
16
+
17
+ /** Without keys every snapshot is accepted (used by the merger); with keys only matching refs are. */
18
+ constructor(keys?: readonly string[]) {
19
+ this.#keys = keys === undefined ? undefined : new Set(keys);
20
+ }
21
+
22
+ get closed(): boolean {
23
+ return this.#closed;
24
+ }
25
+
26
+ push(snapshot: HandleSnapshot): void {
27
+ if (this.#closed || (this.#keys !== undefined && !this.#keys.has(refKey(snapshot.ref)))) return;
28
+ this.#queue.push(snapshot);
29
+ this.#wake?.();
30
+ }
31
+
32
+ close(): void {
33
+ if (this.#closed) return;
34
+ this.#closed = true;
35
+ this.#wake?.();
36
+ }
37
+
38
+ /** Ends the stream with an error: the next pull rejects instead of reporting a clean end. */
39
+ fail(error: unknown): void {
40
+ if (this.#closed) return;
41
+ this.#failed = true;
42
+ this.#failure = error;
43
+ this.close();
44
+ }
45
+
46
+ toWatch(initial: readonly HandleSnapshot[], onClose?: () => void): HandleWatch {
47
+ const close = (): void => {
48
+ this.close();
49
+ onClose?.();
50
+ };
51
+ const next = async (): Promise<IteratorResult<HandleSnapshot>> => {
52
+ while (this.#queue.length === 0 && !this.#closed) {
53
+ await new Promise<void>((resolve) => {
54
+ this.#wake = resolve;
55
+ });
56
+ }
57
+ this.#wake = undefined;
58
+ const value = this.#queue.shift();
59
+ if (value !== undefined) return { done: false, value };
60
+ if (this.#failed) throw this.#failure;
61
+ return { done: true, value: undefined };
62
+ };
63
+ const updates: AsyncIterable<HandleSnapshot> = {
64
+ [Symbol.asyncIterator]: () => ({
65
+ next,
66
+ return: async (): Promise<IteratorResult<HandleSnapshot>> => {
67
+ close();
68
+ return { done: true, value: undefined };
69
+ },
70
+ }),
71
+ };
72
+ return { initial, updates, close };
73
+ }
74
+ }
75
+
76
+ /** One watch over several: `initial` keeps the caller's order, updates from every part are drained eagerly. */
77
+ export function mergeWatches(ordered: readonly HandleSnapshot[], parts: readonly HandleWatch[]): HandleWatch {
78
+ if (parts.length === 1 && parts[0] !== undefined) return { ...parts[0], initial: ordered };
79
+ const queue = new WatchQueue();
80
+ let open = parts.length;
81
+ const drain = async (part: HandleWatch): Promise<void> => {
82
+ try {
83
+ for await (const snapshot of part.updates) queue.push(snapshot);
84
+ } catch (error) {
85
+ queue.fail(error);
86
+ } finally {
87
+ open -= 1;
88
+ if (open === 0) queue.close();
89
+ }
90
+ };
91
+ if (parts.length === 0) queue.close();
92
+ for (const part of parts) void drain(part);
93
+ const merged = queue.toWatch(ordered);
94
+ return {
95
+ initial: merged.initial,
96
+ updates: merged.updates,
97
+ close: () => {
98
+ for (const part of parts) part.close();
99
+ merged.close();
100
+ },
101
+ };
102
+ }
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, jsRuntimeLabel } from "./extension/runtime-info.ts";
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
- const SESSION_LIFECYCLE_EVENTS = [
40
- "session_start",
41
- "session_shutdown",
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
- createEvalTool({
144
- enabledLanguages: runtime.enabledLanguages,
145
- kernelManager: manager,
146
- cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
147
- foregroundWindowSeconds: resolveForegroundWindowSeconds(runtime.settings),
148
- runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
149
- hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
150
- executeTool: runtime.executeTool,
151
- listTools: () => pi.getAllTools(),
152
- complete,
153
- settings: runtime.settings,
154
- artifactsDir: runtime.artifactsDir,
155
- cellManager,
156
- executionTracker: manager,
157
- onCellSettled,
158
- renderers,
159
- monitor,
160
- spawns: runtime.spawns,
161
- spawnDefaultAgent: runtime.settings.taskTools.task,
162
- hostLine: hostLine(),
163
- runtimes: runtime.runtimes,
164
- kernelPreludes: () => activeKernelPreludes(pi),
165
- promptKernelPreludes: preludes,
166
- ...(bunSkillPath === undefined ? {} : { bunSkillPath }),
167
- ...(modelId === undefined ? {} : { modelId }),
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
- createEvalTool({
183
- enabledLanguages: { py: true, js: true, rb: true, jl: true },
184
- kernelManager: manager,
185
- cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
186
- foregroundWindowSeconds: resolveForegroundWindowSeconds(defaultCodemodeSettings),
187
- runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
188
- hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
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
- onStatusChange: showDetachedCells,
200
- onWakeSourceState: emitWakeSourceState,
201
- ...(options.now === undefined ? {} : { now: options.now }),
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
- executionTracker: manager,
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
- pi.on("session_start", async (event, ctx) => {
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 runtime = await createRuntime(pi, ctx, event, complete, options);
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) return;
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
- if (cellManager === undefined) return;
263
- registerEvalForRuntime(runtime, modelId, cellManager);
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
- if (runtime === undefined || cellManager === undefined) return;
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)
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
  import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { SessionEnvironment } from "../session-env.ts";
4
4
  import type { KernelLifecycle } from "../shared/kernel-death.ts";
5
+ import { readProcessGroupCpuTime } from "../shared/process-group-cpu.ts";
5
6
  import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
6
7
  import { SubprocessKernel, type SubprocessKernelMemory, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
7
8
 
@@ -14,7 +15,7 @@ export interface JuliaKernelStartOptions extends KernelLifecycle {
14
15
  readonly command?: string;
15
16
  readonly spawn?: SubprocessSpawn;
16
17
  readonly onMessage?: (message: KernelToHostMessage) => void;
17
- /** Ceiling-only memory management (no notice or globals list: the runner reports no memory). */
18
+ /** Memory management: the runner reports its largest globals and the host reads the interpreter footprint for the ceiling. */
18
19
  readonly memory?: SubprocessKernelMemory;
19
20
  }
20
21
 
@@ -54,6 +55,7 @@ export class JuliaKernel extends SubprocessKernel {
54
55
  onMessage: options.onMessage,
55
56
  memory: options.memory && { language: "jl", ...options.memory },
56
57
  onDeath: options.onDeath,
58
+ startup: { label: "Julia", readGroupCpuTime: readProcessGroupCpuTime },
57
59
  });
58
60
  }
59
61
  }
@@ -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)