@code-yeongyu/senpi 2026.9.15-2 → 2026.9.16

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 (137) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/core/extensions/kernel-tools-context.d.ts +13 -0
  3. package/dist/core/extensions/kernel-tools-context.d.ts.map +1 -0
  4. package/dist/core/extensions/kernel-tools-context.js +3 -0
  5. package/dist/core/extensions/kernel-tools-context.js.map +1 -0
  6. package/dist/core/extensions/runner.d.ts +19 -0
  7. package/dist/core/extensions/runner.d.ts.map +1 -1
  8. package/dist/core/extensions/runner.js +103 -1
  9. package/dist/core/extensions/runner.js.map +1 -1
  10. package/dist/core/extensions/types.d.ts +21 -0
  11. package/dist/core/extensions/types.d.ts.map +1 -1
  12. package/dist/core/extensions/types.js.map +1 -1
  13. package/dist/core/settings-manager.d.ts +23 -0
  14. package/dist/core/settings-manager.d.ts.map +1 -1
  15. package/dist/core/settings-manager.js +41 -0
  16. package/dist/core/settings-manager.js.map +1 -1
  17. package/dist/core/tools/index.d.ts +1 -1
  18. package/dist/core/tools/index.d.ts.map +1 -1
  19. package/dist/core/tools/index.js +4 -3
  20. package/dist/core/tools/index.js.map +1 -1
  21. package/dist/core/tools/read.d.ts +3 -1
  22. package/dist/core/tools/read.d.ts.map +1 -1
  23. package/dist/core/tools/read.js +16 -2
  24. package/dist/core/tools/read.js.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/docs/extensions.md +3 -0
  30. package/docs/settings.md +12 -0
  31. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +21 -0
  32. package/node_modules/@code-yeongyu/senpi-codemode/README.md +24 -0
  33. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  34. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/http-server.ts +7 -1
  35. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/kernel-tools-protocol.ts +85 -0
  36. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +6 -0
  37. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/agent-bridge.ts +30 -11
  38. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +11 -0
  39. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +10 -0
  40. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/prelude.jl +46 -0
  41. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/runner.jl +1 -1
  42. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +29 -0
  43. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +12 -0
  44. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-context.d.ts +13 -0
  45. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-context.js +7 -0
  46. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-define.d.ts +10 -0
  47. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-define.js +12 -0
  48. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-errors.js +22 -0
  49. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-errors.ts +26 -0
  50. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-host.ts +134 -0
  51. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-naming.js +28 -0
  52. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-parse.d.ts +5 -0
  53. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-parse.js +255 -0
  54. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-pump.js +105 -0
  55. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-registry.d.ts +27 -0
  56. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-registry.js +127 -0
  57. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-schema.js +69 -0
  58. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-tools-types.ts +42 -0
  59. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +2 -0
  60. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +26 -3
  61. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +20 -8
  62. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-slot.ts +6 -1
  63. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +9 -7
  64. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/workpool.js +37 -0
  65. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/kernel-tools-unavailable.ts +5 -0
  66. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +13 -0
  67. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +57 -3
  68. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/prelude.rb +13 -2
  69. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/workpool.rb +38 -0
  70. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +13 -0
  71. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +2 -0
  72. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt-template.ts +4 -2
  73. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-handler.ts +8 -1
  74. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +49 -30
  75. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +2 -0
  76. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.d.ts.map +1 -1
  77. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.js +51 -12
  78. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.js.map +1 -1
  79. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/read.d.ts +3 -0
  80. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/read.d.ts.map +1 -1
  81. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/read.js +18 -3
  82. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/read.js.map +1 -1
  83. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/brace-scanner.d.ts +4 -0
  84. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/brace-scanner.d.ts.map +1 -0
  85. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/brace-scanner.js +268 -0
  86. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/brace-scanner.js.map +1 -0
  87. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/header-protection.d.ts +18 -0
  88. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/header-protection.d.ts.map +1 -0
  89. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/header-protection.js +69 -0
  90. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/header-protection.js.map +1 -0
  91. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/index.d.ts +27 -0
  92. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/index.d.ts.map +1 -0
  93. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/index.js +118 -0
  94. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/index.js.map +1 -0
  95. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-context.d.ts +21 -0
  96. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-context.d.ts.map +1 -0
  97. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-context.js +24 -0
  98. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-context.js.map +1 -0
  99. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-spans.d.ts +12 -0
  100. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-spans.d.ts.map +1 -0
  101. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-spans.js +92 -0
  102. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/lexical-spans.js.map +1 -0
  103. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/types.d.ts +47 -0
  104. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/types.d.ts.map +1 -0
  105. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/types.js +8 -0
  106. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/read-folders/types.js.map +1 -0
  107. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/segmented-read-view.d.ts +52 -0
  108. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/segmented-read-view.d.ts.map +1 -0
  109. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/segmented-read-view.js +155 -0
  110. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/segmented-read-view.js.map +1 -0
  111. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts +2 -0
  112. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts.map +1 -1
  113. package/node_modules/@earendil-works/pi-agent-core/dist/index.js +2 -0
  114. package/node_modules/@earendil-works/pi-agent-core/dist/index.js.map +1 -1
  115. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  116. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  117. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  118. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  119. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  120. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  121. package/node_modules/@earendil-works/pi-ai/dist/providers/data/alibaba-token-plan.json +1 -1
  122. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  123. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  124. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  125. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -1
  126. package/node_modules/@earendil-works/pi-ai/dist/utils/empty-response-errors.d.ts +15 -0
  127. package/node_modules/@earendil-works/pi-ai/dist/utils/empty-response-errors.d.ts.map +1 -0
  128. package/node_modules/@earendil-works/pi-ai/dist/utils/empty-response-errors.js +15 -0
  129. package/node_modules/@earendil-works/pi-ai/dist/utils/empty-response-errors.js.map +1 -0
  130. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  131. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +10 -0
  132. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  133. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  134. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  135. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  136. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  137. package/package.json +7 -7
