@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/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,43 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.8] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Internal groundwork for installing Python packages from a cell: per-session environment revisions that are published only after a successful install (a failed or interrupted install leaves the previous revision active), a per-root install lock, and a pip installer that always targets the session's own directory. Not exposed to cells yet ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
22
|
+
- Python kernel tools: `@tool` registers a function that in-process children can call, with its schema inferred from type hints; callbacks are served while the kernel is idle or its cell waits on a host call, never during a running computation, and a reset or redefinition makes old descriptors stale. `tool.defined()` / `tool.undefine()` in Python, and `tool_schema("eval:kernel-tools")` ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
23
|
+
- Internal groundwork for isolated sandbox cells: a vendored copy of the pi codemode runtime (QuickJS in a worker) with two opt-in host options, output streaming bounded by a credit window and a store policy that keeps no state. Nothing uses it yet ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- A cold Python kernel start on a busy machine no longer fails as a hang: startup keeps waiting while the interpreter is still using CPU or writing output, and fails only when it has gone completely still (naming the stage), or after 120 s without becoming ready ([#2718](https://github.com/code-yeongyu/senpi/issues/2718)).
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
## [2026.10.7] - 2026-10-04
|
|
34
|
+
|
|
35
|
+
### Breaking Changes
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- 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)).
|
|
40
|
+
- 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)).
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- 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)).
|
|
47
|
+
- 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)).
|
|
48
|
+
- 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)).
|
|
49
|
+
|
|
50
|
+
### Removed
|
|
51
|
+
|
|
15
52
|
## [2026.10.6] - 2026-10-04
|
|
16
53
|
|
|
17
54
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -200,8 +200,13 @@ options object and asynchronous helpers are `await`-able.
|
|
|
200
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)). |
|
|
201
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 }`. |
|
|
202
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. |
|
|
205
|
+
| `@tool` / `@tool(name=, description=, schema=)` (py) | Registers a Python function as a kernel tool for in-process children; the schema is inferred from its type hints (`tool_schema("eval:kernel-tools")` lists the rules). Callbacks run while the kernel is idle or its cell is parked in a host call, never in the middle of a running computation. `tool.defined()` and `tool.undefine(name)` work as in JavaScript. Ruby and Julia kernels answer `tools_unavailable`. |
|
|
203
206
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
|
204
207
|
| `agent(prompt, ...)` | Delegates to the configured active `taskTools.task` tool. Supports background handles and structured JSON results. |
|
|
208
|
+
| `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")`. |
|
|
209
|
+
| `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")`. |
|
|
205
210
|
| `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. |
|
|
206
211
|
| `output(ids, format?, offset?, limit?)` | Delegates transcript retrieval to the configured active `taskTools.output` tool. |
|
|
207
212
|
| `parallel(thunks)` | Runs thunks through the configured bounded pool while preserving input order. |
|
|
@@ -457,8 +462,21 @@ Homebrew (put `/opt/homebrew/opt/ruby/bin` first on `PATH`; macOS's system Ruby
|
|
|
457
462
|
before `--write-baseline`.
|
|
458
463
|
|
|
459
464
|
The report is gitignored `gate-report.json` by default (`--report <path>` overrides it).
|
|
460
|
-
`test/gate/allowlist.json` contains reviewed additive changes keyed by plan node
|
|
461
|
-
|
|
465
|
+
`test/gate/allowlist.json` contains reviewed additive changes keyed by plan node.
|
|
466
|
+
A pull request that changes the committed `baseline.json` itself (an intended
|
|
467
|
+
schema or prompt change, a renamed contract, a deleted module) must list every
|
|
468
|
+
cell it edits or removes under that node's `changes`, each as
|
|
469
|
+
`{ "key": "<section>/<cell>", "reason": "..." }`. In a pull request the gate reads
|
|
470
|
+
the baseline at the merge base and fails any baseline edit or removal that is
|
|
471
|
+
not listed, any edited or added cell that differs from what the head measures,
|
|
472
|
+
and any listed removal the head still measures. A cell written into the baseline that
|
|
473
|
+
was absent at the merge base must be listed under some node's `additions` (an entry an earlier
|
|
474
|
+
pull request added counts, so a re-record can write in cells approved before); only `changes`
|
|
475
|
+
entries must be new in this pull request. Reviewers read each `changes`
|
|
476
|
+
entry: it is the only record of why a protected surface moved. If the merge
|
|
477
|
+
base cannot be resolved in a pull request, the gate fails closed; the gate job
|
|
478
|
+
checks out full history for this. A local run outside a pull request reviews
|
|
479
|
+
baseline changes only with `--base-ref <ref>`. The test-only
|
|
462
480
|
`SENPI_CODEMODE_GATE_MUTATE=drop-phase` report mutation proves that helper removal
|
|
463
481
|
is rejected. `SENPI_CODEMODE_GATE_MUTATE=leak-kernel` leaves the real kernel
|
|
464
482
|
alive at the teardown witness, then closes it in `finally`; nonzero process,
|
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.8",
|
|
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,15 @@
|
|
|
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.8",
|
|
36
|
+
"quickjs-wasi": "3.6.2",
|
|
36
37
|
"typebox": "1.3.34"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.10.8"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
43
|
+
"@code-yeongyu/senpi": "2026.10.8"
|
|
43
44
|
},
|
|
44
45
|
"keywords": [
|
|
45
46
|
"senpi",
|
|
@@ -92,12 +92,20 @@ export const kernelToolKernelToHostSchemas = [
|
|
|
92
92
|
requestId: Type.String({ minLength: 1 }),
|
|
93
93
|
ok: Type.Literal(true),
|
|
94
94
|
value: Type.Unknown(),
|
|
95
|
+
/** Python: text the tool printed during this call (never the parent cell's output). */
|
|
96
|
+
output: Type.Optional(Type.String()),
|
|
95
97
|
}),
|
|
96
98
|
Type.Object({
|
|
97
99
|
type: Type.Literal("kernel-tool-invoke-reply"),
|
|
98
100
|
requestId: Type.String({ minLength: 1 }),
|
|
99
101
|
ok: Type.Literal(false),
|
|
100
102
|
error: kernelToolErrorSchema,
|
|
103
|
+
output: Type.Optional(Type.String()),
|
|
104
|
+
}),
|
|
105
|
+
/** Python: the kernel's tool names after a define or undefine, for cross-language collision checks. */
|
|
106
|
+
Type.Object({
|
|
107
|
+
type: Type.Literal("kernel-tools-defined"),
|
|
108
|
+
names: Type.Array(Type.String({ minLength: 1 })),
|
|
101
109
|
}),
|
|
102
110
|
] as const;
|
|
103
111
|
|
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,100 @@
|
|
|
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 kernelToolsEntry: EvalSchemaResult = {
|
|
73
|
+
name: "eval:kernel-tools",
|
|
74
|
+
description: [
|
|
75
|
+
"Kernel tools: functions a cell defines that in-process children can call (js and py; rb/jl answer tools_unavailable).",
|
|
76
|
+
"js: tool(fn, {name?, description?, schema?}) registers a named function; arguments are passed in its parameter order; without schema only the parameter names are inferred and every one is required.",
|
|
77
|
+
'py: @tool or @tool(name=..., description=..., schema=...) registers the function and returns it unchanged. The schema is inferred from type hints (str, int, float, bool, None, list[T], dict[str, T], Literal, unions and Optional as nullable, Annotated[T, "description"], keyword-only parameters); a JSON default makes a parameter optional; string annotations (from __future__ import annotations) resolve through the function\'s globals. Positional-only, *args, **kwargs and unresolvable or unsupported annotations are refused with invalid_tool_definition unless schema= is given. The function is never called to infer anything.',
|
|
78
|
+
"tool.defined() lists this kernel's kernel tools (sorted); tool.undefine(name) removes one (true if it existed). The names defined and undefine are reserved.",
|
|
79
|
+
"Grants: a child is given descriptors (name, kernel_generation, definition_revision) before it starts. A reset, restart or redefinition makes an old descriptor fail with kernel_tool_stale; the new definition never satisfies it. A name defined in both js and py fails describe with tool_name_collision.",
|
|
80
|
+
"py callbacks run while the kernel is idle or its cell is parked in a host call (a tool call, wait(), an install); a cell busy in pure computation is never interrupted, the callback waits. A callback's printed text goes to its own reply, never the parent cell's output. Cancelling a call ends it at its next host call (kernel_tool_cancelled); a computation already running finishes and its result is dropped. An awaited object from the parent cell's event loop fails with kernel_tool_loop_mismatch.",
|
|
81
|
+
].join("\n"),
|
|
82
|
+
parameters: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
tool: { description: "js: tool(fn, metadata?); py: @tool / @tool(name=, description=, schema=)" },
|
|
86
|
+
"tool.defined": { description: "() -> names[]" },
|
|
87
|
+
"tool.undefine": { description: "(name) -> boolean" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const VIRTUAL_ENTRIES: ReadonlyMap<string, EvalSchemaResult> = new Map([
|
|
93
|
+
[waitEntry.name, waitEntry],
|
|
94
|
+
[helpersEntry.name, helpersEntry],
|
|
95
|
+
[kernelToolsEntry.name, kernelToolsEntry],
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
export function virtualEvalSchema(name: string): EvalSchemaResult | undefined {
|
|
99
|
+
return VIRTUAL_ENTRIES.get(name);
|
|
100
|
+
}
|
|
@@ -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
|
-
}
|