@cydm/magic-shell-agent-node 0.1.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/adapters/pty-adapter.d.ts +18 -0
- package/dist/adapters/pty-adapter.js +99 -0
- package/dist/adapters/registry.d.ts +28 -0
- package/dist/adapters/registry.js +64 -0
- package/dist/adapters/rpc-adapter.d.ts +19 -0
- package/dist/adapters/rpc-adapter.js +182 -0
- package/dist/adapters/stdio-adapter.d.ts +17 -0
- package/dist/adapters/stdio-adapter.js +107 -0
- package/dist/adapters/types.d.ts +17 -0
- package/dist/adapters/types.js +2 -0
- package/dist/claude-exec.d.ts +11 -0
- package/dist/claude-exec.js +54 -0
- package/dist/claude-worker.d.ts +12 -0
- package/dist/claude-worker.js +163 -0
- package/dist/codex-exec.d.ts +12 -0
- package/dist/codex-exec.js +84 -0
- package/dist/codex-worker.d.ts +12 -0
- package/dist/codex-worker.js +179 -0
- package/dist/directory-browser.d.ts +3 -0
- package/dist/directory-browser.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/local-direct-server.d.ts +38 -0
- package/dist/local-direct-server.js +266 -0
- package/dist/node-conversation.d.ts +21 -0
- package/dist/node-conversation.js +28 -0
- package/dist/node-intent.d.ts +2 -0
- package/dist/node-intent.js +40 -0
- package/dist/node-reply.d.ts +30 -0
- package/dist/node-reply.js +77 -0
- package/dist/node.d.ts +132 -0
- package/dist/node.js +1954 -0
- package/dist/pie-session-control.d.ts +21 -0
- package/dist/pie-session-control.js +28 -0
- package/dist/plugin-loader.d.ts +19 -0
- package/dist/plugin-loader.js +144 -0
- package/dist/plugins/pie.json +7 -0
- package/dist/primary-agent-bridge.d.ts +69 -0
- package/dist/primary-agent-bridge.js +282 -0
- package/dist/session-manager.d.ts +66 -0
- package/dist/session-manager.js +197 -0
- package/dist/terminal-metadata.d.ts +7 -0
- package/dist/terminal-metadata.js +52 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/worker-control.d.ts +15 -0
- package/dist/worker-control.js +89 -0
- package/dist/worker-narration.d.ts +25 -0
- package/dist/worker-narration.js +90 -0
- package/dist/worker-output.d.ts +6 -0
- package/dist/worker-output.js +72 -0
- package/dist/worker-registry.d.ts +45 -0
- package/dist/worker-registry.js +501 -0
- package/dist/worker-runtime.d.ts +18 -0
- package/dist/worker-runtime.js +69 -0
- package/dist/ws-client.d.ts +68 -0
- package/dist/ws-client.js +193 -0
- package/package.json +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PieSessionControlCommand {
|
|
2
|
+
requestId: string;
|
|
3
|
+
message: string;
|
|
4
|
+
mode: "steer" | "follow_up";
|
|
5
|
+
createdAt: number;
|
|
6
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
7
|
+
}
|
|
8
|
+
export interface PieSessionControlState {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
activeRequestId?: string | null;
|
|
12
|
+
lastCompletedRequestId?: string | null;
|
|
13
|
+
lastTurnIndex?: number | null;
|
|
14
|
+
lastAssistantText?: string | null;
|
|
15
|
+
lastError?: string | null;
|
|
16
|
+
status?: "idle" | "busy";
|
|
17
|
+
}
|
|
18
|
+
export declare function getPieSessionControlInboxPath(sessionId: string): string;
|
|
19
|
+
export declare function getPieSessionControlStatePath(sessionId: string): string;
|
|
20
|
+
export declare function writePieSessionControlCommand(sessionId: string, command: PieSessionControlCommand): void;
|
|
21
|
+
export declare function readPieSessionControlState(sessionId: string): PieSessionControlState | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function getSessionsDir() {
|
|
5
|
+
return path.join(os.homedir(), ".pie", "sessions");
|
|
6
|
+
}
|
|
7
|
+
export function getPieSessionControlInboxPath(sessionId) {
|
|
8
|
+
return path.join(getSessionsDir(), `${sessionId}.magic-shell-inbox.json`);
|
|
9
|
+
}
|
|
10
|
+
export function getPieSessionControlStatePath(sessionId) {
|
|
11
|
+
return path.join(getSessionsDir(), `${sessionId}.magic-shell-state.json`);
|
|
12
|
+
}
|
|
13
|
+
export function writePieSessionControlCommand(sessionId, command) {
|
|
14
|
+
fs.mkdirSync(getSessionsDir(), { recursive: true });
|
|
15
|
+
fs.writeFileSync(getPieSessionControlInboxPath(sessionId), JSON.stringify(command, null, 2));
|
|
16
|
+
}
|
|
17
|
+
export function readPieSessionControlState(sessionId) {
|
|
18
|
+
const statePath = getPieSessionControlStatePath(sessionId);
|
|
19
|
+
if (!fs.existsSync(statePath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginConfig } from "./adapters/types.js";
|
|
2
|
+
export interface LoadOptions {
|
|
3
|
+
/** 插件目录路径 */
|
|
4
|
+
dir: string;
|
|
5
|
+
/** 是否递归搜索 */
|
|
6
|
+
recursive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 加载单个插件配置
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadPlugin(path: string): PluginConfig;
|
|
12
|
+
/**
|
|
13
|
+
* 扫描目录加载所有插件
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadPlugins(options: LoadOptions): Map<string, PluginConfig>;
|
|
16
|
+
/**
|
|
17
|
+
* 获取默认插件目录
|
|
18
|
+
*/
|
|
19
|
+
export declare function getDefaultPluginDir(): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
2
|
+
import { join, extname, dirname, isAbsolute, resolve } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
/**
|
|
5
|
+
* 加载单个插件配置
|
|
6
|
+
*/
|
|
7
|
+
export function loadPlugin(path) {
|
|
8
|
+
if (!existsSync(path)) {
|
|
9
|
+
throw new Error(`Plugin file not found: ${path}`);
|
|
10
|
+
}
|
|
11
|
+
const content = readFileSync(path, "utf-8");
|
|
12
|
+
const config = JSON.parse(content);
|
|
13
|
+
const configDir = dirname(path);
|
|
14
|
+
const repoRoot = dirname(configDir);
|
|
15
|
+
// 基础验证
|
|
16
|
+
if (!config.name) {
|
|
17
|
+
throw new Error(`Plugin ${path} missing required field: name`);
|
|
18
|
+
}
|
|
19
|
+
if (!config.type) {
|
|
20
|
+
throw new Error(`Plugin ${path} missing required field: type`);
|
|
21
|
+
}
|
|
22
|
+
if (!config.command) {
|
|
23
|
+
throw new Error(`Plugin ${path} missing required field: command`);
|
|
24
|
+
}
|
|
25
|
+
if (!config.capabilities || !Array.isArray(config.capabilities)) {
|
|
26
|
+
throw new Error(`Plugin ${path} missing or invalid field: capabilities`);
|
|
27
|
+
}
|
|
28
|
+
// 验证 type 值
|
|
29
|
+
if (!["stdio", "pty", "rpc"].includes(config.type)) {
|
|
30
|
+
throw new Error(`Plugin ${path} invalid type: ${config.type}`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...config,
|
|
34
|
+
command: resolveCommandPath(config.command, configDir, repoRoot),
|
|
35
|
+
args: resolveArgPaths(config.args, configDir, repoRoot),
|
|
36
|
+
cwd: resolveOptionalPath(config.cwd, configDir, repoRoot),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveOptionalPath(value, configDir, repoRoot) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return value;
|
|
42
|
+
if (isAbsolute(value))
|
|
43
|
+
return value;
|
|
44
|
+
const repoCandidate = resolve(repoRoot, value);
|
|
45
|
+
if (existsSync(repoCandidate))
|
|
46
|
+
return repoCandidate;
|
|
47
|
+
const configCandidate = resolve(configDir, value);
|
|
48
|
+
if (existsSync(configCandidate))
|
|
49
|
+
return configCandidate;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function resolveCommandPath(command, configDir, repoRoot) {
|
|
53
|
+
if (!command || isAbsolute(command))
|
|
54
|
+
return command;
|
|
55
|
+
if (!command.includes("/") && !command.includes("\\"))
|
|
56
|
+
return command;
|
|
57
|
+
const repoCandidate = resolve(repoRoot, command);
|
|
58
|
+
if (existsSync(repoCandidate))
|
|
59
|
+
return repoCandidate;
|
|
60
|
+
const configCandidate = resolve(configDir, command);
|
|
61
|
+
if (existsSync(configCandidate))
|
|
62
|
+
return configCandidate;
|
|
63
|
+
return command;
|
|
64
|
+
}
|
|
65
|
+
function resolveArgPaths(args, configDir, repoRoot) {
|
|
66
|
+
if (!args)
|
|
67
|
+
return args;
|
|
68
|
+
return args.map((arg) => {
|
|
69
|
+
if (!arg || isAbsolute(arg))
|
|
70
|
+
return arg;
|
|
71
|
+
if (arg.startsWith("-"))
|
|
72
|
+
return arg;
|
|
73
|
+
const looksLikePath = arg.includes("/") || arg.includes("\\") || /\.[a-z0-9]+$/i.test(arg);
|
|
74
|
+
if (!looksLikePath)
|
|
75
|
+
return arg;
|
|
76
|
+
const repoCandidate = resolve(repoRoot, arg);
|
|
77
|
+
if (existsSync(repoCandidate))
|
|
78
|
+
return repoCandidate;
|
|
79
|
+
const configCandidate = resolve(configDir, arg);
|
|
80
|
+
if (existsSync(configCandidate))
|
|
81
|
+
return configCandidate;
|
|
82
|
+
return arg;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 扫描目录加载所有插件
|
|
87
|
+
*/
|
|
88
|
+
export function loadPlugins(options) {
|
|
89
|
+
const { dir, recursive = false } = options;
|
|
90
|
+
if (!existsSync(dir)) {
|
|
91
|
+
console.warn(`[PluginLoader] Directory not found: ${dir}`);
|
|
92
|
+
return new Map();
|
|
93
|
+
}
|
|
94
|
+
const plugins = new Map();
|
|
95
|
+
function scanDirectory(currentDir) {
|
|
96
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const fullPath = join(currentDir, entry.name);
|
|
99
|
+
if (entry.isDirectory() && recursive) {
|
|
100
|
+
scanDirectory(fullPath);
|
|
101
|
+
}
|
|
102
|
+
else if (entry.isFile() && extname(entry.name) === ".json") {
|
|
103
|
+
try {
|
|
104
|
+
// 跳过 schema.json
|
|
105
|
+
if (entry.name === "schema.json")
|
|
106
|
+
continue;
|
|
107
|
+
const config = loadPlugin(fullPath);
|
|
108
|
+
// 检查重复
|
|
109
|
+
if (plugins.has(config.name)) {
|
|
110
|
+
console.warn(`[PluginLoader] Duplicate plugin name: ${config.name}, skipping ${fullPath}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
plugins.set(config.name, config);
|
|
114
|
+
console.log(`[PluginLoader] Loaded: ${config.name} (${config.type})`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`[PluginLoader] Failed to load ${fullPath}:`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
scanDirectory(dir);
|
|
123
|
+
console.log(`[PluginLoader] Total plugins loaded: ${plugins.size}`);
|
|
124
|
+
return plugins;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 获取默认插件目录
|
|
128
|
+
*/
|
|
129
|
+
export function getDefaultPluginDir() {
|
|
130
|
+
// 优先使用环境变量
|
|
131
|
+
if (process.env.MAGIC_SHELL_PLUGINS_DIR) {
|
|
132
|
+
return process.env.MAGIC_SHELL_PLUGINS_DIR;
|
|
133
|
+
}
|
|
134
|
+
const packagePluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "plugins");
|
|
135
|
+
if (existsSync(packagePluginDir)) {
|
|
136
|
+
return packagePluginDir;
|
|
137
|
+
}
|
|
138
|
+
const distPluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "plugins");
|
|
139
|
+
if (existsSync(distPluginDir)) {
|
|
140
|
+
return distPluginDir;
|
|
141
|
+
}
|
|
142
|
+
// 回退到当前工作目录的 plugins,兼容仓库内开发
|
|
143
|
+
return join(process.cwd(), "plugins");
|
|
144
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PluginConfig, AgentRecord } from "./types.js";
|
|
2
|
+
import type { NodeReply } from "./node-reply.js";
|
|
3
|
+
export interface PrimaryCliResponse {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
text?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
provider?: string;
|
|
9
|
+
modelId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PrimaryAgentPromptContext {
|
|
12
|
+
pluginName: string;
|
|
13
|
+
preferredWorkerPluginName?: string;
|
|
14
|
+
status: "starting" | "running" | "stopped" | "failed" | "disabled" | "missing";
|
|
15
|
+
lastTaskSummary?: string;
|
|
16
|
+
lastWorkerSessionId?: string;
|
|
17
|
+
history: Array<{
|
|
18
|
+
role: "user" | "assistant";
|
|
19
|
+
text: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
actionType?: "spawn" | "attach";
|
|
22
|
+
actionLabel?: string;
|
|
23
|
+
actionTaskSummary?: string;
|
|
24
|
+
actionSessionId?: string;
|
|
25
|
+
}>;
|
|
26
|
+
liveWorkers: AgentRecord[];
|
|
27
|
+
}
|
|
28
|
+
export interface QueryPrimaryAgentOptions {
|
|
29
|
+
text: string;
|
|
30
|
+
plugin: PluginConfig;
|
|
31
|
+
primarySessionId?: string;
|
|
32
|
+
context: PrimaryAgentPromptContext;
|
|
33
|
+
isSessionAttachable?: (sessionId: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface PrimarySessionPromptContext {
|
|
36
|
+
pluginName: string;
|
|
37
|
+
preferredWorkerPluginName?: string;
|
|
38
|
+
lastTaskSummary?: string;
|
|
39
|
+
lastWorkerSessionId?: string;
|
|
40
|
+
history: Array<{
|
|
41
|
+
role: "user" | "assistant";
|
|
42
|
+
text: string;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
actionType?: "spawn" | "attach";
|
|
45
|
+
actionLabel?: string;
|
|
46
|
+
actionTaskSummary?: string;
|
|
47
|
+
actionSessionId?: string;
|
|
48
|
+
}>;
|
|
49
|
+
liveWorkers: AgentRecord[];
|
|
50
|
+
}
|
|
51
|
+
export interface PrimarySessionSnapshot {
|
|
52
|
+
messageCount: number;
|
|
53
|
+
lastAssistantText: string;
|
|
54
|
+
updatedAt: number;
|
|
55
|
+
lastMessageRole?: string;
|
|
56
|
+
lastAssistantHasToolCall?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function getPrimaryPieExtensionDistDir(): string;
|
|
59
|
+
export declare function withPrimaryPieExtensionPath(plugin: PluginConfig): PluginConfig;
|
|
60
|
+
export declare function buildPrimaryPrompt(text: string, context: PrimaryAgentPromptContext): string;
|
|
61
|
+
export declare function buildPrimarySessionPrompt(text: string, context: PrimarySessionPromptContext): string;
|
|
62
|
+
export declare function runPrimaryCli(plugin: PluginConfig, prompt: string, options?: {
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
timeoutMs?: number;
|
|
65
|
+
}): Promise<PrimaryCliResponse>;
|
|
66
|
+
export declare function parsePrimaryReply(response: PrimaryCliResponse): NodeReply | null;
|
|
67
|
+
export declare function queryPrimaryAgentReply(options: QueryPrimaryAgentOptions): Promise<NodeReply | null>;
|
|
68
|
+
export declare function readPieSessionSnapshot(sessionId: string): PrimarySessionSnapshot | null;
|
|
69
|
+
export declare const readPrimarySessionSnapshot: typeof readPieSessionSnapshot;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
export function getPrimaryPieExtensionDistDir() {
|
|
7
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = path.resolve(currentDir, "..", "..", "..");
|
|
9
|
+
return path.join(repoRoot, "packages", "primary-pie-extension", "dist");
|
|
10
|
+
}
|
|
11
|
+
export function withPrimaryPieExtensionPath(plugin) {
|
|
12
|
+
if (plugin.name !== "pie") {
|
|
13
|
+
return plugin;
|
|
14
|
+
}
|
|
15
|
+
const extensionDir = getPrimaryPieExtensionDistDir();
|
|
16
|
+
return {
|
|
17
|
+
...plugin,
|
|
18
|
+
args: [...(plugin.args || []), "--extension-path", extensionDir],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function buildPrimaryPrompt(text, context) {
|
|
22
|
+
const liveWorkers = context.liveWorkers.map((worker) => ({
|
|
23
|
+
sessionId: worker.sessionId,
|
|
24
|
+
displayName: worker.displayName,
|
|
25
|
+
title: worker.displayName || worker.taskSummary || worker.agentSessionId || `S:${worker.sessionId.slice(-6)}`,
|
|
26
|
+
status: worker.status,
|
|
27
|
+
phase: worker.phase,
|
|
28
|
+
activityState: worker.activityState,
|
|
29
|
+
cwd: worker.cwd,
|
|
30
|
+
}));
|
|
31
|
+
const recentHistory = context.history.slice(-6);
|
|
32
|
+
return [
|
|
33
|
+
"You are the primary agent inside Magic Shell.",
|
|
34
|
+
"You are deciding how AgentNode should respond to the user.",
|
|
35
|
+
"You are the node's main conversational brain, not a thin status bot.",
|
|
36
|
+
"Reply with JSON only.",
|
|
37
|
+
'Use this schema: {"reply":"string","actionType":"none|spawn|attach","actionLabel":"string","actionTaskSummary":"string","actionSessionId":"string"}',
|
|
38
|
+
"Rules:",
|
|
39
|
+
"- If the user is asking about capabilities or status, answer directly and set actionType to none.",
|
|
40
|
+
"- If the user is asking to do work and no existing worker should continue it, set actionType to spawn and fill actionTaskSummary.",
|
|
41
|
+
"- When spawning a new worker, prefer the configured preferred worker plugin unless the user explicitly asks for another worker type.",
|
|
42
|
+
"- If the user is following up on an existing worker, set actionType to attach, fill actionSessionId, and put the short follow-up instruction in actionTaskSummary.",
|
|
43
|
+
"- Do not spawn a worker just because the user greeted you or asked what you can do.",
|
|
44
|
+
"- If there is already a relevant live worker, prefer attach over spawn.",
|
|
45
|
+
"- If the user says things like continue, keep going, start working, or why are you not doing it, treat that as follow-up on the most relevant live worker when possible.",
|
|
46
|
+
"- Keep reply concise and useful.",
|
|
47
|
+
"- Be explicit about what you are about to do or already did.",
|
|
48
|
+
"Examples:",
|
|
49
|
+
'- User: "你能做什么" -> {"reply":"...","actionType":"none"}',
|
|
50
|
+
'- User: "帮我看看这个目录里有什么" with no relevant worker -> {"reply":"I will start a worker to inspect the directory.","actionType":"spawn","actionLabel":"ATTACH WORKER","actionTaskSummary":"Inspect the current directory and summarize what is here."}',
|
|
51
|
+
'- User: "继续" with a live worker -> {"reply":"I will push the current worker to keep going.","actionType":"attach","actionLabel":"ATTACH WORKER","actionSessionId":"...","actionTaskSummary":"Continue the current task and make visible progress. Summarize what you are doing."}',
|
|
52
|
+
"",
|
|
53
|
+
`Primary plugin: ${context.pluginName}`,
|
|
54
|
+
`Preferred worker plugin: ${context.preferredWorkerPluginName || "pie"}`,
|
|
55
|
+
`Primary status: ${context.status}`,
|
|
56
|
+
`Recent task summary: ${context.lastTaskSummary || ""}`,
|
|
57
|
+
`Recent worker session: ${context.lastWorkerSessionId || ""}`,
|
|
58
|
+
`Live workers: ${JSON.stringify(liveWorkers)}`,
|
|
59
|
+
`Recent conversation: ${JSON.stringify(recentHistory)}`,
|
|
60
|
+
`User message: ${text}`,
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
export function buildPrimarySessionPrompt(text, context) {
|
|
64
|
+
const liveWorkers = context.liveWorkers.map((worker) => ({
|
|
65
|
+
sessionId: worker.sessionId,
|
|
66
|
+
displayName: worker.displayName,
|
|
67
|
+
title: worker.displayName || worker.taskSummary || worker.agentSessionId || `S:${worker.sessionId.slice(-6)}`,
|
|
68
|
+
status: worker.status,
|
|
69
|
+
activityState: worker.activityState,
|
|
70
|
+
cwd: worker.cwd,
|
|
71
|
+
}));
|
|
72
|
+
const recentHistory = context.history.slice(-6);
|
|
73
|
+
const recommendedWorkerSessionId = context.lastWorkerSessionId || "";
|
|
74
|
+
return [
|
|
75
|
+
"You are the protected primary agent inside Magic Shell.",
|
|
76
|
+
"You are the only chat speaker. AgentNode should not narrate worker progress for you.",
|
|
77
|
+
"For any execution, inspection, file-system, shell, directory, repository, path, or follow-up task, use the tool `magic_shell_delegate_worker_turn` instead of answering from memory.",
|
|
78
|
+
"When the task touches a path or repository that may be outside the current cwd, still use the tool and ask the worker to inspect the concrete location before concluding that it is missing.",
|
|
79
|
+
"When the user points to a specific directory or says a target directory lives under a known parent, pass that directory as the tool's cwd when you can infer it.",
|
|
80
|
+
"If a delegated tool call returns a recoverable failure, do not stop. Use that failure result to continue reasoning, correct the path or arguments, and try again when appropriate.",
|
|
81
|
+
"Prefer one clean delegated worker turn that waits for the worker's answer instead of sending multiple retries or asking the user to attach a worker.",
|
|
82
|
+
"Prefer reusing the current worker when it is relevant.",
|
|
83
|
+
"If you are continuing, clarifying, or following up on the most recent worker task, pass `sessionId` explicitly to the tool instead of selecting a random live worker.",
|
|
84
|
+
"If you are not confident which existing worker is intended, do not guess. Start a fresh worker instead.",
|
|
85
|
+
"When multiple workers exist, only reuse one if you can name the intended session confidently; otherwise start a fresh worker.",
|
|
86
|
+
"If the user gives a worker a name, preserve that name by passing `workerName` to the tool.",
|
|
87
|
+
"If the user explicitly asks multiple named workers to do distinct work in one reply, you may call the tool multiple times in the same turn and then summarize all results together.",
|
|
88
|
+
"If you are spawning a new worker instead of reusing an existing session, pass pluginName set to the preferred worker plugin unless the user explicitly requests another worker type.",
|
|
89
|
+
"Do not ask the user to manually attach a worker unless inspection is explicitly requested.",
|
|
90
|
+
"Do not narrate intermediate runtime states back to the user unless they explicitly ask for them.",
|
|
91
|
+
"If you use the worker tool, produce one final user-facing answer that integrates the worker result.",
|
|
92
|
+
"Keep the final answer concise, concrete, and user-facing.",
|
|
93
|
+
"",
|
|
94
|
+
`Primary plugin: ${context.pluginName}`,
|
|
95
|
+
`Preferred worker plugin: ${context.preferredWorkerPluginName || "pie"}`,
|
|
96
|
+
`Current working context should be discovered through the worker tool when needed.`,
|
|
97
|
+
`Recommended current worker sessionId: ${recommendedWorkerSessionId}`,
|
|
98
|
+
`Recent task summary: ${context.lastTaskSummary || ""}`,
|
|
99
|
+
`Live workers: ${JSON.stringify(liveWorkers)}`,
|
|
100
|
+
`Recent conversation: ${JSON.stringify(recentHistory)}`,
|
|
101
|
+
"",
|
|
102
|
+
`User message: ${text}`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
export function runPrimaryCli(plugin, prompt, options = {}) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const runtimePlugin = withPrimaryPieExtensionPath(plugin);
|
|
108
|
+
const args = [
|
|
109
|
+
...(runtimePlugin.args || []),
|
|
110
|
+
"--json-output",
|
|
111
|
+
];
|
|
112
|
+
if (options.sessionId) {
|
|
113
|
+
args.push("--session-id", options.sessionId);
|
|
114
|
+
}
|
|
115
|
+
args.push(prompt);
|
|
116
|
+
const child = spawn(runtimePlugin.command, args, {
|
|
117
|
+
cwd: runtimePlugin.cwd || process.cwd(),
|
|
118
|
+
env: { ...process.env, ...(runtimePlugin.env || {}) },
|
|
119
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
120
|
+
});
|
|
121
|
+
let stdout = "";
|
|
122
|
+
let stderr = "";
|
|
123
|
+
const timeoutMs = options.timeoutMs ?? 45_000;
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
child.kill("SIGTERM");
|
|
126
|
+
reject(new Error("Primary agent CLI timed out"));
|
|
127
|
+
}, timeoutMs);
|
|
128
|
+
child.stdout.on("data", (chunk) => {
|
|
129
|
+
stdout += String(chunk);
|
|
130
|
+
});
|
|
131
|
+
child.stderr.on("data", (chunk) => {
|
|
132
|
+
stderr += String(chunk);
|
|
133
|
+
});
|
|
134
|
+
child.on("error", (err) => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
reject(err);
|
|
137
|
+
});
|
|
138
|
+
child.on("close", (code) => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
const output = stdout.trim();
|
|
141
|
+
let parsed = null;
|
|
142
|
+
try {
|
|
143
|
+
parsed = output ? JSON.parse(output) : null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
parsed = null;
|
|
147
|
+
}
|
|
148
|
+
if (code !== 0) {
|
|
149
|
+
reject(new Error(parsed?.error || stderr.trim() || `Primary agent CLI exited with code ${code}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!parsed || typeof parsed.ok !== "boolean") {
|
|
153
|
+
reject(new Error("Primary agent CLI returned invalid JSON"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!parsed.ok) {
|
|
157
|
+
reject(new Error(parsed.error || "Primary agent CLI returned an error"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve(parsed);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function parsePrimaryReply(response) {
|
|
165
|
+
const candidate = (response.text || "").trim();
|
|
166
|
+
const fenced = candidate.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
167
|
+
const payload = (fenced?.[1] || candidate).trim();
|
|
168
|
+
const start = payload.indexOf("{");
|
|
169
|
+
const end = payload.lastIndexOf("}");
|
|
170
|
+
if (start < 0 || end <= start) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(payload.slice(start, end + 1));
|
|
175
|
+
if (!parsed.reply) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
text: parsed.reply,
|
|
180
|
+
actionType: parsed.actionType && parsed.actionType !== "none" ? parsed.actionType : undefined,
|
|
181
|
+
actionLabel: parsed.actionLabel,
|
|
182
|
+
actionTaskSummary: parsed.actionTaskSummary,
|
|
183
|
+
actionSessionId: parsed.actionSessionId,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function queryPrimaryAgentReply(options) {
|
|
191
|
+
const prompt = buildPrimaryPrompt(options.text, options.context);
|
|
192
|
+
const raw = await runPrimaryCli(options.plugin, prompt, {
|
|
193
|
+
sessionId: options.primarySessionId,
|
|
194
|
+
timeoutMs: 8_000,
|
|
195
|
+
});
|
|
196
|
+
const parsed = parsePrimaryReply(raw);
|
|
197
|
+
if (!parsed) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (parsed.actionType === "attach"
|
|
201
|
+
&& parsed.actionSessionId
|
|
202
|
+
&& options.isSessionAttachable
|
|
203
|
+
&& !options.isSessionAttachable(parsed.actionSessionId)) {
|
|
204
|
+
delete parsed.actionType;
|
|
205
|
+
delete parsed.actionLabel;
|
|
206
|
+
delete parsed.actionSessionId;
|
|
207
|
+
}
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
function collectSessionMessages(raw) {
|
|
211
|
+
if (Array.isArray(raw.messages) && raw.messages.length > 0) {
|
|
212
|
+
return raw.messages;
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(raw.entries) && raw.entries.length > 0) {
|
|
215
|
+
return raw.entries
|
|
216
|
+
.filter((entry) => entry?.type === "message" && entry.message)
|
|
217
|
+
.map((entry) => ({
|
|
218
|
+
role: entry.message?.role,
|
|
219
|
+
content: entry.message?.content,
|
|
220
|
+
timestamp: entry.message?.timestamp || entry.timestamp,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
function readLastMessageMeta(messages) {
|
|
226
|
+
const lastMessage = messages.length ? messages[messages.length - 1] : undefined;
|
|
227
|
+
if (!lastMessage) {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
role: lastMessage.role,
|
|
232
|
+
hasToolCall: Array.isArray(lastMessage.content)
|
|
233
|
+
&& lastMessage.content.some((item) => item?.type === "toolCall"),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export function readPieSessionSnapshot(sessionId) {
|
|
237
|
+
if (!sessionId) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const sessionPath = path.join(os.homedir(), ".pie", "sessions", `${sessionId}.json`);
|
|
241
|
+
if (!fs.existsSync(sessionPath)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const raw = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
|
|
246
|
+
const messages = collectSessionMessages(raw);
|
|
247
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
248
|
+
const message = messages[index];
|
|
249
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const text = message.content
|
|
253
|
+
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
254
|
+
.map((item) => item.text?.trim() || "")
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.join("\n\n")
|
|
257
|
+
.trim();
|
|
258
|
+
if (!text) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
messageCount: raw.metadata?.messageCount || messages.length,
|
|
263
|
+
lastAssistantText: text,
|
|
264
|
+
updatedAt: raw.metadata?.updatedAt || message.timestamp || Date.now(),
|
|
265
|
+
lastMessageRole: readLastMessageMeta(messages).role,
|
|
266
|
+
lastAssistantHasToolCall: readLastMessageMeta(messages).hasToolCall,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const meta = readLastMessageMeta(messages);
|
|
270
|
+
return {
|
|
271
|
+
messageCount: raw.metadata?.messageCount || messages.length,
|
|
272
|
+
lastAssistantText: "",
|
|
273
|
+
updatedAt: raw.metadata?.updatedAt || Date.now(),
|
|
274
|
+
lastMessageRole: meta.role,
|
|
275
|
+
lastAssistantHasToolCall: meta.hasToolCall,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
export const readPrimarySessionSnapshot = readPieSessionSnapshot;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type AdapterRegistry } from "./adapters/registry.js";
|
|
2
|
+
import type { PluginConfig, AdapterOutput } from "./adapters/types.js";
|
|
3
|
+
export interface Session {
|
|
4
|
+
id: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
plugin: PluginConfig;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
lastActivity: number;
|
|
9
|
+
cols: number;
|
|
10
|
+
rows: number;
|
|
11
|
+
outputBuffer: string;
|
|
12
|
+
outputCallback?: (output: AdapterOutput) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 会话管理器
|
|
16
|
+
* 管理所有终端会话的生命周期
|
|
17
|
+
*/
|
|
18
|
+
export declare class SessionManager {
|
|
19
|
+
private registry;
|
|
20
|
+
private sessions;
|
|
21
|
+
private outputCallbacks;
|
|
22
|
+
private exitCallbacks;
|
|
23
|
+
private readonly maxBufferedOutput;
|
|
24
|
+
constructor(registry?: AdapterRegistry);
|
|
25
|
+
/**
|
|
26
|
+
* 创建或恢复会话
|
|
27
|
+
*/
|
|
28
|
+
createSession(sessionId: string, plugin: PluginConfig): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* 销毁会话
|
|
31
|
+
*/
|
|
32
|
+
destroySession(sessionId: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* 发送输入到会话
|
|
35
|
+
*/
|
|
36
|
+
sendInput(sessionId: string, data: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* 调整终端尺寸
|
|
39
|
+
*/
|
|
40
|
+
resize(sessionId: string, cols: number, rows: number): void;
|
|
41
|
+
/**
|
|
42
|
+
* 获取会话信息
|
|
43
|
+
*/
|
|
44
|
+
getSession(sessionId: string): Session | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* 列出所有会话
|
|
47
|
+
*/
|
|
48
|
+
listSessions(): Session[];
|
|
49
|
+
getBufferedOutput(sessionId: string): string;
|
|
50
|
+
appendOutput(sessionId: string, content: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* 停止所有会话
|
|
53
|
+
*/
|
|
54
|
+
stopAll(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 注册输出回调
|
|
57
|
+
*/
|
|
58
|
+
onOutput(callback: (sessionId: string, output: AdapterOutput) => void): void;
|
|
59
|
+
onExit(callback: (sessionId: string, agentId: string, code: number | null) => void): void;
|
|
60
|
+
/**
|
|
61
|
+
* 触发输出事件
|
|
62
|
+
*/
|
|
63
|
+
private emitOutput;
|
|
64
|
+
private emitExit;
|
|
65
|
+
private appendToBuffer;
|
|
66
|
+
}
|