@code-yeongyu/senpi-codemode 2026.9.7 → 2026.9.9-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 +55 -0
- package/README.md +14 -1
- package/package.json +4 -4
- package/src/bridge/protocol.ts +1 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager.ts +7 -0
- package/src/kernels/AGENTS.md +7 -0
- package/src/kernels/jl/kernel.ts +4 -0
- package/src/kernels/js/kernel-contract.ts +3 -0
- package/src/kernels/js/worker-core.js +29 -0
- package/src/kernels/js/worker-indirect-eval.js +10 -6
- package/src/kernels/js/worker-shell-capture.d.ts +16 -0
- package/src/kernels/js/worker-shell-capture.js +44 -8
- package/src/kernels/js/worker-startup.ts +1 -0
- package/src/kernels/py/kernel-contract.ts +3 -0
- package/src/kernels/py/transport.ts +9 -1
- package/src/kernels/rb/kernel.ts +4 -0
- package/src/kernels/session-env.ts +56 -0
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +9 -1
- package/src/output/output-meta.ts +26 -2
- package/src/prompt/eval-prompt.ts +17 -24
- package/src/tool/image.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,61 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.9-2] - 2026-09-09
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
## [2026.9.9] - 2026-09-09
|
|
28
|
+
|
|
29
|
+
### Breaking Changes
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
## [2026.9.8] - 2026-09-08
|
|
40
|
+
|
|
41
|
+
### Breaking Changes
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The eval tool description teaches cell mechanics only (batch independent calls, real code, failures kept verbatim, truncated output re-read) and drops the "default execution surface / never a chain / distilled facts only" wording; routing lives in the model's prompt preset.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
## [2026.9.7-2] - 2026-09-07
|
|
54
|
+
|
|
55
|
+
### Breaking Changes
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- The JS kernel's shell capture now pins the worker's environment view for `Bun.spawnSync` as well as `Bun.spawn`, so a cell calling it without an explicit `env` sees the session's `PI_*` values instead of the inherited OS environ.
|
|
64
|
+
- Eval kernels and every child they spawn now see the active session's `PI_*` environment (`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, `PI_REASONING_LEVEL`) exactly as bash-tool children do: inherited `PI_*` values are dropped before the session values are applied, so subprocesses such as `omo-agent-toolkit ulw-loop` resolve the same session as the `bash` tool instead of a cwd-global one.
|
|
65
|
+
- JavaScript eval cells no longer lose their completion value when a nested function, callback, or try/catch helper contains `return`: the cell wrapper now skips last-expression capture only for a genuine top-level `return`, and a property named `return` no longer primes the statement scanner as the keyword (#1439).
|
|
66
|
+
- Eval output truncation notices now name the real cause: a width-clamped line reports `N line(s) clamped to M columns (… dropped)`, a byte-capped tail reports the actual cap, and a notice never presents the output's own size as a limit.
|
|
67
|
+
|
|
68
|
+
### Removed
|
|
69
|
+
|
|
15
70
|
## [2026.9.7] - 2026-09-07
|
|
16
71
|
|
|
17
72
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -54,6 +54,19 @@ task-tool names are known.
|
|
|
54
54
|
A missing optional interpreter removes that language from the session's `eval`
|
|
55
55
|
schema; it is not an installation failure.
|
|
56
56
|
|
|
57
|
+
### Session environment
|
|
58
|
+
|
|
59
|
+
Every kernel starts with the active session's `PI_*` environment — `PI_SESSION_ID`,
|
|
60
|
+
`PI_SESSION_FILE` (when the session is persistent), `PI_PROVIDER`, `PI_MODEL`, and
|
|
61
|
+
`PI_REASONING_LEVEL` (when set) — resolved at session start, mirroring the bash tool's
|
|
62
|
+
session environment contract. The values are visible to `env()`/`process.env`/`os.environ`
|
|
63
|
+
inside cells and are inherited by every child process a cell spawns
|
|
64
|
+
(`Bun.$`, `Bun.spawn`, `child_process`, `subprocess`, ...). Inherited `PI_*` values from
|
|
65
|
+
the launching environment are dropped first, so a child spawned from a cell sees exactly
|
|
66
|
+
what a child spawned from the bash tool sees. The values snapshot at kernel start, so a
|
|
67
|
+
mid-session model switch updates the bash tool's next command but not already-running
|
|
68
|
+
kernels; a new session starts fresh kernels with fresh values.
|
|
69
|
+
|
|
57
70
|
## Settings
|
|
58
71
|
|
|
59
72
|
Configuration is loaded in this order:
|
|
@@ -116,7 +129,7 @@ options object and asynchronous helpers are `await`-able.
|
|
|
116
129
|
| `print(value, ...)` | Emits text output. |
|
|
117
130
|
| `read(path, offset?, limit?)` | Reads text with 1-indexed line slicing. `local://` paths resolve under the session artifact root. |
|
|
118
131
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
119
|
-
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. |
|
|
132
|
+
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
120
133
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
121
134
|
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
|
|
122
135
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
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.9-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.9-2",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.9-2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.9-2"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -26,6 +26,7 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
26
26
|
type: Type.Literal("init"),
|
|
27
27
|
sessionId: Type.String({ minLength: 1 }),
|
|
28
28
|
connection: connectionConfigSchema,
|
|
29
|
+
sessionEnv: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
29
30
|
}),
|
|
30
31
|
Type.Object({
|
|
31
32
|
type: Type.Literal("run"),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getInterpreterAvailability,
|
|
14
14
|
type InterpreterAvailability,
|
|
15
15
|
} from "../interpreters/detect.ts";
|
|
16
|
+
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
16
17
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
17
18
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
18
19
|
import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
|
|
@@ -66,11 +67,13 @@ export async function createRuntime(
|
|
|
66
67
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
67
68
|
const create = options.createSessionManager ?? createCodemodeSessionManager;
|
|
68
69
|
const sessionId = sessionIdFrom(event);
|
|
70
|
+
const sessionEnv = sessionEnvironmentFrom(ctx);
|
|
69
71
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
70
72
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
71
73
|
const manager = await create({
|
|
72
74
|
sessionId,
|
|
73
75
|
cwd: ctx.cwd,
|
|
76
|
+
sessionEnv,
|
|
74
77
|
settings,
|
|
75
78
|
availability,
|
|
76
79
|
artifactsDir: artifacts.dir,
|
|
@@ -11,6 +11,7 @@ import { JuliaKernel } from "../kernels/jl/kernel.ts";
|
|
|
11
11
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
12
12
|
import { PythonKernel } from "../kernels/py/kernel.ts";
|
|
13
13
|
import { RubyKernel } from "../kernels/rb/kernel.ts";
|
|
14
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
14
15
|
import { marshalToolResult } from "../tool/image.ts";
|
|
15
16
|
import type { EvalKernel, EvalKernelManager, EvalLanguage, ExecuteTool } from "../tool/types.ts";
|
|
16
17
|
|
|
@@ -40,6 +41,8 @@ export interface CreateCodemodeSessionManagerOptions {
|
|
|
40
41
|
readonly localRoots?: Readonly<Record<string, string>>;
|
|
41
42
|
/** Session-adjacent directory used for persisted eval artifacts. */
|
|
42
43
|
readonly artifactsDir?: string;
|
|
44
|
+
/** Per-session PI_* values exposed to every kernel and the children it spawns. */
|
|
45
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
43
46
|
readonly executeTool: ExecuteTool;
|
|
44
47
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
45
48
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
@@ -212,6 +215,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
212
215
|
cwd: this.#options.cwd,
|
|
213
216
|
parallelPoolWidth,
|
|
214
217
|
onMessage,
|
|
218
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
215
219
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
216
220
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
217
221
|
});
|
|
@@ -230,6 +234,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
230
234
|
interpreterPath: detected.path,
|
|
231
235
|
sessionId: this.#options.sessionId,
|
|
232
236
|
cwd: this.#options.cwd,
|
|
237
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
233
238
|
connection,
|
|
234
239
|
onMessage,
|
|
235
240
|
});
|
|
@@ -239,6 +244,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
239
244
|
command: detected.path,
|
|
240
245
|
sessionId: this.#options.sessionId,
|
|
241
246
|
cwd: this.#options.cwd,
|
|
247
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
242
248
|
connection,
|
|
243
249
|
onMessage,
|
|
244
250
|
});
|
|
@@ -247,6 +253,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
247
253
|
command: detected.path,
|
|
248
254
|
sessionId: this.#options.sessionId,
|
|
249
255
|
cwd: this.#options.cwd,
|
|
256
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
250
257
|
connection,
|
|
251
258
|
onMessage,
|
|
252
259
|
});
|
package/src/kernels/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ runner/prelude assets).
|
|
|
15
15
|
| Ruby kernel | `rb/kernel.ts` + `rb/prelude.rb`, `rb/runner.rb` |
|
|
16
16
|
| Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
|
|
17
17
|
| Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
|
|
18
|
+
| Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
|
|
18
19
|
|
|
19
20
|
## CONVENTIONS
|
|
20
21
|
|
|
@@ -29,6 +30,12 @@ runner/prelude assets).
|
|
|
29
30
|
framed subprocesses through `shared/`.
|
|
30
31
|
- Subprocess retirement/restart, worker recovery, timeout, and interrupt
|
|
31
32
|
semantics live here, never in the tool layer.
|
|
33
|
+
- Every kernel exposes the active session's `PI_*` environment (`session-env.ts`):
|
|
34
|
+
inherited values are deleted before the session's values are applied, so any
|
|
35
|
+
child spawned from a cell sees the same session environment a bash-tool child
|
|
36
|
+
sees. The JS worker applies it at init (`worker-core.js`; shell capture pins
|
|
37
|
+
the env view under Bun because `delete process.env.X` does not unsetenv),
|
|
38
|
+
and py/rb/jl spawn with it merged into the interpreter environment.
|
|
32
39
|
|
|
33
40
|
## ANTI-PATTERNS
|
|
34
41
|
|
package/src/kernels/jl/kernel.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
3
4
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
5
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
6
|
|
|
@@ -7,6 +8,8 @@ export interface JuliaKernelStartOptions {
|
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly sessionId: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
11
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
12
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
10
13
|
readonly command?: string;
|
|
11
14
|
readonly spawn?: SubprocessSpawn;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
@@ -42,6 +45,7 @@ export class JuliaKernel extends SubprocessKernel {
|
|
|
42
45
|
],
|
|
43
46
|
cwd: options.cwd,
|
|
44
47
|
sessionId: options.sessionId,
|
|
48
|
+
sessionEnv: options.sessionEnv,
|
|
45
49
|
connection: options.connection,
|
|
46
50
|
spawn: options.spawn,
|
|
47
51
|
onMessage: options.onMessage,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
|
|
3
4
|
export type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
|
|
4
5
|
export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
@@ -11,6 +12,8 @@ export interface JavaScriptKernelOptions {
|
|
|
11
12
|
readonly parallelPoolWidth: number;
|
|
12
13
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
13
14
|
readonly workerEntryUrl?: URL;
|
|
15
|
+
/** Per-session PI_* values applied to the worker environment before the first cell runs. */
|
|
16
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface JavaScriptRunInput {
|
|
@@ -3,6 +3,17 @@ import { JsWorkerRuntime } from "./worker-runtime.js";
|
|
|
3
3
|
// Mirrors INTERRUPT_ACK_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
|
|
4
4
|
const INTERRUPT_ACK_OP = "interrupt-ack";
|
|
5
5
|
|
|
6
|
+
// Mirrors SESSION_ENVIRONMENT_KEYS in src/kernels/session-env.ts (this worker file
|
|
7
|
+
// cannot import TypeScript). Keys the active session does not set must be dropped so a
|
|
8
|
+
// value inherited from the host environment never leaks into a cell or its children.
|
|
9
|
+
const SESSION_ENVIRONMENT_KEYS = [
|
|
10
|
+
"PI_SESSION_ID",
|
|
11
|
+
"PI_SESSION_FILE",
|
|
12
|
+
"PI_PROVIDER",
|
|
13
|
+
"PI_MODEL",
|
|
14
|
+
"PI_REASONING_LEVEL",
|
|
15
|
+
];
|
|
16
|
+
|
|
6
17
|
export function createWorkerCore(transport, options) {
|
|
7
18
|
let runtime = null;
|
|
8
19
|
let activeCell = null;
|
|
@@ -54,6 +65,7 @@ export function createWorkerCore(transport, options) {
|
|
|
54
65
|
|
|
55
66
|
function onMessage(message) {
|
|
56
67
|
if (message.type === "init") {
|
|
68
|
+
applySessionEnvironment(message.sessionEnv);
|
|
57
69
|
runtime = new JsWorkerRuntime({
|
|
58
70
|
cwd: options.cwd,
|
|
59
71
|
parallelPoolWidth: options.parallelPoolWidth,
|
|
@@ -98,6 +110,23 @@ function durationMs(startedAtMs) {
|
|
|
98
110
|
return Math.max(0, Math.round(performance.now() - startedAtMs));
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
function applySessionEnvironment(sessionEnv) {
|
|
114
|
+
const provided = new Set(Object.keys(sessionEnv ?? {}));
|
|
115
|
+
const deleted = [];
|
|
116
|
+
for (const key of SESSION_ENVIRONMENT_KEYS) {
|
|
117
|
+
if (key in process.env && !provided.has(key)) deleted.push(key);
|
|
118
|
+
delete process.env[key];
|
|
119
|
+
}
|
|
120
|
+
const applied = Object.entries(sessionEnv ?? {});
|
|
121
|
+
for (const [key, value] of applied) process.env[key] = value;
|
|
122
|
+
// A worker's process.env is its own view: Bun.$ and node:child_process read it, but Bun.spawn
|
|
123
|
+
// without an explicit env inherits the OS environ, which also still holds deleted keys because
|
|
124
|
+
// `delete process.env.X` does not unsetenv under Bun. installShellCapture reads these flags and
|
|
125
|
+
// pins the worker's environment view for such children (see worker-shell-capture.js).
|
|
126
|
+
globalThis.__senpi_session_env_deletions__ = deleted;
|
|
127
|
+
globalThis.__senpi_session_env_applied__ = applied.length > 0 || deleted.length > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
function valueRepr(value) {
|
|
102
131
|
if (value === undefined) return undefined;
|
|
103
132
|
return JSON.stringify(value);
|
|
@@ -11,7 +11,7 @@ export async function awaitMaybePromise(value) {
|
|
|
11
11
|
|
|
12
12
|
export function wrapUserCode(code) {
|
|
13
13
|
const persistentCode = persistTopLevelDeclarations(code);
|
|
14
|
-
if (
|
|
14
|
+
if (scanTopLevelStatements(persistentCode).hasTopLevelReturn) return `(async () => {\n${persistentCode}\n})()`;
|
|
15
15
|
return `(async () => {\n${captureLastExpression(persistentCode)}\n})()`;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -705,7 +705,7 @@ function skipBlockComment(code, start) {
|
|
|
705
705
|
}
|
|
706
706
|
|
|
707
707
|
function captureLastExpression(code) {
|
|
708
|
-
const start =
|
|
708
|
+
const start = scanTopLevelStatements(code).lastStatementStart;
|
|
709
709
|
const head = code.slice(0, start);
|
|
710
710
|
const tail = code.slice(start).trim();
|
|
711
711
|
if (!tail || isStatementOnly(tail)) return code;
|
|
@@ -726,7 +726,7 @@ function isStatementOnly(source) {
|
|
|
726
726
|
|
|
727
727
|
const CONTROL_PAREN_KEYWORDS = new Set(["catch", "for", "if", "switch", "while", "with"]);
|
|
728
728
|
|
|
729
|
-
function
|
|
729
|
+
function scanTopLevelStatements(code) {
|
|
730
730
|
let start = 0;
|
|
731
731
|
let round = 0;
|
|
732
732
|
let square = 0;
|
|
@@ -734,6 +734,7 @@ function findLastTopLevelStatementStart(code) {
|
|
|
734
734
|
let canStartRegex = true;
|
|
735
735
|
let lastSignificant = "";
|
|
736
736
|
let pendingControlParen = false;
|
|
737
|
+
let hasTopLevelReturn = false;
|
|
737
738
|
const controlParens = [];
|
|
738
739
|
for (let index = 0; index < code.length; index += 1) {
|
|
739
740
|
const char = code[index];
|
|
@@ -763,8 +764,11 @@ function findLastTopLevelStatementStart(code) {
|
|
|
763
764
|
if (isIdentifierStart(char)) {
|
|
764
765
|
const end = readIdentifier(code, index);
|
|
765
766
|
const token = code.slice(index, end);
|
|
766
|
-
|
|
767
|
-
|
|
767
|
+
const isPropertyName = lastSignificant === ".";
|
|
768
|
+
const atTopLevel = round === 0 && square === 0 && curly === 0;
|
|
769
|
+
if (token === "return" && atTopLevel && !isPropertyName) hasTopLevelReturn = true;
|
|
770
|
+
canStartRegex = !isPropertyName && REGEX_PREFIX_KEYWORDS.has(token);
|
|
771
|
+
pendingControlParen = !isPropertyName && CONTROL_PAREN_KEYWORDS.has(token);
|
|
768
772
|
lastSignificant = code[end - 1];
|
|
769
773
|
index = end - 1;
|
|
770
774
|
continue;
|
|
@@ -828,7 +832,7 @@ function findLastTopLevelStatementStart(code) {
|
|
|
828
832
|
pendingControlParen = false;
|
|
829
833
|
}
|
|
830
834
|
}
|
|
831
|
-
return start;
|
|
835
|
+
return { lastStatementStart: start, hasTopLevelReturn };
|
|
832
836
|
}
|
|
833
837
|
|
|
834
838
|
const STATEMENT_CONTINUATION_KEYWORDS = new Set(["catch", "else", "finally"]);
|
|
@@ -17,3 +17,19 @@ export interface ShellCaptureOptions {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function installShellCapture(options: ShellCaptureOptions): ShellCaptureRestore;
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
/**
|
|
23
|
+
* Set by the JS worker core when applying the session environment deleted inherited
|
|
24
|
+
* `PI_*` keys (see worker-core.js). Under Bun a `delete process.env.X` does not
|
|
25
|
+
* unsetenv, so shell capture pins the worker's environment view for spawned children
|
|
26
|
+
* while this list is non-empty.
|
|
27
|
+
*/
|
|
28
|
+
var __senpi_session_env_deletions__: string[] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Set by the JS worker core once a session environment was applied (values set or inherited
|
|
31
|
+
* keys deleted). Bun.spawn without an explicit env inherits the OS environ rather than the
|
|
32
|
+
* worker's process.env, so shell capture pins the worker's view whenever this is true.
|
|
33
|
+
*/
|
|
34
|
+
var __senpi_session_env_applied__: boolean | undefined;
|
|
35
|
+
}
|
|
@@ -13,11 +13,23 @@ export function installShellCapture(options) {
|
|
|
13
13
|
if (!isBunRuntime(bun)) return () => {};
|
|
14
14
|
const originalShell = bun.$;
|
|
15
15
|
const originalSpawn = bun.spawn;
|
|
16
|
+
const originalSpawnSync = typeof bun.spawnSync === "function" ? bun.spawnSync : null;
|
|
17
|
+
const deletedKeys = globalThis.__senpi_session_env_deletions__;
|
|
18
|
+
const pinEnv =
|
|
19
|
+
globalThis.__senpi_session_env_applied__ === true || (Array.isArray(deletedKeys) && deletedKeys.length > 0);
|
|
20
|
+
if (pinEnv && typeof originalShell.env === "function") {
|
|
21
|
+
// Bun.spawn without an explicit env inherits the OS environ, not the worker's process.env,
|
|
22
|
+
// and deleting from process.env does not unsetenv under Bun. Pinning the worker's
|
|
23
|
+
// environment view mirrors the bash tool, which always spawns with an explicit env.
|
|
24
|
+
originalShell.env({ ...process.env });
|
|
25
|
+
}
|
|
16
26
|
bun.$ = capturedShell(originalShell, options);
|
|
17
|
-
bun.spawn = capturedSpawn(originalSpawn, options);
|
|
27
|
+
bun.spawn = capturedSpawn(originalSpawn, options, pinEnv);
|
|
28
|
+
if (originalSpawnSync !== null) bun.spawnSync = capturedSpawnSync(originalSpawnSync, pinEnv);
|
|
18
29
|
return () => {
|
|
19
30
|
bun.$ = originalShell;
|
|
20
31
|
bun.spawn = originalSpawn;
|
|
32
|
+
if (originalSpawnSync !== null) bun.spawnSync = originalSpawnSync;
|
|
21
33
|
};
|
|
22
34
|
}
|
|
23
35
|
|
|
@@ -103,20 +115,44 @@ function outputText(value) {
|
|
|
103
115
|
return typeof value === "string" ? value : "";
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
// Bun.spawnSync inherits the OS environ the same way Bun.spawn does, so a cell calling it
|
|
119
|
+
// without an explicit env must get the worker's view pinned too (measured on Bun 1.4.0).
|
|
120
|
+
function capturedSpawnSync(originalSpawnSync, pinEnv) {
|
|
121
|
+
return (...args) => {
|
|
122
|
+
if (!pinEnv) return originalSpawnSync(...args);
|
|
123
|
+
const [first, second] = args;
|
|
124
|
+
if (Array.isArray(first)) {
|
|
125
|
+
const spawnOptions = second === undefined ? {} : second;
|
|
126
|
+
if (spawnOptions === null || typeof spawnOptions !== "object" || spawnOptions.env !== undefined)
|
|
127
|
+
return originalSpawnSync(...args);
|
|
128
|
+
return originalSpawnSync(first, { ...spawnOptions, env: { ...process.env } });
|
|
129
|
+
}
|
|
130
|
+
if (first !== null && typeof first === "object" && first.env === undefined)
|
|
131
|
+
return originalSpawnSync({ ...first, env: { ...process.env } });
|
|
132
|
+
return originalSpawnSync(...args);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function capturedSpawn(originalSpawn, options, pinEnv) {
|
|
107
137
|
return (...args) => {
|
|
108
138
|
if (!options.isActive()) return originalSpawn(...args);
|
|
109
139
|
const [first, second] = args;
|
|
110
140
|
let child;
|
|
111
141
|
if (Array.isArray(first)) {
|
|
112
142
|
const spawnOptions = second === undefined ? {} : second;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
const effective = pinEnv && spawnOptions.env === undefined ? { ...spawnOptions, env: { ...process.env } } : spawnOptions;
|
|
144
|
+
child = needsStderrCapture(effective)
|
|
145
|
+
? drainStderr(originalSpawn(first, { ...effective, stderr: "pipe" }), options.emitText)
|
|
146
|
+
: effective === spawnOptions
|
|
147
|
+
? originalSpawn(...args)
|
|
148
|
+
: originalSpawn(first, effective);
|
|
116
149
|
} else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
const effective = pinEnv && first !== null && typeof first === "object" && first.env === undefined ? { ...first, env: { ...process.env } } : first;
|
|
151
|
+
child = needsStderrCapture(effective)
|
|
152
|
+
? drainStderr(originalSpawn({ ...effective, stderr: "pipe" }), options.emitText)
|
|
153
|
+
: effective === first
|
|
154
|
+
? originalSpawn(...args)
|
|
155
|
+
: originalSpawn(effective);
|
|
120
156
|
}
|
|
121
157
|
options.onChild?.(child);
|
|
122
158
|
return child;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
import type { KernelSpawnProcess } from "./process.ts";
|
|
3
4
|
import type { PythonTransportResult } from "./transport.ts";
|
|
4
5
|
|
|
@@ -8,6 +9,8 @@ export interface PythonKernelStartOptions {
|
|
|
8
9
|
readonly cwd: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
10
11
|
readonly env?: NodeJS.ProcessEnv;
|
|
12
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
13
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
11
14
|
readonly startupTimeoutMs?: number;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
13
16
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isKernelToHostMessage,
|
|
9
9
|
type KernelToHostMessage,
|
|
10
10
|
} from "../../bridge/protocol.ts";
|
|
11
|
+
import { applySessionEnvironment, type SessionEnvironment } from "../session-env.ts";
|
|
11
12
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
12
13
|
import {
|
|
13
14
|
defaultSpawn,
|
|
@@ -36,6 +37,8 @@ export interface PythonTransportOptions {
|
|
|
36
37
|
readonly cwd: string;
|
|
37
38
|
readonly connection: BridgeConnectionConfig;
|
|
38
39
|
readonly env?: NodeJS.ProcessEnv;
|
|
40
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
41
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
39
42
|
readonly startupTimeoutMs: number;
|
|
40
43
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
41
44
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
@@ -83,7 +86,12 @@ export class PythonKernelTransport {
|
|
|
83
86
|
command: invocation.command,
|
|
84
87
|
args: [...invocation.args, "-u", scriptPath],
|
|
85
88
|
cwd: options.cwd,
|
|
86
|
-
env: {
|
|
89
|
+
env: {
|
|
90
|
+
...applySessionEnvironment(process.env, options.sessionEnv),
|
|
91
|
+
...options.env,
|
|
92
|
+
PYTHONUNBUFFERED: "1",
|
|
93
|
+
PYTHONIOENCODING: "utf-8",
|
|
94
|
+
},
|
|
87
95
|
};
|
|
88
96
|
const child = (options.spawnProcess ?? defaultSpawn)(spawnOptions);
|
|
89
97
|
const transport = new PythonKernelTransport(options, child);
|
package/src/kernels/rb/kernel.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
3
4
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
5
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
6
|
|
|
@@ -7,6 +8,8 @@ export interface RubyKernelStartOptions {
|
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly sessionId: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
11
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
12
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
10
13
|
readonly command?: string;
|
|
11
14
|
readonly spawn?: SubprocessSpawn;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
@@ -31,6 +34,7 @@ export class RubyKernel extends SubprocessKernel {
|
|
|
31
34
|
args: [resolveRubyRunnerPath()],
|
|
32
35
|
cwd: options.cwd,
|
|
33
36
|
sessionId: options.sessionId,
|
|
37
|
+
sessionEnv: options.sessionEnv,
|
|
34
38
|
connection: options.connection,
|
|
35
39
|
spawn: options.spawn,
|
|
36
40
|
onMessage: options.onMessage,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session environment exposed to eval kernels and every child they spawn.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the shell-tool session environment contract in the senpi core
|
|
5
|
+
* (`resolveSpawnContext` in `packages/coding-agent/src/core/tools/bash.ts` and
|
|
6
|
+
* `docs/environment-variables.md`): the per-session `PI_*` variables are
|
|
7
|
+
* deleted from the inherited environment first, then set from the active
|
|
8
|
+
* session, so a stale inherited value never leaks into a kernel child. A child
|
|
9
|
+
* spawned from an eval cell must see the same session environment a child
|
|
10
|
+
* spawned from the bash tool sees.
|
|
11
|
+
*/
|
|
12
|
+
export const SESSION_ENVIRONMENT_KEYS = [
|
|
13
|
+
"PI_SESSION_ID",
|
|
14
|
+
"PI_SESSION_FILE",
|
|
15
|
+
"PI_PROVIDER",
|
|
16
|
+
"PI_MODEL",
|
|
17
|
+
"PI_REASONING_LEVEL",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Resolved per-session values for {@link SESSION_ENVIRONMENT_KEYS}; absent keys stay unset. */
|
|
21
|
+
export type SessionEnvironment = Readonly<Record<string, string>>;
|
|
22
|
+
|
|
23
|
+
/** Structural slice of `ExtensionContext` the session environment is resolved from. */
|
|
24
|
+
export interface SessionEnvironmentSource {
|
|
25
|
+
readonly sessionManager: {
|
|
26
|
+
getSessionId(): string;
|
|
27
|
+
getSessionFile(): string | undefined;
|
|
28
|
+
};
|
|
29
|
+
readonly model?: { readonly provider: string; readonly id: string } | undefined;
|
|
30
|
+
readonly thinkingLevel?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sessionEnvironmentFrom(source: SessionEnvironmentSource): SessionEnvironment {
|
|
34
|
+
const env: Record<string, string> = {};
|
|
35
|
+
env.PI_SESSION_ID = source.sessionManager.getSessionId();
|
|
36
|
+
const sessionFile = source.sessionManager.getSessionFile();
|
|
37
|
+
if (sessionFile) env.PI_SESSION_FILE = sessionFile;
|
|
38
|
+
const model = source.model;
|
|
39
|
+
if (model) {
|
|
40
|
+
env.PI_PROVIDER = model.provider;
|
|
41
|
+
env.PI_MODEL = model.id;
|
|
42
|
+
}
|
|
43
|
+
if (source.thinkingLevel) env.PI_REASONING_LEVEL = source.thinkingLevel;
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Merges a session environment over a base environment the way the bash tool
|
|
49
|
+
* does: every {@link SESSION_ENVIRONMENT_KEYS} entry is dropped from `base`
|
|
50
|
+
* first, then the provided session values are applied.
|
|
51
|
+
*/
|
|
52
|
+
export function applySessionEnvironment(base: NodeJS.ProcessEnv, sessionEnv?: SessionEnvironment): NodeJS.ProcessEnv {
|
|
53
|
+
const merged: NodeJS.ProcessEnv = { ...base };
|
|
54
|
+
for (const key of SESSION_ENVIRONMENT_KEYS) delete merged[key];
|
|
55
|
+
return { ...merged, ...sessionEnv };
|
|
56
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
import type { SubprocessSpawn } from "./subprocess-process.ts";
|
|
3
4
|
|
|
4
5
|
export interface KernelRunInput {
|
|
@@ -15,6 +16,8 @@ export interface SubprocessKernelOptions {
|
|
|
15
16
|
readonly args: readonly string[];
|
|
16
17
|
readonly cwd?: string;
|
|
17
18
|
readonly env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
20
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
18
21
|
readonly sessionId: string;
|
|
19
22
|
readonly connection: BridgeConnectionConfig;
|
|
20
23
|
readonly spawn?: SubprocessSpawn;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
+
import { applySessionEnvironment } from "../session-env.ts";
|
|
4
5
|
import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
|
|
5
6
|
import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
|
|
6
7
|
import { SubprocessRunQueue } from "./subprocess-queue.ts";
|
|
@@ -133,7 +134,14 @@ export class SubprocessKernel {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
private spawnProcess(): void {
|
|
136
|
-
const child = spawnSubprocess(this.options.spawn,
|
|
137
|
+
const child = spawnSubprocess(this.options.spawn, {
|
|
138
|
+
...this.options,
|
|
139
|
+
env:
|
|
140
|
+
this.options.env ??
|
|
141
|
+
(this.options.sessionEnv
|
|
142
|
+
? applySessionEnvironment(globalThis.process.env, this.options.sessionEnv)
|
|
143
|
+
: undefined),
|
|
144
|
+
});
|
|
137
145
|
const process = new SubprocessProcess(child, {
|
|
138
146
|
onLine: (source, line) => this.handleLine(source, line),
|
|
139
147
|
onStderr: (source, data) => this.handleMessage(source, { type: "text", stream: "stderr", data }),
|
|
@@ -28,12 +28,14 @@ export function resolveSessionArtifactsDir(sessionFile: string | undefined): Ses
|
|
|
28
28
|
|
|
29
29
|
export interface TruncationMeta {
|
|
30
30
|
readonly direction: "head" | "tail" | "middle";
|
|
31
|
-
readonly truncatedBy: "lines" | "bytes" | "middle";
|
|
31
|
+
readonly truncatedBy: "lines" | "bytes" | "columns" | "middle";
|
|
32
32
|
readonly totalLines: number;
|
|
33
33
|
readonly totalBytes: number;
|
|
34
34
|
readonly outputLines: number;
|
|
35
35
|
readonly outputBytes: number;
|
|
36
36
|
readonly maxBytes?: number;
|
|
37
|
+
readonly maxColumns?: number;
|
|
38
|
+
readonly columnTruncatedLines?: number;
|
|
37
39
|
readonly shownRange?: { readonly start: number; readonly end: number };
|
|
38
40
|
readonly headRange?: { readonly start: number; readonly end: number };
|
|
39
41
|
readonly tailRange?: { readonly start: number; readonly end: number };
|
|
@@ -72,7 +74,7 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
|
|
|
72
74
|
meta.shownRange !== undefined && meta.shownRange.end >= meta.shownRange.start
|
|
73
75
|
? `Showing lines ${meta.shownRange.start}-${meta.shownRange.end} of ${meta.totalLines}`
|
|
74
76
|
: `Showing ${meta.outputLines} of ${meta.totalLines} lines`;
|
|
75
|
-
|
|
77
|
+
message += formatByteLoss(meta);
|
|
76
78
|
break;
|
|
77
79
|
default:
|
|
78
80
|
return assertNever(meta.direction);
|
|
@@ -81,6 +83,28 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
|
|
|
81
83
|
return `[${message}]`;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function formatByteLoss(meta: TruncationMeta): string {
|
|
87
|
+
const dropped = formatBytes(Math.max(0, meta.totalBytes - meta.outputBytes));
|
|
88
|
+
switch (meta.truncatedBy) {
|
|
89
|
+
case "columns": {
|
|
90
|
+
const clamped = meta.columnTruncatedLines ?? 0;
|
|
91
|
+
const width = meta.maxColumns === undefined ? "the column cap" : `${meta.maxColumns} columns`;
|
|
92
|
+
return `; ${clamped} line${clamped === 1 ? "" : "s"} clamped to ${width} (${dropped} dropped)`;
|
|
93
|
+
}
|
|
94
|
+
case "bytes":
|
|
95
|
+
return meta.maxBytes === undefined ? ` (${dropped} dropped)` : ` (${formatBytes(meta.maxBytes)} limit)`;
|
|
96
|
+
case "lines":
|
|
97
|
+
case "middle":
|
|
98
|
+
return "";
|
|
99
|
+
default:
|
|
100
|
+
return assertNeverCause(meta.truncatedBy);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertNeverCause(value: never): never {
|
|
105
|
+
throw new TypeError(`Unhandled truncation cause: ${String(value)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
export function stripOutputNotice(text: string, meta: TruncationMeta | undefined): string {
|
|
85
109
|
const notice = formatTruncationWarning(meta);
|
|
86
110
|
if (notice === null) return text;
|
|
@@ -70,29 +70,22 @@ const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
|
70
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
71
|
|
|
72
72
|
{{#if styleClaude}}<eval_first_batching>
|
|
73
|
-
|
|
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.
|
|
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.
|
|
76
|
-
- Post-process \`tool.<name>()\` results programmatically — filter, join, aggregate — and return distilled facts, not raw dumps.
|
|
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.
|
|
77
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}}
|
|
78
75
|
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
79
|
-
GPT eval:
|
|
76
|
+
GPT eval: batch a step's independent tool calls in one cell with \`tool.<name>(args)\` and \`parallel(thunks)\` and inspect every result.
|
|
80
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.
|
|
81
78
|
{{/if}}- Long cells detach on timeout and notify on completion; do not poll or re-run them.
|
|
82
|
-
-
|
|
83
|
-
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route
|
|
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
|
|
85
|
-
- Wrap failable calls in try/except inside the cell
|
|
86
|
-
-
|
|
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}}
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
{{#if monitor}}-
|
|
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.
|
|
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.
|
|
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}}
|
|
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}}
|
|
96
89
|
{{#if hostLine}}
|
|
97
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.
|
|
98
91
|
{{/if}}
|
|
@@ -200,12 +193,12 @@ const GPT_MONITOR_BATCHING_GUIDELINE =
|
|
|
200
193
|
|
|
201
194
|
const BATCHING_GUIDELINES: Record<EvalEmphasisStyle, string> = {
|
|
202
195
|
default:
|
|
203
|
-
"
|
|
196
|
+
"Prefer eval when a step's calls are independent: one cell runs them together and keeps every failure in its result; edits and result-dependent calls go one at a time, each observed before the next.",
|
|
204
197
|
claude:
|
|
205
|
-
"Prefer eval for
|
|
206
|
-
codex: "Route
|
|
207
|
-
gpt: "Use eval to
|
|
208
|
-
kimi: "
|
|
198
|
+
"Prefer eval for a step's independent calls: one cell runs them together and keeps every failure in its result.",
|
|
199
|
+
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 timeout and notify on completion, so do not poll.",
|
|
201
|
+
kimi: "Put a step's independent calls into one eval cell with parallel(thunks) and keep every failed item in the result.",
|
|
209
202
|
};
|
|
210
203
|
|
|
211
204
|
function renderTemplate(template: string, context: Context): string {
|
package/src/tool/image.ts
CHANGED
|
@@ -119,7 +119,7 @@ export class EvalOutputCollector {
|
|
|
119
119
|
async finish(): Promise<EvalOutputResult> {
|
|
120
120
|
await this.#processImages();
|
|
121
121
|
const summary = await this.#finalSummary();
|
|
122
|
-
const meta = truncationMetaFromSummary(summary);
|
|
122
|
+
const meta = truncationMetaFromSummary(summary, this.#options.maxColumns);
|
|
123
123
|
const notice = summary.artifactId === undefined ? undefined : artifactNotice(summary.artifactId);
|
|
124
124
|
return {
|
|
125
125
|
output: summary.output.trimEnd(),
|
|
@@ -215,7 +215,7 @@ function formatDisplayJson(value: unknown): string {
|
|
|
215
215
|
return `${text.slice(0, MAX_DISPLAY_TEXT_BYTES)}\n[…${text.length - MAX_DISPLAY_TEXT_BYTES}ch elided…]`;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function truncationMetaFromSummary(summary: OutputSummary): TruncationMeta | undefined {
|
|
218
|
+
function truncationMetaFromSummary(summary: OutputSummary, maxColumns: number): TruncationMeta | undefined {
|
|
219
219
|
if (!summary.truncated) return undefined;
|
|
220
220
|
const artifact = summary.artifactId === undefined ? {} : { artifactId: summary.artifactId };
|
|
221
221
|
if (summary.elidedBytes !== undefined && summary.elidedBytes > 0) {
|
|
@@ -239,9 +239,18 @@ function truncationMetaFromSummary(summary: OutputSummary): TruncationMeta | und
|
|
|
239
239
|
...artifact,
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
+
const droppedBytes = Math.max(0, summary.totalBytes - summary.outputBytes);
|
|
243
|
+
const clampedLines = summary.columnTruncatedLines ?? 0;
|
|
244
|
+
const columnOnly = clampedLines > 0 && (summary.columnDroppedBytes ?? 0) >= droppedBytes;
|
|
245
|
+
const byteCapped = summary.totalBytes - (summary.columnDroppedBytes ?? 0) > DEFAULT_MAX_BYTES;
|
|
242
246
|
return {
|
|
243
247
|
direction: "tail",
|
|
244
|
-
truncatedBy:
|
|
248
|
+
truncatedBy: columnOnly ? "columns" : byteCapped ? "bytes" : "lines",
|
|
249
|
+
...(columnOnly
|
|
250
|
+
? { maxColumns, columnTruncatedLines: clampedLines }
|
|
251
|
+
: byteCapped
|
|
252
|
+
? { maxBytes: DEFAULT_MAX_BYTES }
|
|
253
|
+
: {}),
|
|
245
254
|
totalLines: summary.totalLines,
|
|
246
255
|
totalBytes: summary.totalBytes,
|
|
247
256
|
outputLines: summary.outputLines,
|