@code-yeongyu/senpi-codemode 2026.9.9-2 → 2026.9.10-2
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 +30 -0
- package/README.md +17 -2
- package/package.json +4 -4
- package/src/config/settings.ts +35 -15
- package/src/index.ts +13 -2
- package/src/prompt/eval-prompt-template.ts +76 -0
- package/src/prompt/eval-prompt.ts +6 -77
- package/src/timeouts/run-budget.ts +107 -0
- package/src/tool/cell-deadlines.ts +76 -0
- package/src/tool/cell-execution.ts +25 -14
- package/src/tool/detached-cell-contract.ts +4 -0
- package/src/tool/detached-cell-manager.ts +47 -40
- package/src/tool/detached-cell-notification.ts +2 -0
- package/src/tool/detached-cell-snapshot.ts +5 -0
- package/src/tool/eval-tool-options.ts +8 -3
- package/src/tool/eval-tool.ts +17 -187
- package/src/tool/run-eval-cell.ts +189 -0
- package/src/tool/types.ts +66 -56
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.10-2] - 2026-09-10
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
## [2026.9.10] - 2026-09-10
|
|
28
|
+
|
|
29
|
+
### Breaking Changes
|
|
30
|
+
|
|
31
|
+
- The eval `timeout` argument is now the cell's run budget (a kill deadline for the cell's own execution time) instead of the interactive detach budget; interactive calls detach at `cellTimeoutSeconds` capped by `foregroundWindowSeconds` regardless of `timeout`, and print/json calls are bounded by the run budget instead of a `cellTimeoutSeconds` idle kill.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Every eval cell carries a run budget (`runBudgetSeconds`, default 300s, env `SENPI_CODEMODE_RUN_BUDGET_SECONDS`, per-call `timeout`) that charges only its own execution time, is paused while a host tool call is in flight, keeps counting after detach, and kills the cell through the cooperative interrupt path with a result or notification that names the exhausted budget and the kernel-state outcome.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The eval tool schema and description state the configured run budget, detach point, and hard limit, and say that a killed JavaScript cell that cannot settle restarts its kernel and loses every global.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
15
45
|
## [2026.9.9-2] - 2026-09-09
|
|
16
46
|
|
|
17
47
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ Configuration is loaded in this order:
|
|
|
85
85
|
},
|
|
86
86
|
"cellTimeoutSeconds": 30,
|
|
87
87
|
"foregroundWindowSeconds": 60,
|
|
88
|
+
"runBudgetSeconds": 300,
|
|
89
|
+
"hardLimitSeconds": 1800,
|
|
88
90
|
"parallelPoolWidth": 4,
|
|
89
91
|
"taskTools": {
|
|
90
92
|
"task": "task",
|
|
@@ -101,8 +103,10 @@ Configuration is loaded in this order:
|
|
|
101
103
|
| Key | Default | Effect |
|
|
102
104
|
| --- | --- | --- |
|
|
103
105
|
| `languages` | `py`/`js` enabled; `rb`/`jl` disabled | Selects desired languages before interpreter detection. |
|
|
104
|
-
| `cellTimeoutSeconds` | `30` | Idle
|
|
105
|
-
| `foregroundWindowSeconds` | `60` |
|
|
106
|
+
| `cellTimeoutSeconds` | `30` | Idle time an interactive call blocks the turn before the cell detaches. Print/json calls never detach. |
|
|
107
|
+
| `foregroundWindowSeconds` | `60` | Caps `cellTimeoutSeconds` and the grace a bridge-parked cell gets before it detaches, so an interactive call never blocks the turn longer than this. Env override: `SENPI_CODEMODE_FOREGROUND_SECONDS`. |
|
|
108
|
+
| `runBudgetSeconds` | `300` | Kill deadline for a cell's own execution time - child processes, network, timers, CPU. Time parked in host tool calls (`agent()`, `tool.*`) is not charged, and the budget keeps counting after the cell detaches. A per-call `timeout` replaces it for that cell. Env override: `SENPI_CODEMODE_RUN_BUDGET_SECONDS`. |
|
|
109
|
+
| `hardLimitSeconds` | `1800` | Wall-clock kill deadline for a cell, parked or not; a per-call `timeout` above it raises it. Env override: `SENPI_CODEMODE_HARD_LIMIT_SECONDS`. |
|
|
106
110
|
| `parallelPoolWidth` | `4` | Maximum concurrent `parallel()` thunks. |
|
|
107
111
|
| `taskTools.task` | `"task"` | Registered tool name used by `agent()`. |
|
|
108
112
|
| `taskTools.output` | `"task_output"` | Registered tool name used by `output()`. |
|
|
@@ -173,6 +177,17 @@ cell keeps only its own language kernel busy. A new same-language call returns
|
|
|
173
177
|
a busy error with its cell id and output tail; calls in other languages continue
|
|
174
178
|
normally. Do not re-run the cell.
|
|
175
179
|
|
|
180
|
+
Every cell, detached or not, is bounded by two kill deadlines. The run budget
|
|
181
|
+
(`runBudgetSeconds`, or the call's `timeout`) charges only the cell's own
|
|
182
|
+
execution time and is paused while a host tool call is in flight, so a cell
|
|
183
|
+
waiting on `agent()` survives while a runaway child process or loop does not.
|
|
184
|
+
The hard limit (`hardLimitSeconds`, raised by a larger `timeout`) is wall-clock
|
|
185
|
+
and bounds parked cells too. A cell killed by either deadline reports which one
|
|
186
|
+
in its result or completion notification, together with whether kernel state
|
|
187
|
+
survived; the tool schema states the configured numbers. The `timeout` value
|
|
188
|
+
never changes when an interactive call detaches: that is `cellTimeoutSeconds`
|
|
189
|
+
capped by `foregroundWindowSeconds`.
|
|
190
|
+
|
|
176
191
|
While any cell is detached, the interactive footer shows a highlighted
|
|
177
192
|
`↗ <language> · <summary>` status on the extension status line (the cell id
|
|
178
193
|
when the call had no summary), clearing as soon as the last detached cell settles.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.10-2",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -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.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.10-2",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.10-2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.10-2"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/config/settings.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const codemodeSettingsSchema = Type.Object(
|
|
|
20
20
|
),
|
|
21
21
|
cellTimeoutSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
22
22
|
foregroundWindowSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
23
|
+
runBudgetSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
23
24
|
hardLimitSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
24
25
|
parallelPoolWidth: Type.Optional(Type.Number({ minimum: 1 })),
|
|
25
26
|
taskTools: Type.Optional(
|
|
@@ -64,15 +65,20 @@ export interface CodemodeSettings {
|
|
|
64
65
|
readonly rb: boolean;
|
|
65
66
|
readonly jl: boolean;
|
|
66
67
|
};
|
|
68
|
+
/** Idle time an interactive call blocks the turn before the cell detaches; capped by the foreground window. */
|
|
67
69
|
readonly cellTimeoutSeconds: number;
|
|
68
70
|
/**
|
|
69
|
-
* Longest an interactive eval call blocks the agent loop before the cell detaches,
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* (and print/json) calls, where `timeout` stays the unclamped deadline.
|
|
71
|
+
* Longest an interactive eval call blocks the agent loop before the cell detaches, capping
|
|
72
|
+
* `cellTimeoutSeconds` and the bridge-parked grace. A still-running cell keeps living up to its
|
|
73
|
+
* run budget and the hard limit; this only frees the turn. Print/json calls never detach.
|
|
73
74
|
*/
|
|
74
75
|
readonly foregroundWindowSeconds: number;
|
|
75
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Kill deadline for a cell's own execution time — child processes, network, timers, CPU — with
|
|
78
|
+
* time parked on host tool calls excluded. A per-call `timeout` replaces it for that cell.
|
|
79
|
+
*/
|
|
80
|
+
readonly runBudgetSeconds: number;
|
|
81
|
+
/** Wall-clock kill deadline for a single cell; bounds detached and bridge-parked cells too. */
|
|
76
82
|
readonly hardLimitSeconds: number;
|
|
77
83
|
readonly parallelPoolWidth: number;
|
|
78
84
|
readonly taskTools?: CodemodeTaskTools;
|
|
@@ -116,6 +122,15 @@ export const DEFAULT_FOREGROUND_WINDOW_SECONDS = 60;
|
|
|
116
122
|
|
|
117
123
|
export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SECONDS";
|
|
118
124
|
|
|
125
|
+
/**
|
|
126
|
+
* One language kernel runs one cell at a time and a killed JavaScript cell that cannot settle
|
|
127
|
+
* cooperatively restarts its worker, so a runaway cell costs far more than a runaway bash command:
|
|
128
|
+
* five minutes of own execution time is the default before the cell is killed.
|
|
129
|
+
*/
|
|
130
|
+
export const DEFAULT_RUN_BUDGET_SECONDS = 300;
|
|
131
|
+
|
|
132
|
+
export const RUN_BUDGET_ENVIRONMENT_FLAG = "SENPI_CODEMODE_RUN_BUDGET_SECONDS";
|
|
133
|
+
|
|
119
134
|
// OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
|
|
120
135
|
// defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
|
|
121
136
|
export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
@@ -127,6 +142,7 @@ export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
|
127
142
|
},
|
|
128
143
|
cellTimeoutSeconds: 30,
|
|
129
144
|
foregroundWindowSeconds: DEFAULT_FOREGROUND_WINDOW_SECONDS,
|
|
145
|
+
runBudgetSeconds: DEFAULT_RUN_BUDGET_SECONDS,
|
|
130
146
|
hardLimitSeconds: DEFAULT_HARD_LIMIT_SECONDS,
|
|
131
147
|
parallelPoolWidth: 4,
|
|
132
148
|
taskTools: {
|
|
@@ -178,20 +194,23 @@ export function resolveEnabledLanguages(
|
|
|
178
194
|
|
|
179
195
|
/** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
|
|
180
196
|
export function resolveHardLimitSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
|
|
181
|
-
|
|
182
|
-
if (override === undefined) return settings.hardLimitSeconds;
|
|
183
|
-
const parsed = Number.parseInt(override, 10);
|
|
184
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return settings.hardLimitSeconds;
|
|
185
|
-
return parsed;
|
|
197
|
+
return positiveSecondsOverride(env[HARD_LIMIT_ENVIRONMENT_FLAG]) ?? settings.hardLimitSeconds;
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
/** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
|
|
189
201
|
export function resolveForegroundWindowSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
return positiveSecondsOverride(env[FOREGROUND_WINDOW_ENVIRONMENT_FLAG]) ?? settings.foregroundWindowSeconds;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
|
|
206
|
+
export function resolveRunBudgetSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
|
|
207
|
+
return positiveSecondsOverride(env[RUN_BUDGET_ENVIRONMENT_FLAG]) ?? settings.runBudgetSeconds;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function positiveSecondsOverride(value: string | undefined): number | undefined {
|
|
211
|
+
if (value === undefined) return undefined;
|
|
212
|
+
const parsed = Number.parseInt(value, 10);
|
|
213
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
195
214
|
}
|
|
196
215
|
|
|
197
216
|
async function loadSettingsFile(path: string): Promise<LoadedCodemodeSettings> {
|
|
@@ -229,6 +248,7 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
|
|
|
229
248
|
},
|
|
230
249
|
cellTimeoutSeconds: input.cellTimeoutSeconds ?? defaultCodemodeSettings.cellTimeoutSeconds,
|
|
231
250
|
foregroundWindowSeconds: input.foregroundWindowSeconds ?? defaultCodemodeSettings.foregroundWindowSeconds,
|
|
251
|
+
runBudgetSeconds: input.runBudgetSeconds ?? defaultCodemodeSettings.runBudgetSeconds,
|
|
232
252
|
hardLimitSeconds: input.hardLimitSeconds ?? defaultCodemodeSettings.hardLimitSeconds,
|
|
233
253
|
parallelPoolWidth: input.parallelPoolWidth ?? defaultCodemodeSettings.parallelPoolWidth,
|
|
234
254
|
taskTools: {
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,12 @@ 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 {
|
|
6
|
+
import {
|
|
7
|
+
defaultCodemodeSettings,
|
|
8
|
+
resolveForegroundWindowSeconds,
|
|
9
|
+
resolveHardLimitSeconds,
|
|
10
|
+
resolveRunBudgetSeconds,
|
|
11
|
+
} from "./config/settings.ts";
|
|
7
12
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
8
13
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
9
14
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
@@ -128,6 +133,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
128
133
|
kernelManager: manager,
|
|
129
134
|
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
130
135
|
foregroundWindowSeconds: resolveForegroundWindowSeconds(runtime.settings),
|
|
136
|
+
runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
|
|
137
|
+
hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
|
|
131
138
|
executeTool: runtime.executeTool,
|
|
132
139
|
listTools: () => pi.getAllTools(),
|
|
133
140
|
complete,
|
|
@@ -163,6 +170,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
163
170
|
kernelManager: manager,
|
|
164
171
|
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
165
172
|
foregroundWindowSeconds: resolveForegroundWindowSeconds(defaultCodemodeSettings),
|
|
173
|
+
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
174
|
+
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
166
175
|
executeTool: createExecuteTool(pi),
|
|
167
176
|
listTools: () => pi.getAllTools(),
|
|
168
177
|
complete,
|
|
@@ -170,6 +179,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
170
179
|
cellManager: new EvalDetachedCellManager({
|
|
171
180
|
notifier,
|
|
172
181
|
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
182
|
+
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
173
183
|
onStatusChange: showDetachedCells,
|
|
174
184
|
onWakeSourceState: emitWakeSourceState,
|
|
175
185
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
@@ -185,7 +195,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
185
195
|
);
|
|
186
196
|
pi.registerRemovedToolHint(
|
|
187
197
|
"exec",
|
|
188
|
-
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on
|
|
198
|
+
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on their own and notify when complete.',
|
|
189
199
|
);
|
|
190
200
|
pi.registerRemovedToolHint(
|
|
191
201
|
"wait",
|
|
@@ -207,6 +217,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
207
217
|
artifactsDir: runtime.artifactsDir,
|
|
208
218
|
notifier,
|
|
209
219
|
hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
|
|
220
|
+
runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
|
|
210
221
|
onStatusChange: showDetachedCells,
|
|
211
222
|
onWakeSourceState: emitWakeSourceState,
|
|
212
223
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
**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.
|
|
5
|
+
|
|
6
|
+
{{#if styleClaude}}<eval_first_batching>
|
|
7
|
+
Batch a step's independent calls in one cell with \`parallel(thunks)\`; write real code around them - loops, branches, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
8
|
+
{{#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}}
|
|
9
|
+
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
10
|
+
GPT eval: batch a step's independent tool calls in one cell with \`tool.<name>(args)\` and \`parallel(thunks)\` and inspect every result.
|
|
11
|
+
{{#if monitor}}- A wait or a long run (build, test run, deploy, watch) starts through \`tool.monitor({ command, filter })\` in that same cell with the decisive-line filter; its event wakes the turn, so no cell sits on the wait and no child is spawned for it.
|
|
12
|
+
{{/if}}- Long cells detach on their own and notify on completion; do not poll or re-run them.
|
|
13
|
+
- Keep every failed or missing item in the result verbatim and re-read truncated output before deciding.
|
|
14
|
+
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route a step's independent lookups through one eval cell via \`parallel(thunks)\` and inspect every result.
|
|
15
|
+
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically.
|
|
16
|
+
- Wrap failable calls in try/except inside the cell and keep every failed item in the result verbatim; after two distinct failed strategies for the same fact, fall back to direct tool calls.
|
|
17
|
+
- Re-read truncated output before deciding on it.
|
|
18
|
+
{{#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}}Put a step's independent calls into one cell with \`parallel(thunks)\`.
|
|
19
|
+
- Write real code around the calls - loops, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
20
|
+
{{#if monitor}}- 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}}Batch a step's independent calls in one cell with \`parallel(thunks)\`.
|
|
21
|
+
- Write real code around the calls - loops, branches, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
22
|
+
{{#if monitor}}- Long-running build, test run, deploy, or watch work starts with \`tool.monitor({ command, filter })\`, with the decisive-line filter inside the same cell; keep working until its event wakes the turn.{{/if}}{{/if}}
|
|
23
|
+
{{#if hostLine}}
|
|
24
|
+
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.
|
|
25
|
+
{{/if}}
|
|
26
|
+
|
|
27
|
+
\`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}}.
|
|
28
|
+
|
|
29
|
+
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. Its own execution time is capped at {{runBudgetSeconds}}s — time inside host tool calls is not charged; raise \`timeout\` only for a declared long run — and a killed js cell that cannot settle restarts its kernel with every global lost. Do not re-run a detached cell; read or cancel it with \`eval({ action: "peek", cell_id })\` / \`eval({ action: "stop", cell_id })\`.
|
|
30
|
+
|
|
31
|
+
{{#if py}}Python runs on a live event loop: use top-level \`await\`; \`asyncio.run(…)\` raises.{{/if}}
|
|
32
|
+
{{#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. Shell out through \`Bun.$\` or \`Bun.spawn\`, never \`Bun.spawnSync\`: a synchronous child blocks the worker and cannot be interrupted.{{#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}}
|
|
33
|
+
{{#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}}
|
|
34
|
+
{{#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}}
|
|
35
|
+
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.
|
|
36
|
+
</instruction>
|
|
37
|
+
|
|
38
|
+
<prelude>
|
|
39
|
+
{{#ifAll py js}}Same helpers + arg order, both runtimes. Python: sync, options = trailing kwargs. JS: async/\`await\`able, options = ONE trailing object literal, never positional (extras throw).{{else}}{{#if py}}Sync; options = trailing kwargs.{{/if}}{{#if js}}Async/\`await\`able; options = ONE trailing object literal, never positional (extras throw).{{/if}}{{/ifAll}}{{#if rb}} Ruby: sync, options = trailing keyword args.{{/if}}{{#if jl}} Julia: sync, options = trailing keyword args.{{/if}}
|
|
40
|
+
\`\`\`
|
|
41
|
+
display(value) → None
|
|
42
|
+
Cell output. Images reach you only through display: pass a figure, image bytes, a tool result, or its \`images[i]\`.
|
|
43
|
+
print(value, ...) → None
|
|
44
|
+
Text output.
|
|
45
|
+
read(path, offset?=1, limit?=None) → str
|
|
46
|
+
File as text; offset/limit are 1-indexed lines. Accepts \`local://…\`.
|
|
47
|
+
write(path, content) → str
|
|
48
|
+
Write file (creates parents) → resolved path. \`local://…\` persists across turns/subagents.
|
|
49
|
+
env(key?=None, value?=None) → str | None | dict
|
|
50
|
+
No args → full env dict; one → value; two → set \`key=value\`.
|
|
51
|
+
{{#if spawns}}output(*ids, format?="raw", offset?=None, limit?=None) → str | dict | list[dict]
|
|
52
|
+
Task/agent output by id. Reads immediately: running tasks return their status; \`format\` \`"raw"\` = full, \`"tail"\` = trailing.
|
|
53
|
+
{{/if}}tool.<name>(args) → { text, images?, details?, hasError? }
|
|
54
|
+
Invoke any session tool; image results (e.g. \`tool.read\` on a png) arrive in \`images[i]\` as { mimeType, dataBase64 }.
|
|
55
|
+
tool_schema(name?) → dict
|
|
56
|
+
Parameter schema of a tool (omit \`name\` to list tool names); a failed \`tool.<name>()\` call also returns the expected parameters.
|
|
57
|
+
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
58
|
+
Oneshot, stateless. \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → parsed structured output.
|
|
59
|
+
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
60
|
+
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\`).
|
|
61
|
+
{{/if}}parallel(thunks) → list
|
|
62
|
+
Thunks through a bounded pool (as wide as a \`task\` batch), input order kept; a throwing thunk propagates.
|
|
63
|
+
pipeline(items, ...stages) → list
|
|
64
|
+
Map items through one-arg stages with a barrier between stages; each stage receives the previous stage's result.
|
|
65
|
+
log(message) → None
|
|
66
|
+
Progress line above the status tree.
|
|
67
|
+
phase(title) → None
|
|
68
|
+
Phase grouping subsequent status lines.
|
|
69
|
+
\`\`\`
|
|
70
|
+
</prelude>
|
|
71
|
+
{{#if spawns}}
|
|
72
|
+
<workflow>
|
|
73
|
+
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.
|
|
74
|
+
</workflow>
|
|
75
|
+
{{/if}}
|
|
76
|
+
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_RUN_BUDGET_SECONDS } from "../config/settings.ts";
|
|
1
2
|
import type { EvalRuntimeInfo } from "../tool/types.ts";
|
|
3
|
+
import { EVAL_PROMPT_TEMPLATE } from "./eval-prompt-template.ts";
|
|
2
4
|
|
|
3
5
|
export interface EnabledLanguages {
|
|
4
6
|
readonly py: boolean;
|
|
@@ -26,6 +28,8 @@ export interface EvalPromptOptions {
|
|
|
26
28
|
readonly jsRuntime?: EvalRuntimeInfo;
|
|
27
29
|
/** Absolute path of the active bun-1-4 skill; rendered as a MUST READ pointer only on a bun kernel. */
|
|
28
30
|
readonly bunSkillPath?: string;
|
|
31
|
+
/** Kill deadline for a cell's own execution time, as configured; the description states it. */
|
|
32
|
+
readonly runBudgetSeconds?: number;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/** Prompt dialect for the eval-first batching emphasis. */
|
|
@@ -64,82 +68,6 @@ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyl
|
|
|
64
68
|
|
|
65
69
|
type ContextValue = string | boolean;
|
|
66
70
|
type Context = Readonly<Record<string, ContextValue>>;
|
|
67
|
-
const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
68
|
-
|
|
69
|
-
<instruction>
|
|
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.
|
|
71
|
-
|
|
72
|
-
{{#if styleClaude}}<eval_first_batching>
|
|
73
|
-
Batch a step's independent calls in one cell with \`parallel(thunks)\`; write real code around them - loops, branches, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
74
|
-
{{#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}}
|
|
75
|
-
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
76
|
-
GPT eval: batch a step's independent tool calls in one cell with \`tool.<name>(args)\` and \`parallel(thunks)\` and inspect every result.
|
|
77
|
-
{{#if monitor}}- A wait or a long run (build, test run, deploy, watch) starts through \`tool.monitor({ command, filter })\` in that same cell with the decisive-line filter; its event wakes the turn, so no cell sits on the wait and no child is spawned for it.
|
|
78
|
-
{{/if}}- Long cells detach on timeout and notify on completion; do not poll or re-run them.
|
|
79
|
-
- Keep every failed or missing item in the result verbatim and re-read truncated output before deciding.
|
|
80
|
-
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route a step's independent lookups through one eval cell via \`parallel(thunks)\` and inspect every result.
|
|
81
|
-
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically.
|
|
82
|
-
- Wrap failable calls in try/except inside the cell and keep every failed item in the result verbatim; after two distinct failed strategies for the same fact, fall back to direct tool calls.
|
|
83
|
-
- Re-read truncated output before deciding on it.
|
|
84
|
-
{{#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}}Put a step's independent calls into one cell with \`parallel(thunks)\`.
|
|
85
|
-
- Write real code around the calls - loops, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
86
|
-
{{#if monitor}}- 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}}Batch a step's independent calls in one cell with \`parallel(thunks)\`.
|
|
87
|
-
- Write real code around the calls - loops, branches, joins, a try/except per risky item - and keep every failed or missing item in the result verbatim; re-read truncated output before deciding.
|
|
88
|
-
{{#if monitor}}- Long-running build, test run, deploy, or watch work starts with \`tool.monitor({ command, filter })\`, with the decisive-line filter inside the same cell; keep working until its event wakes the turn.{{/if}}{{/if}}
|
|
89
|
-
{{#if hostLine}}
|
|
90
|
-
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.
|
|
91
|
-
{{/if}}
|
|
92
|
-
|
|
93
|
-
\`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}}.
|
|
94
|
-
|
|
95
|
-
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 })\`.
|
|
96
|
-
|
|
97
|
-
{{#if py}}Python runs on a live event loop: use top-level \`await\`; \`asyncio.run(…)\` raises.{{/if}}
|
|
98
|
-
{{#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. Shell out through \`Bun.$\` or \`Bun.spawn\`, never \`Bun.spawnSync\`: a synchronous child blocks the worker, so a stop or timeout then loses every variable.{{#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}}
|
|
99
|
-
{{#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}}
|
|
100
|
-
{{#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}}
|
|
101
|
-
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.
|
|
102
|
-
</instruction>
|
|
103
|
-
|
|
104
|
-
<prelude>
|
|
105
|
-
{{#ifAll py js}}Same helpers + arg order, both runtimes. Python: sync, options = trailing kwargs. JS: async/\`await\`able, options = ONE trailing object literal, never positional (extras throw).{{else}}{{#if py}}Sync; options = trailing kwargs.{{/if}}{{#if js}}Async/\`await\`able; options = ONE trailing object literal, never positional (extras throw).{{/if}}{{/ifAll}}{{#if rb}} Ruby: sync, options = trailing keyword args.{{/if}}{{#if jl}} Julia: sync, options = trailing keyword args.{{/if}}
|
|
106
|
-
\`\`\`
|
|
107
|
-
display(value) → None
|
|
108
|
-
Cell output. Images reach you only through display: pass a figure, image bytes, a tool result, or its \`images[i]\`.
|
|
109
|
-
print(value, ...) → None
|
|
110
|
-
Text output.
|
|
111
|
-
read(path, offset?=1, limit?=None) → str
|
|
112
|
-
File as text; offset/limit are 1-indexed lines. Accepts \`local://…\`.
|
|
113
|
-
write(path, content) → str
|
|
114
|
-
Write file (creates parents) → resolved path. \`local://…\` persists across turns/subagents.
|
|
115
|
-
env(key?=None, value?=None) → str | None | dict
|
|
116
|
-
No args → full env dict; one → value; two → set \`key=value\`.
|
|
117
|
-
{{#if spawns}}output(*ids, format?="raw", offset?=None, limit?=None) → str | dict | list[dict]
|
|
118
|
-
Task/agent output by id. Reads immediately: running tasks return their status; \`format\` \`"raw"\` = full, \`"tail"\` = trailing.
|
|
119
|
-
{{/if}}tool.<name>(args) → { text, images?, details?, hasError? }
|
|
120
|
-
Invoke any session tool; image results (e.g. \`tool.read\` on a png) arrive in \`images[i]\` as { mimeType, dataBase64 }.
|
|
121
|
-
tool_schema(name?) → dict
|
|
122
|
-
Parameter schema of a tool (omit \`name\` to list tool names); a failed \`tool.<name>()\` call also returns the expected parameters.
|
|
123
|
-
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
124
|
-
Oneshot, stateless. \`model\`: \`"smol"\` fast | \`"default"\` session | \`"slow"\` most capable. \`schema\` (JSON-Schema) → parsed structured output.
|
|
125
|
-
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
126
|
-
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\`).
|
|
127
|
-
{{/if}}parallel(thunks) → list
|
|
128
|
-
Thunks through a bounded pool (as wide as a \`task\` batch), input order kept; a throwing thunk propagates.
|
|
129
|
-
pipeline(items, ...stages) → list
|
|
130
|
-
Map items through one-arg stages with a barrier between stages; each stage receives the previous stage's result.
|
|
131
|
-
log(message) → None
|
|
132
|
-
Progress line above the status tree.
|
|
133
|
-
phase(title) → None
|
|
134
|
-
Phase grouping subsequent status lines.
|
|
135
|
-
\`\`\`
|
|
136
|
-
</prelude>
|
|
137
|
-
{{#if spawns}}
|
|
138
|
-
<workflow>
|
|
139
|
-
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.
|
|
140
|
-
</workflow>
|
|
141
|
-
{{/if}}
|
|
142
|
-
`;
|
|
143
71
|
|
|
144
72
|
export function buildEvalPrompt(
|
|
145
73
|
enabled: EnabledLanguages,
|
|
@@ -167,6 +95,7 @@ export function buildEvalPrompt(
|
|
|
167
95
|
jsBun: options.jsRuntime?.name === "bun",
|
|
168
96
|
jsVersion: options.jsRuntime?.version ?? "",
|
|
169
97
|
bunSkillPath: options.bunSkillPath ?? "",
|
|
98
|
+
runBudgetSeconds: String(options.runBudgetSeconds ?? DEFAULT_RUN_BUDGET_SECONDS),
|
|
170
99
|
};
|
|
171
100
|
const description = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
172
101
|
.replace(/\n{3,}/g, "\n\n")
|
|
@@ -197,7 +126,7 @@ const BATCHING_GUIDELINES: Record<EvalEmphasisStyle, string> = {
|
|
|
197
126
|
claude:
|
|
198
127
|
"Prefer eval for a step's independent calls: one cell runs them together and keeps every failure in its result.",
|
|
199
128
|
codex: "Route a step's independent calls through one eval cell and inspect every result; a direct tool call is right when one call is sufficient.",
|
|
200
|
-
gpt: "Use eval to batch a step's independent tool calls in one cell and inspect every result; long cells detach on
|
|
129
|
+
gpt: "Use eval to batch a step's independent tool calls in one cell and inspect every result; long cells detach on their own and notify on completion, so do not poll.",
|
|
201
130
|
kimi: "Put a step's independent calls into one eval cell with parallel(thunks) and keep every failed item in the result.",
|
|
202
131
|
};
|
|
203
132
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { TimeoutPauseHandle } from "./idle-timeout.ts";
|
|
2
|
+
|
|
3
|
+
export interface RunBudgetEvent {
|
|
4
|
+
readonly cellId: string;
|
|
5
|
+
readonly budgetMs: number;
|
|
6
|
+
readonly error: Error;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RunBudgetOptions {
|
|
10
|
+
readonly cellId: string;
|
|
11
|
+
readonly budgetMs: number;
|
|
12
|
+
readonly onExhausted: (event: RunBudgetEvent) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runBudgetError(cellId: string, budgetSeconds: number): Error {
|
|
16
|
+
const error = new Error(
|
|
17
|
+
`Eval cell ${cellId} exhausted its ${budgetSeconds}s run budget (own execution time; host tool calls excluded) and was killed.`,
|
|
18
|
+
);
|
|
19
|
+
error.name = "TimeoutError";
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bounds a cell's own execution time. Unlike the idle watchdog it accumulates across host tool
|
|
25
|
+
* calls instead of restarting after each one, and time spent parked on a bridge call is never
|
|
26
|
+
* charged, so a long `agent()` wait survives while a runaway loop or child process does not.
|
|
27
|
+
*/
|
|
28
|
+
export class RunBudget implements TimeoutPauseHandle {
|
|
29
|
+
readonly budgetMs: number;
|
|
30
|
+
readonly #cellId: string;
|
|
31
|
+
readonly #onExhausted: (event: RunBudgetEvent) => void;
|
|
32
|
+
#chargedMs = 0;
|
|
33
|
+
#runningSinceMs: number | undefined;
|
|
34
|
+
#pauseDepth = 0;
|
|
35
|
+
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
#settled = false;
|
|
37
|
+
|
|
38
|
+
constructor(options: RunBudgetOptions) {
|
|
39
|
+
this.#cellId = options.cellId;
|
|
40
|
+
this.budgetMs = Math.max(1, Math.floor(options.budgetMs));
|
|
41
|
+
this.#onExhausted = options.onExhausted;
|
|
42
|
+
this.#start();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get consumedMs(): number {
|
|
46
|
+
const running = this.#runningSinceMs === undefined ? 0 : Date.now() - this.#runningSinceMs;
|
|
47
|
+
return this.#chargedMs + running;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pause(): void {
|
|
51
|
+
if (this.#settled) return;
|
|
52
|
+
this.#pauseDepth++;
|
|
53
|
+
if (this.#pauseDepth !== 1) return;
|
|
54
|
+
this.#chargedMs = this.consumedMs;
|
|
55
|
+
this.#runningSinceMs = undefined;
|
|
56
|
+
this.#clearTimer();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
resume(): void {
|
|
60
|
+
if (this.#settled || this.#pauseDepth === 0) return;
|
|
61
|
+
this.#pauseDepth--;
|
|
62
|
+
if (this.#pauseDepth > 0) return;
|
|
63
|
+
this.#start();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
dispose(): void {
|
|
67
|
+
if (this.#settled) return;
|
|
68
|
+
this.#settled = true;
|
|
69
|
+
this.#clearTimer();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#start(): void {
|
|
73
|
+
this.#runningSinceMs = Date.now();
|
|
74
|
+
this.#arm(this.budgetMs - this.#chargedMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#arm(delayMs: number): void {
|
|
78
|
+
this.#clearTimer();
|
|
79
|
+
const timer = setTimeout(() => this.#expire(), Math.max(0, delayMs));
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
this.#timer = timer;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#clearTimer(): void {
|
|
85
|
+
if (this.#timer === undefined) return;
|
|
86
|
+
clearTimeout(this.#timer);
|
|
87
|
+
this.#timer = undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#expire(): void {
|
|
91
|
+
this.#timer = undefined;
|
|
92
|
+
if (this.#settled || this.#pauseDepth > 0) return;
|
|
93
|
+
const remainingMs = this.budgetMs - this.consumedMs;
|
|
94
|
+
if (remainingMs > 0) {
|
|
95
|
+
this.#arm(remainingMs);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.#settled = true;
|
|
99
|
+
this.#chargedMs = this.consumedMs;
|
|
100
|
+
this.#runningSinceMs = undefined;
|
|
101
|
+
this.#onExhausted({
|
|
102
|
+
cellId: this.#cellId,
|
|
103
|
+
budgetMs: this.budgetMs,
|
|
104
|
+
error: runBudgetError(this.#cellId, this.budgetMs / 1_000),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
|
|
2
|
+
import { RunBudget } from "../timeouts/run-budget.ts";
|
|
3
|
+
|
|
4
|
+
export type CellDeadlineKind = "hard-limit" | "run-budget";
|
|
5
|
+
|
|
6
|
+
export interface CellDeadlineExpiry {
|
|
7
|
+
readonly kind: CellDeadlineKind;
|
|
8
|
+
readonly error: Error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CellDeadlinesOptions {
|
|
12
|
+
readonly cellId: string;
|
|
13
|
+
readonly hardLimitSeconds: number;
|
|
14
|
+
readonly runBudgetSeconds: number;
|
|
15
|
+
readonly onExpire: (expiry: CellDeadlineExpiry) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hardLimitError(cellId: string, hardLimitSeconds: number): Error {
|
|
19
|
+
const error = new Error(`Eval cell ${cellId} was killed at the ${hardLimitSeconds}s hard limit.`);
|
|
20
|
+
error.name = "TimeoutError";
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The two kill deadlines every cell carries from creation to settlement, detached or not: the
|
|
26
|
+
* wall-clock hard limit, which nothing pauses, and the run budget, which charges only the cell's
|
|
27
|
+
* own execution time and is paused while a host bridge call is in flight. Whichever expires first
|
|
28
|
+
* ends the cell; the other is disarmed with it.
|
|
29
|
+
*/
|
|
30
|
+
export class CellDeadlines implements TimeoutPauseHandle {
|
|
31
|
+
readonly hardLimitSeconds: number;
|
|
32
|
+
readonly runBudgetSeconds: number;
|
|
33
|
+
readonly #onExpire: (expiry: CellDeadlineExpiry) => void;
|
|
34
|
+
readonly #runBudget: RunBudget;
|
|
35
|
+
#hardLimitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
#settled = false;
|
|
37
|
+
|
|
38
|
+
constructor(options: CellDeadlinesOptions) {
|
|
39
|
+
this.hardLimitSeconds = options.hardLimitSeconds;
|
|
40
|
+
this.runBudgetSeconds = options.runBudgetSeconds;
|
|
41
|
+
this.#onExpire = options.onExpire;
|
|
42
|
+
const hardLimitTimer = setTimeout(
|
|
43
|
+
() => this.#expire({ kind: "hard-limit", error: hardLimitError(options.cellId, options.hardLimitSeconds) }),
|
|
44
|
+
options.hardLimitSeconds * 1_000,
|
|
45
|
+
);
|
|
46
|
+
hardLimitTimer.unref?.();
|
|
47
|
+
this.#hardLimitTimer = hardLimitTimer;
|
|
48
|
+
this.#runBudget = new RunBudget({
|
|
49
|
+
cellId: options.cellId,
|
|
50
|
+
budgetMs: options.runBudgetSeconds * 1_000,
|
|
51
|
+
onExhausted: ({ error }) => this.#expire({ kind: "run-budget", error }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pause(): void {
|
|
56
|
+
this.#runBudget.pause();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
resume(): void {
|
|
60
|
+
this.#runBudget.resume();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear(): void {
|
|
64
|
+
if (this.#settled) return;
|
|
65
|
+
this.#settled = true;
|
|
66
|
+
if (this.#hardLimitTimer !== undefined) clearTimeout(this.#hardLimitTimer);
|
|
67
|
+
this.#hardLimitTimer = undefined;
|
|
68
|
+
this.#runBudget.dispose();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#expire(expiry: CellDeadlineExpiry): void {
|
|
72
|
+
if (this.#settled) return;
|
|
73
|
+
this.clear();
|
|
74
|
+
this.#onExpire(expiry);
|
|
75
|
+
}
|
|
76
|
+
}
|