@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.0
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/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +4 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +64 -4
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
- package/dist/cc-orchestrator/external-agent-driver.js +202 -38
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +96 -13
- package/dist/credentials/access.d.ts +1 -0
- package/dist/credentials/access.js +2 -0
- package/dist/credentials/index.d.ts +2 -1
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/oauth.d.ts +20 -0
- package/dist/credentials/oauth.js +114 -0
- package/dist/credentials/store.d.ts +1 -0
- package/dist/credentials/store.js +3 -1
- package/dist/credentials/types.d.ts +47 -1
- package/dist/engine/engine.d.ts +6 -2
- package/dist/engine/engine.js +159 -192
- package/dist/engine/goal.d.ts +17 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/input-attachments.js +156 -13
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +30 -1
- package/dist/engine/turn-loop.js +112 -17
- package/dist/hooks/goal-stop-hook.d.ts +33 -1
- package/dist/hooks/goal-stop-hook.js +202 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/preset/index.js +14 -4
- package/dist/protocol/server.js +1 -1
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-manager.js +34 -1
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +25 -5
- package/dist/tool-system/builtin/background-jobs.js +105 -7
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
- package/dist/tool-system/builtin/drive-claude-code.js +307 -20
- package/dist/tool-system/builtin/index.js +15 -3
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/context.d.ts +18 -0
- package/dist/tool-system/mcp-manager.d.ts +14 -2
- package/dist/tool-system/mcp-manager.js +56 -7
- package/dist/types.d.ts +23 -7
- package/package.json +1 -1
|
@@ -22,7 +22,11 @@ export const driveAgentToolDef = {
|
|
|
22
22
|
"Runs in the BACKGROUND by default — these tasks are typically long (minutes to hours), so this " +
|
|
23
23
|
"returns immediately and the result is delivered to you later via a completion notification " +
|
|
24
24
|
"that wakes you. Do NOT sleep-poll for it; just continue or end your turn — you'll be woken " +
|
|
25
|
-
"with the result.
|
|
25
|
+
"with the result. " +
|
|
26
|
+
"Before launching writable work in a cwd, call DriveAgentJobs(action:'list', cwd) to see any " +
|
|
27
|
+
"already-running DriveAgent jobs there, their prompt summaries, owner sessions, CLI kind, and " +
|
|
28
|
+
"known changed files; use DriveAgentJobs(action:'cancel', jobId) if you must stop one. " +
|
|
29
|
+
"For a quick task where you want the answer inline, pass background:false. " +
|
|
26
30
|
"It has NO time concept of its own: for 'in N minutes' / 'every N' / looping, use CronCreate " +
|
|
27
31
|
"instead (never sleep). A scheduled CronCreate job runs one codeshell turn whose prompt can " +
|
|
28
32
|
"instruct it to call DriveAgent; to continue a prior session across runs, have that turn pass " +
|
|
@@ -55,6 +59,10 @@ export const driveAgentToolDef = {
|
|
|
55
59
|
type: "string",
|
|
56
60
|
description: "Existing session id to resume (keeps context). Must come from a prior run of the SAME cli. Omit for a fresh session.",
|
|
57
61
|
},
|
|
62
|
+
model: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Optional model override, passed through to `claude --model` / `codex exec --model`. Omit to use the CLI default; only pass when the user explicitly requests a model.",
|
|
65
|
+
},
|
|
58
66
|
cwd: { type: "string", description: "Working directory the run operates in." },
|
|
59
67
|
attachmentPaths: {
|
|
60
68
|
type: "array",
|
|
@@ -80,6 +88,7 @@ const defaultRunner = (opts) => {
|
|
|
80
88
|
command,
|
|
81
89
|
prompt: opts.prompt,
|
|
82
90
|
resumeSessionId: opts.resumeSessionId,
|
|
91
|
+
model: opts.model,
|
|
83
92
|
cwd: opts.cwd,
|
|
84
93
|
permissionMode: opts.permissionMode ?? "default",
|
|
85
94
|
imagePaths: opts.imagePaths,
|
|
@@ -88,6 +97,64 @@ const defaultRunner = (opts) => {
|
|
|
88
97
|
function newDriveJobId() {
|
|
89
98
|
return `cc-${process.hrtime.bigint().toString(36)}`;
|
|
90
99
|
}
|
|
100
|
+
function summarizePrompt(prompt, max = 120) {
|
|
101
|
+
const oneLine = prompt.replace(/\s+/g, " ").trim();
|
|
102
|
+
if (oneLine.length <= max)
|
|
103
|
+
return oneLine;
|
|
104
|
+
return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
|
|
105
|
+
}
|
|
106
|
+
function isDriveAgentJob(job) {
|
|
107
|
+
return job.kind === "drive-agent" || job.description.startsWith("DriveAgent(");
|
|
108
|
+
}
|
|
109
|
+
function changedFilesSummary(job) {
|
|
110
|
+
const files = job.changedFiles ?? [];
|
|
111
|
+
if (files.length === 0)
|
|
112
|
+
return "unknown";
|
|
113
|
+
if (files.length <= 4)
|
|
114
|
+
return files.join(",");
|
|
115
|
+
return `${files.slice(0, 4).join(",")} (+${files.length - 4} more)`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Canonicalize external transcript paths against the DriveAgent cwd. Claude
|
|
119
|
+
* normally records absolute paths while Codex apply_patch normally records
|
|
120
|
+
* relative paths; returning a cwd-relative display path for in-workspace files
|
|
121
|
+
* gives the renderer one stable identity and removes duplicate transcript hits.
|
|
122
|
+
*/
|
|
123
|
+
function normalizeChangedFiles(cwd, files) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
if (typeof file !== "string" || !file.trim())
|
|
128
|
+
continue;
|
|
129
|
+
const absolute = resolve(cwd, file.trim());
|
|
130
|
+
if (seen.has(absolute))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(absolute);
|
|
133
|
+
const rel = relative(cwd, absolute);
|
|
134
|
+
out.push(rel && rel !== ".." && !rel.startsWith(`..${sep}`) ? rel : absolute);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function jobDurationSeconds(job) {
|
|
139
|
+
const end = job.finishedAt ?? Date.now();
|
|
140
|
+
return `${Math.max(0, (end - job.startedAt) / 1000).toFixed(1)}s`;
|
|
141
|
+
}
|
|
142
|
+
function formatDriveJobListLine(job) {
|
|
143
|
+
const prompt = job.promptSummary || job.description || "(no prompt summary)";
|
|
144
|
+
const cwd = job.cwd ?? "(unknown cwd)";
|
|
145
|
+
const cli = job.cli ?? "unknown";
|
|
146
|
+
return [
|
|
147
|
+
`${job.jobId}`,
|
|
148
|
+
`status=${job.status}`,
|
|
149
|
+
`cli=${cli}`,
|
|
150
|
+
`session=${job.sessionId}`,
|
|
151
|
+
`cwd=${cwd}`,
|
|
152
|
+
`startedAt=${new Date(job.startedAt).toISOString()}`,
|
|
153
|
+
`duration=${jobDurationSeconds(job)}`,
|
|
154
|
+
`changedFiles=${changedFilesSummary(job)}`,
|
|
155
|
+
`prompt="${prompt}"`,
|
|
156
|
+
].join(" ");
|
|
157
|
+
}
|
|
91
158
|
function isValidSessionId(sessionId) {
|
|
92
159
|
return typeof sessionId === "string" && sessionId.length > 0;
|
|
93
160
|
}
|
|
@@ -140,8 +207,8 @@ function appendAttachmentPrompt(prompt, paths) {
|
|
|
140
207
|
}
|
|
141
208
|
return `${prompt}\n${lines.join("\n")}`;
|
|
142
209
|
}
|
|
143
|
-
function recordSuccessfulSession(store, cli, cwd, result) {
|
|
144
|
-
if (result.isError || !result.sessionId)
|
|
210
|
+
function recordSuccessfulSession(store, cli, cwd, result, includeErroredSession = false) {
|
|
211
|
+
if ((!includeErroredSession && result.isError) || !result.sessionId)
|
|
145
212
|
return;
|
|
146
213
|
try {
|
|
147
214
|
store.record({ cli, sessionId: result.sessionId, cwd });
|
|
@@ -159,17 +226,60 @@ function recordSuccessfulSession(store, cli, cwd, result) {
|
|
|
159
226
|
function duplicateCwdWarning(cwd, writable) {
|
|
160
227
|
if (!writable)
|
|
161
228
|
return undefined;
|
|
162
|
-
const running = backgroundJobRegistry.listRunningByCwd(cwd);
|
|
229
|
+
const running = backgroundJobRegistry.listRunningByCwd(cwd).filter(isDriveAgentJob);
|
|
163
230
|
if (running.length === 0)
|
|
164
231
|
return undefined;
|
|
165
|
-
const
|
|
166
|
-
return `Warning: another DriveAgent job is already running in cwd ${cwd}
|
|
232
|
+
const jobs = running.map(formatDriveJobListLine).join("; ");
|
|
233
|
+
return (`Warning: another DriveAgent job is already running in cwd ${cwd}. ` +
|
|
234
|
+
"Concurrent writable agents in the same directory can overwrite each other's work. " +
|
|
235
|
+
`Run DriveAgentJobs(action:"list", cwd:"${cwd}") before dispatching parallel work for details/cancellation. ` +
|
|
236
|
+
`Running: ${jobs}`);
|
|
167
237
|
}
|
|
168
238
|
function attachDriveCompletion(params) {
|
|
169
|
-
const { jobId, sessionId, label, cli, cwd, run, sessionStore } = params;
|
|
239
|
+
const { jobId, sessionId, label, cli, cwd, run, sessionStore, readChangedFiles, recordExternalFileChanges, originClientMessageId, } = params;
|
|
170
240
|
void run
|
|
171
241
|
.then((r) => {
|
|
172
|
-
|
|
242
|
+
const jobStatus = backgroundJobRegistry.get(jobId)?.status;
|
|
243
|
+
if (jobStatus !== "running" && jobStatus !== "cancelling")
|
|
244
|
+
return;
|
|
245
|
+
const cancelling = jobStatus === "cancelling";
|
|
246
|
+
recordSuccessfulSession(sessionStore, cli, cwd, r, cancelling);
|
|
247
|
+
// Attribute external changes BEFORE publishing completion. Previously the
|
|
248
|
+
// notification event was emitted first and carried no files; changedFiles
|
|
249
|
+
// only reached the background-work registry/panel, so the chat turn card
|
|
250
|
+
// could never count DriveAgent edits.
|
|
251
|
+
const rawChangedFiles = r.sessionId ? readChangedFiles(cli, cwd, r.sessionId) : [];
|
|
252
|
+
const changedFiles = normalizeChangedFiles(cwd, rawChangedFiles);
|
|
253
|
+
if (changedFiles.length > 0) {
|
|
254
|
+
recordExternalFileChanges?.({
|
|
255
|
+
jobId,
|
|
256
|
+
description: label,
|
|
257
|
+
cli,
|
|
258
|
+
cwd,
|
|
259
|
+
status: cancelling ? "cancelled" : r.isError ? "failed" : "completed",
|
|
260
|
+
changedFiles,
|
|
261
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
logger.debug("changed_files.drive_completion", {
|
|
265
|
+
cat: "changed_files",
|
|
266
|
+
jobId,
|
|
267
|
+
sessionId,
|
|
268
|
+
externalSessionId: r.sessionId || undefined,
|
|
269
|
+
cli,
|
|
270
|
+
cwd,
|
|
271
|
+
originClientMessageId,
|
|
272
|
+
rawSize: rawChangedFiles.length,
|
|
273
|
+
size: changedFiles.length,
|
|
274
|
+
files: changedFiles,
|
|
275
|
+
});
|
|
276
|
+
if (cancelling) {
|
|
277
|
+
backgroundJobRegistry.recordArtifacts(jobId, {
|
|
278
|
+
ccSessionId: r.sessionId || undefined,
|
|
279
|
+
changedFiles,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
173
283
|
// Deliver the result back so the woken agent actually sees the answer
|
|
174
284
|
// (not just "a job finished"). Mirrors the video/sub-agent completion
|
|
175
285
|
// path — enqueue lands in the same notificationQueue the wakeup drains.
|
|
@@ -181,6 +291,8 @@ function attachDriveCompletion(params) {
|
|
|
181
291
|
workKind: "cc",
|
|
182
292
|
error: r.finalText || "(no output)",
|
|
183
293
|
ccSessionId: r.sessionId || undefined,
|
|
294
|
+
...(changedFiles.length ? { changedFiles, cwd } : {}),
|
|
295
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
184
296
|
enqueuedAt: Date.now(),
|
|
185
297
|
}
|
|
186
298
|
: {
|
|
@@ -190,12 +302,10 @@ function attachDriveCompletion(params) {
|
|
|
190
302
|
workKind: "cc",
|
|
191
303
|
finalText: r.finalText,
|
|
192
304
|
ccSessionId: r.sessionId || undefined,
|
|
305
|
+
...(changedFiles.length ? { changedFiles, cwd } : {}),
|
|
306
|
+
...(originClientMessageId ? { originClientMessageId } : {}),
|
|
193
307
|
enqueuedAt: Date.now(),
|
|
194
308
|
}, sessionId);
|
|
195
|
-
// Attribute the files the external agent changed by parsing its own
|
|
196
|
-
// transcript (#6) — those Edit/Write calls are invisible to the host's
|
|
197
|
-
// in-session aggregator. Best-effort; [] on any failure.
|
|
198
|
-
const changedFiles = r.sessionId ? readExternalChangedFiles(cli, cwd, r.sessionId) : [];
|
|
199
309
|
// Retain the job in the panel with its result + the external CLI
|
|
200
310
|
// session id + changed files.
|
|
201
311
|
backgroundJobRegistry.finish(jobId, {
|
|
@@ -206,6 +316,8 @@ function attachDriveCompletion(params) {
|
|
|
206
316
|
});
|
|
207
317
|
})
|
|
208
318
|
.catch((err) => {
|
|
319
|
+
if (backgroundJobRegistry.get(jobId)?.status !== "running")
|
|
320
|
+
return;
|
|
209
321
|
const msg = err?.message ?? String(err);
|
|
210
322
|
notificationQueue.enqueue({
|
|
211
323
|
agentId: jobId,
|
|
@@ -221,8 +333,19 @@ function attachDriveCompletion(params) {
|
|
|
221
333
|
function trackBackgroundRun(params) {
|
|
222
334
|
const warning = duplicateCwdWarning(params.cwd, params.writable);
|
|
223
335
|
const jobId = newDriveJobId();
|
|
224
|
-
|
|
225
|
-
|
|
336
|
+
const run = params.start();
|
|
337
|
+
backgroundJobRegistry.start(jobId, params.sessionId, params.label, {
|
|
338
|
+
kind: "drive-agent",
|
|
339
|
+
cwd: params.cwd,
|
|
340
|
+
cli: params.cli,
|
|
341
|
+
promptSummary: params.promptSummary,
|
|
342
|
+
originClientMessageId: params.originClientMessageId,
|
|
343
|
+
abort: async () => {
|
|
344
|
+
params.abort();
|
|
345
|
+
await run.catch(() => undefined);
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
attachDriveCompletion({ ...params, jobId, run });
|
|
226
349
|
return { jobId, ...(warning ? { warning } : {}) };
|
|
227
350
|
}
|
|
228
351
|
async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
@@ -243,6 +366,18 @@ async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
|
243
366
|
clearTimeout(timer);
|
|
244
367
|
}
|
|
245
368
|
}
|
|
369
|
+
function makeAbortController(parent, linkParent = false) {
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
if (!parent)
|
|
372
|
+
return controller;
|
|
373
|
+
if (parent.aborted) {
|
|
374
|
+
controller.abort(parent.reason);
|
|
375
|
+
}
|
|
376
|
+
else if (linkParent) {
|
|
377
|
+
parent.addEventListener("abort", () => controller.abort(parent.reason), { once: true });
|
|
378
|
+
}
|
|
379
|
+
return controller;
|
|
380
|
+
}
|
|
246
381
|
/** Factory so tests can inject a fake runner. `fixedCli` (back-compat) forces a
|
|
247
382
|
* cli and hides the `cli` arg — that's how DriveClaudeCode stays a thin alias. */
|
|
248
383
|
export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {}) {
|
|
@@ -261,6 +396,7 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
261
396
|
if (cli === "invalid")
|
|
262
397
|
return `Error: unknown cli "${String(args.cli)}" (expected "claude" or "codex")`;
|
|
263
398
|
const resumeSessionId = typeof args.resumeSessionId === "string" ? args.resumeSessionId : undefined;
|
|
399
|
+
const model = typeof args.model === "string" && args.model.trim() ? args.model : undefined;
|
|
264
400
|
const sessionStore = options.sessionStore ?? externalAgentSessionStore;
|
|
265
401
|
let cwd = requestedCwd;
|
|
266
402
|
let resumeNote = "";
|
|
@@ -306,13 +442,15 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
306
442
|
: [];
|
|
307
443
|
const cliName = cli === "codex" ? "Codex" : "Claude Code";
|
|
308
444
|
const label = `DriveAgent(${cli}): ${prompt.slice(0, 40)}`;
|
|
309
|
-
const
|
|
445
|
+
const promptSummary = summarizePrompt(prompt);
|
|
446
|
+
const callerSignal = ctx?.signal ?? argSignal(args);
|
|
447
|
+
const runOptsBase = {
|
|
310
448
|
cli,
|
|
311
449
|
prompt: promptWithAttachments,
|
|
312
450
|
resumeSessionId,
|
|
451
|
+
model,
|
|
313
452
|
cwd,
|
|
314
453
|
permissionMode,
|
|
315
|
-
signal: ctx?.signal ?? argSignal(args),
|
|
316
454
|
imagePaths,
|
|
317
455
|
};
|
|
318
456
|
const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
|
|
@@ -329,14 +467,20 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
329
467
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
330
468
|
return `Error: cannot start a background ${cliName} job without a session — its result notification would be dropped. Retry with background:false, or ensure the tool runs inside a session.`;
|
|
331
469
|
}
|
|
470
|
+
const abortController = makeAbortController(callerSignal, false);
|
|
332
471
|
const tracked = trackBackgroundRun({
|
|
333
472
|
sessionId,
|
|
334
473
|
label,
|
|
335
474
|
cli,
|
|
336
475
|
cwd,
|
|
337
|
-
|
|
476
|
+
promptSummary,
|
|
477
|
+
start: () => startRun(runner, { ...runOptsBase, signal: abortController.signal }),
|
|
478
|
+
abort: () => abortController.abort(),
|
|
338
479
|
sessionStore,
|
|
339
480
|
writable: isWritableRun,
|
|
481
|
+
readChangedFiles: options.readChangedFiles ?? readExternalChangedFiles,
|
|
482
|
+
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
483
|
+
originClientMessageId: ctx?.originClientMessageId,
|
|
340
484
|
});
|
|
341
485
|
return [
|
|
342
486
|
resumeNote,
|
|
@@ -346,7 +490,8 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
346
490
|
.filter(Boolean)
|
|
347
491
|
.join("\n");
|
|
348
492
|
}
|
|
349
|
-
const
|
|
493
|
+
const foregroundAbort = makeAbortController(callerSignal, true);
|
|
494
|
+
const run = startRun(runner, { ...runOptsBase, signal: foregroundAbort.signal });
|
|
350
495
|
const result = await waitForForegroundOrHandoff(run, foregroundHandoffMs);
|
|
351
496
|
if (result.kind === "handoff" && isValidSessionId(ctx?.sessionId)) {
|
|
352
497
|
const tracked = trackBackgroundRun({
|
|
@@ -354,9 +499,14 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
354
499
|
label,
|
|
355
500
|
cli,
|
|
356
501
|
cwd,
|
|
357
|
-
|
|
502
|
+
promptSummary,
|
|
503
|
+
start: () => run,
|
|
504
|
+
abort: () => foregroundAbort.abort(),
|
|
358
505
|
sessionStore,
|
|
359
506
|
writable: isWritableRun,
|
|
507
|
+
readChangedFiles: options.readChangedFiles ?? readExternalChangedFiles,
|
|
508
|
+
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
509
|
+
originClientMessageId: ctx?.originClientMessageId,
|
|
360
510
|
});
|
|
361
511
|
return [
|
|
362
512
|
resumeNote,
|
|
@@ -375,6 +525,142 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
375
525
|
};
|
|
376
526
|
}
|
|
377
527
|
export const driveAgentTool = makeDriveAgentTool();
|
|
528
|
+
export const driveAgentJobsToolDef = {
|
|
529
|
+
name: "DriveAgentJobs",
|
|
530
|
+
description: "List, inspect, or cancel background DriveAgent jobs. Use action:'list' before launching " +
|
|
531
|
+
"writable DriveAgent work in a cwd to see already-running jobs there, including prompt " +
|
|
532
|
+
"summary, owner session, cwd, CLI kind, status, start time, and known changed files. " +
|
|
533
|
+
"Use action:'inspect' with jobId for full details, or action:'cancel' with jobId to abort " +
|
|
534
|
+
"a running DriveAgent external CLI process and deliver a cancellation notification.",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {
|
|
538
|
+
action: {
|
|
539
|
+
type: "string",
|
|
540
|
+
enum: ["list", "inspect", "cancel"],
|
|
541
|
+
description: "What to do. Defaults to list.",
|
|
542
|
+
},
|
|
543
|
+
jobId: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description: "DriveAgent jobId for inspect or cancel.",
|
|
546
|
+
},
|
|
547
|
+
cwd: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "When listing, restrict to DriveAgent jobs in this cwd. This is cross-session so it can catch same-directory write conflicts before dispatch.",
|
|
550
|
+
},
|
|
551
|
+
status: {
|
|
552
|
+
type: "string",
|
|
553
|
+
enum: ["running", "all"],
|
|
554
|
+
description: "When listing: running (default) or all retained DriveAgent jobs.",
|
|
555
|
+
},
|
|
556
|
+
all: {
|
|
557
|
+
type: "boolean",
|
|
558
|
+
description: "When listing without cwd: true lists DriveAgent jobs from every session; default lists only this session when session context exists.",
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
function jobIdArg(args) {
|
|
564
|
+
const camel = typeof args.jobId === "string" ? args.jobId.trim() : "";
|
|
565
|
+
if (camel)
|
|
566
|
+
return camel;
|
|
567
|
+
const snake = typeof args.job_id === "string" ? args.job_id.trim() : "";
|
|
568
|
+
return snake || undefined;
|
|
569
|
+
}
|
|
570
|
+
function listDriveAgentJobs(args, ctx) {
|
|
571
|
+
const rawCwd = typeof args.cwd === "string" && args.cwd.trim() ? args.cwd : undefined;
|
|
572
|
+
const cwd = rawCwd ? normalizeCwdPath(rawCwd) : undefined;
|
|
573
|
+
const sessionId = ctx?.sessionId;
|
|
574
|
+
const listAllSessions = args.all === true || !!cwd || !sessionId;
|
|
575
|
+
const status = args.status === "all" ? "all" : "running";
|
|
576
|
+
let jobs = listAllSessions || !sessionId
|
|
577
|
+
? backgroundJobRegistry.list()
|
|
578
|
+
: backgroundJobRegistry.listForSession(sessionId);
|
|
579
|
+
jobs = jobs.filter(isDriveAgentJob);
|
|
580
|
+
if (cwd)
|
|
581
|
+
jobs = jobs.filter((job) => job.cwd === cwd);
|
|
582
|
+
if (status === "running") {
|
|
583
|
+
jobs = jobs.filter((job) => job.status === "running" || job.status === "cancelling");
|
|
584
|
+
}
|
|
585
|
+
if (jobs.length === 0) {
|
|
586
|
+
if (cwd)
|
|
587
|
+
return `No ${status} DriveAgent jobs in cwd ${cwd}.`;
|
|
588
|
+
if (listAllSessions)
|
|
589
|
+
return `No ${status} DriveAgent jobs in this process.`;
|
|
590
|
+
return `No ${status} DriveAgent jobs in this session.`;
|
|
591
|
+
}
|
|
592
|
+
return jobs.map(formatDriveJobListLine).join("\n");
|
|
593
|
+
}
|
|
594
|
+
function inspectDriveAgentJob(jobId) {
|
|
595
|
+
if (!jobId)
|
|
596
|
+
return "Error: jobId is required.";
|
|
597
|
+
const job = backgroundJobRegistry.get(jobId);
|
|
598
|
+
if (!job || !isDriveAgentJob(job))
|
|
599
|
+
return `Error: DriveAgent jobId "${jobId}" not found.`;
|
|
600
|
+
const lines = [
|
|
601
|
+
`jobId: ${job.jobId}`,
|
|
602
|
+
`status: ${job.status}`,
|
|
603
|
+
`cli: ${job.cli ?? "unknown"}`,
|
|
604
|
+
`session: ${job.sessionId}`,
|
|
605
|
+
`cwd: ${job.cwd ?? "(unknown cwd)"}`,
|
|
606
|
+
`startedAt: ${new Date(job.startedAt).toISOString()}`,
|
|
607
|
+
`duration: ${jobDurationSeconds(job)}`,
|
|
608
|
+
`prompt: ${job.promptSummary || job.description || "(no prompt summary)"}`,
|
|
609
|
+
`description: ${job.description}`,
|
|
610
|
+
];
|
|
611
|
+
if (job.finishedAt !== undefined)
|
|
612
|
+
lines.push(`finishedAt: ${new Date(job.finishedAt).toISOString()}`);
|
|
613
|
+
if (job.ccSessionId)
|
|
614
|
+
lines.push(`ccSessionId: ${job.ccSessionId}`);
|
|
615
|
+
if (job.changedFiles && job.changedFiles.length > 0) {
|
|
616
|
+
lines.push("changedFiles:", ...job.changedFiles.map((file) => `- ${file}`));
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
lines.push("changedFiles: unknown");
|
|
620
|
+
}
|
|
621
|
+
if (job.finalText)
|
|
622
|
+
lines.push("finalText:", job.finalText);
|
|
623
|
+
return lines.join("\n");
|
|
624
|
+
}
|
|
625
|
+
async function cancelDriveAgentJob(jobId) {
|
|
626
|
+
if (!jobId)
|
|
627
|
+
return "Error: jobId is required.";
|
|
628
|
+
const job = backgroundJobRegistry.get(jobId);
|
|
629
|
+
if (!job || !isDriveAgentJob(job))
|
|
630
|
+
return `Error: DriveAgent jobId "${jobId}" not found.`;
|
|
631
|
+
if (job.status !== "running") {
|
|
632
|
+
return `DriveAgent job ${jobId} is already ${job.status}; nothing to cancel.`;
|
|
633
|
+
}
|
|
634
|
+
if (!job.abort) {
|
|
635
|
+
return `Error: DriveAgent job ${jobId} has no cancellation handle recorded.`;
|
|
636
|
+
}
|
|
637
|
+
const finalText = `DriveAgent job ${jobId} cancelled by DriveAgentJobs.`;
|
|
638
|
+
const ok = await backgroundJobRegistry.cancel(jobId, { finalText });
|
|
639
|
+
if (!ok)
|
|
640
|
+
return `Failed to cancel DriveAgent job ${jobId}.`;
|
|
641
|
+
notificationQueue.enqueue({
|
|
642
|
+
agentId: jobId,
|
|
643
|
+
description: job.description,
|
|
644
|
+
status: "cancelled",
|
|
645
|
+
workKind: "cc",
|
|
646
|
+
error: finalText,
|
|
647
|
+
ccSessionId: job.ccSessionId,
|
|
648
|
+
...(job.changedFiles?.length ? { changedFiles: job.changedFiles, cwd: job.cwd } : {}),
|
|
649
|
+
...(job.originClientMessageId ? { originClientMessageId: job.originClientMessageId } : {}),
|
|
650
|
+
enqueuedAt: Date.now(),
|
|
651
|
+
}, job.sessionId);
|
|
652
|
+
return `DriveAgent job ${jobId} cancelled.`;
|
|
653
|
+
}
|
|
654
|
+
export async function driveAgentJobsTool(args, ctx) {
|
|
655
|
+
const action = args.action === "inspect" || args.action === "cancel" || args.action === "list"
|
|
656
|
+
? args.action
|
|
657
|
+
: "list";
|
|
658
|
+
if (action === "inspect")
|
|
659
|
+
return inspectDriveAgentJob(jobIdArg(args));
|
|
660
|
+
if (action === "cancel")
|
|
661
|
+
return await cancelDriveAgentJob(jobIdArg(args));
|
|
662
|
+
return listDriveAgentJobs(args, ctx);
|
|
663
|
+
}
|
|
378
664
|
// ── Back-compat: DriveClaudeCode = DriveAgent pinned to cli:"claude" ──────────
|
|
379
665
|
// Kept so old prompts / memories / call sites that reference DriveClaudeCode
|
|
380
666
|
// keep working. It's a thin alias over the same machinery (fixedCli "claude").
|
|
@@ -394,6 +680,7 @@ export const driveClaudeCodeToolDef = {
|
|
|
394
680
|
type: "string",
|
|
395
681
|
description: "Existing CC session id to resume (keeps context). Omit for a fresh session.",
|
|
396
682
|
},
|
|
683
|
+
model: driveAgentToolDef.inputSchema.properties.model,
|
|
397
684
|
cwd: driveAgentToolDef.inputSchema.properties.cwd,
|
|
398
685
|
attachmentPaths: driveAgentToolDef.inputSchema.properties.attachmentPaths,
|
|
399
686
|
permissionMode: driveAgentToolDef.inputSchema.properties.permissionMode,
|
|
@@ -404,7 +691,7 @@ export const driveClaudeCodeToolDef = {
|
|
|
404
691
|
};
|
|
405
692
|
export function makeDriveClaudeCodeTool(runner, options) {
|
|
406
693
|
const generic = runner
|
|
407
|
-
? ({ prompt, resumeSessionId, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, cwd, permissionMode, signal })
|
|
694
|
+
? ({ prompt, resumeSessionId, model, cwd, permissionMode, signal }) => runner({ prompt, resumeSessionId, model, cwd, permissionMode, signal })
|
|
408
695
|
: undefined;
|
|
409
696
|
return makeDriveAgentTool(generic ?? defaultRunner, "claude", options);
|
|
410
697
|
}
|
|
@@ -20,12 +20,14 @@ import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanM
|
|
|
20
20
|
import { toolSearchToolDef, toolSearchTool } from "./tool-search.js";
|
|
21
21
|
import { todoWriteToolDef, todoWriteTool } from "./task.js";
|
|
22
22
|
import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool, switchSessionWorkspaceToolDef, switchSessionWorkspaceTool, } from "./worktree.js";
|
|
23
|
-
import { sleepToolDef
|
|
23
|
+
import { sleepToolDef } from "./sleep.definition.js";
|
|
24
|
+
import { sleepTool } from "./sleep.js";
|
|
24
25
|
import { configToolDef, configTool } from "./config.js";
|
|
25
26
|
import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
|
|
26
27
|
import { lspToolDef, lspTool } from "./lsp.js";
|
|
27
|
-
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool,
|
|
28
|
-
import {
|
|
28
|
+
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListTool, } from "./cron.js";
|
|
29
|
+
import { cronListToolDef } from "./cron-list.definition.js";
|
|
30
|
+
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, driveAgentJobsToolDef, driveAgentJobsTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
|
|
29
31
|
import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
|
|
30
32
|
import { skillToolDef, skillTool } from "./skill.js";
|
|
31
33
|
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool, } from "./mcp-tools.js";
|
|
@@ -448,6 +450,16 @@ export const BUILTIN_TOOLS = [
|
|
|
448
450
|
},
|
|
449
451
|
execute: driveAgentTool,
|
|
450
452
|
},
|
|
453
|
+
{
|
|
454
|
+
definition: {
|
|
455
|
+
...driveAgentJobsToolDef,
|
|
456
|
+
source: "builtin",
|
|
457
|
+
permissionDefault: "allow",
|
|
458
|
+
isReadOnly: false,
|
|
459
|
+
isConcurrencySafe: false,
|
|
460
|
+
},
|
|
461
|
+
execute: driveAgentJobsTool,
|
|
462
|
+
},
|
|
451
463
|
// Back-compat alias: DriveClaudeCode = DriveAgent pinned to cli:claude. Kept
|
|
452
464
|
// registered so old prompts/memories that name DriveClaudeCode still resolve.
|
|
453
465
|
{
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SleepTool — pause execution for a specified duration.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
export declare const sleepToolDef: ToolDefinition;
|
|
4
|
+
export { sleepToolDef } from "./sleep.definition.js";
|
|
6
5
|
export declare function sleepTool(args: Record<string, unknown>): Promise<string>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep tool metadata.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the executor so registration and presets can consume the
|
|
5
|
+
* definition without importing implementation code.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolDefinition } from "../../types.js";
|
|
8
|
+
export declare const sleepToolDef: ToolDefinition;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep tool metadata.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the executor so registration and presets can consume the
|
|
5
|
+
* definition without importing implementation code.
|
|
6
|
+
*/
|
|
7
|
+
export const sleepToolDef = {
|
|
8
|
+
name: "Sleep",
|
|
9
|
+
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
10
|
+
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
11
|
+
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
12
|
+
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
13
|
+
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
14
|
+
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
15
|
+
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
16
|
+
"That returns control to you at the interval without burning a turn spinning. " +
|
|
17
|
+
"Maximum duration is 300 seconds (5 minutes).",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
seconds: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: "Number of seconds to sleep (max 300)",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["seconds"],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SleepTool — pause execution for a specified duration.
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
5
|
-
name: "Sleep",
|
|
6
|
-
description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
|
|
7
|
-
"Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
|
|
8
|
-
"the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
|
|
9
|
-
"If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
|
|
10
|
-
"instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
|
|
11
|
-
"({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
|
|
12
|
-
"prompt: 'check whether <that task> finished; if still running, wait again' }). " +
|
|
13
|
-
"That returns control to you at the interval without burning a turn spinning. " +
|
|
14
|
-
"Maximum duration is 300 seconds (5 minutes).",
|
|
15
|
-
inputSchema: {
|
|
16
|
-
type: "object",
|
|
17
|
-
properties: {
|
|
18
|
-
seconds: {
|
|
19
|
-
type: "number",
|
|
20
|
-
description: "Number of seconds to sleep (max 300)",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["seconds"],
|
|
24
|
-
},
|
|
25
|
-
};
|
|
4
|
+
export { sleepToolDef } from "./sleep.definition.js";
|
|
26
5
|
export async function sleepTool(args) {
|
|
27
6
|
const seconds = Math.min(Math.max(Number(args.seconds) || 1, 0.1), 300);
|
|
28
7
|
const signal = args.__signal;
|
|
@@ -184,6 +184,15 @@ export interface ToolVisibilityContext {
|
|
|
184
184
|
hasGoal: boolean;
|
|
185
185
|
settingsScope?: import("../settings/manager.js").SettingsScope;
|
|
186
186
|
}
|
|
187
|
+
export interface ExternalFileChangesRecord {
|
|
188
|
+
jobId: string;
|
|
189
|
+
description: string;
|
|
190
|
+
cli: "claude" | "codex";
|
|
191
|
+
cwd: string;
|
|
192
|
+
status: "completed" | "failed" | "cancelled";
|
|
193
|
+
changedFiles: string[];
|
|
194
|
+
originClientMessageId?: string;
|
|
195
|
+
}
|
|
187
196
|
export interface ToolContext {
|
|
188
197
|
/** Active working directory for this Engine. */
|
|
189
198
|
cwd: string;
|
|
@@ -261,6 +270,15 @@ export interface ToolContext {
|
|
|
261
270
|
* narrow context, or standalone tool tests. (B2 — Gate 1, standard §S3.)
|
|
262
271
|
*/
|
|
263
272
|
sessionId?: string;
|
|
273
|
+
/** Stable id of the real user turn that owns async work launched here. */
|
|
274
|
+
originClientMessageId?: string;
|
|
275
|
+
/**
|
|
276
|
+
* Persist file attribution from an async external DriveAgent completion in
|
|
277
|
+
* the owning CodeShell transcript. The callback closes over the live parent
|
|
278
|
+
* transcript, so a background job can record its files after the run that
|
|
279
|
+
* launched it has already returned.
|
|
280
|
+
*/
|
|
281
|
+
recordExternalFileChanges?: (record: ExternalFileChangesRecord) => void;
|
|
264
282
|
/**
|
|
265
283
|
* Skill names the user has hidden from the LLM (full namespaced names
|
|
266
284
|
* for plugin skills, e.g. "docs:pdf"). The skill builtin tool uses
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import type { MCPServerConfig, RegisteredTool } from "../types.js";
|
|
8
|
+
import type { CredentialType } from "../credentials/types.js";
|
|
8
9
|
import { ToolRegistry } from "./registry.js";
|
|
9
10
|
import { type CredentialAccess } from "../credentials/access.js";
|
|
10
11
|
interface MCPResourceInfo {
|
|
@@ -21,6 +22,16 @@ interface MCPResourceInfo {
|
|
|
21
22
|
* and the env var.
|
|
22
23
|
*/
|
|
23
24
|
export declare function readRequiredEnv(serverName: string, field: string, envName: string): string;
|
|
25
|
+
export interface ResolvedMcpCredential {
|
|
26
|
+
secret: string;
|
|
27
|
+
type?: CredentialType;
|
|
28
|
+
label?: string;
|
|
29
|
+
}
|
|
30
|
+
export type HttpCredentialResolverResult = string | ResolvedMcpCredential | undefined;
|
|
31
|
+
export interface BuildHttpHeadersOptions {
|
|
32
|
+
now?: () => number;
|
|
33
|
+
oauthRefreshSkewMs?: number;
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* Build the spawned stdio server's environment. Priority (lowest → highest):
|
|
26
37
|
* a minimal inherited allowlist < forwarded `envVars` (read from process.env by
|
|
@@ -34,8 +45,9 @@ export declare function buildStdioEnv(serverName: string, config: MCPServerConfi
|
|
|
34
45
|
* base; env-sourced secrets (`bearerTokenEnvVar`, `envHeaders`) layer on top
|
|
35
46
|
* and win on conflict. Pure + exported for unit testing.
|
|
36
47
|
*/
|
|
37
|
-
export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) =>
|
|
38
|
-
export declare function
|
|
48
|
+
export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => HttpCredentialResolverResult, options?: BuildHttpHeadersOptions): Record<string, string>;
|
|
49
|
+
export declare function bearerTokenFromMcpCredential(serverName: string, credentialId: string, credential: ResolvedMcpCredential, options?: BuildHttpHeadersOptions): string;
|
|
50
|
+
export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue"> & Partial<Pick<CredentialAccess, "resolveMeta">>, options?: BuildHttpHeadersOptions): Promise<Record<string, string>>;
|
|
39
51
|
/**
|
|
40
52
|
* Infer the transport when the config doesn't name one: a url-only entry is
|
|
41
53
|
* HTTP, everything else stdio. This is the CC `.mcp.json` convention —
|