@code-yeongyu/senpi 2026.11.15-2 → 2026.11.15-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bundle/chunks/{chunk-Q4RJ4WLE.js → chunk-225TDKTA.js} +4 -4
  3. package/dist/bundle/chunks/{chunk-QSU2456D.js → chunk-3KR3CDGM.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-AOTHPTMU.js → chunk-5GCZFK4X.js} +2 -2
  5. package/dist/bundle/chunks/{chunk-BMADOJHZ.js → chunk-5QIFH624.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-THYIV26N.js → chunk-GQEXLLWJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-3RILB2OD.js → chunk-OJOPOUAY.js} +9 -9
  8. package/dist/bundle/chunks/{chunk-64FJ4QQJ.js → chunk-PPCAJQDJ.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-74HCKLRH.js → chunk-TEXE4HT6.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-RRFDLGX6.js → chunk-TNYFCGI7.js} +156 -156
  11. package/dist/bundle/chunks/{cli-main-GNQY2LTU.js → cli-main-CV3ODX4A.js} +1 -1
  12. package/dist/bundle/chunks/{hand-entry-D3DNH6XL.js → hand-entry-2NN7ZTY6.js} +1 -1
  13. package/dist/bundle/chunks/{interactive-mode-ATBPY4M5.js → interactive-mode-TGHZWM2T.js} +1 -1
  14. package/dist/bundle/chunks/{main-OMFVSMUR.js → main-B7WEY4JW.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-ACQ5N4UB.js → multi-session-host-53YLL2OD.js} +1 -1
  16. package/dist/bundle/chunks/{package-manager-cli-2VMRV4HL.js → package-manager-cli-2U54AVKW.js} +1 -1
  17. package/dist/bundle/chunks/{role-BTH5PVME.js → role-4IXZZAL4.js} +1 -1
  18. package/dist/bundle/chunks/{rpc-mode-XUK5SYKH.js → rpc-mode-RRYXENML.js} +1 -1
  19. package/dist/bundle/chunks/{runtime-session-VO3X4OE3.js → runtime-session-KKHEYZOH.js} +1 -1
  20. package/dist/bundle/chunks/{session-control-endpoint-Y5EIFFPY.js → session-control-endpoint-HVULSIUR.js} +1 -1
  21. package/dist/bundle/chunks/session-worker.js +331 -331
  22. package/dist/bundle/cli.js +2 -2
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/bundle/runtime-manifest.json +1 -1
  26. package/dist/core/agent-session.d.ts +6 -0
  27. package/dist/core/agent-session.js +34 -3
  28. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -0
  29. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  30. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  31. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +22 -4
  32. package/dist/core/extensions/builtin/anthropic-subscription/index.js +7 -2
  33. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.d.ts +15 -0
  34. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.js +55 -0
  35. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.d.ts +1 -0
  36. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +1 -0
  37. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +8 -5
  38. package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -1
  39. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +3 -1
  40. package/dist/core/extensions/builtin/terminal/extension-state.js +15 -0
  41. package/dist/core/extensions/builtin/terminal/notify.d.ts +2 -1
  42. package/dist/core/extensions/builtin/terminal/notify.js +15 -6
  43. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +5 -1
  44. package/dist/core/extensions/builtin/terminal/session-bundle.js +41 -1
  45. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +4 -0
  46. package/dist/core/extensions/loader.js +5 -0
  47. package/dist/core/extensions/runner.d.ts +1 -0
  48. package/dist/core/extensions/runner.js +5 -0
  49. package/dist/core/extensions/types.d.ts +7 -0
  50. package/dist/core/run-background-jobs.d.ts +33 -0
  51. package/dist/core/run-background-jobs.js +47 -0
  52. package/dist/core/tools/bash-core.d.ts +50 -0
  53. package/dist/core/tools/bash-core.js +169 -0
  54. package/dist/core/tools/bash-schema.d.ts +48 -0
  55. package/dist/core/tools/bash-schema.js +34 -0
  56. package/dist/core/tools/bash.d.ts +4 -70
  57. package/dist/core/tools/bash.js +32 -409
  58. package/dist/core/tools/coding-tools-core.d.ts +24 -0
  59. package/dist/core/tools/coding-tools-core.js +14 -0
  60. package/dist/core/tools/edit-core.d.ts +56 -0
  61. package/dist/core/tools/edit-core.js +175 -0
  62. package/dist/core/tools/edit-diff.d.ts +0 -5
  63. package/dist/core/tools/edit-diff.js +0 -32
  64. package/dist/core/tools/edit-preview.d.ts +7 -0
  65. package/dist/core/tools/edit-preview.js +34 -0
  66. package/dist/core/tools/edit.d.ts +5 -43
  67. package/dist/core/tools/edit.js +10 -158
  68. package/dist/core/tools/encoding.d.ts +10 -0
  69. package/dist/core/tools/encoding.js +58 -0
  70. package/dist/core/tools/file-tool-host.d.ts +25 -0
  71. package/dist/core/tools/file-tool-host.js +109 -0
  72. package/dist/core/tools/fs-error-code.d.ts +2 -0
  73. package/dist/core/tools/fs-error-code.js +15 -0
  74. package/dist/core/tools/machine-output-spill.d.ts +11 -0
  75. package/dist/core/tools/machine-output-spill.js +65 -0
  76. package/dist/core/tools/machine.d.ts +4 -24
  77. package/dist/core/tools/machine.js +4 -44
  78. package/dist/core/tools/output-accumulator-core.d.ts +84 -0
  79. package/dist/core/tools/output-accumulator-core.js +200 -0
  80. package/dist/core/tools/output-accumulator.d.ts +9 -61
  81. package/dist/core/tools/output-accumulator.js +90 -265
  82. package/dist/core/tools/path-utils.js +3 -48
  83. package/dist/core/tools/posix-signals.d.ts +3 -0
  84. package/dist/core/tools/posix-signals.js +113 -0
  85. package/dist/core/tools/read-core.d.ts +64 -0
  86. package/dist/core/tools/read-core.js +180 -0
  87. package/dist/core/tools/read-text.d.ts +23 -0
  88. package/dist/core/tools/read-text.js +90 -0
  89. package/dist/core/tools/read.d.ts +6 -47
  90. package/dist/core/tools/read.js +13 -218
  91. package/dist/core/tools/renderers/bash.d.ts +0 -1
  92. package/dist/core/tools/renderers/bash.js +0 -1
  93. package/dist/core/tools/renderers/edit.d.ts +1 -1
  94. package/dist/core/tools/renderers/edit.js +1 -1
  95. package/dist/core/tools/secret-output-redactor.js +2 -1
  96. package/dist/core/tools/shell-operations.d.ts +53 -0
  97. package/dist/core/tools/shell-operations.js +115 -0
  98. package/dist/core/tools/shell-output.d.ts +20 -0
  99. package/dist/core/tools/shell-output.js +152 -0
  100. package/dist/core/tools/tail-window.js +5 -7
  101. package/dist/core/tools/tool-machine.d.ts +30 -0
  102. package/dist/core/tools/tool-machine.js +47 -0
  103. package/dist/core/tools/truncate.js +15 -14
  104. package/dist/core/tools/write-core.d.ts +47 -0
  105. package/dist/core/tools/write-core.js +108 -0
  106. package/dist/core/tools/write-result.d.ts +0 -1
  107. package/dist/core/tools/write-result.js +0 -13
  108. package/dist/core/tools/write.d.ts +6 -31
  109. package/dist/core/tools/write.js +25 -77
  110. package/dist/core/turn-origin.d.ts +4 -2
  111. package/dist/core/turn-origin.js +15 -8
  112. package/dist/modes/interactive/session-control-session-commands.js +1 -1
  113. package/dist/modes/rpc/connection-handler.js +2 -2
  114. package/dist/modes/rpc/effect-commands.d.ts +1 -4
  115. package/dist/modes/rpc/method-classes-schema-session.d.ts +1 -0
  116. package/dist/modes/rpc/method-classes-schema-session.js +5 -1
  117. package/dist/modes/rpc/method-classes-schema.d.ts +1 -0
  118. package/dist/modes/rpc/pause-command-validation.js +2 -0
  119. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +2 -4
  120. package/dist/modes/rpc/rpc-session-command-types.d.ts +1 -0
  121. package/dist/modes/rpc/session-control-actions-data.d.ts +7 -3
  122. package/dist/modes/rpc/session-control-actions-data.js +12 -6
  123. package/dist/tools-core.d.ts +14 -0
  124. package/dist/tools-core.js +9 -0
  125. package/dist/utils/image-mime.d.ts +4 -0
  126. package/dist/utils/image-mime.js +99 -0
  127. package/dist/utils/mime.d.ts +1 -1
  128. package/dist/utils/mime.js +2 -97
  129. package/docs/extensions.md +7 -1
  130. package/docs/rpc.md +14 -0
  131. package/package.json +11 -7
  132. package/vendor/senpi-machine-ops/files/local-files.js +34 -5
