@cjhyy/code-shell-core 0.7.0 → 0.7.1
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/dist/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +10 -1
- package/dist/credentials/access.js +76 -2
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +6 -1
- package/dist/credentials/oauth.js +66 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.js +16 -7
- package/dist/credentials/types.d.ts +38 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +61 -32
- package/dist/engine/engine.js +289 -55
- package/dist/engine/goal.d.ts +5 -3
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/turn-loop.d.ts +18 -0
- package/dist/engine/turn-loop.js +202 -27
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +14 -4
- package/dist/hooks/goal-stop-hook.js +626 -71
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +290 -72
- package/dist/protocol/types.d.ts +34 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +362 -26
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
- package/dist/tool-system/builtin/background-jobs.js +15 -11
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +45 -2
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +17 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +6 -0
- package/dist/tool-system/mcp-manager.js +55 -5
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
|
@@ -10,11 +10,15 @@ export class Transcript {
|
|
|
10
10
|
events = [];
|
|
11
11
|
filePath;
|
|
12
12
|
currentTurn = 0;
|
|
13
|
+
writer;
|
|
14
|
+
dirty = false;
|
|
15
|
+
lastFlushFailure;
|
|
13
16
|
getFilePath() {
|
|
14
17
|
return this.filePath;
|
|
15
18
|
}
|
|
16
|
-
constructor(filePath) {
|
|
19
|
+
constructor(filePath, writer = appendFileSync) {
|
|
17
20
|
this.filePath = filePath;
|
|
21
|
+
this.writer = writer;
|
|
18
22
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
19
23
|
if (!existsSync(filePath)) {
|
|
20
24
|
writeFileSync(filePath, "", "utf-8");
|
|
@@ -179,18 +183,65 @@ export class Transcript {
|
|
|
179
183
|
get eventCount() {
|
|
180
184
|
return this.events.length;
|
|
181
185
|
}
|
|
186
|
+
/** True once any event failed both persistence attempts. Sticky by design. */
|
|
187
|
+
flushFailed() {
|
|
188
|
+
return this.dirty;
|
|
189
|
+
}
|
|
190
|
+
/** Structured details for the most recent unrecoverable flush failure. */
|
|
191
|
+
getFlushFailure() {
|
|
192
|
+
return this.lastFlushFailure ? { ...this.lastFlushFailure } : undefined;
|
|
193
|
+
}
|
|
182
194
|
findMessageByClientId(clientMessageId) {
|
|
183
195
|
return this.events.find((event) => event.type === "message" &&
|
|
184
196
|
event.data.clientMessageId === clientMessageId);
|
|
185
197
|
}
|
|
186
198
|
flush(event) {
|
|
199
|
+
const line = JSON.stringify(event) + "\n";
|
|
187
200
|
try {
|
|
188
|
-
|
|
201
|
+
this.writer(this.filePath, line, "utf-8");
|
|
202
|
+
return true;
|
|
189
203
|
}
|
|
190
|
-
catch {
|
|
191
|
-
|
|
204
|
+
catch (firstError) {
|
|
205
|
+
try {
|
|
206
|
+
this.writer(this.filePath, line, "utf-8");
|
|
207
|
+
logger.warn("transcript.flush_retry_recovered", {
|
|
208
|
+
filePath: this.filePath,
|
|
209
|
+
errno: this.errorErrno(firstError),
|
|
210
|
+
message: this.errorMessage(firstError),
|
|
211
|
+
});
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch (retryError) {
|
|
215
|
+
const err = retryError;
|
|
216
|
+
const failure = {
|
|
217
|
+
errno: this.errorErrno(retryError),
|
|
218
|
+
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
219
|
+
message: this.errorMessage(retryError),
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
attempts: 2,
|
|
222
|
+
recoverable: false,
|
|
223
|
+
filePath: this.filePath,
|
|
224
|
+
};
|
|
225
|
+
// Sticky: a later successful append cannot restore an earlier missing
|
|
226
|
+
// JSONL event, so the transcript remains degraded for this instance.
|
|
227
|
+
this.dirty = true;
|
|
228
|
+
this.lastFlushFailure = failure;
|
|
229
|
+
logger.error("transcript.flush_failed", { ...failure });
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
192
232
|
}
|
|
193
233
|
}
|
|
234
|
+
errorErrno(error) {
|
|
235
|
+
const err = error;
|
|
236
|
+
if (typeof err?.errno === "number" || typeof err?.errno === "string")
|
|
237
|
+
return err.errno;
|
|
238
|
+
if (typeof err?.code === "string")
|
|
239
|
+
return err.code;
|
|
240
|
+
return "UNKNOWN";
|
|
241
|
+
}
|
|
242
|
+
errorMessage(error) {
|
|
243
|
+
return error instanceof Error ? error.message : String(error);
|
|
244
|
+
}
|
|
194
245
|
/**
|
|
195
246
|
* Repair tool_result pairing issues:
|
|
196
247
|
* - Orphaned tool_results (no matching tool_use) get removed
|
|
@@ -227,6 +278,70 @@ export class Transcript {
|
|
|
227
278
|
return true;
|
|
228
279
|
});
|
|
229
280
|
}
|
|
281
|
+
static readEvents(filePath) {
|
|
282
|
+
let raw;
|
|
283
|
+
try {
|
|
284
|
+
raw = readFileSync(filePath, "utf-8");
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return { events: [], malformedLineCount: 0 };
|
|
288
|
+
}
|
|
289
|
+
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
290
|
+
const events = [];
|
|
291
|
+
let malformedLineCount = 0;
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
try {
|
|
294
|
+
events.push(JSON.parse(line));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
malformedLineCount++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { events, malformedLineCount };
|
|
301
|
+
}
|
|
302
|
+
static eventsToMessages(events) {
|
|
303
|
+
const messages = [];
|
|
304
|
+
for (const event of events) {
|
|
305
|
+
switch (event.type) {
|
|
306
|
+
case "message": {
|
|
307
|
+
const { role, content } = event.data;
|
|
308
|
+
messages.push({ role, content });
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case "tool_use":
|
|
312
|
+
break;
|
|
313
|
+
case "tool_result": {
|
|
314
|
+
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
315
|
+
const block = {
|
|
316
|
+
type: "tool_result",
|
|
317
|
+
tool_use_id: toolCallId,
|
|
318
|
+
content: error
|
|
319
|
+
? `Error: ${error}`
|
|
320
|
+
: Array.isArray(contentBlocks) && contentBlocks.length > 0
|
|
321
|
+
? contentBlocks
|
|
322
|
+
: (result ?? "(no output)"),
|
|
323
|
+
};
|
|
324
|
+
const last = messages[messages.length - 1];
|
|
325
|
+
if (last?.role === "user" && Array.isArray(last.content)) {
|
|
326
|
+
last.content.push(block);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
messages.push({ role: "user", content: [block] });
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "summary": {
|
|
334
|
+
const { summary } = event.data;
|
|
335
|
+
messages.push({
|
|
336
|
+
role: "user",
|
|
337
|
+
content: `<system-reminder>Previous conversation was summarized:\n${summary}</system-reminder>`,
|
|
338
|
+
});
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return messages;
|
|
344
|
+
}
|
|
230
345
|
static loadFromFile(filePath) {
|
|
231
346
|
const transcript = new Transcript(filePath);
|
|
232
347
|
if (!existsSync(filePath))
|
|
@@ -269,7 +269,9 @@ streamOverride) {
|
|
|
269
269
|
// that no longer exists. The child Engine is a fresh instance; plan-mode
|
|
270
270
|
// isolation between parent and child is enforced via separate Engine
|
|
271
271
|
// instances (finalized in T6).
|
|
272
|
-
const { text } = await spawner.spawn({ ...opts, streamOverride });
|
|
272
|
+
const { text, usage } = await spawner.spawn({ ...opts, streamOverride });
|
|
273
|
+
if (usage)
|
|
274
|
+
opts.recordBilledUsage?.(usage);
|
|
273
275
|
const finalText = text || `Agent completed but produced no text output.`;
|
|
274
276
|
safeEmit(startEndSink, { type: "agent_end", agentId, name, description, text: finalText, agentType });
|
|
275
277
|
emitSubAgentHook(opts.hooks, "subagent_finish", { agentId, description, text: finalText });
|
|
@@ -372,6 +374,7 @@ export async function agentTool(args, ctx) {
|
|
|
372
374
|
appendSystemPrompt: overrides.appendSystemPrompt,
|
|
373
375
|
readOnlySession: overrides.resolvedType === "researcher" || overrides.resolvedType === "explorer",
|
|
374
376
|
hooks: ctx?.hooks,
|
|
377
|
+
recordBilledUsage: ctx?.recordBilledUsage,
|
|
375
378
|
signal: controller.signal,
|
|
376
379
|
}, parentStream, // uiStream: agent_start/end → main feed
|
|
377
380
|
transcriptSink)
|
|
@@ -581,6 +584,7 @@ async function runSyncSubAgent(args) {
|
|
|
581
584
|
readOnlySession: overrides.resolvedType === "researcher" || overrides.resolvedType === "explorer",
|
|
582
585
|
resumeSessionId,
|
|
583
586
|
hooks: ctx?.hooks,
|
|
587
|
+
recordBilledUsage: ctx?.recordBilledUsage,
|
|
584
588
|
signal: syncController.signal,
|
|
585
589
|
});
|
|
586
590
|
// Sentinel that the timer resolves with, so we can tell "agent finished" from
|
|
@@ -390,6 +390,7 @@ export async function arenaTool(args, ctx) {
|
|
|
390
390
|
signal,
|
|
391
391
|
});
|
|
392
392
|
const result = await arena.run(topic);
|
|
393
|
+
ctx.recordBilledUsage?.(result.usage);
|
|
393
394
|
// Use session renderer (markdown, no ANSI) for tool results.
|
|
394
395
|
// Terminal renderer (chalk) is only for direct stdout printing.
|
|
395
396
|
const formattedResult = formatArenaResultForSession(result);
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
type Listener = () => void;
|
|
24
24
|
export type BackgroundJobStatus = "running" | "cancelling" | "completed" | "failed" | "cancelled";
|
|
25
25
|
export type BackgroundJobKind = "drive-agent" | "video" | "job";
|
|
26
|
+
export type ExternalCliKind = "claude" | "codex";
|
|
26
27
|
/** A background job, running or finished. */
|
|
27
28
|
export interface BackgroundJobEntry {
|
|
28
29
|
jobId: string;
|
|
@@ -48,11 +49,13 @@ export interface BackgroundJobEntry {
|
|
|
48
49
|
/** DriveAgent prompt summary, separate from the UI-oriented description. */
|
|
49
50
|
promptSummary?: string;
|
|
50
51
|
/** External CLI kind for DriveAgent jobs. */
|
|
51
|
-
cli?:
|
|
52
|
+
cli?: ExternalCliKind;
|
|
52
53
|
/** Client message that launched this external job. */
|
|
53
54
|
originClientMessageId?: string;
|
|
54
|
-
/** Optional cancellation hook for jobs backed by a live process.
|
|
55
|
-
|
|
55
|
+
/** Optional cancellation hook for jobs backed by a live process. It may
|
|
56
|
+
* return a richer terminal outcome when cancellation races with a result
|
|
57
|
+
* that must still be retained (for example, a remotely rendered video). */
|
|
58
|
+
abort?: () => void | BackgroundJobOutcome | Promise<void | BackgroundJobOutcome>;
|
|
56
59
|
}
|
|
57
60
|
/** Outcome passed to finish() to record how a job ended. */
|
|
58
61
|
export interface BackgroundJobOutcome {
|
|
@@ -65,9 +68,9 @@ export interface BackgroundJobStartOptions {
|
|
|
65
68
|
kind?: BackgroundJobKind;
|
|
66
69
|
cwd?: string;
|
|
67
70
|
promptSummary?: string;
|
|
68
|
-
cli?:
|
|
71
|
+
cli?: ExternalCliKind;
|
|
69
72
|
originClientMessageId?: string;
|
|
70
|
-
abort?: () => void | Promise<void>;
|
|
73
|
+
abort?: () => void | BackgroundJobOutcome | Promise<void | BackgroundJobOutcome>;
|
|
71
74
|
}
|
|
72
75
|
declare class BackgroundJobRegistry {
|
|
73
76
|
private jobs;
|
|
@@ -34,16 +34,18 @@ function isActiveStatus(status) {
|
|
|
34
34
|
async function waitForAbort(abort) {
|
|
35
35
|
let timer;
|
|
36
36
|
try {
|
|
37
|
-
await Promise.race([
|
|
37
|
+
const result = await Promise.race([
|
|
38
38
|
Promise.resolve().then(abort),
|
|
39
39
|
new Promise((resolve) => {
|
|
40
|
-
timer = setTimeout(resolve, CANCEL_WAIT_TIMEOUT_MS);
|
|
40
|
+
timer = setTimeout(() => resolve(undefined), CANCEL_WAIT_TIMEOUT_MS);
|
|
41
41
|
}),
|
|
42
42
|
]);
|
|
43
|
+
return result && typeof result === "object" ? result : undefined;
|
|
43
44
|
}
|
|
44
45
|
catch {
|
|
45
46
|
// Cancellation is best-effort; the terminal state still closes after the
|
|
46
47
|
// abort hook settles or the hard deadline expires.
|
|
48
|
+
return undefined;
|
|
47
49
|
}
|
|
48
50
|
finally {
|
|
49
51
|
if (timer)
|
|
@@ -118,20 +120,22 @@ class BackgroundJobRegistry {
|
|
|
118
120
|
// process as active until the abort hook confirms exit.
|
|
119
121
|
entry.status = "cancelling";
|
|
120
122
|
this.notify();
|
|
121
|
-
|
|
122
|
-
await waitForAbort(entry.abort);
|
|
123
|
+
const abortOutcome = entry.abort ? await waitForAbort(entry.abort) : undefined;
|
|
123
124
|
// Session teardown/reset may have removed the entry while termination was
|
|
124
125
|
// in flight. Do not resurrect or notify for a closed session.
|
|
125
126
|
if (this.jobs.get(jobId) !== entry || entry.status !== "cancelling")
|
|
126
127
|
return false;
|
|
127
|
-
entry.status = "cancelled";
|
|
128
|
+
entry.status = abortOutcome?.status ?? "cancelled";
|
|
128
129
|
entry.finishedAt = Date.now();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const finalText = abortOutcome?.finalText ?? outcome?.finalText;
|
|
131
|
+
const ccSessionId = abortOutcome?.ccSessionId ?? outcome?.ccSessionId;
|
|
132
|
+
const changedFiles = abortOutcome?.changedFiles ?? outcome?.changedFiles;
|
|
133
|
+
if (finalText !== undefined)
|
|
134
|
+
entry.finalText = finalText;
|
|
135
|
+
if (ccSessionId !== undefined)
|
|
136
|
+
entry.ccSessionId = ccSessionId;
|
|
137
|
+
if (changedFiles !== undefined)
|
|
138
|
+
entry.changedFiles = changedFiles;
|
|
135
139
|
this.evictTerminalOverCap(entry.sessionId);
|
|
136
140
|
this.notify();
|
|
137
141
|
return true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AsyncAgentStatus } from "./agent-registry.js";
|
|
2
|
-
import { type BackgroundJobStatus } from "./background-jobs.js";
|
|
2
|
+
import { type BackgroundJobKind, type BackgroundJobStatus, type ExternalCliKind } from "./background-jobs.js";
|
|
3
3
|
import { type BgShell } from "../../runtime/background-shell.js";
|
|
4
4
|
export type BackgroundWorkKind = "subagent" | "job" | "shell";
|
|
5
5
|
export interface BackgroundWorkItem {
|
|
@@ -51,6 +51,11 @@ export type BackgroundWorkEntry = WithSource<{
|
|
|
51
51
|
finalText?: string;
|
|
52
52
|
/** Files an external agent (DriveAgent) changed, parsed from its transcript. */
|
|
53
53
|
changedFiles?: string[];
|
|
54
|
+
/** Machine-readable job kind and external-session linkage for DriveAgent. */
|
|
55
|
+
jobKind?: BackgroundJobKind;
|
|
56
|
+
externalSessionId?: string;
|
|
57
|
+
cli?: ExternalCliKind;
|
|
58
|
+
cwd?: string;
|
|
54
59
|
}>;
|
|
55
60
|
/**
|
|
56
61
|
* Every background-work item spawned by `sessionId`, with per-kind detail, for
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { readFileSync, statSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { asyncAgentRegistry } from "./agent-registry.js";
|
|
17
|
-
import { backgroundJobRegistry } from "./background-jobs.js";
|
|
17
|
+
import { backgroundJobRegistry, } from "./background-jobs.js";
|
|
18
18
|
import { backgroundShellManager } from "../../runtime/background-shell.js";
|
|
19
19
|
import { codeShellHome } from "../../session/session-manager.js";
|
|
20
20
|
/** List every still-running background work item spawned by `sessionId`. */
|
|
@@ -125,6 +125,10 @@ export function listBackgroundWorkForUI(sessionId, opts = {}) {
|
|
|
125
125
|
...(j.finishedAt != null ? { finishedAt: j.finishedAt } : {}),
|
|
126
126
|
...(j.finalText != null ? { finalText: j.finalText } : {}),
|
|
127
127
|
...(j.changedFiles && j.changedFiles.length ? { changedFiles: j.changedFiles } : {}),
|
|
128
|
+
...(j.kind ? { jobKind: j.kind } : {}),
|
|
129
|
+
...(j.ccSessionId ? { externalSessionId: j.ccSessionId } : {}),
|
|
130
|
+
...(j.cli === "claude" || j.cli === "codex" ? { cli: j.cli } : {}),
|
|
131
|
+
...(j.cwd ? { cwd: j.cwd } : {}),
|
|
128
132
|
sourceSession: sourceSession(sessionId, j.sessionId),
|
|
129
133
|
});
|
|
130
134
|
}
|
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
* Killed by signal, the truncation message, sandbox denial hints).
|
|
17
17
|
*/
|
|
18
18
|
import type { ToolContext } from "../context.js";
|
|
19
|
-
import type { ToolDefinition
|
|
19
|
+
import type { ToolDefinition } from "../../types.js";
|
|
20
|
+
import type { ToolExecutionResult } from "./index.js";
|
|
20
21
|
export declare const bashToolDef: ToolDefinition;
|
|
21
|
-
export declare function bashTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string |
|
|
22
|
-
result: string;
|
|
23
|
-
sandbox: ToolResult["sandbox"];
|
|
24
|
-
}>;
|
|
22
|
+
export declare function bashTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolExecutionResult>;
|
|
@@ -82,7 +82,8 @@ export async function bashTool(args, ctx) {
|
|
|
82
82
|
const sandbox = sandboxMark(backend);
|
|
83
83
|
// Tag every textual return below with the sandbox mark so the UI badge
|
|
84
84
|
// shows up whether the command succeeded, failed, timed out, or aborted.
|
|
85
|
-
const mark = (result) => ({ result, sandbox });
|
|
85
|
+
const mark = (result) => ({ ok: true, result, sandbox });
|
|
86
|
+
const fail = (error, details = {}) => ({ ok: false, error, sandbox, ...details });
|
|
86
87
|
// Project localEnvironment.env (ctx.shellEnv) layers on top of either the
|
|
87
88
|
// full passthrough (off) or the hardened allowlist (sandboxed) — see
|
|
88
89
|
// mergeShellEnv. Background mode merges the same way inside the manager.
|
|
@@ -93,7 +94,8 @@ export async function bashTool(args, ctx) {
|
|
|
93
94
|
// blocked, and Engine.run's wait-for-background loop never waits on it
|
|
94
95
|
// (§难点5: that loop only looks at asyncAgentRegistry).
|
|
95
96
|
if (args.run_in_background === true) {
|
|
96
|
-
|
|
97
|
+
const backgroundResult = runInBackground(command, ctx);
|
|
98
|
+
return backgroundResult.startsWith("Error:") ? fail(backgroundResult) : mark(backgroundResult);
|
|
97
99
|
}
|
|
98
100
|
// A6: lifecycle (spawn, kill cascade, IO drain, byte cap, abort/timeout)
|
|
99
101
|
// is centralized in safeSpawnShell. We still handle Bash's user-facing
|
|
@@ -112,13 +114,13 @@ export async function bashTool(args, ctx) {
|
|
|
112
114
|
signal: ctx?.signal,
|
|
113
115
|
});
|
|
114
116
|
if (result.aborted) {
|
|
115
|
-
return
|
|
117
|
+
return fail(wasAbortedBeforeStart ? "Bash aborted before starting." : "Bash aborted by signal.");
|
|
116
118
|
}
|
|
117
119
|
if (result.timedOut) {
|
|
118
|
-
return
|
|
120
|
+
return fail(`Command timed out after ${timeout}ms`);
|
|
119
121
|
}
|
|
120
122
|
if (result.spawnFailed) {
|
|
121
|
-
return
|
|
123
|
+
return fail(`Failed to spawn command: ${result.error ?? "unknown error"}`);
|
|
122
124
|
}
|
|
123
125
|
let body = "";
|
|
124
126
|
if (result.stdout)
|
|
@@ -150,6 +152,9 @@ export async function bashTool(args, ctx) {
|
|
|
150
152
|
const hint = backend.hintForBlockedOutput?.(result.stderr);
|
|
151
153
|
if (hint)
|
|
152
154
|
output += hint;
|
|
155
|
+
if ((code !== 0 && code !== null) || (code === null && sig)) {
|
|
156
|
+
return fail(output, { exitCode: code, signal: sig, stderr: result.stderr });
|
|
157
|
+
}
|
|
153
158
|
return mark(output);
|
|
154
159
|
}
|
|
155
160
|
/**
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { ToolDefinition } from "../../types.js";
|
|
24
24
|
import type { ToolContext } from "../context.js";
|
|
25
|
-
import type {
|
|
25
|
+
import type { BuiltinToolReturn } from "./index.js";
|
|
26
26
|
export declare const browserObserveToolDef: ToolDefinition;
|
|
27
|
-
export declare function browserObserveTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<
|
|
27
|
+
export declare function browserObserveTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<BuiltinToolReturn>;
|
|
28
28
|
export declare const browserActToolDef: ToolDefinition;
|
|
29
29
|
export declare function browserActTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
30
30
|
export declare const browserNavigateToolDef: ToolDefinition;
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
+
import type { ToolFailure } from "./index.js";
|
|
6
7
|
export declare const editToolDef: ToolDefinition;
|
|
7
|
-
export declare function editTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
8
|
+
export declare function editTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolFailure>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in Edit file tool — exact string replacement.
|
|
3
3
|
*/
|
|
4
|
-
import { readFile
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, resolve } from "node:path";
|
|
7
7
|
import { fileCache } from "./file-cache.js";
|
|
8
8
|
import { detectEol, toLf, applyEol } from "./eol.js";
|
|
9
|
+
import { getFinalWritePathSnapshot, revalidateFinalWritePath, writeFileNoFollow, } from "../path-policy.js";
|
|
9
10
|
export const editToolDef = {
|
|
10
11
|
name: "Edit",
|
|
11
12
|
description: "Perform exact string replacements in a file. " +
|
|
@@ -50,7 +51,11 @@ export async function editTool(args, ctx) {
|
|
|
50
51
|
if (!existsSync(filePath))
|
|
51
52
|
return `Error: File not found: ${filePath}`;
|
|
52
53
|
try {
|
|
53
|
-
const
|
|
54
|
+
const approvedPath = getFinalWritePathSnapshot(args, filePath, cwd);
|
|
55
|
+
const beforeRead = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
56
|
+
if ("error" in beforeRead)
|
|
57
|
+
return { ok: false, error: beforeRead.error };
|
|
58
|
+
const raw = await readFile(beforeRead.resolvedPath, "utf-8");
|
|
54
59
|
// CRLF handling: detect the file's line ending, then match/replace in LF
|
|
55
60
|
// space (the model emits LF, so a CRLF file would never match otherwise),
|
|
56
61
|
// and write back in the ORIGINAL EOL so a CRLF file stays CRLF (no whole-
|
|
@@ -72,7 +77,10 @@ export async function editTool(args, ctx) {
|
|
|
72
77
|
const updatedLf = replaceAll
|
|
73
78
|
? content.split(oldLf).join(newLf)
|
|
74
79
|
: content.replace(oldLf, newLf);
|
|
75
|
-
|
|
80
|
+
const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
81
|
+
if ("error" in beforeWrite)
|
|
82
|
+
return { ok: false, error: beforeWrite.error };
|
|
83
|
+
await writeFileNoFollow(beforeWrite.resolvedPath, applyEol(updatedLf, eol));
|
|
76
84
|
fileCache.invalidate(filePath);
|
|
77
85
|
const count = replaceAll
|
|
78
86
|
? (content.split(oldLf).length - 1)
|
|
@@ -84,7 +92,7 @@ export async function editTool(args, ctx) {
|
|
|
84
92
|
return `Successfully edited ${filePath} (${count} replacement${count > 1 ? "s" : ""})\n${diffSummary}`;
|
|
85
93
|
}
|
|
86
94
|
catch (err) {
|
|
87
|
-
return `Error editing file: ${err.message}
|
|
95
|
+
return { ok: false, error: `Error editing file: ${err.message}` };
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
function generateCompactDiff(oldLines, newLines, filePath) {
|
|
@@ -32,6 +32,10 @@ export declare function __normalizeImagesForTests(images: string[] | undefined,
|
|
|
32
32
|
ok: false;
|
|
33
33
|
error: string;
|
|
34
34
|
}>;
|
|
35
|
+
export declare function __setVideoPollingLimitsForTests(limits: {
|
|
36
|
+
maxPollMs: number;
|
|
37
|
+
requestTimeoutMs: number;
|
|
38
|
+
} | null): void;
|
|
35
39
|
export declare const generateVideoToolDef: ToolDefinition;
|
|
36
40
|
export declare function __setVideoProviderForTests(p: VideoProvider | null): void;
|
|
37
41
|
export declare function isGenerateVideoAvailable(cwd?: string, nowMs?: number): boolean;
|