@code-yeongyu/senpi-codemode 2026.9.3 → 2026.9.4-3
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 +71 -0
- package/README.md +2 -0
- package/package.json +5 -5
- package/src/config/settings.ts +29 -0
- package/src/index.ts +20 -1
- package/src/kernels/js/worker-runtime.js +6 -0
- package/src/kernels/js/worker-shell-capture.d.ts +10 -0
- package/src/kernels/js/worker-shell-capture.js +126 -0
- package/src/prompt/eval-prompt.ts +34 -92
- package/src/tool/cell-execution.ts +7 -0
- package/src/tool/eval-tool-options.ts +8 -0
- package/src/tool/eval-tool.ts +15 -2
- package/src/tool/types.ts +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,76 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.4-3] - 2026-09-04
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The package `test` script runs `vitest run test/` instead of `npx tsx …/vitest/dist/cli.js`, matching every other workspace package. The old form spawned npm and tsx to reach the vitest CLI that is already a direct dependency.
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
### Removed
|
|
27
|
+
|
|
28
|
+
## [2026.9.4-2] - 2026-09-04
|
|
29
|
+
|
|
30
|
+
### Breaking Changes
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `foregroundWindowSeconds` codemode setting (default `60`, env `SENPI_CODEMODE_FOREGROUND_SECONDS`): the longest an interactive `eval` call blocks the turn before the cell detaches. A larger `timeout` now frees the turn at this window while the cell keeps running to the hard limit, instead of blocking the agent loop for the whole `timeout`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- The Bun kernel line of the `eval` description now names `new Bun.WebView()` as the headless browser and states when to reach for it (a page that needs JS, a login, or a screenshot) instead of `curl` or a browser CLI. The line previously advertised `Bun.*` builtins generically, so sessions on a Bun kernel resolved page work to `curl`/`fetch` and never discovered the in-process browser. Node kernels are unchanged.
|
|
39
|
+
- The `eval` tool description is dieted a second time: the `Fields:` list now defers to the parameter schema (its single home), the detach guidance is one paragraph, and helper lines keep every signature with fewer words. gpt/codex dialect 1,489 -> 1,087 o200k tokens (description + guidelines); claude 1,173, kimi 1,190, default 1,189. Also fixes the fused `jl` handle form in the all-languages render.
|
|
40
|
+
|
|
41
|
+
- The `eval` tool description is dieted from ~2002 to ~1588 tokens (codex dialect): the three reuse-chain JSON examples, the `<workflow>` graph prose, the repeated state-persistence rules, and the per-dialect wait-doctrine clause are removed or folded; every helper signature and dialect routing is kept. The workflow block's fused `handle=True{ handle: true }` is fixed into per-language correct forms.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
## [2026.9.4] - 2026-09-04
|
|
48
|
+
|
|
49
|
+
### Breaking Changes
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
## [2026.9.3-3] - 2026-09-03
|
|
60
|
+
|
|
61
|
+
### Breaking Changes
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
### Removed
|
|
70
|
+
|
|
71
|
+
## [2026.9.3-2] - 2026-09-03
|
|
72
|
+
|
|
73
|
+
### Breaking Changes
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- JavaScript eval cells no longer leak child-process output onto the host terminal under Bun: `Bun.$` commands awaited without `.quiet()`/`.text()` and `Bun.spawn` children with the default stderr now route their output into the cell's stdout/stderr streams instead of the inherited fd 1/2 that the interactive TUI owns.
|
|
82
|
+
|
|
83
|
+
### Removed
|
|
84
|
+
|
|
15
85
|
## [2026.9.3] - 2026-09-03
|
|
16
86
|
|
|
17
87
|
### Breaking Changes
|
|
@@ -209,6 +279,7 @@
|
|
|
209
279
|
|
|
210
280
|
### Fixed
|
|
211
281
|
|
|
282
|
+
- An explicit `timeout` no longer silently disables detach for interactive `eval` cells. Previously `timeout` was both the detach budget and the hard-limit extension with no cap, so a call like `timeout: 7000` (intended to keep a long detached cell alive) blocked the agent loop for ~2h before the hard limit killed it. The detach point is now capped at the foreground window; `on_timeout: "error"` (and print/json) keep `timeout` as the unclamped deadline, and the hard-limit extension (`max(hardLimitSeconds, timeout)`) is unchanged.
|
|
212
283
|
- Detached-eval same-language busy errors now name each idle enabled kernel and tell the agent to continue the step there (`continue this step in an idle kernel: js`), instead of only pointing at peek and the output tail. A busy Python kernel no longer reads as "eval is unavailable", which previously sent agents to `bash`+`python3` while JavaScript (or another idle kernel) was free. Single-language sessions and fully-busy sessions omit the idle-kernel claim.
|
|
213
284
|
- JavaScript eval cells now persist only top-level declarations, including destructuring bindings and uninitialized variables, without rewriting declaration-shaped text inside literals or comments.
|
|
214
285
|
- Eval completion and detached-cell handling retain explicit lifecycle observability: nested tool counts, wall/kernel timing, detach state, `peek`, `stop`, hard limits, and crash recovery remain bounded and machine-readable for hosts and telemetry consumers.
|
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Configuration is loaded in this order:
|
|
|
71
71
|
"jl": false
|
|
72
72
|
},
|
|
73
73
|
"cellTimeoutSeconds": 30,
|
|
74
|
+
"foregroundWindowSeconds": 60,
|
|
74
75
|
"parallelPoolWidth": 4,
|
|
75
76
|
"taskTools": {
|
|
76
77
|
"task": "task",
|
|
@@ -88,6 +89,7 @@ Configuration is loaded in this order:
|
|
|
88
89
|
| --- | --- | --- |
|
|
89
90
|
| `languages` | `py`/`js` enabled; `rb`/`jl` disabled | Selects desired languages before interpreter detection. |
|
|
90
91
|
| `cellTimeoutSeconds` | `30` | Idle timeout for one cell unless the call supplies `timeout`; interactive calls detach by default and print/json calls error. |
|
|
92
|
+
| `foregroundWindowSeconds` | `60` | Longest an interactive (detach-behavior) call blocks the turn before the cell detaches, capping the `timeout` detach budget. A larger `timeout` still raises the hard limit and keeps the cell running, but the turn is freed at this window. `on_timeout: "error"` calls keep the full `timeout` as an uncapped deadline. Env override: `SENPI_CODEMODE_FOREGROUND_SECONDS`. |
|
|
91
93
|
| `parallelPoolWidth` | `4` | Maximum concurrent `parallel()` thunks. |
|
|
92
94
|
| `taskTools.task` | `"task"` | Registered tool name used by `agent()`. |
|
|
93
95
|
| `taskTools.output` | `"task_output"` | Registered tool name used by `output()`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.3",
|
|
3
|
+
"version": "2026.9.4-3",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "vitest run test/",
|
|
20
20
|
"clean": "node -e \"\"",
|
|
21
21
|
"build": "node -e \"\""
|
|
22
22
|
},
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@babel/parser": "8.0.4",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.3",
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.4-3",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.3"
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.4-3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.3"
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.4-3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/config/settings.ts
CHANGED
|
@@ -19,6 +19,7 @@ export const codemodeSettingsSchema = Type.Object(
|
|
|
19
19
|
),
|
|
20
20
|
),
|
|
21
21
|
cellTimeoutSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
22
|
+
foregroundWindowSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
22
23
|
hardLimitSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
23
24
|
parallelPoolWidth: Type.Optional(Type.Number({ minimum: 1 })),
|
|
24
25
|
taskTools: Type.Optional(
|
|
@@ -64,6 +65,13 @@ export interface CodemodeSettings {
|
|
|
64
65
|
readonly jl: boolean;
|
|
65
66
|
};
|
|
66
67
|
readonly cellTimeoutSeconds: number;
|
|
68
|
+
/**
|
|
69
|
+
* Longest an interactive eval call blocks the agent loop before the cell detaches, independent
|
|
70
|
+
* of `timeout` (which becomes the detach budget only up to this window). A still-running cell
|
|
71
|
+
* keeps living up to the hard limit; this only frees the turn. Ignored for `on_timeout: "error"`
|
|
72
|
+
* (and print/json) calls, where `timeout` stays the unclamped deadline.
|
|
73
|
+
*/
|
|
74
|
+
readonly foregroundWindowSeconds: number;
|
|
67
75
|
/** Wall-clock kill deadline for a single cell; bounds detached cells too. */
|
|
68
76
|
readonly hardLimitSeconds: number;
|
|
69
77
|
readonly parallelPoolWidth: number;
|
|
@@ -98,6 +106,16 @@ export const DEFAULT_HARD_LIMIT_SECONDS = 1800;
|
|
|
98
106
|
|
|
99
107
|
export const HARD_LIMIT_ENVIRONMENT_FLAG = "SENPI_CODEMODE_HARD_LIMIT_SECONDS";
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Bash parity: `terminal/tools/foreground-window.ts` auto-detaches a still-running bash command to a
|
|
111
|
+
* background session at 60s regardless of its `timeout` kill deadline. An eval cell gets the same
|
|
112
|
+
* default foreground window so a large `timeout` extends the cell's lifetime without holding the turn
|
|
113
|
+
* hostage for hours.
|
|
114
|
+
*/
|
|
115
|
+
export const DEFAULT_FOREGROUND_WINDOW_SECONDS = 60;
|
|
116
|
+
|
|
117
|
+
export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SECONDS";
|
|
118
|
+
|
|
101
119
|
// OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
|
|
102
120
|
// defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
|
|
103
121
|
export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
@@ -108,6 +126,7 @@ export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
|
108
126
|
jl: false,
|
|
109
127
|
},
|
|
110
128
|
cellTimeoutSeconds: 30,
|
|
129
|
+
foregroundWindowSeconds: DEFAULT_FOREGROUND_WINDOW_SECONDS,
|
|
111
130
|
hardLimitSeconds: DEFAULT_HARD_LIMIT_SECONDS,
|
|
112
131
|
parallelPoolWidth: 4,
|
|
113
132
|
taskTools: {
|
|
@@ -166,6 +185,15 @@ export function resolveHardLimitSeconds(settings: CodemodeSettings, env: Environ
|
|
|
166
185
|
return parsed;
|
|
167
186
|
}
|
|
168
187
|
|
|
188
|
+
/** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
|
|
189
|
+
export function resolveForegroundWindowSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
|
|
190
|
+
const override = env[FOREGROUND_WINDOW_ENVIRONMENT_FLAG];
|
|
191
|
+
if (override === undefined) return settings.foregroundWindowSeconds;
|
|
192
|
+
const parsed = Number.parseInt(override, 10);
|
|
193
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return settings.foregroundWindowSeconds;
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
|
|
169
197
|
async function loadSettingsFile(path: string): Promise<LoadedCodemodeSettings> {
|
|
170
198
|
const raw = await readFile(path, "utf8");
|
|
171
199
|
let parsed: unknown;
|
|
@@ -200,6 +228,7 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
|
|
|
200
228
|
jl: input.languages?.jl ?? defaultCodemodeSettings.languages.jl,
|
|
201
229
|
},
|
|
202
230
|
cellTimeoutSeconds: input.cellTimeoutSeconds ?? defaultCodemodeSettings.cellTimeoutSeconds,
|
|
231
|
+
foregroundWindowSeconds: input.foregroundWindowSeconds ?? defaultCodemodeSettings.foregroundWindowSeconds,
|
|
203
232
|
hardLimitSeconds: input.hardLimitSeconds ?? defaultCodemodeSettings.hardLimitSeconds,
|
|
204
233
|
parallelPoolWidth: input.parallelPoolWidth ?? defaultCodemodeSettings.parallelPoolWidth,
|
|
205
234
|
taskTools: {
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
|
3
3
|
import type { AgentExecuteTool } from "./bridges/agent-bridge.ts";
|
|
4
4
|
import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
5
5
|
import { type CompletionRequest, type CompletionResult, createCompletionHandler } from "./completion/handler.ts";
|
|
6
|
-
import { defaultCodemodeSettings, resolveHardLimitSeconds } from "./config/settings.ts";
|
|
6
|
+
import { defaultCodemodeSettings, resolveForegroundWindowSeconds, resolveHardLimitSeconds } from "./config/settings.ts";
|
|
7
7
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
8
8
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
9
9
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
@@ -64,6 +64,15 @@ export interface SenpiCodemodeOptions {
|
|
|
64
64
|
readonly now?: () => number;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Whether the session registry holds `monitor`; false when the runtime cannot be read yet. */
|
|
68
|
+
function monitorIsRegistered(pi: CodemodeExtensionAPI): boolean {
|
|
69
|
+
try {
|
|
70
|
+
return pi.getAllTools().some((tool) => tool.name === "monitor");
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCodemodeOptions = {}): void {
|
|
68
77
|
const manager = new SessionManagerProxy();
|
|
69
78
|
const complete = options.complete ?? ((request, ctx) => createCompletionHandler()(ctx)(request));
|
|
@@ -108,11 +117,17 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
108
117
|
pi.rpc?.emit(EVAL_EXECUTION_EVENT, toEvalExecutionRpcPayload(payload));
|
|
109
118
|
pi.events?.emit(EVAL_EXECUTION_EVENT, payload);
|
|
110
119
|
};
|
|
120
|
+
// `listTools` below survives because it is lazy; this read is eager, and the loader's
|
|
121
|
+
// action methods throw while extensions are still loading (the bundled codemode path
|
|
122
|
+
// reaches this before the runtime is bound). An unreadable registry means "do not teach
|
|
123
|
+
// a tool we cannot confirm"; session_start / model_select re-register once it is live.
|
|
124
|
+
const monitor = monitorIsRegistered(pi);
|
|
111
125
|
pi.registerTool(
|
|
112
126
|
createEvalTool({
|
|
113
127
|
enabledLanguages: runtime.enabledLanguages,
|
|
114
128
|
kernelManager: manager,
|
|
115
129
|
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
130
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(runtime.settings),
|
|
116
131
|
executeTool: runtime.executeTool,
|
|
117
132
|
listTools: () => pi.getAllTools(),
|
|
118
133
|
complete,
|
|
@@ -122,6 +137,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
122
137
|
executionTracker: manager,
|
|
123
138
|
onCellSettled,
|
|
124
139
|
renderers,
|
|
140
|
+
monitor,
|
|
125
141
|
spawns: runtime.spawns,
|
|
126
142
|
spawnDefaultAgent: runtime.settings.taskTools.task,
|
|
127
143
|
hostLine: hostLine(),
|
|
@@ -146,6 +162,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
146
162
|
enabledLanguages: { py: true, js: true, rb: true, jl: true },
|
|
147
163
|
kernelManager: manager,
|
|
148
164
|
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
165
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(defaultCodemodeSettings),
|
|
149
166
|
executeTool: createExecuteTool(pi),
|
|
150
167
|
listTools: () => pi.getAllTools(),
|
|
151
168
|
complete,
|
|
@@ -159,6 +176,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
159
176
|
}),
|
|
160
177
|
executionTracker: manager,
|
|
161
178
|
renderers,
|
|
179
|
+
// The baseline tool is registered before extensions such as monitor load.
|
|
180
|
+
monitor: false,
|
|
162
181
|
hostLine: hostLine(),
|
|
163
182
|
runtimes: { js: jsRuntimeInfo() },
|
|
164
183
|
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
4
4
|
import { inspect } from "node:util";
|
|
5
5
|
import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect-eval.js";
|
|
6
|
+
import { installShellCapture } from "./worker-shell-capture.js";
|
|
6
7
|
|
|
7
8
|
const PREPARED_CELL_PREFIX = "/*senpi:prepared-cell*/";
|
|
8
9
|
const INTERNAL_URL = /^([a-z][a-z0-9+.-]*):\/\/(.*)$/iu;
|
|
@@ -84,11 +85,16 @@ export class JsWorkerRuntime {
|
|
|
84
85
|
process.stderr.write = routeWrite(process.stderr, originalStderrWrite, "stderr");
|
|
85
86
|
console.log = (...values) => this.#emitText("stdout", `${values.map(formatValue).join(" ")}\n`);
|
|
86
87
|
console.error = (...values) => this.#emitText("stderr", `${values.map(formatValue).join(" ")}\n`);
|
|
88
|
+
const restoreShellCapture = installShellCapture({
|
|
89
|
+
isActive: () => this.#hooks !== null,
|
|
90
|
+
emitText: (stream, data) => this.#emitText(stream, data),
|
|
91
|
+
});
|
|
87
92
|
globalThis.__senpi_restore_console__ = () => {
|
|
88
93
|
console.log = originalLog;
|
|
89
94
|
console.error = originalError;
|
|
90
95
|
process.stdout.write = originalStdoutWrite;
|
|
91
96
|
process.stderr.write = originalStderrWrite;
|
|
97
|
+
restoreShellCapture();
|
|
92
98
|
};
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ShellCaptureStream = "stdout" | "stderr";
|
|
2
|
+
|
|
3
|
+
export type ShellCaptureRestore = () => void;
|
|
4
|
+
|
|
5
|
+
export interface ShellCaptureOptions {
|
|
6
|
+
readonly isActive: () => boolean;
|
|
7
|
+
readonly emitText: (stream: ShellCaptureStream, data: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function installShellCapture(options: ShellCaptureOptions): ShellCaptureRestore;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const SHELL_CONFIG_METHODS = ["env", "cwd", "nothrow", "throws"];
|
|
2
|
+
const SHELL_READ_METHODS = ["text", "json", "lines", "arrayBuffer", "bytes", "blob"];
|
|
3
|
+
|
|
4
|
+
export function installShellCapture(options) {
|
|
5
|
+
const bun = globalThis.Bun;
|
|
6
|
+
if (!isBunRuntime(bun)) return () => {};
|
|
7
|
+
const originalShell = bun.$;
|
|
8
|
+
const originalSpawn = bun.spawn;
|
|
9
|
+
bun.$ = capturedShell(originalShell, options);
|
|
10
|
+
bun.spawn = capturedSpawn(originalSpawn, options);
|
|
11
|
+
return () => {
|
|
12
|
+
bun.$ = originalShell;
|
|
13
|
+
bun.spawn = originalSpawn;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isBunRuntime(bun) {
|
|
18
|
+
return bun !== null && typeof bun === "object" && typeof bun.$ === "function" && typeof bun.spawn === "function";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function capturedShell(originalShell, options) {
|
|
22
|
+
const shell = (strings, ...expressions) => {
|
|
23
|
+
const promise = originalShell(strings, ...expressions);
|
|
24
|
+
return options.isActive() ? captureShellPromise(promise, options.emitText) : promise;
|
|
25
|
+
};
|
|
26
|
+
for (const key of Object.keys(originalShell)) shell[key] = originalShell[key];
|
|
27
|
+
for (const method of SHELL_CONFIG_METHODS) {
|
|
28
|
+
shell[method] = (...args) => {
|
|
29
|
+
originalShell[method](...args);
|
|
30
|
+
return shell;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return shell;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function captureShellPromise(promise, emitText) {
|
|
37
|
+
const prototype = Object.getPrototypeOf(promise);
|
|
38
|
+
let echo = true;
|
|
39
|
+
const echoOnce = (output) => {
|
|
40
|
+
if (!echo) return;
|
|
41
|
+
echo = false;
|
|
42
|
+
emitShellOutput(output, emitText);
|
|
43
|
+
};
|
|
44
|
+
prototype.quiet.call(promise);
|
|
45
|
+
promise.quiet = function quiet() {
|
|
46
|
+
echo = false;
|
|
47
|
+
return prototype.quiet.call(this);
|
|
48
|
+
};
|
|
49
|
+
for (const method of SHELL_READ_METHODS) {
|
|
50
|
+
if (typeof prototype[method] !== "function") continue;
|
|
51
|
+
promise[method] = function read(...args) {
|
|
52
|
+
echo = false;
|
|
53
|
+
return prototype[method].apply(this, args);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
promise.then = function then(onFulfilled, onRejected) {
|
|
57
|
+
return prototype.then.call(
|
|
58
|
+
this,
|
|
59
|
+
(output) => {
|
|
60
|
+
echoOnce(output);
|
|
61
|
+
return onFulfilled ? onFulfilled(output) : output;
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
echoOnce(error);
|
|
65
|
+
if (onRejected) return onRejected(error);
|
|
66
|
+
throw error;
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
return promise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emitShellOutput(output, emitText) {
|
|
74
|
+
if (output === null || typeof output !== "object") return;
|
|
75
|
+
const stdout = outputText(output.stdout);
|
|
76
|
+
if (stdout) emitText("stdout", stdout);
|
|
77
|
+
const stderr = outputText(output.stderr);
|
|
78
|
+
if (stderr) emitText("stderr", stderr);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function outputText(value) {
|
|
82
|
+
if (value instanceof Uint8Array) return new TextDecoder().decode(value);
|
|
83
|
+
return typeof value === "string" ? value : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function capturedSpawn(originalSpawn, options) {
|
|
87
|
+
return (...args) => {
|
|
88
|
+
if (!options.isActive()) return originalSpawn(...args);
|
|
89
|
+
const [first, second] = args;
|
|
90
|
+
if (Array.isArray(first)) {
|
|
91
|
+
const spawnOptions = second === undefined ? {} : second;
|
|
92
|
+
if (!needsStderrCapture(spawnOptions)) return originalSpawn(...args);
|
|
93
|
+
return drainStderr(originalSpawn(first, { ...spawnOptions, stderr: "pipe" }), options.emitText);
|
|
94
|
+
}
|
|
95
|
+
if (!needsStderrCapture(first)) return originalSpawn(...args);
|
|
96
|
+
return drainStderr(originalSpawn({ ...first, stderr: "pipe" }), options.emitText);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function needsStderrCapture(spawnOptions) {
|
|
101
|
+
return (
|
|
102
|
+
spawnOptions !== null &&
|
|
103
|
+
typeof spawnOptions === "object" &&
|
|
104
|
+
spawnOptions.stdio === undefined &&
|
|
105
|
+
spawnOptions.stderr === undefined
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function drainStderr(child, emitText) {
|
|
110
|
+
const stream = child?.stderr;
|
|
111
|
+
if (!(stream instanceof ReadableStream)) return child;
|
|
112
|
+
void readStream(stream, emitText).catch((error) => {
|
|
113
|
+
emitText("stderr", `[spawn stderr capture failed: ${String(error)}]\n`);
|
|
114
|
+
});
|
|
115
|
+
return child;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readStream(stream, emitText) {
|
|
119
|
+
const decoder = new TextDecoder();
|
|
120
|
+
for await (const chunk of stream) {
|
|
121
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
122
|
+
if (text) emitText("stderr", text);
|
|
123
|
+
}
|
|
124
|
+
const tail = decoder.decode();
|
|
125
|
+
if (tail) emitText("stderr", tail);
|
|
126
|
+
}
|
|
@@ -15,6 +15,8 @@ export interface EvalPromptParts {
|
|
|
15
15
|
|
|
16
16
|
export interface EvalPromptOptions {
|
|
17
17
|
readonly spawns: boolean;
|
|
18
|
+
/** Whether the session registry exposes the monitor tool through eval. */
|
|
19
|
+
readonly monitor?: boolean;
|
|
18
20
|
readonly spawnDefaultAgent?: string;
|
|
19
21
|
/** Active model id; selects the emphasis dialect of the batching guidance. */
|
|
20
22
|
readonly modelId?: string;
|
|
@@ -62,86 +64,48 @@ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyl
|
|
|
62
64
|
|
|
63
65
|
type ContextValue = string | boolean;
|
|
64
66
|
type Context = Readonly<Record<string, ContextValue>>;
|
|
65
|
-
type EvalPromptExample = {
|
|
66
|
-
readonly caption: string;
|
|
67
|
-
readonly language: keyof EnabledLanguages;
|
|
68
|
-
readonly summary: string;
|
|
69
|
-
readonly code: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// senpi ToolDefinition has no examples field, so description embeds the examples.
|
|
73
|
-
// ADAPTATION: payloads diverge from omp's json-config chain to teach batch read,
|
|
74
|
-
// comprehension filtering, and parallel tool.<name> fan-out while keeping the
|
|
75
|
-
// three-cell reuse narrative.
|
|
76
|
-
const REUSE_CHAIN_EXAMPLES = [
|
|
77
|
-
{
|
|
78
|
-
caption: "First call — set up once",
|
|
79
|
-
language: "js",
|
|
80
|
-
summary: "Count all TypeScript source files under src/ excluding tests",
|
|
81
|
-
code: "import { readdir } from 'node:fs/promises'\nimport { extname } from 'node:path'\nconst files = (await readdir('src', { recursive: true })).filter(f => extname(f) === '.ts' && !f.includes('test'))\nprint(files.length)",
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
caption: "Second call — reuse `files`, fan out session tools in parallel",
|
|
85
|
-
language: "js",
|
|
86
|
-
summary: "Grep legacyClient per directory in one cell",
|
|
87
|
-
code: "const dirs = [...new Set(files.map(f => f.split('/')[0]))]\nconst hits = await Promise.all(dirs.map(d => tool.grep({ pattern: 'legacyClient', path: d })))\ndisplay(hits.map(h => h.matches?.length ?? 0))",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
caption: "JS kernel is busy with a detached cell — continue in py",
|
|
91
|
-
language: "py",
|
|
92
|
-
summary: "Aggregate legacyClient hits while JS is busy",
|
|
93
|
-
code: "from pathlib import Path\nprint(sum('legacyClient' in read(p) for p in Path('src').rglob('*.ts')))",
|
|
94
|
-
},
|
|
95
|
-
] as const satisfies readonly EvalPromptExample[];
|
|
96
|
-
|
|
97
67
|
const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
98
68
|
|
|
99
69
|
<instruction>
|
|
100
|
-
**One eval call = one cell = one logical step.**
|
|
101
|
-
|
|
102
|
-
Work incrementally: imports in one call, define in the next, test, then use — each its own eval call. Re-run setup ONLY after \`reset\`, a kernel crash, or a \`NameError\`/\`ReferenceError\` proving the state is gone.
|
|
70
|
+
**One eval call = one cell = one logical step.** Top-level names persist per language across eval calls{{#if spawns}}, tool calls and \`task\` subagents{{else}} and tool calls{{/if}}: define helpers and clients once and reuse them instead of re-importing or re-reading. Rebuild state only after \`reset\`, a kernel restart, or a \`NameError\`/\`ReferenceError\`, and check a sentinel variable first so a re-run cannot duplicate side effects.
|
|
103
71
|
|
|
104
72
|
{{#if styleClaude}}<eval_first_batching>
|
|
105
73
|
\`eval\` is your default execution surface: if a step needs more than one tool call, write ONE cell that performs the whole step — never issue the calls one at a time.
|
|
106
74
|
- Enumerate every lookup the step needs, then run all independent ones simultaneously with \`parallel(thunks)\` inside the cell; keep calls sequential only when one result feeds the next.
|
|
107
75
|
- Write real code around the calls: loop or comprehend over file sets with \`read()\`/stdlib, branch per case, and wrap risky calls in try/except so one failure degrades only its item — recover or retry inside the cell, keep the batch alive.
|
|
108
|
-
- Post-process \`tool.<name>()\` results programmatically and return distilled facts, not raw dumps.
|
|
76
|
+
- Post-process \`tool.<name>()\` results programmatically — filter, join, aggregate — and return distilled facts, not raw dumps.
|
|
77
|
+
{{#if monitor}}- Start long-running work (build, test run, deploy, or watch) through \`tool.monitor({ command, filter })\`, putting the decisive-line filter inside the same cell, then keep working until its event wakes the turn.{{/if}}
|
|
109
78
|
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
110
79
|
GPT eval: compose multi-tool work inside one cell with \`tool.<name>(args)\` and \`parallel(thunks)\`; do not split a planned step into serial tool calls.
|
|
111
|
-
- Long
|
|
112
|
-
-
|
|
80
|
+
- Long cells detach on timeout and notify on completion; do not poll or re-run them.
|
|
81
|
+
- Filter, join, and aggregate tool results in the cell; return only decision-relevant facts.
|
|
82
|
+
{{#if monitor}}- For long-running build, test run, deploy, or watch work, start \`tool.monitor({ command, filter })\` with the decisive-line filter in the same cell; keep working until its event wakes the turn.{{/if}}
|
|
113
83
|
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route multi-call steps through eval: one cell per step, independent lookups dispatched together via \`parallel(thunks)\`; keep work sequential only when one result determines the next action.
|
|
114
|
-
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically.
|
|
84
|
+
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically — filter, join, aggregate.
|
|
115
85
|
- Wrap failable calls in try/except inside the cell; a failed item degrades only itself. After two distinct failed strategies for the same fact, fall back to direct tool calls.
|
|
116
|
-
- Reduce large results in-kernel to the facts the task needs before returning.
|
|
86
|
+
- Reduce large results in-kernel to the facts the task needs before returning.
|
|
87
|
+
{{#if monitor}}- Long-running build/test/deploy/watch work: start \`tool.monitor({ command, filter })\` with the decisive-line filter inside the same cell, then continue working until its event wakes the turn.{{/if}}{{/if}}{{#if styleKimi}}**EVAL IS YOUR SUPERPOWER — MAKE IT YOUR DEFAULT WAY TO ACT.** Before any step, think: "how do I execute this WHOLE step in ONE parallelized cell?" — then write that ONE cell.
|
|
117
88
|
- **BATCH EVERYTHING AT ONCE:** enumerate EVERY independent lookup the step needs and dispatch them ALL simultaneously with \`parallel(thunks)\` in that cell; keep calls sequential only when one result feeds the next.
|
|
118
89
|
- **WRITE REAL CODE, NOT CALL CHAINS:** loop or comprehend over file sets with \`read()\`/stdlib, post-process \`tool.<name>()\` results programmatically, and put try/except around each risky call so the rest of the batch completes.
|
|
119
|
-
- **DISTILL IN-KERNEL:** filter and aggregate results in code, then return ONLY the distilled facts.
|
|
90
|
+
- **DISTILL IN-KERNEL:** filter, join, and aggregate \`tool.<name>()\` results in code, then return ONLY the distilled facts.
|
|
91
|
+
{{#if monitor}}- **DO start long-running build, test run, deploy, or watch work with \`tool.monitor({ command, filter })\`, put the decisive-line filter INSIDE THE SAME CELL, and KEEP WORKING until its event wakes the turn.**{{/if}}{{/if}}{{#if styleDefault}}**EVAL IS YOUR PRIMARY EXECUTION SURFACE.** Any step that needs MORE THAN ONE tool call MUST be written as ONE cell — NEVER as a chain of single tool calls.
|
|
120
92
|
- **PLAN THE WHOLE STEP, THEN BATCH IT.** Enumerate every read/search/lookup the step needs and dispatch ALL independent ones through \`parallel(thunks)\` in one cell.
|
|
121
93
|
- **WRITE REAL CODE, NOT CALL LISTS.** Loop or comprehend over file sets with \`read()\`/stdlib, branch \`if\`/\`else\` per case, post-process \`tool.<name>()\` results programmatically, and wrap EVERY risky call in try/except so ONE failure NEVER kills the batch.
|
|
122
|
-
- **DISTILL IN-KERNEL.** Filter, diff, and aggregate in code before returning; return facts, NOT dumps.
|
|
94
|
+
- **DISTILL IN-KERNEL.** Filter, join, diff, and aggregate in code before returning; return facts, NOT dumps.
|
|
95
|
+
{{#if monitor}}- **LONG-RUNNING build, test run, deploy, or watch work MUST start with \`tool.monitor({ command, filter })\`, with the decisive-line filter INSIDE THE SAME CELL; KEEP WORKING until its event wakes the turn.**{{/if}}{{/if}}
|
|
123
96
|
{{#if hostLine}}
|
|
124
97
|
Host: {{hostLine}} — cells execute here. Size \`parallel(thunks)\` pools to its cores; \`tool.<name>()\` shell commands must fit this platform, even when the code you are writing targets another machine.
|
|
125
98
|
{{/if}}
|
|
126
99
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- \`language\` — {{#if py}}\`"py"\` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}\`"js"\` persistent JavaScript VM{{/if}}{{#if rb}}{{#ifAny py js}}, {{/ifAny}}\`"rb"\` persistent Ruby kernel{{/if}}{{#if jl}}{{#ifAny py js rb}}, {{/ifAny}}\`"jl"\` persistent Julia kernel{{/if}}.
|
|
130
|
-
- \`code\` — cell body, verbatim. Newlines/quotes JSON-encoded; no fences, no headers.
|
|
131
|
-
- \`summary\` (REQUIRED for run) — ONE line in the USER'S conversational language stating WHAT this cell does and FOR WHAT PURPOSE (e.g. Korean conversation -> "src 전체에서 legacyClient 사용처 집계"); shown in the TUI while the cell runs; >80 chars is force-truncated.
|
|
132
|
-
- \`timeout\` (optional) — seconds. Raise only for heavy compute or long{{#if spawns}} non-agent{{/if}} tool calls.
|
|
133
|
-
- \`on_timeout\` (optional) — \`"detach"\` keeps pure computation running in interactive sessions (the default); \`"error"\` interrupts for deadline-sensitive work and is the print/json default.
|
|
134
|
-
- Every cell is killed at a wall-clock hard limit (default 1800s) that survives detach and is never paused by tool calls; a larger explicit \`timeout\` raises it, and a killed cell notifies you that it hit the limit.
|
|
135
|
-
- \`reset\` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a \`py\` reset never touches the JS VM.{{/ifAll}}
|
|
136
|
-
- \`action\` (optional) — defaults to \`"run"\`. A detached cell returns its id: use \`eval({ action: "peek", cell_id })\` for buffered output/state or \`eval({ action: "stop", cell_id })\` to cancel it.
|
|
100
|
+
\`language\`: {{#if py}}\`"py"\` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}\`"js"\` persistent JavaScript VM{{/if}}{{#if rb}}{{#ifAny py js}}, {{/ifAny}}\`"rb"\` persistent Ruby kernel{{/if}}{{#if jl}}{{#ifAny py js rb}}, {{/ifAny}}\`"jl"\` persistent Julia kernel{{/if}}.
|
|
137
101
|
|
|
138
|
-
A
|
|
102
|
+
A cell that outlives the foreground window detaches: it keeps its language kernel busy (another language can continue) and completes as one notification with its value or error and buffered output. Do not re-run a detached cell; read or cancel it with \`eval({ action: "peek", cell_id })\` / \`eval({ action: "stop", cell_id })\`.
|
|
139
103
|
|
|
140
|
-
{{#if py}}
|
|
141
|
-
{{#if js}}{{#if jsBun}}JS runs in-process on Bun {{jsVersion}}: top-level \`await\`/\`return\` work; \`Bun.*\` builtins available.{{#if bunSkillPath}} MUST READ the bun-1-4 skill at {{bunSkillPath}} before your first js cell — its builtins replace the npm packages you would otherwise install.{{/if}}{{else}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}{{/if}}
|
|
104
|
+
{{#if py}}Python runs on a live event loop: use top-level \`await\`; \`asyncio.run(…)\` raises.{{/if}}
|
|
105
|
+
{{#if js}}{{#if jsBun}}JS runs in-process on Bun {{jsVersion}}: top-level \`await\`/\`return\` work; \`Bun.*\` builtins available, including \`new Bun.WebView()\` — a headless browser (navigate/click/evaluate/screenshot) to reach for before \`curl\` or a browser CLI when a page needs JS, a login, or a screenshot.{{#if bunSkillPath}} MUST READ the bun-1-4 skill at {{bunSkillPath}} before your first js cell — its builtins replace the npm packages you would otherwise install.{{/if}}{{else}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}{{/if}}
|
|
142
106
|
{{#if rb}}Ruby: synchronous; helper options are keyword args{{#if spawns}} (e.g. \`output("id", limit: 2)\`){{/if}}; the last expression auto-displays unless it is \`nil\`, an assignment, or a definition (like IRB).{{/if}}
|
|
143
107
|
{{#if jl}}Julia: synchronous; helper options are standard keyword args{{#if spawns}} (e.g. \`output("id", limit=2)\`){{/if}}; the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
|
144
|
-
On error, fix and re-run only the failing step
|
|
108
|
+
On error, fix and re-run only the failing step; a normal error keeps state, while a timeout or stop message says whether the kernel restarted.
|
|
145
109
|
</instruction>
|
|
146
110
|
|
|
147
111
|
<prelude>
|
|
@@ -156,25 +120,21 @@ read(path, offset?=1, limit?=None) → str
|
|
|
156
120
|
write(path, content) → str
|
|
157
121
|
Write file (creates parents) → resolved path. \`local://…\` persists across turns/subagents.
|
|
158
122
|
env(key?=None, value?=None) → str | None | dict
|
|
159
|
-
No args → full env dict; one → value
|
|
123
|
+
No args → full env dict; one → value; two → set \`key=value\`.
|
|
160
124
|
{{#if spawns}}output(*ids, format?="raw", offset?=None, limit?=None) → str | dict | list[dict]
|
|
161
|
-
Task/agent output by id. Reads immediately: running tasks return their status; \`format\`
|
|
125
|
+
Task/agent output by id. Reads immediately: running tasks return their status; \`format\` \`"raw"\` = full, \`"tail"\` = trailing.
|
|
162
126
|
{{/if}}tool.<name>(args) → unknown
|
|
163
127
|
Invoke any session tool; \`args\` = its parameter object.
|
|
164
128
|
tool_schema(name?) → dict
|
|
165
|
-
Parameter schema of a tool
|
|
166
|
-
Use it before calling a tool you have not called before — a failed call also
|
|
167
|
-
returns the expected parameters, so fix the args and retry in the next cell
|
|
168
|
-
instead of abandoning eval.
|
|
129
|
+
Parameter schema of a tool (omit \`name\` to list tool names); a failed \`tool.<name>()\` call also returns the expected parameters.
|
|
169
130
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
170
|
-
Oneshot, stateless
|
|
131
|
+
Oneshot, stateless. \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → parsed structured output.
|
|
171
132
|
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
172
|
-
Run a subagent → final output. \`agent\` picks
|
|
173
|
-
{{
|
|
174
|
-
|
|
175
|
-
Thunks through a bounded pool (wide as a \`task\` batch — don't pre-shrink), input order kept; returns when all finish, a throwing thunk propagates.
|
|
133
|
+
Run a subagent → final output. \`agent\` picks a discovered agent. \`schema\` as in completion(). \`handle\` → workflow node { text, output, handle: \`agent://<id>\`, id, agent } (parsed under \`data\` with \`schema\`).
|
|
134
|
+
{{/if}}parallel(thunks) → list
|
|
135
|
+
Thunks through a bounded pool (as wide as a \`task\` batch), input order kept; a throwing thunk propagates.
|
|
176
136
|
pipeline(items, ...stages) → list
|
|
177
|
-
Map items through one-arg stages
|
|
137
|
+
Map items through one-arg stages with a barrier between stages; each stage receives the previous stage's result.
|
|
178
138
|
log(message) → None
|
|
179
139
|
Progress line above the status tree.
|
|
180
140
|
phase(title) → None
|
|
@@ -183,18 +143,10 @@ phase(title) → None
|
|
|
183
143
|
</prelude>
|
|
184
144
|
{{#if spawns}}
|
|
185
145
|
<workflow>
|
|
186
|
-
|
|
187
|
-
- **Name nodes.** Capture each \`agent(…, {{#if py}}handle=True{{/if}}{{#if js}}{ handle: true }{{/if}}{{#if jl}}handle=true{{/if}})\` result; carries \`handle\` (\`agent://<id>\`) + \`output\`.
|
|
188
|
-
- **Wire edges by reference.** Put an upstream node's \`handle\`/\`output\` in the dependent stage's prompt — large transcript never re-inlined. Bulk: \`write("local://<name>.md", …)\`, pass the URI.
|
|
189
|
-
- **\`pipeline(items, *stages)\` = staged waves**, barrier between stages (every item clears stage N before any enters N+1). **\`parallel(thunks)\` = one wave** of independent nodes.
|
|
190
|
-
- **Isolate failure.** A raising node re-raises the lowest-index error, aborts its wave; wrap risky nodes in try/except so a failure degrades only its dependent subtree, independent branches finish.
|
|
191
|
-
- **Acyclic only.** A node never waits on its own descendant.
|
|
146
|
+
Multi-agent work is an acyclic graph in code: one \`agent(…)\` node per step with its handle option ({{#if py}}\`handle=True\`{{/if}}{{#ifAll py js}} / {{/ifAll}}{{#if js}}\`{ handle: true }\`{{/if}}{{#if jl}}{{#ifAny py js}} / {{/ifAny}}\`handle=true\`{{/if}}), \`parallel(thunks)\` for independent nodes, \`pipeline(items, *stages)\` for staged waves. Pass an upstream node's \`handle\` or \`output\` (or a \`write("local://…")\` URI for bulk text) into dependents instead of re-inlining transcripts, and wrap risky nodes in try/except so a failure aborts only its subtree.
|
|
192
147
|
</workflow>
|
|
193
148
|
{{/if}}
|
|
194
|
-
|
|
195
|
-
<critical>
|
|
196
|
-
Prior top-level names (\`data\`, \`sessions\`, helpers, imports) survive into the next eval call — reuse them; NEVER re-import, re-require, or re-declare a helper. Re-read a file only if it may have changed since the last read.
|
|
197
|
-
</critical>`;
|
|
149
|
+
`;
|
|
198
150
|
|
|
199
151
|
export function buildEvalPrompt(
|
|
200
152
|
enabled: EnabledLanguages,
|
|
@@ -211,6 +163,7 @@ export function buildEvalPrompt(
|
|
|
211
163
|
rb: enabled.rb,
|
|
212
164
|
jl: enabled.jl,
|
|
213
165
|
spawns: options.spawns,
|
|
166
|
+
monitor: options.monitor === true,
|
|
214
167
|
spawnDefaultAgent,
|
|
215
168
|
styleClaude: style === "claude",
|
|
216
169
|
styleCodex: style === "codex",
|
|
@@ -222,20 +175,9 @@ export function buildEvalPrompt(
|
|
|
222
175
|
jsVersion: options.jsRuntime?.version ?? "",
|
|
223
176
|
bunSkillPath: options.bunSkillPath ?? "",
|
|
224
177
|
};
|
|
225
|
-
const
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
return `### ${example.caption}\n\`\`\`json\n${JSON.stringify(call, null, 2)}\n\`\`\``;
|
|
229
|
-
})
|
|
230
|
-
.join("\n\n");
|
|
231
|
-
const description = [
|
|
232
|
-
renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
233
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
234
|
-
.trim(),
|
|
235
|
-
examples === "" ? "" : `<examples>\n${examples}\n</examples>`,
|
|
236
|
-
]
|
|
237
|
-
.filter((part) => part !== "")
|
|
238
|
-
.join("\n\n");
|
|
178
|
+
const description = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
179
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
180
|
+
.trim();
|
|
239
181
|
return {
|
|
240
182
|
description,
|
|
241
183
|
promptSnippet: "Run one incremental code cell in a persistent language kernel.",
|
|
@@ -17,6 +17,12 @@ export interface CellExecutionOptions {
|
|
|
17
17
|
readonly callerSignal: AbortSignal;
|
|
18
18
|
readonly cellId: string;
|
|
19
19
|
readonly timeoutMs: number;
|
|
20
|
+
/**
|
|
21
|
+
* Caps how long a host-bridge pause may suspend the idle watchdog. When the cell will detach on
|
|
22
|
+
* timeout this is set to the foreground window so a bridge-parked cell still frees the turn at the
|
|
23
|
+
* window; left undefined (error mode) it keeps the idle-timeout default grace.
|
|
24
|
+
*/
|
|
25
|
+
readonly maxPauseGraceMs?: number;
|
|
20
26
|
readonly timeoutFactory: EvalTimeoutFactory;
|
|
21
27
|
readonly onTimeout: (error: Error) => void;
|
|
22
28
|
readonly onAbort: (error: Error) => void;
|
|
@@ -46,6 +52,7 @@ export class CellExecution {
|
|
|
46
52
|
this.#watchdog = options.timeoutFactory.create({
|
|
47
53
|
cellId: options.cellId,
|
|
48
54
|
timeoutMs: options.timeoutMs,
|
|
55
|
+
...(options.maxPauseGraceMs === undefined ? {} : { maxPauseGraceMs: options.maxPauseGraceMs }),
|
|
49
56
|
onTimeout: ({ error }) => options.onTimeout(error),
|
|
50
57
|
});
|
|
51
58
|
this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, {
|
|
@@ -21,6 +21,12 @@ export interface CreateEvalToolOptions {
|
|
|
21
21
|
readonly enabledLanguages: EnabledEvalLanguages;
|
|
22
22
|
readonly kernelManager: EvalKernelManager;
|
|
23
23
|
readonly cellTimeoutSeconds: number;
|
|
24
|
+
/**
|
|
25
|
+
* Longest an interactive (detach-behavior) call blocks the agent loop before the cell detaches,
|
|
26
|
+
* capping the `timeout` detach budget. Defaults to {@link DEFAULT_FOREGROUND_WINDOW_SECONDS}.
|
|
27
|
+
* Does not affect `on_timeout: "error"` calls or the wall-clock hard limit.
|
|
28
|
+
*/
|
|
29
|
+
readonly foregroundWindowSeconds?: number;
|
|
24
30
|
/** Wall-clock kill deadline applied to every cell; only used when this factory creates its own manager. */
|
|
25
31
|
readonly hardLimitSeconds?: number;
|
|
26
32
|
readonly executeTool: ExecuteTool;
|
|
@@ -36,6 +42,8 @@ export interface CreateEvalToolOptions {
|
|
|
36
42
|
readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
|
|
37
43
|
readonly renderers?: Pick<ToolDefinition<EvalInputSchema, EvalToolDetails>, "renderCall" | "renderResult">;
|
|
38
44
|
readonly spawns?: boolean;
|
|
45
|
+
/** Whether the session registry exposes the monitor tool through eval. */
|
|
46
|
+
readonly monitor?: boolean;
|
|
39
47
|
readonly spawnDefaultAgent?: string;
|
|
40
48
|
readonly modelId?: string;
|
|
41
49
|
readonly hostLine?: string;
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { AgentToolResult, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
|
|
4
|
-
import { defaultCodemodeSettings } from "../config/settings.ts";
|
|
4
|
+
import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
|
|
5
5
|
import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
|
|
6
6
|
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
7
7
|
import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
|
|
@@ -28,6 +28,7 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
28
28
|
const parameters = createEvalInputSchema(options.enabledLanguages);
|
|
29
29
|
const prompt = buildEvalPrompt(options.enabledLanguages, {
|
|
30
30
|
spawns: options.spawns ?? false,
|
|
31
|
+
monitor: options.monitor,
|
|
31
32
|
...(options.spawnDefaultAgent === undefined ? {} : { spawnDefaultAgent: options.spawnDefaultAgent }),
|
|
32
33
|
...(options.modelId === undefined ? {} : { modelId: options.modelId }),
|
|
33
34
|
...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
|
|
@@ -100,8 +101,19 @@ async function runEvalCell(
|
|
|
100
101
|
invocation: EvalCellInvocation,
|
|
101
102
|
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
102
103
|
if (invocation.signal.aborted) throw abortError(invocation.signal.reason);
|
|
103
|
-
const timeoutMs = Math.floor((invocation.input.timeout ?? options.cellTimeoutSeconds) * 1_000);
|
|
104
104
|
const timeoutBehavior = evalTimeoutBehavior(invocation.input, invocation.ctx);
|
|
105
|
+
const requestedTimeoutMs = Math.floor((invocation.input.timeout ?? options.cellTimeoutSeconds) * 1_000);
|
|
106
|
+
// The `timeout` (and its `cellTimeoutSeconds` default) is the detach budget for interactive calls.
|
|
107
|
+
// Cap it at the foreground window so a large `timeout` — whose real purpose is to raise the
|
|
108
|
+
// wall-clock hard limit (see EvalDetachedCellManager) — frees the turn at the window instead of
|
|
109
|
+
// blocking the agent loop for its full duration. `on_timeout: "error"` (and print/json) keep the
|
|
110
|
+
// unclamped deadline, since there the cell is killed rather than detached.
|
|
111
|
+
const foregroundWindowMs = (options.foregroundWindowSeconds ?? DEFAULT_FOREGROUND_WINDOW_SECONDS) * 1_000;
|
|
112
|
+
const timeoutMs =
|
|
113
|
+
timeoutBehavior === "detach" ? Math.min(requestedTimeoutMs, foregroundWindowMs) : requestedTimeoutMs;
|
|
114
|
+
// A cell that pauses its watchdog for a host bridge call would otherwise wait the full pause grace
|
|
115
|
+
// (~10 min) before detaching; cap the grace at the foreground window too so the detach guarantee
|
|
116
|
+
// holds for bridge-parked cells. Error mode keeps the default grace (its timeout is the deadline).
|
|
105
117
|
const bridgeAbortController = new AbortController();
|
|
106
118
|
const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
|
|
107
119
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
@@ -130,6 +142,7 @@ async function runEvalCell(
|
|
|
130
142
|
callerSignal: invocation.signal,
|
|
131
143
|
cellId: invocation.cellId,
|
|
132
144
|
timeoutMs,
|
|
145
|
+
...(timeoutBehavior === "detach" ? { maxPauseGraceMs: foregroundWindowMs } : {}),
|
|
133
146
|
timeoutFactory: options.timeoutFactory ?? defaultTimeoutFactory,
|
|
134
147
|
onTimeout: (error) => {
|
|
135
148
|
if (timeoutBehavior === "detach" && cellManager.detach(cell)) {
|
package/src/tool/types.ts
CHANGED
|
@@ -13,6 +13,12 @@ export function enabledLanguageList(enabled: EnabledEvalLanguages): EvalLanguage
|
|
|
13
13
|
|
|
14
14
|
export const EVAL_SUMMARY_MAX_LENGTH = 80;
|
|
15
15
|
|
|
16
|
+
const TIMEOUT_FIELD_DESCRIPTION =
|
|
17
|
+
"Seconds the cell may block the turn before it detaches, and the amount by which it raises the wall-clock hard limit. In interactive sessions the detach point is capped at the foreground window (default 60s), so a large value frees the turn at the window while the cell keeps running; on_timeout:'error' (and print/json) keep the full value as the uncapped deadline.";
|
|
18
|
+
|
|
19
|
+
const ON_TIMEOUT_FIELD_DESCRIPTION =
|
|
20
|
+
"Timeout behavior. Interactive sessions detach by default (at the foreground window); print/json sessions error by default. 'error' uses the full timeout as an uncapped deadline.";
|
|
21
|
+
|
|
16
22
|
export interface EvalToolInput {
|
|
17
23
|
readonly language: EvalLanguage;
|
|
18
24
|
readonly code: string;
|
|
@@ -47,10 +53,10 @@ const fullEvalInputSchema = Type.Object({
|
|
|
47
53
|
"REQUIRED for run. ONE line in the USER'S conversational language (Korean conversation -> Korean summary) stating WHAT this cell does and FOR WHAT PURPOSE; shown in the TUI while the cell runs. Longer values are force-truncated to 80 chars.",
|
|
48
54
|
}),
|
|
49
55
|
),
|
|
50
|
-
timeout: Type.Optional(Type.Number({ minimum: 1, description:
|
|
56
|
+
timeout: Type.Optional(Type.Number({ minimum: 1, description: TIMEOUT_FIELD_DESCRIPTION })),
|
|
51
57
|
on_timeout: Type.Optional(
|
|
52
58
|
Type.Union([Type.Literal("detach"), Type.Literal("error")], {
|
|
53
|
-
description:
|
|
59
|
+
description: ON_TIMEOUT_FIELD_DESCRIPTION,
|
|
54
60
|
}),
|
|
55
61
|
),
|
|
56
62
|
reset: Type.Optional(Type.Boolean({ description: "Reset this language kernel before running." })),
|
|
@@ -83,11 +89,10 @@ export function createEvalInputSchema(enabled: EnabledEvalLanguages): EvalInputS
|
|
|
83
89
|
"REQUIRED for run. ONE line in the USER'S conversational language (Korean conversation -> Korean summary) stating WHAT this cell does and FOR WHAT PURPOSE; shown in the TUI while the cell runs. Longer values are force-truncated to 80 chars.",
|
|
84
90
|
}),
|
|
85
91
|
),
|
|
86
|
-
timeout: Type.Optional(Type.Number({ minimum: 1, description:
|
|
92
|
+
timeout: Type.Optional(Type.Number({ minimum: 1, description: TIMEOUT_FIELD_DESCRIPTION })),
|
|
87
93
|
on_timeout: Type.Optional(
|
|
88
94
|
Type.Union([Type.Literal("detach"), Type.Literal("error")], {
|
|
89
|
-
description:
|
|
90
|
-
"Timeout behavior. Interactive sessions detach by default; print/json sessions error by default.",
|
|
95
|
+
description: ON_TIMEOUT_FIELD_DESCRIPTION,
|
|
91
96
|
}),
|
|
92
97
|
),
|
|
93
98
|
reset: Type.Optional(Type.Boolean({ description: "Reset this language kernel before running." })),
|