@@ -1,147 +1,41 @@
1
- // allow: SIZE_OK - pre-existing cohesive shell integration; this patch only hardens its output-finalization seam.
2
1
  import { constants } from "node:fs";
3
2
  import { access as fsAccess } from "node:fs/promises";
4
3
  import { constants as osConstants } from "node:os";
5
- import { Type } from "typebox";
6
4
  import { annotateShellTrust } from "../../engine-trust/shell-trust.js";
7
5
  import { getShellConfig, getShellEnv, noteDetachedChildExited, pruneTrackedDetachedChildren, trackDetachedChildPid, } from "../../utils/shell.js";
6
+ import { createShellToolCore } from "./bash-core.js";
7
+ import { bashToolConfig, } from "./bash-schema.js";
8
8
  import { defaultToolMachine } from "./machine.js";
9
- import { modelOnlyText } from "./model-only-text.js";
10
- import { OutputAccumulator } from "./output-accumulator.js";
11
- import { SecretOutputRedactor } from "./secret-output-redactor.js";
12
- import { BASH_UPDATE_THROTTLE_MS, createShellRenderers } from "./renderers/bash.js";
9
+ import { createTempFileSpill } from "./output-accumulator.js";
10
+ import { createShellRenderers } from "./renderers/bash.js";
11
+ import { createShellOperations } from "./shell-operations.js";
13
12
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
14
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "./truncate.js";
15
- const MAX_TIMEOUT_MS = 2_147_483_647;
16
- /** Output limit of `structuredContent.output`, which programmatic callers such as codemode scripts receive. */
17
- const STRUCTURED_OUTPUT_MAX_BYTES = 1024 * 1024;
18
- const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
19
- function resolveTimeoutMs(timeout) {
20
- if (timeout === undefined)
21
- return undefined;
22
- if (!Number.isFinite(timeout) || timeout <= 0) {
23
- throw new Error("Invalid timeout: must be a finite number of seconds");
24
- }
25
- const timeoutMs = timeout * 1000;
26
- if (timeoutMs > MAX_TIMEOUT_MS) {
27
- throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
28
- }
29
- return timeoutMs;
30
- }
31
- const bashSchema = Type.Object({
32
- command: Type.String({ description: "Shell command to execute" }),
33
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
34
- });
35
- export const bashToolSystemPromptContribution = {
36
- snippet: "Execute bash commands (ls, rg, find, etc.)",
37
- guidelines: ["PI_* env vars = current model + session details."],
38
- fullGuidelines: ["You can inspect PI_* environment variables for current model and session details."],
39
- };
40
- /**
41
- * Result for programmatic callers such as codemode scripts. A non-zero exit code is an error result for the model, but scripts still resolve to this value.
42
- * `output` is not limited like the model-facing output: callers decide how much of it reaches the model.
43
- */
44
- const bashOutputSchema = Type.Object({
45
- output: Type.String({
46
- description: "Combined stdout and stderr, up to 1 MiB. Longer output keeps its first and last 512 KiB around an omission marker.",
47
- }),
48
- truncated: Type.Boolean({ description: "Whether `output` omits part of the command output" }),
49
- full_output_path: Type.Optional(Type.String({ description: "Temp file with the full output, when truncated" })),
50
- exit_code: Type.Number(),
51
- wall_time_seconds: Type.Number(),
52
- });
13
+ export { bashToolSystemPromptContribution, } from "./bash-schema.js";
53
14
  /**
54
15
  * Shared process execution used by the built-in shell tools: one `process.exec` of the operation layer (own
55
16
  * process group, scope token, abort/timeout kill the group). The tool keeps its own output handling.
56
17
  */
