@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.
- package/CHANGELOG.md +41 -0
- package/README.md +20 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +79 -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/config/feature-settings.ts +110 -0
- package/src/config/memory-settings.ts +8 -0
- package/src/config/settings-constants.ts +7 -0
- package/src/config/settings-overrides.ts +72 -0
- package/src/config/settings.ts +35 -78
- package/src/config/unknown-settings.ts +19 -0
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +41 -0
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-tools-probe.ts +24 -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 +58 -58
- 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/kernel.ts +3 -1
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +160 -1
- package/src/kernels/js/context-manager.ts +1 -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-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/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-global-sizes.js +84 -14
- 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 +2 -0
- package/src/kernels/py/kernel.ts +48 -4
- package/src/kernels/py/prelude.py +145 -4
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/kernel.ts +3 -1
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +141 -0
- package/src/kernels/shared/kernel-memory-host.ts +14 -6
- package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
- package/src/kernels/shared/process-group-cpu.ts +139 -0
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +33 -1
- package/src/kernels/shared/subprocess-startup.ts +90 -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 +7 -6
- package/src/tool/types.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,47 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.7] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- JavaScript kernel tools: `tool.defined()` lists the defined kernel tools and `tool.undefine(name)` removes one; `tool(fn, { name })` registers a tool under an explicit name while keeping the function's argument order ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
22
|
+
- In-cell `wait(handles, {timeout, mode})` barrier and `handle(node | ref | {pool_id})` rich views in all four kernels: `wait` returns values in input order (`all`), the first success (`any`) or every outcome (`settled`), times out with `eval_wait_timeout` without cancelling work, and pauses the run budget while parked; `handle(node).control` offers `status()`, `output()`, `send()`, `cancel()` and `wait()` fenced by owner, id and `run_epoch` through the host's `EvalHandleHost` capability (agent and workpool handles fail with `eval_wait_unavailable` on a host without it); `completion(prompt, {handle: true})` returns an opt-in completion handle bounded by its cell's hard deadline; `tool_schema("eval:helpers")` and `tool_schema("eval:wait")` document the surface and the removed-tool hint for `wait` points at them. The legacy `agent(..., {handle: true})` record, the eval description and the eval input schema are unchanged; the Python runner's dispatcher was renamed `_handle_message` so `handle()` is the helper ([#2687](https://github.com/code-yeongyu/senpi/pull/2687)).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The Ruby kernel's memory notice names the largest globals again on Ruby 2.6 (the sizer used a Ruby 2.7 method) ([#2696](https://github.com/code-yeongyu/senpi/issues/2696)).
|
|
29
|
+
- Eval no longer fails with "codemode session manager is disposed" for the rest of a session after a session switch or fork that another extension cancelled: codemode only tears its kernels down when the session actually ends ([#1706](https://github.com/code-yeongyu/senpi/issues/1706)).
|
|
30
|
+
- A session whose codemode runtime failed to start is recovered by the next eval call (once, with one stderr line naming the failed start); if re-creation also fails, the call says "codemode runtime could not be re-created: <reason>" and how to bring eval back ([#1706](https://github.com/code-yeongyu/senpi/issues/1706)).
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
## [2026.10.6] - 2026-10-04
|
|
35
|
+
|
|
36
|
+
### Breaking Changes
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- An opt-in `memory.idleParkMinutes` setting (off by default) closes a kernel that had no cell running or queued for that many minutes to give its memory back; the next cell starts a fresh kernel and its result says every earlier global is lost ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
41
|
+
- Ruby eval results name their largest globals, and Ruby and Julia kernels now get the same large-memory notice as JavaScript and Python when the interpreter footprint crosses `memory.noticeMb` ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
42
|
+
- Julia eval results name their largest globals in that notice ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Kernel tool descriptors may name any eval language (`js`, `py`, `rb`, `jl`), not only `js`; today only JavaScript kernels define tools, so nothing a session sees changes ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- A Ruby or Julia kernel whose start hangs no longer leaves its cells waiting forever: startup fails, naming the stage it stalled in, once the runner has printed nothing, changed no stage and its process group has used no CPU for 30 s, so a slow but busy start (a cold Julia compiling its prelude) is never cut off ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
49
|
+
- A `codemode.json` that names a key this version does not know no longer throws away every other setting: the unknown key gets one warning and the rest still apply (known nested objects stay strict). Optional keys for upcoming features (`environments`, `isolation`, `sandbox`, `prompt.advertiseHelpers`, `kernelTools`, `languages.pyInterpreter`) parse with today's behaviour as their defaults ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
50
|
+
|
|
51
|
+
- A Python cell's host calls (`tool.*`, `completion`) no longer go through a configured HTTP proxy: the loopback bridge request ignores proxy settings from the environment and, on Windows, the registry, so a proxy can't refuse a `127.0.0.1` call that never needed it ([#2619](https://github.com/code-yeongyu/senpi/issues/2619)).
|
|
52
|
+
- A JavaScript memory report no longer runs user code: array elements are read through their own descriptors (an index accessor is skipped and the estimate marked approximate), and typed arrays, buffers, Blob, Map and Set are sized through the built-in getters, so a subclass that overrides `byteLength` or `size` is never called ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
|
|
15
56
|
## [2026.10.5] - 2026-10-03
|
|
16
57
|
|
|
17
58
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -156,17 +156,31 @@ Configuration is loaded in this order:
|
|
|
156
156
|
| `outputSink.maxColumns` | `768` | Maximum columns per printed output line; `0` disables column clamping. A cell's return value is never column-clamped (the byte and line budgets still apply). |
|
|
157
157
|
| `statusEvents` | `true` | Enables kernel status-event forwarding and rendering. Each cell retains at most 100 status rows; after overflow, one omitted-count row precedes the latest 99 events. |
|
|
158
158
|
| `memory.gcWatermarkMb` | `256` | JavaScript kernel: a finished cell whose heap reached this size and grew runs a full collection before its result; whenever at least this much stays live, an idle full collection runs about a second after the cell, so dropped globals return their memory without a reset. Python kernel: a finished cell whose process footprint reached this size and grew runs `gc.collect()` (plus glibc `malloc_trim(0)` on Linux) before its result, and while at least this much stays live the next cells collect too (at most 1/20 of the time), so `del rows` returns its memory. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_GC_WATERMARK_MB`. |
|
|
159
|
-
| `memory.noticeMb` | `1024` |
|
|
159
|
+
| `memory.noticeMb` | `1024` | When live memory (JS heap or Python footprint after a collection; Ruby and Julia interpreter footprint read by the host after each cell) reaches this size (first time, or 25% more than at the last notice), the result gets one bracketed notice naming the largest globals and how to drop them (`rows = undefined`, `del rows`, `rows = nil`, `rows = nothing`), plus `details.memory`. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_NOTICE_MB`. |
|
|
160
160
|
| `memory.ceilingMb` | a quarter of physical memory, 2048-8192 | Every kernel (JS heap and Python footprint after a collection; Ruby and Julia interpreter footprint read by the host after each result, without a globals list): when live memory reaches this size, the result says so and the kernel restarts once no cell is running or queued on it; the next result says it was restarted (`details.memory.recycled`). `0` disables. Env override: `SENPI_CODEMODE_MEMORY_CEILING_MB`. Non-zero memory thresholds must satisfy watermark <= notice <= ceiling, otherwise all three use their defaults. |
|
|
161
161
|
| `memory.retainedResultsMb` | `32` | In-memory byte budget (MiB) for the settled cells kept for `peek`/`list`, on top of the 32-cell count cap; the oldest go first and the newest is always kept. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_RESULTS_MB` (a non-negative integer). |
|
|
162
162
|
| `memory.retainedImagesMb` | `256` | Disk budget (MiB) for settled-cell images. Images of settled cells (foreground and detached) are written as base64 files under `<session artifacts>/settled-images/` instead of staying in memory, and `peek` reads them back, so it returns the full result. Beyond the budget the oldest files are deleted first; an evicted cell's files are deleted with it; the directory is removed when the session ends. A `peek` whose image file is gone returns the text plus a one-line note. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_IMAGES_MB` (a non-negative integer). |
|
|
163
|
+
| `memory.idleParkMinutes` | `0` | Off by default. When greater than 0, a kernel with no cell running or queued for this many minutes is closed to give its memory back, and the next cell for that language starts a fresh one; that cell's result says the kernel was restarted and every earlier global is lost. Applies to every language. |
|
|
164
|
+
| `languages.pyInterpreter` | unset | Explicit Python interpreter path. Unset keeps today's `PATH` detection. |
|
|
165
|
+
| `environments.managedRoot` | unset | Root directory for managed per-session environments; unset uses the session's default location. |
|
|
166
|
+
| `environments.autoProvision` | `true` | Lets installs provision a managed environment. `false` makes installs refuse with `environment_installer_unavailable`. |
|
|
167
|
+
| `environments.js.installer` | `auto` | Installer for the managed JavaScript environment: `auto`, `bun` or `npm`. |
|
|
168
|
+
| `environments.py.installer` | `pip` | Installer for the managed Python environment. |
|
|
169
|
+
| `isolation.js` | `worker` | JavaScript kernel isolation: `worker` (today's worker thread) or `process`. Env override: `SENPI_CODEMODE_JS_ISOLATION`. |
|
|
170
|
+
| `sandbox.enabled` | `false` | Allows sandbox cells. While `false` the sandbox option is neither accepted nor advertised. |
|
|
171
|
+
| `sandbox.memoryMb` | `64` | Memory cap (MiB) for a sandbox cell. Env override: `SENPI_CODEMODE_SANDBOX_MEMORY_MB` (a positive integer wins). |
|
|
172
|
+
| `sandbox.timeoutSeconds` | `300` | Time limit for a sandbox cell. |
|
|
173
|
+
| `prompt.advertiseHelpers` | `false` | When `true`, one pointer line to `tool_schema('eval:helpers')` is appended to the eval description. |
|
|
174
|
+
| `kernelTools.enabled` | `true` | Allows cells to define kernel tools (`tool(fn)`, `@tool`). `false` makes them refuse with `tools_unavailable`. |
|
|
175
|
+
|
|
176
|
+
The `languages.pyInterpreter`, `environments.*`, `isolation.*`, `sandbox.*`, `prompt.*` and `kernelTools.*` keys are accepted and validated now, with the defaults shown, which match today's behaviour. The effect each of those rows describes takes effect when its feature ships; until then, setting a key changes nothing.
|
|
163
177
|
|
|
164
178
|
`SENPI_CODEMODE_PY`, `SENPI_CODEMODE_JS`, `SENPI_CODEMODE_RB`, and
|
|
165
179
|
`SENPI_CODEMODE_JL` override the corresponding file setting. `1` or `true`
|
|
166
180
|
enables; `0` or `false` disables. Any other value leaves the file setting in
|
|
167
181
|
effect.
|
|
168
182
|
|
|
169
|
-
Malformed JSON or invalid
|
|
183
|
+
A top-level key this version does not know is ignored with one warning naming it, and the rest of the file still applies, so a settings file written for a newer senpi keeps working. Malformed JSON, or an invalid value or an unknown key inside a known setting, falls back to all defaults with a warning.
|
|
170
184
|
The detached-cell environment override uses the run-budget parser: a positive
|
|
171
185
|
base-10 integer wins over the file value; zero, negative, and malformed values
|
|
172
186
|
leave the file value in effect.
|
|
@@ -186,8 +200,12 @@ options object and asynchronous helpers are `await`-able.
|
|
|
186
200
|
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
187
201
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
188
202
|
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
|
|
203
|
+
| `tool(fn, metadata?)` (js) | Registers a named function as a kernel tool for in-process children. `metadata.name` registers it under that name instead of the function's; arguments are still passed in the function's parameter order. |
|
|
204
|
+
| `tool.defined()` / `tool.undefine(name)` (js) | List the kernel tools this kernel defines (sorted), and remove one (`true` if it existed). A descriptor taken before `undefine` can no longer be invoked. Both names are reserved: a kernel tool can't be registered as `defined` or `undefine` (`reserved_tool_name`), and a host tool with either name is shadowed in the `tool` namespace, so it can't be called from a JavaScript cell. |
|
|
189
205
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
|
190
206
|
| `agent(prompt, ...)` | Delegates to the configured active `taskTools.task` tool. Supports background handles and structured JSON results. |
|
|
207
|
+
| `wait(handles, timeout?, mode?)` | Blocks the cell until the given handles settle (agent handle records, `handle()` views, completion handles, closed workpools, or saved `{kind, id, run_epoch}` references). `mode` is `all` (values in input order; the first failed, cancelled, or lost handle raises), `any` (`{index, ref, value}` of the first success), or `settled` (one outcome per input slot). `timeout` is wall-clock seconds from entry; on expiry `eval_wait_timeout` is raised and nothing is cancelled. Rides the bridge-call path, so the run budget pauses while parked. Agent and workpool handles need the host's `EvalHandleHost` capability (`eval_wait_unavailable` without it); completion handles always work. Julia extends `Base.wait` for handle views (`wait(handle(node))`). Details: `tool_schema("eval:wait")`. |
|
|
208
|
+
| `handle(node \| ref \| {pool_id})` | Returns a rich view: the legacy record's fields plus a non-enumerable `control` (Python: attribute on a `dict` subclass; Ruby: singleton method) with `status()`, `output(format?, offset?, limit?)`, `send(message)` (agent handles only), `cancel()` (idempotent for that run epoch; never touches a successor run), and `wait(timeout?)`. Every control call is fenced by owner, id, and `run_epoch` inside the task owner (`eval_handle_stale`, `eval_handle_forbidden`). `completion(prompt, handle: true)` returns such a view for a host completion. The `agent(..., handle: true)` record itself is unchanged. Details: `tool_schema("eval:helpers")`. |
|
|
191
209
|
| `workpool(agent, name, mode?)` | Creates a thin adapter over the normal host `workpool` tool; exposes `pool_id`, `push(items)`, `close()`, `inspect()`, and `cancel()`. JS awaits creation and operations. |
|
|
192
210
|
| `output(ids, format?, offset?, limit?)` | Delegates transcript retrieval to the configured active `taskTools.output` tool. |
|
|
193
211
|
| `parallel(thunks)` | Runs thunks through the configured bounded pool while preserving input order. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.7",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@babel/parser": "8.0.4",
|
|
35
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
35
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.7",
|
|
36
36
|
"typebox": "1.3.34"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
39
|
+
"@code-yeongyu/senpi": "2026.10.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
42
|
+
"@code-yeongyu/senpi": "2026.10.7"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"senpi",
|
|
@@ -29,7 +29,7 @@ export const kernelToolDescriptorSchema = Type.Object({
|
|
|
29
29
|
name: Type.String({ minLength: 1 }),
|
|
30
30
|
description: Type.String(),
|
|
31
31
|
input_schema: Type.Unknown(),
|
|
32
|
-
language: Type.Literal("js"),
|
|
32
|
+
language: Type.Union([Type.Literal("js"), Type.Literal("py"), Type.Literal("rb"), Type.Literal("jl")]),
|
|
33
33
|
kernel_generation: Type.Integer({ minimum: 0 }),
|
|
34
34
|
definition_revision: Type.Integer({ minimum: 1 }),
|
|
35
35
|
});
|
package/src/bridge/reserved.ts
CHANGED
|
@@ -5,6 +5,16 @@ export const RESERVED_AGENT_TOOL = "__agent__" as const;
|
|
|
5
5
|
export const RESERVED_OUTPUT_TOOL = "__output__" as const;
|
|
6
6
|
/** ADAPTATION: senpi resolves tool parameter schemas through a reserved kernel-side tool name. */
|
|
7
7
|
export const RESERVED_SCHEMA_TOOL = "__schema__" as const;
|
|
8
|
+
/** In-cell `wait(handles)` barrier over the host's EvalHandleHost subscription capability. */
|
|
9
|
+
export const RESERVED_WAIT_TOOL = "__wait__" as const;
|
|
10
|
+
/** `handle(node).control.status()`: one epoch-fenced snapshot through the host's `watch`. */
|
|
11
|
+
export const RESERVED_HANDLE_STATUS_TOOL = "__handle_status__" as const;
|
|
12
|
+
/** `handle(node).control.output(opts)`: the transcript of exactly `ref.run_epoch`. */
|
|
13
|
+
export const RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__" as const;
|
|
14
|
+
/** `handle(node).control.send(message)`: agent handles only, fenced inside the task owner. */
|
|
15
|
+
export const RESERVED_HANDLE_SEND_TOOL = "__handle_send__" as const;
|
|
16
|
+
/** `handle(node).control.cancel()`: idempotent for its epoch; a successor run is never touched. */
|
|
17
|
+
export const RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__" as const;
|
|
8
18
|
/** Canonical oh-my-pi eval-timeout pause operation. */
|
|
9
19
|
export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
|
|
10
20
|
/** Canonical oh-my-pi eval-timeout resume operation. */
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EvalSchemaResult } from "./schema-bridge.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Virtual `tool_schema("eval:*")` entries: documentation a cell can read on demand. They are not tools,
|
|
5
|
+
* never execute anything, and never appear in the ordinary catalog listing, so the eval prompt and the
|
|
6
|
+
* host tool list stay byte-identical.
|
|
7
|
+
*/
|
|
8
|
+
const handleRefSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
description:
|
|
11
|
+
"A saved handle reference. agent(..., {handle: true}) records, handle() views, completion handles and workpools ({pool_id}) are accepted directly.",
|
|
12
|
+
properties: {
|
|
13
|
+
kind: { enum: ["agent", "completion", "workpool"] },
|
|
14
|
+
id: { type: "string" },
|
|
15
|
+
run_epoch: {
|
|
16
|
+
type: "integer",
|
|
17
|
+
minimum: 0,
|
|
18
|
+
description:
|
|
19
|
+
"The run this reference is bound to; a resumed task gets a new epoch and the old reference fails with eval_handle_stale.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ["kind", "id", "run_epoch"],
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const waitEntry: EvalSchemaResult = {
|
|
26
|
+
name: "eval:wait",
|
|
27
|
+
description: [
|
|
28
|
+
"wait(handles, {timeout?, mode?}) blocks the cell until the given handles settle (js: await wait(...); py/rb: wait(...); jl: wait(handle(node)) or wait([handle(a), handle(b)])).",
|
|
29
|
+
'mode "all" (default) returns the successful values in input order and raises the first failed, cancelled or lost handle\'s error; "any" returns {index, ref, value} of the first success or eval_wait_all_rejected; "settled" returns one {status, ref, value|error} outcome per input slot.',
|
|
30
|
+
"Duplicates keep their slots with one subscription. Empty handles: all/settled -> [], any -> eval_wait_empty.",
|
|
31
|
+
"timeout is wall-clock seconds from entry (finite, >= 0; 0 = one atomic check; omitted = no wait-specific deadline). On timeout the subscription closes and `eval_wait_timeout: wait() timed out after Ns; k/n handles settled; work was not cancelled` is raised. wait() never cancels work.",
|
|
32
|
+
"wait() rides the bridge-call path: the cell's run budget pauses while parked, the hard limit never pauses, and a long barrier detaches like any bridge-parked cell. Cancelling the cell closes its subscriptions at once. Task and workpool notifications are neither consumed nor suppressed.",
|
|
33
|
+
"A workpool must be closed first (eval_pool_open) and succeeds only when every key succeeds (eval_workpool_failed carries the keyed aggregate).",
|
|
34
|
+
"Agent and workpool handles need the host capability; without it wait() fails with eval_wait_unavailable and never polls task_output. Completion handles always work.",
|
|
35
|
+
].join("\n"),
|
|
36
|
+
parameters: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
handles: { type: "array", items: handleRefSchema },
|
|
40
|
+
timeout: { type: "number", minimum: 0, description: "Wall-clock seconds from entry." },
|
|
41
|
+
mode: { enum: ["all", "any", "settled"], default: "all" },
|
|
42
|
+
},
|
|
43
|
+
required: ["handles"],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const helpersEntry: EvalSchemaResult = {
|
|
48
|
+
name: "eval:helpers",
|
|
49
|
+
description: [
|
|
50
|
+
"Advanced cell helpers (all four languages; js helpers are awaited).",
|
|
51
|
+
"handle(node | ref | {pool_id}) returns a rich view: the legacy fields plus a non-enumerable `control` (py: attribute, rb: singleton method) with status(), output({format, offset, limit}), send(message) (agent handles only; others eval_handle_operation_unsupported), cancel() (idempotent for that run epoch; never cancels a successor run) and wait({timeout}).",
|
|
52
|
+
"control.status() goes through the host subscription; control.send/cancel/output are fenced by owner, id and run_epoch inside the task owner (stale -> eval_handle_stale, foreign -> eval_handle_forbidden); control.output() returns only that epoch's transcript. The plain output() helper and task tools are unchanged.",
|
|
53
|
+
"The legacy agent(..., {handle: true}) record is unchanged: .output stays a string and no enumerable key is added. A saved {kind, id, run_epoch} (or the record itself) can be rebound with handle(ref) in a later cell or after a kernel reset while the owner and run epoch are valid.",
|
|
54
|
+
"completion(prompt, {handle: true}) (py: handle=True) returns a control handle after preflight validation; provider failures surface in its outcome; its deadline derives from the creating cell's hard deadline. Completion handles are codemode-owned and need no host capability; once the session generation is dropped they fail with eval_handle_stale.",
|
|
55
|
+
"See tool_schema('eval:wait') for the barrier semantics.",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
parameters: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
wait: { description: "wait(handles, {timeout?, mode?}) -> values | {index, ref, value} | outcomes[]" },
|
|
61
|
+
handle: { description: "handle(node | ref | {pool_id}) -> view with .control" },
|
|
62
|
+
"control.status": { description: "() -> {ref, phase, host_status, revision}" },
|
|
63
|
+
"control.output": { description: "({format?, offset?, limit?}) -> {ref, text, offset, total, truncated}" },
|
|
64
|
+
"control.send": { description: "(message) -> snapshot; agent handles only" },
|
|
65
|
+
"control.cancel": { description: "() -> {ref, cancelled, phase}" },
|
|
66
|
+
"control.wait": { description: "({timeout?}) -> value" },
|
|
67
|
+
completion: { description: "completion(prompt, {handle: true}) -> control handle" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const VIRTUAL_ENTRIES: ReadonlyMap<string, EvalSchemaResult> = new Map([
|
|
73
|
+
[waitEntry.name, waitEntry],
|
|
74
|
+
[helpersEntry.name, helpersEntry],
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
export function virtualEvalSchema(name: string): EvalSchemaResult | undefined {
|
|
78
|
+
return VIRTUAL_ENTRIES.get(name);
|
|
79
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { EvalHandleError, type EvalHandleHost } from "@code-yeongyu/senpi";
|
|
2
|
+
import {
|
|
3
|
+
RESERVED_HANDLE_CANCEL_TOOL,
|
|
4
|
+
RESERVED_HANDLE_OUTPUT_TOOL,
|
|
5
|
+
RESERVED_HANDLE_SEND_TOOL,
|
|
6
|
+
RESERVED_HANDLE_STATUS_TOOL,
|
|
7
|
+
RESERVED_WAIT_TOOL,
|
|
8
|
+
} from "../bridge/reserved.ts";
|
|
9
|
+
import { parseOutputArgs, parseRefArgs, parseSendArgs, parseWaitArgs } from "../handles/handle-args.ts";
|
|
10
|
+
import type { HandleBackend, HandleRegistry } from "../handles/handle-registry.ts";
|
|
11
|
+
import { waitForHandles } from "../handles/wait.ts";
|
|
12
|
+
|
|
13
|
+
export const HANDLE_TOOL_NAMES = [
|
|
14
|
+
RESERVED_WAIT_TOOL,
|
|
15
|
+
RESERVED_HANDLE_STATUS_TOOL,
|
|
16
|
+
RESERVED_HANDLE_OUTPUT_TOOL,
|
|
17
|
+
RESERVED_HANDLE_SEND_TOOL,
|
|
18
|
+
RESERVED_HANDLE_CANCEL_TOOL,
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type HandleToolName = (typeof HANDLE_TOOL_NAMES)[number];
|
|
22
|
+
|
|
23
|
+
export interface HandleBridgeContext {
|
|
24
|
+
/** The session generation's registry; absent only for the pre-session baseline tool. */
|
|
25
|
+
readonly registry: HandleRegistry | undefined;
|
|
26
|
+
/** `ctx.evalHandleHost` at dispatch time; absent on a runtime without a provider. */
|
|
27
|
+
readonly host: EvalHandleHost | undefined;
|
|
28
|
+
/** The calling cell's signal: cancelling the cell closes its subscriptions immediately. */
|
|
29
|
+
readonly signal: AbortSignal | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isHandleToolName(toolName: string): toolName is HandleToolName {
|
|
33
|
+
return HANDLE_TOOL_NAMES.some((name) => name === toolName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Dispatches the five reserved handle tools; every path is fenced by the registry before any host call. */
|
|
37
|
+
export async function runHandleTool(toolName: HandleToolName, args: unknown, context: HandleBridgeContext) {
|
|
38
|
+
const registry = context.registry;
|
|
39
|
+
if (registry === undefined) {
|
|
40
|
+
throw new EvalHandleError(
|
|
41
|
+
"eval_wait_unavailable",
|
|
42
|
+
"wait()/handle() are not supported by this runtime: this session has no handle registry yet",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const backend: HandleBackend = { host: context.host, signal: context.signal };
|
|
46
|
+
switch (toolName) {
|
|
47
|
+
case RESERVED_WAIT_TOOL:
|
|
48
|
+
return await waitForHandles(parseWaitArgs(args), {
|
|
49
|
+
watch: (refs) => registry.watch(refs, backend),
|
|
50
|
+
result: (ref) => registry.result(ref, backend),
|
|
51
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
52
|
+
});
|
|
53
|
+
case RESERVED_HANDLE_STATUS_TOOL:
|
|
54
|
+
return await registry.status(parseRefArgs(args, "control.status()"), backend);
|
|
55
|
+
case RESERVED_HANDLE_OUTPUT_TOOL: {
|
|
56
|
+
const { ref, request } = parseOutputArgs(args);
|
|
57
|
+
return await registry.output(ref, request, backend);
|
|
58
|
+
}
|
|
59
|
+
case RESERVED_HANDLE_SEND_TOOL: {
|
|
60
|
+
const { ref, message } = parseSendArgs(args);
|
|
61
|
+
return await registry.send(ref, message, backend);
|
|
62
|
+
}
|
|
63
|
+
case RESERVED_HANDLE_CANCEL_TOOL:
|
|
64
|
+
return await registry.cancel(parseRefArgs(args, "control.cancel()"), backend);
|
|
65
|
+
default:
|
|
66
|
+
return assertNever(toolName);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertNever(value: never): never {
|
|
71
|
+
throw new TypeError(`Unsupported handle tool: ${String(value)}`);
|
|
72
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
1
|
+
import type { AgentToolResult, EvalHandleHost } from "@code-yeongyu/senpi";
|
|
2
2
|
import { RESERVED_AGENT_TOOL, RESERVED_OUTPUT_TOOL, RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
|
|
3
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
3
4
|
import type { EvalStatusEvent, ExecuteTool } from "../tool/types.ts";
|
|
4
5
|
import { type AgentExecuteTool, runEvalAgent } from "./agent-bridge.ts";
|
|
6
|
+
import { isHandleToolName, runHandleTool } from "./handle-bridge.ts";
|
|
5
7
|
import { type MarshalledToolResult, type OutputExecuteTool, runEvalOutput } from "./output-bridge.ts";
|
|
6
8
|
import { type EvalSchemaToolInfo, runEvalSchema } from "./schema-bridge.ts";
|
|
7
9
|
|
|
@@ -15,6 +17,10 @@ export interface ReservedDispatchContext {
|
|
|
15
17
|
readonly signal: AbortSignal | undefined;
|
|
16
18
|
readonly emitStatus: (event: EvalStatusEvent) => void;
|
|
17
19
|
readonly marshalToolResult: (result: AgentToolResult<unknown>) => MarshalledToolResult;
|
|
20
|
+
/** The session generation's handle registry; `wait()`/`handle()` fail with `eval_wait_unavailable` without one. */
|
|
21
|
+
readonly handles?: HandleRegistry;
|
|
22
|
+
/** `ctx.evalHandleHost` at dispatch time; agent/workpool refs need it, completion refs do not. */
|
|
23
|
+
readonly evalHandleHost?: EvalHandleHost;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
class SchemaUnavailableError extends Error {
|
|
@@ -26,10 +32,22 @@ class SchemaUnavailableError extends Error {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export function isReservedToolName(toolName: string): boolean {
|
|
29
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
toolName === RESERVED_AGENT_TOOL ||
|
|
37
|
+
toolName === RESERVED_OUTPUT_TOOL ||
|
|
38
|
+
toolName === RESERVED_SCHEMA_TOOL ||
|
|
39
|
+
isHandleToolName(toolName)
|
|
40
|
+
);
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
export async function runReservedTool(toolName: string, context: ReservedDispatchContext): Promise<unknown> {
|
|
44
|
+
if (isHandleToolName(toolName)) {
|
|
45
|
+
return await runHandleTool(toolName, context.args, {
|
|
46
|
+
registry: context.handles,
|
|
47
|
+
host: context.evalHandleHost,
|
|
48
|
+
signal: context.signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
33
51
|
if (toolName === RESERVED_AGENT_TOOL) {
|
|
34
52
|
return await runEvalAgent(context.args, {
|
|
35
53
|
callId: context.callId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { Check, Errors } from "typebox/value";
|
|
4
|
+
import { virtualEvalSchema } from "./eval-virtual-schemas.ts";
|
|
4
5
|
|
|
5
6
|
const schemaArgsSchema = Type.Object(
|
|
6
7
|
{ name: Type.Optional(Type.String({ minLength: 1 })) },
|
|
@@ -46,6 +47,9 @@ export function runEvalSchema(args: unknown, options: RunEvalSchemaOptions): Eva
|
|
|
46
47
|
const tools = options.listTools();
|
|
47
48
|
if (parsed.name === undefined) return { tools: tools.map((tool) => tool.name) };
|
|
48
49
|
|
|
50
|
+
// Documentation-only entries; they are not tools and the listing above stays the ordinary catalog.
|
|
51
|
+
const virtual = virtualEvalSchema(parsed.name);
|
|
52
|
+
if (virtual !== undefined) return virtual;
|
|
49
53
|
const match = tools.find((tool) => tool.name === parsed.name);
|
|
50
54
|
if (match) return { name: match.name, description: match.description, parameters: match.parameters };
|
|
51
55
|
throw new SchemaUnknownToolError(parsed.name, nearestNames(parsed.name, tools));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ExtensionContext, HandleRef } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
3
|
+
import type { CompletionRequest, CompletionResult } from "./handler.ts";
|
|
4
|
+
|
|
5
|
+
/** What `completion(prompt, {handle: true})` returns: a saved reference every prelude turns into a control view. */
|
|
6
|
+
export interface CompletionHandleRecord {
|
|
7
|
+
readonly kind: "completion";
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly run_epoch: number;
|
|
10
|
+
readonly handle: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StartCompletionHandleOptions {
|
|
14
|
+
readonly registry: HandleRegistry;
|
|
15
|
+
readonly request: CompletionRequest;
|
|
16
|
+
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
17
|
+
readonly ctx: ExtensionContext;
|
|
18
|
+
/** The creating cell's hard deadline (absolute ms); the completion is aborted at it. */
|
|
19
|
+
readonly deadlineMs: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COMPLETION_TIERS: ReadonlySet<string> = new Set(["smol", "default", "slow"]);
|
|
23
|
+
|
|
24
|
+
class CompletionHandleArgumentsError extends Error {
|
|
25
|
+
readonly name = "CompletionHandleArgumentsError";
|
|
26
|
+
readonly code = "eval_handle_invalid_arguments";
|
|
27
|
+
|
|
28
|
+
constructor(detail: string) {
|
|
29
|
+
super(`eval_handle_invalid_arguments: completion(prompt, {handle: true}) ${detail}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when the completion call opted into a handle (`{handle: true}` in its options). */
|
|
34
|
+
export function wantsCompletionHandle(opts: unknown): boolean {
|
|
35
|
+
return typeof opts === "object" && opts !== null && "handle" in opts && opts.handle === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Preflight-validates the request, then starts the completion in the session registry and returns its
|
|
40
|
+
* reference at once. Provider failures surface in the handle's outcome, never here.
|
|
41
|
+
*/
|
|
42
|
+
export function startCompletionHandle(options: StartCompletionHandleOptions): CompletionHandleRecord {
|
|
43
|
+
const request = withoutHandleOption(options.request);
|
|
44
|
+
if (request.prompt.trim().length === 0) throw new CompletionHandleArgumentsError("needs a non-empty prompt");
|
|
45
|
+
const tier = request.model ?? tierFromOpts(request.opts);
|
|
46
|
+
if (tier !== undefined && !COMPLETION_TIERS.has(tier)) {
|
|
47
|
+
throw new CompletionHandleArgumentsError(`got model tier "${tier}"; expected "smol", "default", or "slow"`);
|
|
48
|
+
}
|
|
49
|
+
const ref: HandleRef = options.registry.startCompletion({
|
|
50
|
+
deadlineMs: options.deadlineMs,
|
|
51
|
+
run: async (signal) => replyValue(await options.complete(request, { ...options.ctx, signal })),
|
|
52
|
+
});
|
|
53
|
+
return { kind: "completion", id: ref.id, run_epoch: ref.run_epoch, handle: `completion://${ref.id}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function replyValue(result: CompletionResult): unknown {
|
|
57
|
+
return "value" in result ? result.value : result.text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function tierFromOpts(opts: unknown): string | undefined {
|
|
61
|
+
if (typeof opts !== "object" || opts === null || !("model" in opts)) return undefined;
|
|
62
|
+
return typeof opts.model === "string" ? opts.model : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withoutHandleOption(request: CompletionRequest): CompletionRequest {
|
|
66
|
+
if (typeof request.opts !== "object" || request.opts === null || !("handle" in request.opts)) return request;
|
|
67
|
+
const { handle: _handle, ...opts } = request.opts;
|
|
68
|
+
return { ...request, opts };
|
|
69
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
3
4
|
import type { EvalKernel } from "../tool/types.ts";
|
|
5
|
+
import { replyValue, startCompletionHandle, wantsCompletionHandle } from "./handle.ts";
|
|
4
6
|
import type { CompletionRequest, CompletionResult } from "./handler.ts";
|
|
5
7
|
|
|
6
8
|
export interface CompletionToolCallOptions {
|
|
@@ -9,20 +11,28 @@ export interface CompletionToolCallOptions {
|
|
|
9
11
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
10
12
|
readonly ctx: ExtensionContext;
|
|
11
13
|
readonly isActive: () => boolean;
|
|
14
|
+
/** Needed only for `completion(prompt, {handle: true})`; without it that option is refused. */
|
|
15
|
+
readonly handles?: HandleRegistry;
|
|
16
|
+
/** The creating cell's hard deadline (absolute ms) bounding a completion handle. */
|
|
17
|
+
readonly hardDeadlineMs?: number;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export type CompletionToolCallSummary = { readonly ok: true } | { readonly ok: false; readonly error: string };
|
|
15
21
|
|
|
16
22
|
export async function handleCompletionToolCall(options: CompletionToolCallOptions): Promise<CompletionToolCallSummary> {
|
|
17
23
|
try {
|
|
18
|
-
const
|
|
24
|
+
const request = toCompletionRequest(options.message.args);
|
|
25
|
+
const value = wantsCompletionHandle(request.opts)
|
|
26
|
+
? startCompletionHandle({
|
|
27
|
+
registry: requireRegistry(options.handles),
|
|
28
|
+
request,
|
|
29
|
+
complete: options.complete,
|
|
30
|
+
ctx: options.ctx,
|
|
31
|
+
deadlineMs: options.hardDeadlineMs ?? Number.POSITIVE_INFINITY,
|
|
32
|
+
})
|
|
33
|
+
: replyValue(await options.complete(request, options.ctx));
|
|
19
34
|
if (!options.isActive()) return { ok: false, error: "completion() result ignored after eval finalization" };
|
|
20
|
-
options.kernel.deliverToolReply({
|
|
21
|
-
type: "tool-reply",
|
|
22
|
-
callId: options.message.callId,
|
|
23
|
-
ok: true,
|
|
24
|
-
value: completionReplyValue(result),
|
|
25
|
-
});
|
|
35
|
+
options.kernel.deliverToolReply({ type: "tool-reply", callId: options.message.callId, ok: true, value });
|
|
26
36
|
return { ok: true };
|
|
27
37
|
} catch (error) {
|
|
28
38
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -31,12 +41,25 @@ export async function handleCompletionToolCall(options: CompletionToolCallOption
|
|
|
31
41
|
type: "tool-reply",
|
|
32
42
|
callId: options.message.callId,
|
|
33
43
|
ok: false,
|
|
34
|
-
error: {
|
|
44
|
+
error: {
|
|
45
|
+
message,
|
|
46
|
+
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
47
|
+
? { code: error.code }
|
|
48
|
+
: {}),
|
|
49
|
+
},
|
|
35
50
|
});
|
|
36
51
|
return { ok: false, error: message };
|
|
37
52
|
}
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
function requireRegistry(handles: HandleRegistry | undefined): HandleRegistry {
|
|
56
|
+
if (handles !== undefined) return handles;
|
|
57
|
+
throw Object.assign(
|
|
58
|
+
new Error("eval_wait_unavailable: completion handles are not available before the session starts"),
|
|
59
|
+
{ code: "eval_wait_unavailable" },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
function toCompletionRequest(value: unknown): CompletionRequest {
|
|
41
64
|
if (typeof value === "object" && value !== null && "prompt" in value && typeof value.prompt === "string") {
|
|
42
65
|
return {
|
|
@@ -49,7 +72,3 @@ function toCompletionRequest(value: unknown): CompletionRequest {
|
|
|
49
72
|
}
|
|
50
73
|
throw new Error("completion() received invalid arguments");
|
|
51
74
|
}
|
|
52
|
-
|
|
53
|
-
function completionReplyValue(result: CompletionResult): unknown {
|
|
54
|
-
return "value" in result ? result.value : result.text;
|
|
55
|
-
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Static } from "typebox";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
5
|
+
|
|
6
|
+
export const JS_ISOLATION_ENVIRONMENT_FLAG = "SENPI_CODEMODE_JS_ISOLATION";
|
|
7
|
+
export const SANDBOX_MEMORY_ENVIRONMENT_FLAG = "SENPI_CODEMODE_SANDBOX_MEMORY_MB";
|
|
8
|
+
|
|
9
|
+
export const featureSettingsProperties = {
|
|
10
|
+
environments: Type.Optional(
|
|
11
|
+
Type.Object(
|
|
12
|
+
{
|
|
13
|
+
managedRoot: Type.Optional(Type.String({ minLength: 1 })),
|
|
14
|
+
autoProvision: Type.Optional(Type.Boolean()),
|
|
15
|
+
js: Type.Optional(
|
|
16
|
+
Type.Object(
|
|
17
|
+
{ installer: Type.Union([Type.Literal("auto"), Type.Literal("bun"), Type.Literal("npm")]) },
|
|
18
|
+
{ additionalProperties: false },
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
py: Type.Optional(Type.Object({ installer: Type.Literal("pip") }, { additionalProperties: false })),
|
|
22
|
+
},
|
|
23
|
+
{ additionalProperties: false },
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
isolation: Type.Optional(
|
|
27
|
+
Type.Object(
|
|
28
|
+
{ js: Type.Optional(Type.Union([Type.Literal("worker"), Type.Literal("process")])) },
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
sandbox: Type.Optional(
|
|
33
|
+
Type.Object(
|
|
34
|
+
{
|
|
35
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
36
|
+
memoryMb: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
37
|
+
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
|
+
},
|
|
39
|
+
{ additionalProperties: false },
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
prompt: Type.Optional(
|
|
43
|
+
Type.Object({ advertiseHelpers: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
44
|
+
),
|
|
45
|
+
kernelTools: Type.Optional(Type.Object({ enabled: Type.Optional(Type.Boolean()) }, { additionalProperties: false })),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const featureSettingsSchema = Type.Object(featureSettingsProperties);
|
|
49
|
+
|
|
50
|
+
export type CodemodeFeatureSettings = Static<typeof featureSettingsSchema>;
|
|
51
|
+
|
|
52
|
+
export type JsIsolation = "worker" | "process";
|
|
53
|
+
|
|
54
|
+
export interface ResolvedSandbox {
|
|
55
|
+
readonly enabled: boolean;
|
|
56
|
+
readonly memoryMb: number;
|
|
57
|
+
readonly timeoutSeconds: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResolvedEnvironments {
|
|
61
|
+
readonly managedRoot?: string;
|
|
62
|
+
readonly autoProvision: boolean;
|
|
63
|
+
readonly jsInstaller: "auto" | "bun" | "npm";
|
|
64
|
+
readonly pyInstaller: "pip";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pickFeatureSettings(input: CodemodeFeatureSettings): CodemodeFeatureSettings {
|
|
68
|
+
return {
|
|
69
|
+
...(input.environments === undefined ? {} : { environments: input.environments }),
|
|
70
|
+
...(input.isolation === undefined ? {} : { isolation: input.isolation }),
|
|
71
|
+
...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
|
|
72
|
+
...(input.prompt === undefined ? {} : { prompt: input.prompt }),
|
|
73
|
+
...(input.kernelTools === undefined ? {} : { kernelTools: input.kernelTools }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The environment wins over the file; an unknown value is ignored. Default: today's worker-thread kernel. */
|
|
78
|
+
export function resolveJsIsolation(settings: CodemodeFeatureSettings, env: Environment = process.env): JsIsolation {
|
|
79
|
+
const fromEnv = env[JS_ISOLATION_ENVIRONMENT_FLAG];
|
|
80
|
+
if (fromEnv === "worker" || fromEnv === "process") return fromEnv;
|
|
81
|
+
return settings.isolation?.js ?? "worker";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Off by default; the environment's memory cap wins over the file when it is a positive integer. */
|
|
85
|
+
export function resolveSandbox(settings: CodemodeFeatureSettings, env: Environment = process.env): ResolvedSandbox {
|
|
86
|
+
const fromEnv = Number.parseInt(env[SANDBOX_MEMORY_ENVIRONMENT_FLAG] ?? "", 10);
|
|
87
|
+
return {
|
|
88
|
+
enabled: settings.sandbox?.enabled ?? false,
|
|
89
|
+
memoryMb: Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (settings.sandbox?.memoryMb ?? 64),
|
|
90
|
+
timeoutSeconds: settings.sandbox?.timeoutSeconds ?? 300,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveEnvironments(settings: CodemodeFeatureSettings): ResolvedEnvironments {
|
|
95
|
+
const environments = settings.environments;
|
|
96
|
+
return {
|
|
97
|
+
...(environments?.managedRoot === undefined ? {} : { managedRoot: environments.managedRoot }),
|
|
98
|
+
autoProvision: environments?.autoProvision ?? true,
|
|
99
|
+
jsInstaller: environments?.js?.installer ?? "auto",
|
|
100
|
+
pyInstaller: environments?.py?.installer ?? "pip",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveAdvertiseHelpers(settings: CodemodeFeatureSettings): boolean {
|
|
105
|
+
return settings.prompt?.advertiseHelpers ?? false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveKernelToolsEnabled(settings: CodemodeFeatureSettings): boolean {
|
|
109
|
+
return settings.kernelTools?.enabled ?? true;
|
|
110
|
+
}
|