@cydm/magic-shell-agent-node 0.1.18 → 0.1.20

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.
@@ -0,0 +1,1019 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const pieRequireBase = (() => {
11
+ try {
12
+ if (process.argv[1]) {
13
+ return fs.realpathSync(process.argv[1]);
14
+ }
15
+ } catch {
16
+ // fall through
17
+ }
18
+ return import.meta.url;
19
+ })();
20
+ const require = createRequire(pieRequireBase);
21
+ const { Type } = require("@sinclair/typebox");
22
+ const sessionsDir = path.join(os.homedir(), ".pie", "sessions");
23
+ const inboxPollers = new Map();
24
+ const activeRequests = new Map();
25
+ const delegatedTurnState = new Map();
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ function appendDebugLog(message) {
32
+ try {
33
+ fs.appendFileSync("/tmp/magic-shell-agent-extension.log", `${new Date().toISOString()} ${message}\n`);
34
+ } catch {
35
+ // ignore debug logging failures
36
+ }
37
+ }
38
+
39
+ function safeCwd(ctx) {
40
+ try {
41
+ return ctx?.cwd || process.cwd();
42
+ } catch {
43
+ return process.cwd();
44
+ }
45
+ }
46
+
47
+ function tryDirectory(candidate) {
48
+ try {
49
+ if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
50
+ return candidate;
51
+ }
52
+ } catch {
53
+ // ignore bad candidate
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function looksLikeStandaloneRepo(directory) {
59
+ try {
60
+ if (!directory) return false;
61
+ return fs.existsSync(path.join(directory, ".git")) || fs.existsSync(path.join(directory, "package.json"));
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function findNamedDirectoryNear(directoryName, startCwd) {
68
+ if (!directoryName || !startCwd) return null;
69
+ let current = startCwd;
70
+ while (current && current !== path.dirname(current)) {
71
+ const directChild = tryDirectory(path.join(current, directoryName));
72
+ const siblingUnderParent = tryDirectory(path.join(path.dirname(current), directoryName));
73
+ if (directChild && siblingUnderParent) {
74
+ if (looksLikeStandaloneRepo(siblingUnderParent) && !looksLikeStandaloneRepo(directChild)) {
75
+ return siblingUnderParent;
76
+ }
77
+ return directChild;
78
+ }
79
+ if (siblingUnderParent) return siblingUnderParent;
80
+ if (directChild) return directChild;
81
+
82
+ current = path.dirname(current);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function findMagicShellRoot(startCwd) {
88
+ let current = startCwd;
89
+ while (current && current !== path.dirname(current)) {
90
+ const packageJsonPath = path.join(current, "package.json");
91
+ const cliPath = path.join(current, "packages", "cli", "dist", "cli.js");
92
+ if (fs.existsSync(packageJsonPath) && fs.existsSync(cliPath)) {
93
+ return current;
94
+ }
95
+ current = path.dirname(current);
96
+ }
97
+ return startCwd;
98
+ }
99
+
100
+ function getExtensionRuntimeDir() {
101
+ try {
102
+ return path.dirname(fileURLToPath(import.meta.url));
103
+ } catch {
104
+ return process.cwd();
105
+ }
106
+ }
107
+
108
+ function findExistingFile(candidates) {
109
+ for (const candidate of candidates) {
110
+ if (!candidate) continue;
111
+ try {
112
+ if (fs.existsSync(candidate)) {
113
+ return candidate;
114
+ }
115
+ } catch {
116
+ // ignore invalid candidate
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+
122
+ function getCliPath(cwd) {
123
+ const runtimeDir = getExtensionRuntimeDir();
124
+ const siblingPackagedCli = path.resolve(runtimeDir, "../../../../../magic-shell/dist/cli.js");
125
+ const nestedPackagedCli = path.resolve(runtimeDir, "../../../../../../dist/cli.js");
126
+ const devCli = path.resolve(runtimeDir, "../../../../cli/dist/cli.js");
127
+ const envCli = process.env.MAGIC_SHELL_CLI_PATH
128
+ ? path.resolve(process.env.MAGIC_SHELL_CLI_PATH)
129
+ : null;
130
+ const discovered = findExistingFile([
131
+ envCli,
132
+ siblingPackagedCli,
133
+ nestedPackagedCli,
134
+ devCli,
135
+ ]);
136
+ if (discovered) {
137
+ return discovered;
138
+ }
139
+
140
+ const root = findMagicShellRoot(cwd);
141
+ return path.join(root, "packages", "cli", "dist", "cli.js");
142
+ }
143
+
144
+ function getCliWorkingDirectory(cwd, cliPath) {
145
+ if (!cliPath) {
146
+ return cwd || process.cwd();
147
+ }
148
+ if (process.env.MAGIC_SHELL_CLI_PATH && fs.existsSync(process.env.MAGIC_SHELL_CLI_PATH)) {
149
+ return cwd || process.cwd();
150
+ }
151
+ if (cliPath.includes(`${path.sep}dist${path.sep}cli.js`)) {
152
+ const packageRoot = path.resolve(path.dirname(cliPath), "..");
153
+ if (fs.existsSync(path.join(packageRoot, "package.json"))) {
154
+ return packageRoot;
155
+ }
156
+ }
157
+ return findMagicShellRoot(cwd);
158
+ }
159
+
160
+ function inferTargetDirectory(task, cwd) {
161
+ const text = String(task || "").trim();
162
+ if (!text) return null;
163
+ const lowered = text.toLowerCase();
164
+
165
+ const homePathMatches = text.match(/~\/[^\s"'`,。;、]+/g) || [];
166
+ for (const candidate of homePathMatches) {
167
+ const expanded = path.join(os.homedir(), candidate.slice(2));
168
+ try {
169
+ if (fs.existsSync(expanded) && fs.statSync(expanded).isDirectory()) {
170
+ return expanded;
171
+ }
172
+ } catch {
173
+ // ignore bad candidate
174
+ }
175
+ }
176
+
177
+ if (
178
+ text === "~"
179
+ || lowered.includes("home directory")
180
+ || lowered.includes("home dir")
181
+ || text.includes("家目录")
182
+ || text.includes("主目录")
183
+ ) {
184
+ return os.homedir();
185
+ }
186
+
187
+ const absolutePathMatches = text.match(/\/[^\s"'`,。;、]+/g) || [];
188
+ for (const candidate of absolutePathMatches) {
189
+ try {
190
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
191
+ return candidate;
192
+ }
193
+ } catch {
194
+ // ignore bad candidate
195
+ }
196
+ }
197
+
198
+ const locatedUnderMatch = text.match(/([A-Za-z0-9._-]+)\s*目录在\s*([A-Za-z0-9._-]+)\s*下面/u);
199
+ if (locatedUnderMatch) {
200
+ const childName = locatedUnderMatch[1];
201
+ const parentName = locatedUnderMatch[2];
202
+ let current = cwd;
203
+ while (current && current !== path.dirname(current)) {
204
+ if (path.basename(current) === parentName) {
205
+ const candidate = tryDirectory(path.join(current, childName));
206
+ if (candidate) return candidate;
207
+ }
208
+ const parentSibling = tryDirectory(path.join(path.dirname(current), parentName, childName));
209
+ if (parentSibling) return parentSibling;
210
+ current = path.dirname(current);
211
+ }
212
+ }
213
+
214
+ const namedDirectoryMatch = text.match(/(?:去|到|在|进|进入|spawn.*?到|在)\s*([A-Za-z0-9._-]+)\s*目录/u)
215
+ || text.match(/\b([A-Za-z0-9._-]+)\s+directory\b/i)
216
+ || text.match(/\b([A-Za-z0-9._-]+)\b(?=.*\b(?:repo|repository|目录|folder|directory)\b)/i);
217
+ if (namedDirectoryMatch?.[1]) {
218
+ const nearby = findNamedDirectoryNear(namedDirectoryMatch[1], cwd);
219
+ if (nearby) {
220
+ return nearby;
221
+ }
222
+ }
223
+
224
+ return null;
225
+ }
226
+
227
+ function normalizeDelegatedCwd(rawCwd, baseCwd) {
228
+ const value = String(rawCwd || "").trim();
229
+ if (!value) return null;
230
+
231
+ if (value === "~") {
232
+ return os.homedir();
233
+ }
234
+ if (value.startsWith("~/")) {
235
+ return tryDirectory(path.join(os.homedir(), value.slice(2)));
236
+ }
237
+ if (path.isAbsolute(value)) {
238
+ const absolute = tryDirectory(value);
239
+ if (!absolute) {
240
+ return null;
241
+ }
242
+ if (!looksLikeStandaloneRepo(absolute)) {
243
+ let current = baseCwd || process.cwd();
244
+ while (current && current !== path.dirname(current)) {
245
+ const sibling = tryDirectory(path.join(path.dirname(current), path.basename(absolute)));
246
+ if (sibling && sibling !== absolute && looksLikeStandaloneRepo(sibling)) {
247
+ return sibling;
248
+ }
249
+ current = path.dirname(current);
250
+ }
251
+ }
252
+ return absolute;
253
+ }
254
+ const resolved = path.resolve(baseCwd || process.cwd(), value);
255
+ if (tryDirectory(resolved)) {
256
+ if (!looksLikeStandaloneRepo(resolved)) {
257
+ let current = baseCwd || process.cwd();
258
+ while (current && current !== path.dirname(current)) {
259
+ const sibling = tryDirectory(path.join(path.dirname(current), path.basename(resolved)));
260
+ if (sibling && sibling !== resolved && looksLikeStandaloneRepo(sibling)) {
261
+ return sibling;
262
+ }
263
+ current = path.dirname(current);
264
+ }
265
+ }
266
+ return resolved;
267
+ }
268
+ const basename = path.basename(value);
269
+ if (basename) {
270
+ const nearby = findNamedDirectoryNear(basename, baseCwd || process.cwd());
271
+ if (nearby) {
272
+ return nearby;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ function inferWorkerName(task) {
279
+ const text = String(task || "").trim();
280
+ if (!text) return null;
281
+ const match = text.match(/(?:叫|名叫|叫做|名字是|named|call(?:ed)?)(?:\s*[::]?\s*)([A-Za-z0-9_\-\u4e00-\u9fa5]{2,20})/iu);
282
+ if (!match?.[1]) return null;
283
+ return match[1].trim();
284
+ }
285
+
286
+ function inferMentionedWorkerName(task, snapshot) {
287
+ const text = String(task || "").trim();
288
+ if (!text) return null;
289
+ const workers = Array.isArray(snapshot?.workers) ? snapshot.workers : [];
290
+ const liveNames = workers
291
+ .filter((worker) => worker?.status !== "stopped" && worker?.status !== "failed" && worker?.displayName)
292
+ .map((worker) => String(worker.displayName).trim())
293
+ .filter(Boolean)
294
+ .sort((a, b) => b.length - a.length);
295
+ for (const name of liveNames) {
296
+ if (text.includes(name)) {
297
+ return name;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function buildDelegationCacheKey({ sessionId, workerName, cwd, mode, task }) {
304
+ return JSON.stringify({
305
+ sessionId: sessionId || null,
306
+ workerName: workerName || null,
307
+ cwd: cwd || null,
308
+ mode: mode || "steer",
309
+ task: normalizeDelegatedText(task || ""),
310
+ });
311
+ }
312
+
313
+ function getCurrentSessionId() {
314
+ const args = process.argv.slice(2);
315
+ const flagIndex = args.indexOf("--session-id");
316
+ return flagIndex >= 0 && args[flagIndex + 1] ? args[flagIndex + 1] : null;
317
+ }
318
+
319
+ function getInboxPath(sessionId) {
320
+ return path.join(sessionsDir, `${sessionId}.magic-shell-inbox.json`);
321
+ }
322
+
323
+ function getStatePath(sessionId) {
324
+ return path.join(sessionsDir, `${sessionId}.magic-shell-state.json`);
325
+ }
326
+
327
+ function writeSessionControlState(sessionId, patch) {
328
+ fs.mkdirSync(sessionsDir, { recursive: true });
329
+ const statePath = getStatePath(sessionId);
330
+ let current = {};
331
+ if (fs.existsSync(statePath)) {
332
+ try {
333
+ current = JSON.parse(fs.readFileSync(statePath, "utf8"));
334
+ } catch {
335
+ current = {};
336
+ }
337
+ }
338
+ const next = {
339
+ sessionId,
340
+ updatedAt: Date.now(),
341
+ ...current,
342
+ ...patch,
343
+ };
344
+ fs.writeFileSync(statePath, JSON.stringify(next, null, 2));
345
+ }
346
+
347
+ function extractAssistantText(message) {
348
+ if (!message || !Array.isArray(message.content)) {
349
+ return "";
350
+ }
351
+ return message.content
352
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
353
+ .map((item) => item.text.trim())
354
+ .filter(Boolean)
355
+ .join("\n\n")
356
+ .trim();
357
+ }
358
+
359
+ function startInboxPoller(ctx, sessionId) {
360
+ if (!sessionId || inboxPollers.has(sessionId)) {
361
+ return;
362
+ }
363
+ fs.mkdirSync(sessionsDir, { recursive: true });
364
+ const tick = () => {
365
+ if (!ctx.isIdle()) {
366
+ return;
367
+ }
368
+ const inboxPath = getInboxPath(sessionId);
369
+ if (!fs.existsSync(inboxPath)) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+ const command = JSON.parse(fs.readFileSync(inboxPath, "utf8"));
375
+ if (!command?.requestId || !command?.message) {
376
+ return;
377
+ }
378
+ const current = activeRequests.get(sessionId);
379
+ if (current?.requestId === command.requestId) {
380
+ return;
381
+ }
382
+ activeRequests.set(sessionId, command);
383
+ writeSessionControlState(sessionId, {
384
+ activeRequestId: command.requestId,
385
+ status: "busy",
386
+ lastError: null,
387
+ });
388
+ fs.unlinkSync(inboxPath);
389
+ ctx.sendUserMessage(command.message);
390
+ } catch (error) {
391
+ writeSessionControlState(sessionId, {
392
+ lastError: error instanceof Error ? error.message : String(error),
393
+ });
394
+ }
395
+ };
396
+ const interval = setInterval(tick, 250);
397
+ inboxPollers.set(sessionId, interval);
398
+ writeSessionControlState(sessionId, {
399
+ activeRequestId: null,
400
+ status: "idle",
401
+ });
402
+ }
403
+
404
+ async function runMagicShellCli(cwd, args) {
405
+ const cliPath = getCliPath(cwd);
406
+ if (!fs.existsSync(cliPath)) {
407
+ throw new Error(`Magic Shell CLI is unavailable: ${cliPath}. Start via magic-shell or set MAGIC_SHELL_CLI_PATH.`);
408
+ }
409
+
410
+ const { stdout, stderr } = await execFileAsync(process.execPath, [cliPath, ...args], {
411
+ cwd: getCliWorkingDirectory(cwd, cliPath),
412
+ env: {
413
+ ...process.env,
414
+ MAGIC_SHELL_CLI_PATH: cliPath,
415
+ },
416
+ maxBuffer: 1024 * 1024 * 4,
417
+ });
418
+ const output = String(stdout || "").trim();
419
+ if (!output) {
420
+ if (stderr?.trim()) {
421
+ throw new Error(stderr.trim());
422
+ }
423
+ throw new Error("Magic Shell CLI returned empty output");
424
+ }
425
+
426
+ try {
427
+ return JSON.parse(output);
428
+ } catch (error) {
429
+ throw new Error(`Magic Shell CLI returned invalid JSON: ${output.slice(0, 240)}`);
430
+ }
431
+ }
432
+
433
+ function pickWorker(snapshot, preferredSessionId, pluginName, desiredCwd, workerName, { allowExactReuseAcrossCwd = false } = {}) {
434
+ const workers = Array.isArray(snapshot?.workers) ? snapshot.workers : [];
435
+ const pluginMatches = (worker) => !pluginName || !worker?.agentType || worker.agentType === pluginName;
436
+ const cwdMatches = (worker) => !desiredCwd || !worker?.cwd || path.resolve(worker.cwd) === path.resolve(desiredCwd);
437
+ if (preferredSessionId) {
438
+ const exact = workers.find((worker) =>
439
+ worker.sessionId === preferredSessionId
440
+ && pluginMatches(worker)
441
+ && worker.status !== "stopped"
442
+ && worker.status !== "failed",
443
+ );
444
+ if (exact) {
445
+ if (allowExactReuseAcrossCwd || cwdMatches(exact)) {
446
+ return exact;
447
+ }
448
+ return null;
449
+ }
450
+ }
451
+ if (workerName) {
452
+ const named = workers.find((worker) =>
453
+ worker.displayName === workerName
454
+ && pluginMatches(worker)
455
+ && worker.status !== "stopped"
456
+ && worker.status !== "failed",
457
+ );
458
+ if (named) {
459
+ if (allowExactReuseAcrossCwd || cwdMatches(named)) {
460
+ return named;
461
+ }
462
+ return null;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ function shouldPreferExistingWorker(task, preferredSessionId, workerName) {
469
+ if (preferredSessionId || workerName) {
470
+ return true;
471
+ }
472
+ const text = String(task || "");
473
+ return text.includes("继续")
474
+ || text.includes("接着")
475
+ || text.includes("再")
476
+ || text.includes("让他")
477
+ || text.includes("让她")
478
+ || text.toLowerCase().includes("follow up");
479
+ }
480
+
481
+ function shouldTreatAsSpawnRequest(task) {
482
+ const text = String(task || "").trim();
483
+ if (!text) return false;
484
+ const lowered = text.toLowerCase();
485
+ return text.includes("spawn")
486
+ || text.includes("启动")
487
+ || text.includes("创建")
488
+ || text.includes("新建")
489
+ || lowered.includes("create worker")
490
+ || lowered.includes("start worker");
491
+ }
492
+
493
+ function resolveDelegatedCwd(rawCwd, task, baseCwd, existingWorker = null) {
494
+ const normalized = normalizeDelegatedCwd(rawCwd, baseCwd);
495
+ if (normalized) {
496
+ return normalized;
497
+ }
498
+ if (existingWorker?.cwd && shouldPreferExistingWorker(task, existingWorker.sessionId, existingWorker.displayName)) {
499
+ return existingWorker.cwd;
500
+ }
501
+ return inferTargetDirectory(task, baseCwd);
502
+ }
503
+
504
+ function buildFirstTurnMessage(task) {
505
+ return `${task}
506
+
507
+ Work in your own session. Complete the task and return the actual findings in this same turn. If you inspect files or run commands, report the concrete result directly instead of restating the task. If the task cannot be completed, explain the real failure briefly.`;
508
+ }
509
+
510
+ function normalizeDelegatedText(value) {
511
+ return String(value || "").replace(/\s+/g, " ").trim();
512
+ }
513
+
514
+ function looksLikeDelegatedNoise(value) {
515
+ const normalized = normalizeDelegatedText(value).toLowerCase();
516
+ if (!normalized) return true;
517
+ return normalized.includes("queued message for")
518
+ || normalized.includes("message_update")
519
+ || normalized.includes("looks ready for input")
520
+ || normalized.includes("work in your own session")
521
+ || normalized.includes("reply to the primary agent now")
522
+ || normalized.startsWith("the user asked me")
523
+ || normalized.startsWith("用户要求我");
524
+ }
525
+
526
+ function hasUsableDelegatedText(value) {
527
+ const normalized = normalizeDelegatedText(value);
528
+ return !!normalized && !looksLikeDelegatedNoise(normalized);
529
+ }
530
+
531
+ function isUsableDelegatedTurn(result) {
532
+ return hasUsableDelegatedText(result?.message) || hasUsableDelegatedText(result?.summary);
533
+ }
534
+
535
+ async function readDelegatedSessionResult(cwd, sessionId) {
536
+ const [messageResult, summaryResult] = await Promise.all([
537
+ runMagicShellCli(cwd, ["session-message", "--session", sessionId, "--json"]),
538
+ runMagicShellCli(cwd, ["session-summary", "--session", sessionId, "--json"]),
539
+ ]);
540
+ return {
541
+ sessionId,
542
+ message: messageResult?.message || null,
543
+ summary: summaryResult?.summary || null,
544
+ };
545
+ }
546
+
547
+ async function waitForDelegatedSessionResult(cwd, sessionId, timeoutMs = 20000) {
548
+ const deadline = Date.now() + timeoutMs;
549
+ while (Date.now() <= deadline) {
550
+ const snapshot = await readDelegatedSessionResult(cwd, sessionId);
551
+ if (isUsableDelegatedTurn(snapshot)) {
552
+ return snapshot;
553
+ }
554
+ await sleep(750);
555
+ }
556
+ return null;
557
+ }
558
+
559
+ async function waitForWorkerSession(cwd, sessionId, timeoutMs = 12000) {
560
+ const deadline = Date.now() + timeoutMs;
561
+ while (Date.now() <= deadline) {
562
+ try {
563
+ const workersResult = await runMagicShellCli(cwd, ["workers", "--json"]);
564
+ const workers = Array.isArray(workersResult) ? workersResult : [];
565
+ const worker = workers.find((entry) => entry?.sessionId === sessionId);
566
+ if (worker && worker.status !== "stopped" && worker.status !== "failed") {
567
+ return worker;
568
+ }
569
+ } catch {
570
+ // ignore transient lookup failures and keep waiting
571
+ }
572
+ await sleep(500);
573
+ }
574
+ return null;
575
+ }
576
+
577
+ function formatDelegatedResult(result) {
578
+ const parts = [];
579
+ if (hasUsableDelegatedText(result.message)) {
580
+ parts.push(`Worker reply:\n${result.message}`);
581
+ }
582
+ if (hasUsableDelegatedText(result.summary) && result.summary !== result.message) {
583
+ parts.push(`Worker summary:\n${result.summary}`);
584
+ }
585
+ if (!parts.length) {
586
+ parts.push("The worker completed a turn, but it did not produce a visible assistant reply.");
587
+ }
588
+ return parts.join("\n\n");
589
+ }
590
+
591
+ function taskNeedsExplicitPathResolution(task) {
592
+ const text = String(task || "").trim();
593
+ if (!text) return false;
594
+ const lowered = text.toLowerCase();
595
+ return text.includes("目录")
596
+ || text.includes("路径")
597
+ || text.includes("文件夹")
598
+ || text.includes("在")
599
+ || text.includes("~/")
600
+ || lowered.includes("directory")
601
+ || lowered.includes("folder")
602
+ || lowered.includes("path");
603
+ }
604
+
605
+ function isSpawnOnlyDelegation(task) {
606
+ const text = String(task || "").trim();
607
+ if (!text) return false;
608
+ const lowered = text.toLowerCase();
609
+ const asksToSpawn = text.includes("spawn")
610
+ || text.includes("启动")
611
+ || text.includes("创建")
612
+ || text.includes("新建")
613
+ || lowered.includes("create worker")
614
+ || lowered.includes("start worker");
615
+ const alsoRequestsWork = text.includes("看看")
616
+ || text.includes("查看")
617
+ || text.includes("检查")
618
+ || text.includes("读")
619
+ || text.includes("统计")
620
+ || text.includes("运行")
621
+ || text.includes("执行")
622
+ || lowered.includes("inspect")
623
+ || lowered.includes("check")
624
+ || lowered.includes("read")
625
+ || lowered.includes("list")
626
+ || lowered.includes("summarize")
627
+ || lowered.includes("run ");
628
+ return asksToSpawn && !alsoRequestsWork;
629
+ }
630
+
631
+ function buildDelegatedFailureText({ phase, recoverable, reason, message, cwd, task }) {
632
+ const lines = [
633
+ `Delegated worker turn failed.`,
634
+ `phase: ${phase}`,
635
+ `recoverable: ${recoverable ? "yes" : "no"}`,
636
+ `reason: ${reason}`,
637
+ message,
638
+ ];
639
+ if (cwd) {
640
+ lines.push(`cwd: ${cwd}`);
641
+ }
642
+ if (task) {
643
+ lines.push(`task: ${task}`);
644
+ }
645
+ lines.push("You should continue reasoning from this tool result instead of stopping. If recoverable is yes, either refine the target path or try a new worker turn with corrected arguments.");
646
+ return lines.filter(Boolean).join("\n");
647
+ }
648
+
649
+ function buildDelegatedFailureResult({ phase, recoverable, reason, message, cwd, task, sessionId = null, spawned = false, pluginName = "pie", mode = "steer" }) {
650
+ return {
651
+ content: [{
652
+ type: "text",
653
+ text: buildDelegatedFailureText({ phase, recoverable, reason, message, cwd, task }),
654
+ }],
655
+ details: {
656
+ ok: false,
657
+ phase,
658
+ recoverable,
659
+ reason,
660
+ message,
661
+ cwd: cwd || null,
662
+ task: task || null,
663
+ sessionId,
664
+ spawned,
665
+ pluginName,
666
+ mode,
667
+ },
668
+ isError: false,
669
+ };
670
+ }
671
+
672
+ function clearDelegatedTurnProgress(sessionId) {
673
+ if (!sessionId) return;
674
+ const current = delegatedTurnState.get(sessionId) || { cachedResults: {} };
675
+ delegatedTurnState.set(sessionId, {
676
+ inProgress: false,
677
+ cachedResults: current.cachedResults || {},
678
+ });
679
+ }
680
+
681
+ export default function magicShellAgentExtension(ctx) {
682
+ ctx.log("Magic Shell delegated-turn extension loaded");
683
+ const currentSessionId = getCurrentSessionId();
684
+ const isPrimarySession = !!currentSessionId && currentSessionId.startsWith("primary-");
685
+ appendDebugLog(`load session=${currentSessionId || "unknown"} cwd=${safeCwd(ctx)}`);
686
+
687
+ ctx.on("tool:call", async (event) => {
688
+ if (!isPrimarySession) {
689
+ return undefined;
690
+ }
691
+ if (event.toolName === "magic_shell_delegate_worker_turn") {
692
+ return undefined;
693
+ }
694
+ appendDebugLog(`blocked tool=${event.toolName} session=${currentSessionId || "unknown"}`);
695
+ return {
696
+ block: true,
697
+ reason: "The protected primary agent must use magic_shell_delegate_worker_turn for execution tasks instead of calling other tools directly.",
698
+ };
699
+ });
700
+
701
+ if (currentSessionId) {
702
+ startInboxPoller(ctx, currentSessionId);
703
+ if (isPrimarySession) {
704
+ ctx.on("agent:start", async () => {
705
+ appendDebugLog(`agent:start session=${currentSessionId} setActiveTools=magic_shell_delegate_worker_turn`);
706
+ ctx.setActiveTools(["magic_shell_delegate_worker_turn"]);
707
+ });
708
+ }
709
+ ctx.on("turn:start", async () => {
710
+ const active = activeRequests.get(currentSessionId);
711
+ delegatedTurnState.set(currentSessionId, {
712
+ inProgress: false,
713
+ cachedResults: {},
714
+ });
715
+ writeSessionControlState(currentSessionId, {
716
+ activeRequestId: active?.requestId || null,
717
+ status: "busy",
718
+ });
719
+ });
720
+ ctx.on("turn:end", async (event) => {
721
+ const active = activeRequests.get(currentSessionId);
722
+ writeSessionControlState(currentSessionId, {
723
+ activeRequestId: null,
724
+ lastCompletedRequestId: active?.requestId || null,
725
+ lastTurnIndex: typeof event?.turnIndex === "number" ? event.turnIndex : null,
726
+ lastAssistantText: extractAssistantText(event?.message) || null,
727
+ lastError: null,
728
+ status: "idle",
729
+ });
730
+ delegatedTurnState.delete(currentSessionId);
731
+ activeRequests.delete(currentSessionId);
732
+ });
733
+ ctx.on("agent:end", async () => {
734
+ delegatedTurnState.delete(currentSessionId);
735
+ writeSessionControlState(currentSessionId, {
736
+ status: "idle",
737
+ });
738
+ });
739
+ }
740
+
741
+ if (!isPrimarySession) {
742
+ return;
743
+ }
744
+
745
+ ctx.registerTool({
746
+ name: "magic_shell_delegate_worker_turn",
747
+ description: `Delegate one worker turn through Magic Shell and return the result in the same conversation turn.
748
+
749
+ Use this when the user wants the primary agent to inspect files, run commands, continue work, or ask another session to do something concrete.`,
750
+ parameters: Type.Object({
751
+ task: Type.String({
752
+ description: "The concrete task or instruction for the worker session.",
753
+ }),
754
+ sessionId: Type.Optional(Type.String({
755
+ description: "Preferred worker session ID to reuse when continuing an existing task.",
756
+ })),
757
+ cwd: Type.Optional(Type.String({
758
+ description: "Optional working directory for a new worker session.",
759
+ })),
760
+ workerName: Type.Optional(Type.String({
761
+ description: "Optional display name for the worker session.",
762
+ })),
763
+ pluginName: Type.Optional(Type.String({
764
+ description: "Worker plugin name to use when spawning. Defaults to pie.",
765
+ })),
766
+ mode: Type.Optional(Type.Union([
767
+ Type.Literal("steer"),
768
+ Type.Literal("follow_up"),
769
+ ], {
770
+ description: "Delegation mode. Use steer for a fresh task and follow_up for a continuation.",
771
+ })),
772
+ }, {
773
+ additionalProperties: false,
774
+ }),
775
+ category: "agent",
776
+ async execute(args, context) {
777
+ const turnState = currentSessionId
778
+ ? (delegatedTurnState.get(currentSessionId) || { inProgress: false, cachedResults: {} })
779
+ : { inProgress: false, cachedResults: {} };
780
+ const pluginName = args.pluginName || "pie";
781
+ const mode = args.mode === "follow_up" ? "follow_up" : "steer";
782
+ let runtimeSnapshot;
783
+ try {
784
+ runtimeSnapshot = await runMagicShellCli(context.cwd, ["status", "--json"]);
785
+ } catch (error) {
786
+ clearDelegatedTurnProgress(currentSessionId);
787
+ return buildDelegatedFailureResult({
788
+ phase: "runtime_snapshot",
789
+ recoverable: true,
790
+ reason: "runtime_unavailable",
791
+ message: error instanceof Error ? error.message : String(error),
792
+ cwd: null,
793
+ task: args.task,
794
+ pluginName,
795
+ mode,
796
+ });
797
+ }
798
+ const workerName = args.workerName
799
+ || inferWorkerName(args.task)
800
+ || inferMentionedWorkerName(args.task, runtimeSnapshot);
801
+ let worker = pickWorker(runtimeSnapshot, args.sessionId, pluginName, null, workerName, {
802
+ allowExactReuseAcrossCwd: true,
803
+ });
804
+ const delegatedCwd = resolveDelegatedCwd(args.cwd, args.task, context.cwd, worker);
805
+ const cacheKey = buildDelegationCacheKey({
806
+ sessionId: args.sessionId,
807
+ workerName,
808
+ cwd: delegatedCwd,
809
+ mode,
810
+ task: args.task,
811
+ });
812
+ const cachedResult = currentSessionId ? turnState.cachedResults?.[cacheKey] : null;
813
+ if (currentSessionId && cachedResult) {
814
+ appendDebugLog(`tool reuse cached session=${currentSessionId} key=${cacheKey}`);
815
+ return {
816
+ ...cachedResult,
817
+ details: {
818
+ ...(cachedResult.details || {}),
819
+ reusedWithinTurn: true,
820
+ },
821
+ };
822
+ }
823
+
824
+ appendDebugLog(`tool execute session=${currentSessionId || "unknown"} task=${String(args.task || "").slice(0, 120)}`);
825
+ appendDebugLog(`tool plan session=${currentSessionId || "unknown"} cwd=${delegatedCwd || "(default)"} mode=${mode}`);
826
+ if (currentSessionId) {
827
+ delegatedTurnState.set(currentSessionId, {
828
+ ...turnState,
829
+ inProgress: true,
830
+ });
831
+ }
832
+ context.reportProgress({ message: "Checking runtime" });
833
+
834
+ if (!delegatedCwd && taskNeedsExplicitPathResolution(args.task)) {
835
+ clearDelegatedTurnProgress(currentSessionId);
836
+ return buildDelegatedFailureResult({
837
+ phase: "resolve_path",
838
+ recoverable: true,
839
+ reason: "target_path_unresolved",
840
+ message: "Could not resolve the target directory from the task description. First determine the concrete path, then call the worker tool again.",
841
+ cwd: null,
842
+ task: args.task,
843
+ pluginName,
844
+ mode,
845
+ });
846
+ }
847
+
848
+ worker = pickWorker(runtimeSnapshot, args.sessionId, pluginName, delegatedCwd, workerName, {
849
+ allowExactReuseAcrossCwd: !shouldTreatAsSpawnRequest(args.task),
850
+ }) || worker;
851
+ let spawned = false;
852
+
853
+ if (!worker) {
854
+ context.reportProgress({ message: "Spawning worker" });
855
+ let spawnResult;
856
+ try {
857
+ spawnResult = await runMagicShellCli(context.cwd, [
858
+ "spawn",
859
+ "--plugin", pluginName,
860
+ "--task", args.task,
861
+ ...(workerName ? ["--name", workerName] : []),
862
+ ...(delegatedCwd ? ["--cwd", delegatedCwd] : []),
863
+ "--json",
864
+ ]);
865
+ } catch (error) {
866
+ clearDelegatedTurnProgress(currentSessionId);
867
+ return buildDelegatedFailureResult({
868
+ phase: "spawn_worker",
869
+ recoverable: true,
870
+ reason: "spawn_failed",
871
+ message: error instanceof Error ? error.message : String(error),
872
+ cwd: delegatedCwd,
873
+ task: args.task,
874
+ pluginName,
875
+ mode,
876
+ spawned: false,
877
+ });
878
+ }
879
+ const sessionId = spawnResult?.sessionId;
880
+ if (!sessionId) {
881
+ clearDelegatedTurnProgress(currentSessionId);
882
+ return buildDelegatedFailureResult({
883
+ phase: "spawn_worker",
884
+ recoverable: true,
885
+ reason: "missing_session_id",
886
+ message: "Magic Shell did not return a worker session ID after spawn.",
887
+ cwd: delegatedCwd,
888
+ task: args.task,
889
+ pluginName,
890
+ mode,
891
+ spawned: false,
892
+ });
893
+ }
894
+ spawned = true;
895
+ worker = { sessionId, agentType: pluginName };
896
+ const readyWorker = await waitForWorkerSession(context.cwd, sessionId, 12000);
897
+ if (readyWorker) {
898
+ worker = readyWorker;
899
+ } else {
900
+ await sleep(1200);
901
+ }
902
+ }
903
+
904
+ const sessionId = worker.sessionId;
905
+ if (isSpawnOnlyDelegation(args.task)) {
906
+ const response = {
907
+ content: [{
908
+ type: "text",
909
+ text: [
910
+ `Spawned worker session ${sessionId}.`,
911
+ workerName ? `Worker name: ${workerName}` : null,
912
+ delegatedCwd ? `Working directory: ${delegatedCwd}` : null,
913
+ "Worker is ready for follow-up tasks.",
914
+ ].filter(Boolean).join("\n"),
915
+ }],
916
+ details: {
917
+ ok: true,
918
+ sessionId,
919
+ spawned,
920
+ pluginName,
921
+ displayName: workerName || null,
922
+ cwd: delegatedCwd || null,
923
+ mode,
924
+ spawnOnly: true,
925
+ },
926
+ isError: false,
927
+ };
928
+ if (currentSessionId) {
929
+ delegatedTurnState.set(currentSessionId, {
930
+ inProgress: false,
931
+ cachedResults: {
932
+ ...(turnState.cachedResults || {}),
933
+ [cacheKey]: response,
934
+ },
935
+ });
936
+ }
937
+ return response;
938
+ }
939
+ context.reportProgress({ message: `Delegating turn to ${sessionId.slice(0, 8)}...` });
940
+
941
+ const attemptArgs = [
942
+ "session-turn",
943
+ "--session", sessionId,
944
+ "--mode", mode,
945
+ "--data",
946
+ ];
947
+ let first;
948
+ try {
949
+ first = await runMagicShellCli(context.cwd, [
950
+ ...attemptArgs,
951
+ buildFirstTurnMessage(args.task),
952
+ "--timeout-ms", "35000",
953
+ "--poll-interval-ms", "250",
954
+ "--json",
955
+ ]);
956
+ } catch (error) {
957
+ clearDelegatedTurnProgress(currentSessionId);
958
+ return buildDelegatedFailureResult({
959
+ phase: "delegate_turn",
960
+ recoverable: true,
961
+ reason: "session_turn_failed",
962
+ message: error instanceof Error ? error.message : String(error),
963
+ cwd: delegatedCwd,
964
+ task: args.task,
965
+ pluginName,
966
+ mode,
967
+ sessionId,
968
+ spawned,
969
+ });
970
+ }
971
+
972
+ let finalTurn = first;
973
+ if (!isUsableDelegatedTurn(finalTurn)) {
974
+ context.reportProgress({ message: "Waiting for the worker to settle the delegated turn" });
975
+ const settled = await waitForDelegatedSessionResult(context.cwd, sessionId, 12000);
976
+ if (settled) {
977
+ finalTurn = {
978
+ ...finalTurn,
979
+ ...settled,
980
+ changed: true,
981
+ timedOut: false,
982
+ };
983
+ }
984
+ }
985
+
986
+ const text = [
987
+ `Delegated to worker session ${sessionId}.`,
988
+ formatDelegatedResult(finalTurn),
989
+ ].join("\n\n");
990
+
991
+ const response = {
992
+ content: [{ type: "text", text }],
993
+ details: {
994
+ ok: true,
995
+ sessionId,
996
+ spawned,
997
+ pluginName,
998
+ displayName: workerName || null,
999
+ cwd: delegatedCwd || null,
1000
+ mode,
1001
+ message: finalTurn?.message || null,
1002
+ summary: finalTurn?.summary || null,
1003
+ timedOut: !!finalTurn?.timedOut,
1004
+ },
1005
+ isError: !!finalTurn?.timedOut && !finalTurn?.message && !finalTurn?.summary,
1006
+ };
1007
+ if (currentSessionId) {
1008
+ delegatedTurnState.set(currentSessionId, {
1009
+ inProgress: false,
1010
+ cachedResults: {
1011
+ ...(turnState.cachedResults || {}),
1012
+ [cacheKey]: response,
1013
+ },
1014
+ });
1015
+ }
1016
+ return response;
1017
+ },
1018
+ });
1019
+ }