@@ -0,0 +1,69 @@
1
+ import { kernelToolError } from "./kernel-tools-errors.js";
2
+
3
+ export function defaultInputSchema(params) {
4
+ return {
5
+ type: "object",
6
+ properties: Object.fromEntries(params.map((name) => [name, {}])),
7
+ required: [...params],
8
+ additionalProperties: false,
9
+ };
10
+ }
11
+
12
+ export function resolveToolMetadata(metadata, params) {
13
+ if (metadata === undefined) return { description: "", input_schema: defaultInputSchema(params) };
14
+ if (!isPlainJsonObject(metadata)) {
15
+ throw kernelToolError("invalid_tool_definition", "tool() metadata must be plain JSON");
16
+ }
17
+ const description = metadata.description === undefined ? "" : metadata.description;
18
+ if (typeof description !== "string") {
19
+ throw kernelToolError("invalid_tool_definition", "tool() description must be a string");
20
+ }
21
+ if (metadata.schema === undefined) return { description, input_schema: defaultInputSchema(params) };
22
+ const schema = metadata.schema;
23
+ if (!isPlainJsonObject(schema) || schema.type !== "object" || !isPlainJsonObject(schema.properties)) {
24
+ throw kernelToolError("invalid_tool_definition", "tool() metadata schema must be a JSON object schema");
25
+ }
26
+ const propertyKeys = Object.keys(schema.properties);
27
+ if (propertyKeys.length !== params.length || params.some((name) => !Object.hasOwn(schema.properties, name))) {
28
+ throw kernelToolError("invalid_tool_definition", "tool() metadata schema properties must match parameters");
29
+ }
30
+ return { description, input_schema: schema };
31
+ }
32
+
33
+ export function validateInvokeArgs(schema, args) {
34
+ if (!isPlainJsonObject(args)) throw kernelToolError("invalid_tool_definition", "kernel tool args must be a JSON object");
35
+ const required = Array.isArray(schema.required) ? schema.required : [];
36
+ for (const key of required) {
37
+ if (!Object.hasOwn(args, key)) {
38
+ throw kernelToolError("invalid_tool_definition", `missing kernel tool argument: ${key}`);
39
+ }
40
+ }
41
+ if (schema.additionalProperties === false) {
42
+ const allowed = new Set(Object.keys(schema.properties ?? {}));
43
+ for (const key of Object.keys(args)) {
44
+ if (!allowed.has(key)) {
45
+ throw kernelToolError("invalid_tool_definition", `unexpected kernel tool argument: ${key}`);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export function isPlainJsonObject(value) {
52
+ return isJsonValue(value) && value !== null && typeof value === "object" && !Array.isArray(value);
53
+ }
54
+
55
+ export function isJsonValue(value) {
56
+ if (value === null) return true;
57
+ const type = typeof value;
58
+ if (type === "string" || type === "boolean") return true;
59
+ if (type === "number") return Number.isFinite(value);
60
+ if (type !== "object") return false;
61
+ if (Array.isArray(value)) return value.every(isJsonValue);
62
+ const proto = Object.getPrototypeOf(value);
63
+ if (proto !== Object.prototype && proto !== null) return false;
64
+ return Object.values(value).every(isJsonValue);
65
+ }
66
+
67
+ export function orderedArgs(params, args) {
68
+ return params.map((name) => args[name]);
69
+ }
@@ -0,0 +1,42 @@
1
+ import type { KernelToolErrorCode } from "./kernel-tools-errors.ts";
2
+
3
+ export type { KernelToolErrorCode };
4
+
5
+ export type KernelToolDescriptor = {
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly input_schema: unknown;
9
+ readonly language: "js";
10
+ readonly kernel_generation: number;
11
+ readonly definition_revision: number;
12
+ };
13
+
14
+ export type KernelToolsInvokeRequest = {
15
+ readonly name: string;
16
+ readonly kernel_generation: number;
17
+ readonly definition_revision: number;
18
+ readonly args: unknown;
19
+ readonly call_id: string;
20
+ };
21
+
22
+ export type KernelToolsDescribeEntry =
23
+ | { readonly name: string; readonly ok: true; readonly descriptor: KernelToolDescriptor }
24
+ | {
25
+ readonly name: string;
26
+ readonly ok: false;
27
+ readonly error: { readonly code: KernelToolErrorCode; readonly message: string };
28
+ };
29
+
30
+ export type KernelToolsDescribeResult = {
31
+ readonly results: readonly KernelToolsDescribeEntry[];
32
+ };
33
+
34
+ export type KernelToolsCapability = {
35
+ describe(names: readonly string[]): Promise<KernelToolsDescribeResult>;
36
+ invoke(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown>;
37
+ };
38
+
39
+ export const KERNEL_TOOLS_UNSUPPORTED = {
40
+ code: "tools_unavailable" as const,
41
+ message: "Kernel tools require a live JavaScript worker context",
42
+ };
@@ -7,9 +7,11 @@ export const JAVASCRIPT_KERNEL_PRELUDE = [
7
7
  "read(path, options?): read UTF-8 text; plain paths use cwd and local:// uses the session local root.",
8
8
  "write(path, content): write UTF-8 or binary data and return the resolved path.",
9
9
  "tool.<name>(args): request a host tool call through the bridge; resolves to { text, images?, details?, hasError? }.",
10
+ "tool(fn, metadata?): register a named JavaScript function as a fenced kernel tool for in-process children.",
10
11
  "completion(prompt, options?): request a host completion bridge call.",
11
12
  "output(...ids, options?): retrieve task output through the reserved output bridge.",
12
13
  "agent(prompt, options?): delegate work through the reserved agent bridge.",
14
+ "workpool(agent, name, options?): await a host-owned pool adapter with pool_id, push, close, inspect, and cancel; options only accept mode.",
13
15
  "parallel(thunks): run async thunks through the configured bounded pool; preserves order and rethrows the lowest-index error after all settle.",
14
16
  "pipeline(items, ...stages): map items through staged async transforms with a barrier between stages.",
15
17
  ].join("\n");
@@ -1,3 +1,6 @@
1
+ import { kernelToolCallContext } from "./kernel-tools-context.js";
2
+ import { kernelToolError } from "./kernel-tools-errors.js";
3
+ import { createKernelToolPump } from "./kernel-tools-pump.js";
1
4
  import { JsWorkerRuntime } from "./worker-runtime.js";
2
5
 
3
6
  // Mirrors INTERRUPT_ACK_OP and CHILD_LIFECYCLE_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
@@ -21,6 +24,12 @@ export function createWorkerCore(transport, options) {
21
24
  let runtime = null;
22
25
  let activeCell = null;
23
26
  const pendingTools = new Map();
27
+ const nestedInvokes = new Map();
28
+ const kernelTools = createKernelToolPump({
29
+ getRuntime: () => runtime,
30
+ emit: (message) => transport.send(message),
31
+ nestedInvokes,
32
+ });
24
33
 
25
34
  function emit(message) {
26
35
  transport.send(message);
@@ -47,9 +56,12 @@ export function createWorkerCore(transport, options) {
47
56
  }
48
57
 
49
58
  async function callTool(toolName, args) {
50
- if (activeCell?.interruption) throw activeCell.interruption;
59
+ const nested = kernelToolCallContext.getStore();
60
+ if (!nested && activeCell?.interruption) throw activeCell.interruption;
61
+ if (nested?.signal.aborted) throw nested.signal.reason;
62
+ const bag = nested?.pendingTools ?? pendingTools;
51
63
  const callId = `js-${crypto.randomUUID()}`;
52
- const promise = new Promise((resolve, reject) => pendingTools.set(callId, { resolve, reject }));
64
+ const promise = new Promise((resolve, reject) => bag.set(callId, { resolve, reject }));
53
65
  emit({ type: "tool-call", callId, toolName, args });
54
66
  return await promise;
55
67
  }
@@ -63,10 +75,16 @@ export function createWorkerCore(transport, options) {
63
75
  pendingTools.delete(callId);
64
76
  pending.reject(interruption);
65
77
  }
78
+ kernelTools.abortAll(kernelToolError("kernel_tool_stale", interruption.message));
66
79
  runtime.interrupt();
67
80
  }
68
81
 
69
82
  function onMessage(message) {
83
+ if (kernelTools.handle(message)) return;
84
+ if (message.type === "kernel-tools-names") {
85
+ runtime?.kernelTools.setCollisionNames(message.hostToolNames ?? [], message.foreignLanguageNames ?? []);
86
+ return;
87
+ }
70
88
  if (message.type === "init") {
71
89
  applySessionEnvironment(message.sessionEnv);
72
90
  runtime = new JsWorkerRuntime({
@@ -74,6 +92,9 @@ export function createWorkerCore(transport, options) {
74
92
  parallelPoolWidth: options.parallelPoolWidth,
75
93
  localRoots: message.connection.localRoots,
76
94
  artifactsDir: message.connection.artifactsDir,
95
+ kernelGeneration: message.kernelGeneration ?? 1,
96
+ hostToolNames: message.hostToolNames ?? [],
97
+ foreignLanguageNames: message.foreignLanguageNames ?? [],
77
98
  onChildEvent: (event) => emit({ type: "status", event: { op: CHILD_LIFECYCLE_OP, ...event } }),
78
99
  });
79
100
  emit({ type: "ready" });
@@ -84,6 +105,7 @@ export function createWorkerCore(transport, options) {
84
105
  return;
85
106
  }
86
107
  if (message.type === "tool-reply") {
108
+ if (kernelTools.settleToolReply(message)) return;
87
109
  const pending = pendingTools.get(message.callId);
88
110
  if (!pending) return;
89
111
  pendingTools.delete(message.callId);
@@ -144,7 +166,7 @@ function cellInterruptedError(reason) {
144
166
 
145
167
  function bridgeError(error) {
146
168
  if (error instanceof Error) {
147
- return { name: error.name, message: error.message, stack: error.stack };
169
+ return { name: error.name, message: error.message, stack: error.stack, ...(typeof error.code === "string" ? { code: error.code } : {}) };
148
170
  }
149
171
  return { message: String(error) };
150
172
  }
@@ -153,5 +175,6 @@ function errorFromBridge(error) {
153
175
  const result = new Error(error.message);
154
176
  if (error.name) result.name = error.name;
155
177
  if (error.stack) result.stack = error.stack;
178
+ if (typeof error.code === "string") result.code = error.code;
156
179
  return result;
157
180
  }
@@ -6,6 +6,10 @@ import { encodeDisplayImage, resolveDisplayOps } from "./display-image.js";
6
6
  import { terminateProcessTrees } from "./process-tree.js";
7
7
  import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect-eval.js";
8
8
  import { installShellCapture } from "./worker-shell-capture.js";
9
+ import { createWorkpool } from "./workpool.js";
10
+ import { inKernelToolInvoke } from "./kernel-tools-context.js";
11
+ import { kernelToolError } from "./kernel-tools-errors.js";
12
+ import { createKernelToolRegistry, createToolNamespace } from "./kernel-tools-registry.js";
9
13
 
10
14
  const PREPARED_CELL_PREFIX = "/*senpi:prepared-cell*/";
11
15
  // How long a child gets to honour SIGTERM before SIGKILL. Short, because the
@@ -22,6 +26,7 @@ export class JsWorkerRuntime {
22
26
  #pendingDisplays = [];
23
27
  #children = new Set();
24
28
  #onChildEvent;
29
+ #tools;
25
30
 
26
31
  constructor(options) {
27
32
  this.#cwd = options.cwd;
@@ -29,9 +34,18 @@ export class JsWorkerRuntime {
29
34
  this.#onChildEvent = typeof options.onChildEvent === "function" ? options.onChildEvent : null;
30
35
  this.#localRoots = { ...(options.localRoots ?? {}) };
31
36
  if (options.artifactsDir && !this.#localRoots.local) this.#localRoots.local = join(options.artifactsDir, "local");
37
+ this.#tools = createKernelToolRegistry({
38
+ generation: options.kernelGeneration ?? 1,
39
+ hostToolNames: options.hostToolNames ?? [],
40
+ foreignLanguageNames: options.foreignLanguageNames ?? [],
41
+ });
32
42
  this.#installGlobals();
33
43
  }
34
44
 
45
+ get kernelTools() {
46
+ return this.#tools;
47
+ }
48
+
35
49
  async run(code, cellId, hooks) {
36
50
  this.#hooks = hooks;
37
51
  try {
@@ -107,17 +121,13 @@ export class JsWorkerRuntime {
107
121
  globalThis.output = async (...args) => await this.#output(args);
108
122
  globalThis.tool_schema = async name => await this.#toolSchema(name);
109
123
  globalThis.agent = async (prompt, options, ...rest) => await this.#agent(prompt, options, rest);
124
+ globalThis.workpool = (agent, name, options) => createWorkpool((toolName, args) => this.#callTool(toolName, args), agent, name, options);
110
125
  globalThis.parallel = async thunks => await this.#parallel(thunks);
111
126
  globalThis.pipeline = async (items, ...stages) => await this.#pipeline(items, stages);
112
127
  globalThis.completion = async (prompt, opts) => await this.#callTool("completion", { prompt, opts });
113
- globalThis.tool = new Proxy(
114
- {},
115
- {
116
- get: (_target, prop) => {
117
- if (typeof prop !== "string") return undefined;
118
- return async args => await this.#callTool(prop, args ?? {});
119
- },
120
- },
128
+ globalThis.tool = createToolNamespace(
129
+ (fn, metadata) => this.#tools.define(fn, metadata),
130
+ async (name, args) => await this.#callTool(name, args),
121
131
  );
122
132
  globalThis.tools = globalThis.tool;
123
133
  const originalLog = console.log.bind(console);
@@ -273,6 +283,7 @@ export class JsWorkerRuntime {
273
283
  }
274
284
 
275
285
  async #agent(prompt, options, rest) {
286
+ if (inKernelToolInvoke()) throw kernelToolError("kernel_tool_recursion", "kernel tools may not invoke agent()");
276
287
  const parsed = optionsArg({
277
288
  name: "agent",
278
289
  value: options,
@@ -302,6 +313,7 @@ export class JsWorkerRuntime {
302
313
  output: text,
303
314
  handle: details.handle ?? `agent://${id}`,
304
315
  id,
316
+ run_epoch: details.run_epoch,
305
317
  agent: details.agent ?? callArgs.agent ?? null,
306
318
  };
307
319
  if (Object.hasOwn(callArgs, "schema")) node.data = output;
@@ -39,18 +39,23 @@ export class WorkerSlot {
39
39
  return this.#startupAbort !== null;
40
40
  }
41
41
 
42
+ get generation(): number {
43
+ return this.#generation;
44
+ }
45
+
42
46
  postMessage(message: HostToKernelMessage): void {
43
47
  this.#worker?.postMessage(message);
44
48
  }
45
49
 
46
50
  async ensureReady(): Promise<void> {
47
51
  if (!this.#ready) {
48
- const generation = ++this.#generation;
52
+ const generation = this.#generation === 0 ? ++this.#generation : this.#generation;
49
53
  const controller = new AbortController();
50
54
  this.#startupAbort = controller;
51
55
  const ready = startWorkerWithInlineFallback(
52
56
  {
53
57
  options: this.#options,
58
+ kernelGeneration: generation,
54
59
  publish: (worker) => this.#publish(worker, generation),
55
60
  isCurrent: (worker) => this.#isCurrent(worker, generation),
56
61
  retire: (worker) => {
@@ -2,6 +2,7 @@ import { dirname, join } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
4
4
  import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
5
+ import { resolveKernelToolNameSource } from "./kernel-contract.ts";
5
6
  import { type JavaScriptKernelOptions, localBridgeConnection } from "./local-module-loader.ts";
6
7
  import { spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
7
8
 
@@ -16,6 +17,7 @@ export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions
16
17
 
17
18
  export interface WorkerStartupHooks {
18
19
  readonly options: JavaScriptKernelOptions;
20
+ readonly kernelGeneration: number;
19
21
  /** Wires the worker into the kernel; throws `WorkerStartupCancelledError` once the generation is stale. */
20
22
  publish(worker: WorkerLike): void;
21
23
  isCurrent(worker: WorkerLike): boolean;
@@ -27,7 +29,7 @@ export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, s
27
29
  let worker = spawnWorker(hooks.options);
28
30
  hooks.publish(worker);
29
31
  try {
30
- await initializeWorker(worker, hooks.options, signal);
32
+ await initializeWorker(worker, hooks, signal);
31
33
  return;
32
34
  } catch (error) {
33
35
  if (!hooks.isCurrent(worker) || error instanceof WorkerStartupCancelledError) {
@@ -41,7 +43,7 @@ export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, s
41
43
  if (!hooks.canFallBackInline()) throw new WorkerStartupCancelledError();
42
44
  worker = createInlineWorker(hooks.options.cwd, hooks.options.parallelPoolWidth);
43
45
  hooks.publish(worker);
44
- await initializeWorker(worker, hooks.options, signal);
46
+ await initializeWorker(worker, hooks, signal);
45
47
  }
46
48
 
47
49
  function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
@@ -54,16 +56,16 @@ function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
54
56
  }
55
57
  }
56
58
 
57
- async function initializeWorker(
58
- worker: WorkerLike,
59
- options: JavaScriptKernelOptions,
60
- signal: AbortSignal,
61
- ): Promise<void> {
59
+ async function initializeWorker(worker: WorkerLike, hooks: WorkerStartupHooks, signal: AbortSignal): Promise<void> {
62
60
  const ready = waitForReady(worker, signal);
61
+ const options = hooks.options;
63
62
  worker.postMessage({
64
63
  type: "init",
65
64
  sessionId: options.sessionId,
66
65
  connection: localBridgeConnection(options),
66
+ kernelGeneration: hooks.kernelGeneration,
67
+ hostToolNames: resolveKernelToolNameSource(options.hostToolNames),
68
+ foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
67
69
  ...(options.sessionEnv === undefined ? {} : { sessionEnv: options.sessionEnv }),
68
70
  });
69
71
  await ready;
@@ -0,0 +1,37 @@
1
+ import { inKernelToolInvoke } from "./kernel-tools-context.js";
2
+ import { kernelToolError } from "./kernel-tools-errors.js";
3
+
4
+ // Only host-tool sugar: no admission, worker, or queue state belongs to the kernel.
5
+ export async function createWorkpool(callTool, agent, name, options = {}) {
6
+ if (inKernelToolInvoke()) throw kernelToolError("kernel_tool_recursion", "kernel tools may not invoke workpool()");
7
+ if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "mode")) {
8
+ throw new TypeError("workpool() options only accept mode");
9
+ }
10
+ const call = async args => {
11
+ try {
12
+ return await callTool("workpool", args);
13
+ } catch (error) {
14
+ if (error?.code === "unknown_tool" || error?.code === "inactive_tool") {
15
+ throw workpoolError("workpool_unavailable", "No active host workpool tool", error);
16
+ }
17
+ throw error;
18
+ }
19
+ };
20
+ const result = await call({ op: "create", agent, name, ...options });
21
+ if (result.details?.error) throw workpoolError(result.details.error.code, result.details.error.message);
22
+ const pool_id = result.details?.pool_id;
23
+ if (result.hasError || typeof pool_id !== "string" || !/^wp_[0-9a-f]{32}$/.test(pool_id)) {
24
+ throw workpoolError("workpool_unavailable", "Host did not return a workpool identity");
25
+ }
26
+ return Object.freeze({
27
+ pool_id,
28
+ push: items => call({ op: "push", pool_id, items }),
29
+ close: () => call({ op: "close", pool_id }),
30
+ inspect: () => call({ op: "inspect", pool_id }),
31
+ cancel: () => call({ op: "cancel", pool_id }),
32
+ });
33
+ }
34
+
35
+ function workpoolError(code, message, cause) {
36
+ return Object.assign(new Error(message, { cause }), { code });
37
+ }
@@ -0,0 +1,5 @@
1
+ import { kernelToolError } from "./js/kernel-tools-errors.ts";
2
+
3
+ export function rejectKernelToolsUnavailable(): Promise<never> {
4
+ return Promise.reject(kernelToolError("tools_unavailable", "Kernel tools are JavaScript-only"));
5
+ }
@@ -1,4 +1,5 @@
1
1
  import type { KernelInterruptHandle } from "../../tool/types.ts";
2
+ import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
2
3
  import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
3
4
  import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
4
5
 
@@ -31,6 +32,18 @@ export class PythonKernel {
31
32
  return kernel;
32
33
  }
33
34
 
35
+ listKernelToolNames(): readonly string[] {
36
+ return [];
37
+ }
38
+
39
+ describeKernelTools(_names: readonly string[]): Promise<never> {
40
+ return rejectKernelToolsUnavailable();
41
+ }
42
+
43
+ invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
44
+ return rejectKernelToolsUnavailable();
45
+ }
46
+
34
47
  run(input: PythonKernelRunOptions): Promise<ResultMessage> {
35
48
  if (this.#failure) return Promise.reject(this.#failure);
36
49
  if (this.#closed) return Promise.reject(new Error("Python kernel is closed"));
@@ -24,7 +24,7 @@ from collections.abc import Iterable
24
24
  from concurrent.futures import ThreadPoolExecutor, as_completed
25
25
  from pathlib import Path
26
26
  from threading import Lock, Thread
27
- from typing import Any, Callable
27
+ from typing import Any, Callable, Union
28
28
  from urllib.parse import unquote
29
29
 
30
30
  SESSION_ID = ""
@@ -43,7 +43,11 @@ TIMEOUT_RESUME_OP = "timeout-resume"
43
43
 
44
44
 
45
45
  class PreludeRuntimeError(RuntimeError):
46
- """Host bridge or magic execution failed."""
46
+ """Host bridge or magic execution failed, retaining its machine code."""
47
+
48
+ def __init__(self, message: str, code: str | None = None) -> None:
49
+ super().__init__(message)
50
+ self.code = code
47
51
 
48
52
 
49
53
  class PreludeValueError(ValueError):
@@ -370,7 +374,7 @@ def bridge_post(path: str, payload: dict[str, Any]) -> Any:
370
374
  return body.get("value")
371
375
  error = body.get("error") if isinstance(body, dict) else body
372
376
  if isinstance(error, dict):
373
- raise PreludeRuntimeError(str(error.get("message", error)))
377
+ raise PreludeRuntimeError(str(error.get("message", error)), error.get("code"))
374
378
  raise PreludeRuntimeError(str(error))
375
379
 
376
380
 
@@ -416,6 +420,54 @@ class ToolProxy:
416
420
 
417
421
  tool = ToolProxy()
418
422
 
423
+ JsonValue = Union[str, int, float, bool, None, list["JsonValue"], dict[str, "JsonValue"]]
424
+
425
+
426
+ def _workpool_call(args: dict[str, JsonValue]) -> dict[str, JsonValue]:
427
+ try:
428
+ return tool.workpool(args)
429
+ except PreludeRuntimeError as error:
430
+ if error.code in ("unknown_tool", "inactive_tool"):
431
+ raise PreludeRuntimeError("No active host workpool tool", "workpool_unavailable") from error
432
+ raise
433
+
434
+
435
+ class Workpool:
436
+ """An opaque host identity, not a worker queue."""
437
+
438
+ __slots__ = ("pool_id",)
439
+
440
+ def __init__(self, pool_id: str) -> None:
441
+ self.pool_id = pool_id
442
+
443
+ def push(self, items: list[JsonValue]) -> dict[str, JsonValue]:
444
+ return _workpool_call({"op": "push", "pool_id": self.pool_id, "items": items})
445
+
446
+ def close(self) -> dict[str, JsonValue]:
447
+ return _workpool_call({"op": "close", "pool_id": self.pool_id})
448
+
449
+ def inspect(self) -> dict[str, JsonValue]:
450
+ return _workpool_call({"op": "inspect", "pool_id": self.pool_id})
451
+
452
+ def cancel(self) -> dict[str, JsonValue]:
453
+ return _workpool_call({"op": "cancel", "pool_id": self.pool_id})
454
+
455
+
456
+ def workpool(agent: dict[str, JsonValue], name: str, *, mode: str | None = None) -> Workpool:
457
+ args: dict[str, JsonValue] = {"op": "create", "agent": agent, "name": name}
458
+ if mode is not None:
459
+ args["mode"] = mode
460
+ result = _workpool_call(args)
461
+ details = result.get("details")
462
+ if isinstance(details, dict):
463
+ error = details.get("error")
464
+ if isinstance(error, dict):
465
+ raise PreludeRuntimeError(str(error["message"]), str(error["code"]))
466
+ pool_id = details.get("pool_id")
467
+ if not result.get("hasError") and isinstance(pool_id, str) and re.fullmatch(r"wp_[0-9a-f]{32}", pool_id):
468
+ return Workpool(pool_id)
469
+ raise PreludeRuntimeError("Host did not return a workpool identity", "workpool_unavailable")
470
+
419
471
 
420
472
  def completion(
421
473
  prompt: str,
@@ -522,6 +574,7 @@ def agent(
522
574
  "output": text_value,
523
575
  "handle": handle_value,
524
576
  "id": agent_id,
577
+ "run_epoch": response_record.get("run_epoch"),
525
578
  "agent": response_record.get("agent", agent),
526
579
  }
527
580
  if schema is not None:
@@ -917,6 +970,7 @@ USER_NS.update(
917
970
  "tool": tool,
918
971
  "completion": completion,
919
972
  "agent": agent,
973
+ "workpool": workpool,
920
974
  "output": output,
921
975
  "tool_schema": tool_schema,
922
976
  "__senpi_magic": _magic,
@@ -140,13 +140,22 @@ def __senpi_bridge_request(path, payload)
140
140
  return body["value"] if body.is_a?(Hash) && body["ok"] == true
141
141
 
142
142
  error = body.is_a?(Hash) ? body["error"] : body
143
- raise(error.is_a?(Hash) ? error["message"].to_s : error.to_s)
143
+ raise SenpiBridgeError.new(error.is_a?(Hash) ? error["message"].to_s : error.to_s, error.is_a?(Hash) ? error["code"] : nil)
144
144
  end
145
145
 
146
146
  def __senpi_call_tool(name, args)
147
147
  __senpi_bridge_request("/call", { "callId" => "rb-#{Process.pid}-#{rand(1_000_000)}", "toolName" => name, "args" => args })
148
148
  end
149
149
 
150
+ class SenpiBridgeError < StandardError
151
+ attr_reader :code
152
+
153
+ def initialize(message, code = nil)
154
+ super(message)
155
+ @code = code
156
+ end
157
+ end
158
+
150
159
  class SenpiToolCallable
151
160
  def initialize(name)
152
161
  @name = name
@@ -177,6 +186,8 @@ def tool
177
186
  $__senpi_tool_proxy ||= SenpiToolProxy.new
178
187
  end
179
188
 
189
+ require_relative "workpool"
190
+
180
191
  def completion(prompt, model: "default", system: nil, schema: nil, **kwargs)
181
192
  options = { "model" => model }.merge(kwargs.transform_keys(&:to_s))
182
193
  options["system"] = system unless system.nil?
@@ -213,7 +224,7 @@ def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, isolated:
213
224
  text_value = record.fetch("text", response)
214
225
  result = schema.nil? ? text_value : record.key?("data") ? record["data"] : JSON.parse(text_value.to_s)
215
226
  return result unless handle
216
- { "text" => text_value, "output" => text_value, "handle" => record["handle"] || (record["id"] && "agent://#{record["id"]}"), "id" => record["id"], "agent" => record.fetch("agent", agent) }.tap do |node|
227
+ { "text" => text_value, "output" => text_value, "handle" => record["handle"] || (record["id"] && "agent://#{record["id"]}"), "id" => record["id"], "run_epoch" => record["run_epoch"], "agent" => record.fetch("agent", agent) }.tap do |node|
217
228
  node["data"] = result unless schema.nil?
218
229
  end
219
230
  end
@@ -0,0 +1,38 @@
1
+ # Only an opaque host identity is retained; all work remains engine-owned.
2
+ def __senpi_workpool_call(args)
3
+ tool.workpool(args)
4
+ rescue SenpiBridgeError => error
5
+ raise SenpiBridgeError.new("No active host workpool tool", "workpool_unavailable") if ["unknown_tool", "inactive_tool"].include?(error.code)
6
+ raise
7
+ end
8
+
9
+ class SenpiWorkpool
10
+ attr_reader :pool_id
11
+
12
+ def initialize(pool_id)
13
+ @pool_id = pool_id
14
+ freeze
15
+ end
16
+
17
+ def push(items)
18
+ __senpi_workpool_call({ "op" => "push", "pool_id" => @pool_id, "items" => items })
19
+ end
20
+
21
+ [:close, :inspect, :cancel].each do |op|
22
+ define_method(op) { __senpi_workpool_call({ "op" => op.to_s, "pool_id" => @pool_id }) }
23
+ end
24
+ end
25
+
26
+ def workpool(agent, name, mode: nil)
27
+ args = { "op" => "create", "agent" => agent, "name" => name }
28
+ args["mode"] = mode unless mode.nil?
29
+ result = __senpi_workpool_call(args)
30
+ details = result["details"]
31
+ if details.is_a?(Hash)
32
+ error = details["error"]
33
+ raise SenpiBridgeError.new(error["message"], error["code"]) if error.is_a?(Hash)
34
+ pool_id = details["pool_id"]
35
+ return SenpiWorkpool.new(pool_id) if !result["hasError"] && pool_id.is_a?(String) && /\Awp_[0-9a-f]{32}\z/.match?(pool_id)
36
+ end
37
+ raise SenpiBridgeError.new("Host did not return a workpool identity", "workpool_unavailable")
38
+ end
@@ -1,6 +1,7 @@
1
1
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
2
  import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { KernelInterruptHandle } from "../../tool/types.ts";
4
+ import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
4
5
  import { applySessionEnvironment } from "../session-env.ts";
5
6
  import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
6
7
  import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
@@ -69,6 +70,18 @@ export class SubprocessKernel {
69
70
  return { stateRetained: Promise.resolve(false) };
70
71
  }
71
72
 
73
+ listKernelToolNames(): readonly string[] {
74
+ return [];
75
+ }
76
+
77
+ describeKernelTools(_names: readonly string[]): Promise<never> {
78
+ return rejectKernelToolsUnavailable();
79
+ }
80
+
81
+ invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
82
+ return rejectKernelToolsUnavailable();
83
+ }
84
+
72
85
  nextToolCall(): Promise<ToolCallMessage> {
73
86
  return this.runs.nextToolCall();
74
87
  }
@@ -96,6 +96,8 @@ export class SubprocessRunQueue {
96
96
  case "ready":
97
97
  case "init-failed":
98
98
  case "closed":
99
+ case "kernel-tool-describe-reply":
100
+ case "kernel-tool-invoke-reply":
99
101
  onMessage?.(message);
100
102
  return false;
101
103
  default: {