57
18
  export function createLocalShellOperations(shellName, resolveShellConfig, machine = defaultToolMachine()) {
58
- return {
59
- exec: async (command, cwd, { onData, signal, timeout, env }) => {
60
- const timeoutMs = resolveTimeoutMs(timeout);
61
- if (signal?.aborted) {
62
- throw new Error("aborted");
63
- }
19
+ return createShellOperations(shellName, machine, {
20
+ resolveShell: () => {
64
21
  const shellConfig = resolveShellConfig();
65
- try {
66
- await fsAccess(cwd, constants.F_OK);
67
- }
68
- catch {
69
- throw new Error(`Working directory does not exist: ${cwd}\nCannot execute ${shellName} commands.`);
70
- }
71
- let streamCallbackError;
72
- let hasStreamCallbackError = false;
73
- // A failing output callback stops the command the same way an abort does.
74
- const callbackAbort = new AbortController();
75
- const failStream = (error) => {
76
- if (hasStreamCallbackError)
77
- return;
78
- streamCallbackError = error;
79
- hasStreamCallbackError = true;
80
- callbackAbort.abort();
81
- };
82
- const handleData = (data, stream) => {
83
- if (hasStreamCallbackError)
84
- return;
85
- try {
86
- const callbackResult = onData(data, stream);
87
- if (callbackResult && typeof callbackResult.then === "function") {
88
- void Promise.resolve(callbackResult).catch(failStream);
89
- }
90
- }
91
- catch (error) {
92
- failStream(error);
93
- }
94
- };
95
- const shell = {
22
+ return {
96
23
  path: shellConfig.shell,
97
24
  args: shellConfig.args,
98
25
  commandTransport: shellConfig.commandTransport === "stdin" ? "stdin" : "argv",
99
26
  };
100
- const execSignal = signal ? AbortSignal.any([signal, callbackAbort.signal]) : callbackAbort.signal;
101
- let result;
102
- try {
103
- result = await machine.ops.process.exec({
104
- command,
105
- cwd,
106
- env: env ?? getShellEnv(),
107
- envMode: "exact",
108
- shell,
109
- timeoutMs: timeoutMs ?? Number.POSITIVE_INFINITY,
110
- onOutput: handleData,
111
- // Tracked as a process group: whatever the shell backgrounds keeps running in it, and ownership
112
- // is released only once that group is empty (senpi#1697).
113
- onSpawn: trackDetachedChildPid,
114
- onExit: noteDetachedChildExited,
115
- }, machine.context(execSignal));
116
- }
117
- catch (error) {
118
- if (hasStreamCallbackError)
119
- throw streamCallbackError;
120
- if (signal?.aborted)
121
- throw new Error("aborted");
122
- throw error;
123
- }
124
- finally {
125
- // Groups kept past their shell's exit must not accumulate across a session.
126
- pruneTrackedDetachedChildren();
127
- }
128
- if (hasStreamCallbackError) {
129
- throw streamCallbackError;
130
- }
131
- if (signal?.aborted) {
132
- throw new Error("aborted");
133
- }
134
- if (result.timedOut) {
135
- throw new Error(`timeout:${timeout}`);
136
- }
137
- // A signal-killed shell has no exit code. Use the standard shell convention so
138
- // callers do not mistake the termination for a successful command.
139
- return {
140
- exitCode: result.exitCode ??
141
- (result.signal ? 128 + (osConstants.signals[result.signal] ?? 0) : 1),
142
- };
143
27
  },
144
- };
28
+ checkCwd: (cwd) => fsAccess(cwd, constants.F_OK),
29
+ envMode: "exact",
30
+ defaultEnv: getShellEnv,
31
+ signalNumber: (signal) => osConstants.signals[signal],
32
+ // Tracked as a process group: whatever the shell backgrounds keeps running in it, and ownership
33
+ // is released only once that group is empty (senpi#1697).
34
+ onSpawn: trackDetachedChildPid,
35
+ onExit: noteDetachedChildExited,
36
+ // Groups kept past their shell's exit must not accumulate across a session.
37
+ afterExec: pruneTrackedDetachedChildren,
38
+ });
145
39
  }
146
40
  /**
147
41
  * Create bash operations using pi's built-in local shell execution backend.
@@ -165,38 +59,6 @@ const toolsViaDaemon = (tool, env = process.env) => (env.OMO_TOOLS_VIA_DAEMON ??
165
59
  .split(",")
166
60
  .map((name) => name.trim())
167
61
  .includes(tool);
168
- function resolveSpawnContext(command, cwd, spawnHook, exposeSessionEnvironment, ctx) {
169
- const env = { ...getShellEnv() };
170
- delete env.PI_SESSION_ID;
171
- delete env.PI_SESSION_FILE;
172
- delete env.PI_SESSION_CWD;
173
- delete env.PI_GOAL_STORE_FILE;
174
- delete env.PI_PROVIDER;
175
- delete env.PI_MODEL;
176
- delete env.PI_REASONING_LEVEL;
177
- delete env.OMO_BROWSER_ENGINE;
178
- if (exposeSessionEnvironment && ctx) {
179
- const model = ctx.model;
180
- env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
181
- env.PI_SESSION_CWD = ctx.cwd;
182
- if (ctx.goalStoreFile)
183
- env.PI_GOAL_STORE_FILE = ctx.goalStoreFile;
184
- const sessionFile = ctx.sessionManager.getSessionFile();
185
- if (sessionFile)
186
- env.PI_SESSION_FILE = sessionFile;
187
- if (model) {
188
- env.PI_PROVIDER = model.provider;
189
- env.PI_MODEL = model.id;
190
- }
191
- if (ctx.thinkingLevel)
192
- env.PI_REASONING_LEVEL = ctx.thinkingLevel;
193
- if (ctx.browserEngine)
194
- env.OMO_BROWSER_ENGINE = ctx.browserEngine;
195
- Object.assign(env, ctx.sessionEnv);
196
- }
197
- const baseContext = { command, cwd, env };
198
- return spawnHook ? spawnHook(baseContext) : baseContext;
199
- }
200
62
  export function createShellToolDefinition(cwd, config, options) {
201
63
  const machine = options?.machine ?? defaultToolMachine();
202
64
  // OMO_TOOLS_VIA_DAEMON=bash (default off): run on the session's device, never on this machine. The flag decides even
@@ -216,261 +78,22 @@ export function createShellToolDefinition(cwd, config, options) {
216
78
  },
217
79
  }
218
80
  : (options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath, machine }));
219
- const commandPrefix = options?.commandPrefix;
220
- const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
221
- const spawnHook = options?.spawnHook;
222
81
  return {
223
- ...(options?.operations !== undefined || spawnHook !== undefined ? { serverPinned: true } : {}),
224
- name: config.name,
225
- label: config.label,
226
- description: `Execute a ${config.shellName} command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
227
- promptSnippet: config.promptSnippet,
228
- promptGuidelines: exposeSessionEnvironment && config.promptGuidelines ? [...config.promptGuidelines] : undefined,
229
- fullPromptGuidelines: exposeSessionEnvironment && config.fullPromptGuidelines ? [...config.fullPromptGuidelines] : undefined,
230
- parameters: bashSchema,
231
- outputSchema: bashOutputSchema,
232
- constrainedSampling: { type: "json_schema", strict: "prefer" },
233
- async execute(_toolCallId, { command, timeout }, signal, onUpdate, ctx) {
234
- const admitted = machine.begin("process.exec", { command, timeout }, signal);
235
- if (admitted !== undefined)
236
- await admitted;
237
- const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
238
- const spawnContext = resolveSpawnContext(resolvedCommand, ctx?.cwd || cwd, spawnHook, exposeSessionEnvironment, ctx);
239
- const output = new OutputAccumulator({ tempFilePrefix: config.tempFilePrefix });
240
- const streams = options?.redactSecretsInOutput === true
241
- ? new Map()
242
- : undefined;
243
- let acceptingOutput = true;
244
- let updateTimer;
245
- let updateDirty = false;
246
- let lastUpdateAt = 0;
247
- let outputUpdateError;
248
- let hasOutputUpdateError = false;
249
- const emitOutputUpdate = () => {
250
- if (!onUpdate || !updateDirty || hasOutputUpdateError)
251
- return;
252
- updateDirty = false;
253
- lastUpdateAt = Date.now();
254
- const snapshot = output.snapshot({ persistIfTruncated: true });
255
- try {
256
- onUpdate({
257
- content: [{ type: "text", text: snapshot.content || "" }],
258
- details: {
259
- truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
260
- fullOutputPath: snapshot.fullOutputPath,
261
- },
262
- });
263
- }
264
- catch (error) {
265
- outputUpdateError = error;
266
- hasOutputUpdateError = true;
267
- }
268
- };
269
- const clearUpdateTimer = () => {
270
- if (updateTimer) {
271
- clearTimeout(updateTimer);
272
- updateTimer = undefined;
273
- }
274
- };
275
- const scheduleOutputUpdate = () => {
276
- if (!onUpdate)
277
- return;
278
- updateDirty = true;
279
- const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
280
- if (delay <= 0) {
281
- clearUpdateTimer();
282
- emitOutputUpdate();
283
- return;
284
- }
285
- updateTimer ??= setTimeout(() => {
286
- updateTimer = undefined;
287
- emitOutputUpdate();
288
- }, delay);
289
- };
290
- if (onUpdate) {
291
- onUpdate({ content: [], details: undefined });
292
- }
293
- const handleData = (data, stream = "stdout") => {
294
- if (!acceptingOutput)
295
- return;
296
- if (streams) {
297
- let state = streams.get(stream);
298
- if (!state) {
299
- state = { redactor: new SecretOutputRedactor(), decoder: new TextDecoder() };
300
- streams.set(stream, state);
301
- }
302
- output.appendText(state.redactor.push(state.decoder.decode(data, { stream: true })));
303
- }
304
- else
305
- output.append(data);
306
- scheduleOutputUpdate();
307
- };
308
- const finishOutput = async () => {
309
- acceptingOutput = false;
310
- clearUpdateTimer();
311
- let primaryError;
312
- let hasPrimaryError = false;
313
- let snapshot;
314
- try {
315
- if (streams) {
316
- for (const { redactor, decoder } of streams.values()) {
317
- output.appendText(redactor.push(decoder.decode()) + redactor.finish());
318
- }
319
- updateDirty = true;
320
- }
321
- output.finish();
322
- emitOutputUpdate();
323
- snapshot = output.snapshot({ persistIfTruncated: true });
324
- if (hasOutputUpdateError) {
325
- primaryError = outputUpdateError;
326
- hasPrimaryError = true;
327
- }
328
- }
329
- catch (error) {
330
- primaryError = error;
331
- hasPrimaryError = true;
332
- }
333
- try {
334
- await output.closeTempFile();
335
- }
336
- catch (closeError) {
337
- if (hasPrimaryError) {
338
- throw new AggregateError([primaryError, closeError], "Bash output finalization and spill cleanup failed");
339
- }
340
- throw closeError;
341
- }
342
- if (hasPrimaryError) {
343
- try {
344
- await output.removeTempFile();
345
- }
346
- catch (unlinkError) {
347
- if (!(unlinkError instanceof Error))
348
- throw unlinkError;
349
- throw new AggregateError([primaryError, unlinkError], "Bash output finalization and spill cleanup failed");
350
- }
351
- throw primaryError;
352
- }
353
- if (snapshot === undefined) {
354
- throw new Error("Bash output finalization produced no snapshot");
355
- }
356
- return snapshot;
357
- };
358
- const formatOutput = (snapshot, emptyText = "(no output)") => {
359
- const truncation = snapshot.truncation;
360
- const text = snapshot.content || emptyText;
361
- let notice;
362
- let details;
363
- if (truncation.truncated) {
364
- details = { truncation, fullOutputPath: snapshot.fullOutputPath };
365
- const startLine = truncation.totalLines - truncation.outputLines + 1;
366
- const endLine = truncation.totalLines;
367
- if (truncation.lastLinePartial) {
368
- const lastLineSize = formatSize(output.getLastLineBytes());
369
- notice = `[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
370
- }
371
- else if (truncation.truncatedBy === "lines") {
372
- notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
373
- }
374
- else {
375
- notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
376
- }
377
- }
378
- const content = [{ type: "text", text: notice === undefined ? text : `${text}\n` }];
379
- if (notice !== undefined)
380
- content.push(modelOnlyText(notice));
381
- return { text: notice === undefined ? text : `${text}\n\n${notice}`, content, details };
382
- };
383
- const appendStatus = (text, status) => `${text ? `${text}\n\n` : ""}${status}`;
384
- const startedAt = performance.now();
385
- try {
386
- let exitCode;
387
- try {
388
- const result = await ops.exec(spawnContext.command, spawnContext.cwd, {
389
- onData: handleData,
390
- redactSecretsInOutput: options?.redactSecretsInOutput === true,
391
- signal,
392
- timeout,
393
- env: spawnContext.env,
394
- });
395
- exitCode = result.exitCode;
396
- }
397
- catch (err) {
398
- let snapshot;
399
- try {
400
- snapshot = await finishOutput();
401
- }
402
- catch (cleanupError) {
403
- throw new AggregateError([err, cleanupError], "Bash command and output cleanup failed");
404
- }
405
- const { text } = formatOutput(snapshot, "");
406
- if (err instanceof Error && err.message === "aborted") {
407
- throw new Error(appendStatus(text, "Command aborted"));
408
- }
409
- if (err instanceof Error && err.message.startsWith("timeout:")) {
410
- const timeoutSecs = err.message.split(":")[1];
411
- throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));
412
- }
413
- try {
414
- await output.removeTempFile();
415
- }
416
- catch (unlinkError) {
417
- throw new AggregateError([err, unlinkError], "Bash command and output cleanup failed");
418
- }
419
- throw err;
420
- }
421
- const snapshot = await finishOutput();
422
- const { text: outputText, content, details } = formatOutput(snapshot);
423
- if (exitCode === null) {
424
- throw new Error(appendStatus(outputText, "Command terminated without an exit code"));
425
- }
426
- const wallTimeSeconds = Math.round((performance.now() - startedAt) / 100) / 10;
427
- const fullOutput = await output.readFullOutput(STRUCTURED_OUTPUT_MAX_BYTES);
428
- const structuredContent = {
429
- output: fullOutput.content,
430
- truncated: fullOutput.truncated,
431
- ...(fullOutput.truncated && snapshot.fullOutputPath
432
- ? { full_output_path: snapshot.fullOutputPath }
433
- : {}),
434
- exit_code: exitCode,
435
- wall_time_seconds: wallTimeSeconds,
436
- };
437
- if (exitCode !== 0) {
438
- const text = appendStatus(outputText, `Command exited with code ${exitCode}`);
439
- // The engine trust annotation (omo#10762) rides only on error results, so the success
440
- // `outputSchema` (and the prompt bytes it costs) stays unchanged.
441
- const annotation = annotateShellTrust({ exitCode, output: fullOutput.content });
442
- if (annotation === undefined) {
443
- return { content: [{ type: "text", text }], details, structuredContent, isError: true };
444
- }
445
- return {
446
- content: [
447
- { type: "text", text: annotation.text === undefined ? text : `${text}\n\n${annotation.text}` },
448
- ],
449
- details: { ...details, trust: annotation.trust },
450
- structuredContent: { ...structuredContent, trust: annotation.trust },
451
- isError: true,
452
- };
453
- }
454
- // Success keeps the fork's model-only truncation notice; the error text above carries it inline.
455
- return { content, details, structuredContent };
456
- }
457
- finally {
458
- clearUpdateTimer();
459
- }
460
- },
82
+ ...(options?.operations !== undefined || options?.spawnHook !== undefined ? { serverPinned: true } : {}),
83
+ ...createShellToolCore(cwd, config, {
84
+ machine,
85
+ operations: ops,
86
+ outputSpill: createTempFileSpill,
87
+ shellEnv: getShellEnv,
88
+ annotateTrust: annotateShellTrust,
89
+ redactSecretsInOutput: options?.redactSecretsInOutput,
90
+ commandPrefix: options?.commandPrefix,
91
+ exposeSessionEnvironment: options?.exposeSessionEnvironment,
92
+ spawnHook: options?.spawnHook,
93
+ }),
461
94
  ...createShellRenderers(config.prompt),
462
95
  };
463
96
  }
464
- const bashToolConfig = {
465
- name: "bash",
466
- label: "bash",
467
- shellName: "bash",
468
- prompt: "$",
469
- promptSnippet: bashToolSystemPromptContribution.snippet,
470
- promptGuidelines: bashToolSystemPromptContribution.guidelines,
471
- fullPromptGuidelines: bashToolSystemPromptContribution.fullGuidelines,
472
- tempFilePrefix: "pi-bash",
473
- };
474
97
  export function createBashToolDefinition(cwd, options) {
475
98
  return {
476
99
  ...createShellToolDefinition(cwd, bashToolConfig, options),
@@ -0,0 +1,24 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { type CoreBashToolOptions } from "./bash-core.ts";
3
+ import type { bashSchema } from "./bash-schema.ts";
4
+ import { type CoreEditToolOptions, type editSchema } from "./edit-core.ts";
5
+ import { type CoreReadToolOptions, type readSchema } from "./read-core.ts";
6
+ import type { ToolMachine } from "./tool-machine.ts";
7
+ import { type CoreWriteToolOptions, type writeSchema } from "./write-core.ts";
8
+ export interface CoreCodingToolsOptions {
9
+ /** The one effect channel of all four tools. */
10
+ machine: ToolMachine;
11
+ read?: Omit<CoreReadToolOptions, "machine">;
12
+ bash?: Omit<CoreBashToolOptions, "machine">;
13
+ edit?: Omit<CoreEditToolOptions, "machine">;
14
+ write?: Omit<CoreWriteToolOptions, "machine">;
15
+ }
16
+ /** read, bash, edit and write, in `createCodingTools` order, for a host whose only effect channel is `machine`. */
17
+ export type CoreCodingTools = [
18
+ AgentTool<typeof readSchema>,
19
+ AgentTool<typeof bashSchema>,
20
+ AgentTool<typeof editSchema>,
21
+ AgentTool<typeof writeSchema>
22
+ ];
23
+ export declare function createCoreCodingTools(cwd: string, options: CoreCodingToolsOptions): CoreCodingTools;
24
+ //# sourceMappingURL=coding-tools-core.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { createCoreBashTool } from "./bash-core.js";
2
+ import { createCoreEditTool } from "./edit-core.js";
3
+ import { createCoreReadTool } from "./read-core.js";
4
+ import { createCoreWriteTool } from "./write-core.js";
5
+ export function createCoreCodingTools(cwd, options) {
6
+ const { machine } = options;
7
+ return [
8
+ createCoreReadTool(cwd, { ...options.read, machine }),
9
+ createCoreBashTool(cwd, { ...options.bash, machine }),
10
+ createCoreEditTool(cwd, { ...options.edit, machine }),
11
+ createCoreWriteTool(cwd, { ...options.write, machine }),
12
+ ];
13
+ }
14
+ //# sourceMappingURL=coding-tools-core.js.map
@@ -0,0 +1,56 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { type Static, Type } from "typebox";
3
+ import type { FilesystemPolicyChecker, ToolDefinition } from "../extensions/types.ts";
4
+ import { type FileMutationQueue, type ToolPathCanonicalizer, type ToolPathResolver } from "./file-tool-host.ts";
5
+ import type { ToolMachine } from "./tool-machine.ts";
6
+ declare const editSchema: Type.TObject<{
7
+ path: Type.TString;
8
+ edits: Type.TArray<Type.TObject<{
9
+ oldText: Type.TString;
10
+ newText: Type.TString;
11
+ }>>;
12
+ }>;
13
+ export declare const editToolSystemPromptContribution: {
14
+ readonly snippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call";
15
+ readonly guidelines: readonly ["All changes to one file -> one edit call."];
16
+ readonly fullGuidelines: readonly ["Use edit for precise changes (edits[].oldText must match exactly)", "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls", "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.", "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."];
17
+ };
18
+ export type EditToolInput = Static<typeof editSchema>;
19
+ export interface EditToolDetails {
20
+ /** Display-oriented diff of the changes made */
21
+ diff: string;
22
+ /** Standard unified patch of the changes made */
23
+ patch: string;
24
+ /** Line number of the first change in the new file (for editor navigation) */
25
+ firstChangedLine?: number;
26
+ }
27
+ /**
28
+ * Pluggable operations for the edit tool.
29
+ * Override these to delegate file editing to remote systems (for example SSH).
30
+ */
31
+ export interface EditOperations {
32
+ /** Read file contents */
33
+ readFile: (absolutePath: string) => Promise<Uint8Array>;
34
+ /** Write content to a file */
35
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
36
+ /** Check if file is readable and writable (throw if not) */
37
+ access: (absolutePath: string) => Promise<void>;
38
+ }
39
+ export interface CoreEditToolOptions {
40
+ /** Operation layer and policy gate. */
41
+ machine: ToolMachine;
42
+ /** Custom operations for file editing. Default: edits through `machine` */
43
+ operations?: EditOperations;
44
+ /** Extension-registered filesystem policy checker. */
45
+ filesystemPolicy?: FilesystemPolicyChecker;
46
+ /** Default: POSIX resolution against the cwd. */
47
+ resolvePath?: ToolPathResolver;
48
+ /** Default: one queue per path string in this isolate, shared with the core write tool. */
49
+ withMutationQueue?: FileMutationQueue;
50
+ /** The path `filesystemPolicy` judges. Default: the resolved path as is. */
51
+ canonicalizePath?: ToolPathCanonicalizer;
52
+ }
53
+ export declare function createCoreEditToolDefinition(cwd: string, options: CoreEditToolOptions): ToolDefinition<typeof editSchema, EditToolDetails | undefined>;
54
+ export declare function createCoreEditTool(cwd: string, options: CoreEditToolOptions): AgentTool<typeof editSchema>;
55
+ export { editSchema };
56
+ //# sourceMappingURL=edit-core.d.ts.map