shennian 0.2.43 → 0.2.47
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/src/agents/claude.d.ts +5 -0
- package/dist/src/agents/claude.js +17 -0
- package/dist/src/agents/codex.d.ts +5 -0
- package/dist/src/agents/codex.js +12 -1
- package/dist/src/agents/command-spec.js +74 -3
- package/dist/src/agents/detect.js +12 -2
- package/dist/src/agents/manager.d.ts +16 -0
- package/dist/src/agents/manager.js +145 -0
- package/dist/src/agents/model-registry/discovery.js +6 -0
- package/dist/src/agents/pi.d.ts +1 -0
- package/dist/src/agents/pi.js +18 -2
- package/dist/src/channels/base.d.ts +62 -0
- package/dist/src/channels/base.js +3 -0
- package/dist/src/channels/registry.d.ts +7 -0
- package/dist/src/channels/registry.js +30 -0
- package/dist/src/channels/runtime.d.ts +60 -0
- package/dist/src/channels/runtime.js +158 -0
- package/dist/src/channels/secret-registry.d.ts +17 -0
- package/dist/src/channels/secret-registry.js +44 -0
- package/dist/src/channels/websocket.d.ts +48 -0
- package/dist/src/channels/websocket.js +314 -0
- package/dist/src/channels/wecom.d.ts +48 -0
- package/dist/src/channels/wecom.js +315 -0
- package/dist/src/commands/manager.d.ts +2 -0
- package/dist/src/commands/manager.js +168 -0
- package/dist/src/config/index.js +5 -1
- package/dist/src/index.js +3 -1
- package/dist/src/manager/prompt.d.ts +2 -0
- package/dist/src/manager/prompt.js +46 -0
- package/dist/src/manager/registry.d.ts +92 -0
- package/dist/src/manager/registry.js +267 -0
- package/dist/src/manager/runtime.d.ts +59 -0
- package/dist/src/manager/runtime.js +534 -0
- package/dist/src/native-fusion/parsers.js +6 -0
- package/dist/src/native-fusion/service.d.ts +1 -0
- package/dist/src/native-fusion/service.js +5 -3
- package/dist/src/native-fusion/types.d.ts +2 -1
- package/dist/src/region.js +7 -19
- package/dist/src/session/handlers/agents.js +1 -1
- package/dist/src/session/handlers/chat.js +121 -2
- package/dist/src/session/handlers/control.js +1 -1
- package/dist/src/session/manager.d.ts +2 -0
- package/dist/src/session/manager.js +16 -0
- package/dist/src/session/projection.d.ts +3 -0
- package/dist/src/session/projection.js +54 -0
- package/dist/src/session/store.d.ts +24 -1
- package/dist/src/session/store.js +66 -3
- package/dist/src/session/types.d.ts +2 -0
- package/package.json +5 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentAdapter } from './adapter.js';
|
|
2
2
|
export declare function normalizeClaudeModelId(modelId?: string | null): string;
|
|
3
3
|
export declare class ClaudeAdapter extends AgentAdapter {
|
|
4
|
+
private readonly options;
|
|
4
5
|
readonly type: "claude";
|
|
5
6
|
private process;
|
|
6
7
|
private agentSessionId;
|
|
@@ -10,6 +11,10 @@ export declare class ClaudeAdapter extends AgentAdapter {
|
|
|
10
11
|
private runId;
|
|
11
12
|
private hasEmittedText;
|
|
12
13
|
private terminalState;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
systemPrompt?: string;
|
|
16
|
+
hidden?: boolean;
|
|
17
|
+
});
|
|
13
18
|
start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
14
19
|
send(text: string, modelId?: string): Promise<void>;
|
|
15
20
|
resume(agentSessionId: string): Promise<void>;
|
|
@@ -8,6 +8,7 @@ export function normalizeClaudeModelId(modelId) {
|
|
|
8
8
|
return trimmed || 'default';
|
|
9
9
|
}
|
|
10
10
|
export class ClaudeAdapter extends AgentAdapter {
|
|
11
|
+
options;
|
|
11
12
|
type = 'claude';
|
|
12
13
|
process = null;
|
|
13
14
|
agentSessionId = null;
|
|
@@ -17,6 +18,10 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
17
18
|
runId = '';
|
|
18
19
|
hasEmittedText = false;
|
|
19
20
|
terminalState = 'open';
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
20
25
|
async start(sessionId, workDir, agentSessionId) {
|
|
21
26
|
this.sessionId = sessionId;
|
|
22
27
|
this.workDir = workDir;
|
|
@@ -29,6 +34,12 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
29
34
|
this.runId = randomUUID();
|
|
30
35
|
this.resetRunState();
|
|
31
36
|
const args = ['-p', text, '--output-format', 'stream-json', '--verbose'];
|
|
37
|
+
if (this.options.systemPrompt) {
|
|
38
|
+
args.push('--append-system-prompt', this.options.systemPrompt);
|
|
39
|
+
}
|
|
40
|
+
if (this.options.hidden) {
|
|
41
|
+
args.push('--name', 'Shennian Manager Substrate');
|
|
42
|
+
}
|
|
32
43
|
if (process.getuid?.() !== 0) {
|
|
33
44
|
args.push('--dangerously-skip-permissions');
|
|
34
45
|
}
|
|
@@ -44,6 +55,12 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
44
55
|
this.runId = randomUUID();
|
|
45
56
|
this.resetRunState();
|
|
46
57
|
const resumeArgs = ['--resume', agentSessionId, '--output-format', 'stream-json', '--verbose'];
|
|
58
|
+
if (this.options.systemPrompt) {
|
|
59
|
+
resumeArgs.push('--append-system-prompt', this.options.systemPrompt);
|
|
60
|
+
}
|
|
61
|
+
if (this.options.hidden) {
|
|
62
|
+
resumeArgs.push('--name', 'Shennian Manager Substrate');
|
|
63
|
+
}
|
|
47
64
|
if (process.getuid?.() !== 0) {
|
|
48
65
|
resumeArgs.push('--dangerously-skip-permissions');
|
|
49
66
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentAdapter } from './adapter.js';
|
|
2
2
|
export declare class CodexAdapter extends AgentAdapter {
|
|
3
|
+
private readonly options;
|
|
3
4
|
readonly type: "codex";
|
|
4
5
|
private process;
|
|
5
6
|
private workDir;
|
|
@@ -20,6 +21,10 @@ export declare class CodexAdapter extends AgentAdapter {
|
|
|
20
21
|
private namedThread;
|
|
21
22
|
private deltaItemIds;
|
|
22
23
|
private lastTransientStatus;
|
|
24
|
+
constructor(options?: {
|
|
25
|
+
modelInstructionsFile?: string;
|
|
26
|
+
hidden?: boolean;
|
|
27
|
+
});
|
|
23
28
|
start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
24
29
|
send(text: string, modelId?: string): Promise<void>;
|
|
25
30
|
resume(agentSessionId: string): Promise<void>;
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -6,6 +6,7 @@ import { AgentAdapter, registerAgent } from './adapter.js';
|
|
|
6
6
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
7
|
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
8
8
|
export class CodexAdapter extends AgentAdapter {
|
|
9
|
+
options;
|
|
9
10
|
type = 'codex';
|
|
10
11
|
process = null;
|
|
11
12
|
workDir = null;
|
|
@@ -26,6 +27,10 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
26
27
|
namedThread = false;
|
|
27
28
|
deltaItemIds = new Set();
|
|
28
29
|
lastTransientStatus = '';
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
29
34
|
async start(_sessionId, workDir, agentSessionId) {
|
|
30
35
|
this.workDir = workDir;
|
|
31
36
|
this.seq = 0;
|
|
@@ -119,7 +124,11 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
119
124
|
this.emit('error', new Error('Command "codex" not found. Is OpenAI Codex CLI installed?'));
|
|
120
125
|
return;
|
|
121
126
|
}
|
|
122
|
-
const
|
|
127
|
+
const args = ['app-server', '--listen', 'stdio://'];
|
|
128
|
+
if (this.options.modelInstructionsFile) {
|
|
129
|
+
args.push('-c', `model_instructions_file=${JSON.stringify(this.options.modelInstructionsFile)}`);
|
|
130
|
+
}
|
|
131
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
123
132
|
cwd: this.workDir ?? undefined,
|
|
124
133
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
134
|
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
@@ -193,6 +202,7 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
193
202
|
approvalPolicy: 'never',
|
|
194
203
|
sandbox: 'danger-full-access',
|
|
195
204
|
persistExtendedHistory: true,
|
|
205
|
+
...(this.options.hidden ? { threadSource: 'subagent' } : {}),
|
|
196
206
|
...(codexModelId ? { model: codexModelId } : {}),
|
|
197
207
|
});
|
|
198
208
|
this.agentSessionId = response.thread?.id ?? this.agentSessionId;
|
|
@@ -204,6 +214,7 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
204
214
|
approvalPolicy: 'never',
|
|
205
215
|
sandbox: 'danger-full-access',
|
|
206
216
|
serviceName: 'Shennian',
|
|
217
|
+
threadSource: this.options.hidden ? 'subagent' : 'user',
|
|
207
218
|
experimentalRawEvents: false,
|
|
208
219
|
persistExtendedHistory: true,
|
|
209
220
|
...(codexModelId ? { model: codexModelId } : {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @arch docs/architecture/cli/windows-agent-launch.md
|
|
2
2
|
// @test src/__tests__/command-spec.test.ts
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
const BUILTIN_COMMANDS = {
|
|
6
7
|
claude: [
|
|
@@ -24,6 +25,9 @@ const BUILTIN_COMMANDS = {
|
|
|
24
25
|
pi: [
|
|
25
26
|
{ command: 'shennian', args: [], display: 'shennian' },
|
|
26
27
|
],
|
|
28
|
+
manager: [
|
|
29
|
+
{ command: 'shennian', args: ['manager'], display: 'shennian manager' },
|
|
30
|
+
],
|
|
27
31
|
};
|
|
28
32
|
const availabilityCache = new Map();
|
|
29
33
|
const resolvedCache = new Map();
|
|
@@ -44,19 +48,82 @@ function splitLines(text) {
|
|
|
44
48
|
.map((line) => line.trim())
|
|
45
49
|
.filter(Boolean);
|
|
46
50
|
}
|
|
51
|
+
function getFallbackCommandCandidates(command) {
|
|
52
|
+
if (path.basename(command) !== command)
|
|
53
|
+
return [command];
|
|
54
|
+
const home = os.homedir();
|
|
55
|
+
if (getProcessPlatform() === 'win32') {
|
|
56
|
+
const names = path.extname(command)
|
|
57
|
+
? [command]
|
|
58
|
+
: [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`];
|
|
59
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
60
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
61
|
+
const dirs = [
|
|
62
|
+
path.join(appData, 'npm'),
|
|
63
|
+
path.join(localAppData, 'pnpm'),
|
|
64
|
+
path.join(home, 'scoop', 'shims'),
|
|
65
|
+
path.join('C:\\', 'Program Files', 'nodejs'),
|
|
66
|
+
];
|
|
67
|
+
return dirs.flatMap((dir) => names.map((name) => path.join(dir, name)));
|
|
68
|
+
}
|
|
69
|
+
const dirs = [
|
|
70
|
+
path.join(home, '.npm-global', 'bin'),
|
|
71
|
+
path.join(home, '.local', 'bin'),
|
|
72
|
+
path.join(home, '.bun', 'bin'),
|
|
73
|
+
'/opt/homebrew/bin',
|
|
74
|
+
'/usr/local/bin',
|
|
75
|
+
'/usr/bin',
|
|
76
|
+
'/bin',
|
|
77
|
+
];
|
|
78
|
+
return dirs.map((dir) => path.join(dir, command));
|
|
79
|
+
}
|
|
80
|
+
function buildFallbackPathEnv(currentPath, command) {
|
|
81
|
+
const parts = currentPath?.split(path.delimiter).filter(Boolean) ?? [];
|
|
82
|
+
if (getProcessPlatform() === 'win32') {
|
|
83
|
+
if (command && path.basename(command) !== command) {
|
|
84
|
+
const commandDir = path.dirname(command);
|
|
85
|
+
if (!parts.includes(commandDir))
|
|
86
|
+
parts.unshift(commandDir);
|
|
87
|
+
}
|
|
88
|
+
return parts.join(path.delimiter);
|
|
89
|
+
}
|
|
90
|
+
if (command && path.basename(command) !== command) {
|
|
91
|
+
const commandDir = path.dirname(command);
|
|
92
|
+
if (!parts.includes(commandDir))
|
|
93
|
+
parts.unshift(commandDir);
|
|
94
|
+
}
|
|
95
|
+
for (const candidate of getFallbackCommandCandidates('shennian')) {
|
|
96
|
+
const dir = path.dirname(candidate);
|
|
97
|
+
if (!parts.includes(dir))
|
|
98
|
+
parts.push(dir);
|
|
99
|
+
}
|
|
100
|
+
return parts.join(path.delimiter);
|
|
101
|
+
}
|
|
47
102
|
function lookupCommandPaths(command) {
|
|
48
103
|
if (pathLookupCache.has(command)) {
|
|
49
104
|
return pathLookupCache.get(command) ?? [];
|
|
50
105
|
}
|
|
51
|
-
const
|
|
106
|
+
const isWindows = getProcessPlatform() === 'win32';
|
|
107
|
+
const lookup = isWindows ? 'where' : 'which';
|
|
52
108
|
const result = spawnSync(lookup, [command], {
|
|
53
109
|
encoding: 'utf-8',
|
|
54
110
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
111
|
timeout: 3000,
|
|
112
|
+
env: {
|
|
113
|
+
...process.env,
|
|
114
|
+
PATH: isWindows ? process.env.PATH : buildFallbackPathEnv(process.env.PATH, command),
|
|
115
|
+
},
|
|
56
116
|
});
|
|
57
117
|
const paths = result.status === 0 ? splitLines(result.stdout ?? '') : [];
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
const withFallbacks = [...paths];
|
|
119
|
+
if (!isWindows) {
|
|
120
|
+
for (const candidate of getFallbackCommandCandidates(command)) {
|
|
121
|
+
if (!withFallbacks.includes(candidate))
|
|
122
|
+
withFallbacks.push(candidate);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
pathLookupCache.set(command, withFallbacks);
|
|
126
|
+
return withFallbacks;
|
|
60
127
|
}
|
|
61
128
|
function getWindowsShellPath() {
|
|
62
129
|
return process.env.ComSpec || 'cmd.exe';
|
|
@@ -286,6 +353,10 @@ export function spawnCommandString(commandString, runtimeArgs, options = {}) {
|
|
|
286
353
|
return spawn(launch.command, launch.args, {
|
|
287
354
|
...options,
|
|
288
355
|
cwd: launch.cwd,
|
|
356
|
+
env: {
|
|
357
|
+
...options.env,
|
|
358
|
+
PATH: buildFallbackPathEnv(options.env?.PATH ?? process.env.PATH, parts[0]),
|
|
359
|
+
},
|
|
289
360
|
...(getProcessPlatform() === 'win32' && invocation?.kind === 'cmd-shim'
|
|
290
361
|
? { windowsVerbatimArguments: true }
|
|
291
362
|
: {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AVAILABLE_BUILTIN_AGENT_TYPES } from '@shennian/wire';
|
|
2
2
|
import { loadConfig } from '../config/index.js';
|
|
3
3
|
import { getCommandVersion, resolveBuiltinCommand } from './command-spec.js';
|
|
4
|
-
const AGENTS = AVAILABLE_BUILTIN_AGENT_TYPES.filter((agent) => agent !== 'pi');
|
|
4
|
+
const AGENTS = AVAILABLE_BUILTIN_AGENT_TYPES.filter((agent) => agent !== 'pi' && agent !== 'manager');
|
|
5
5
|
function getCustomAgentModels(caps) {
|
|
6
6
|
const ids = caps.models?.length
|
|
7
7
|
? caps.models
|
|
@@ -16,9 +16,19 @@ function getCustomAgentModels(caps) {
|
|
|
16
16
|
}));
|
|
17
17
|
}
|
|
18
18
|
export function detectAgents() {
|
|
19
|
-
// Pi
|
|
19
|
+
// Pi and Manager are always available. Manager delegates to Codex/Claude at run time.
|
|
20
20
|
const detected = [
|
|
21
21
|
{ type: 'pi', command: 'shennian', args: [], version: undefined },
|
|
22
|
+
{
|
|
23
|
+
type: 'manager',
|
|
24
|
+
command: 'shennian',
|
|
25
|
+
args: ['manager'],
|
|
26
|
+
version: undefined,
|
|
27
|
+
models: [
|
|
28
|
+
{ id: 'codex', name: 'Codex', provider: 'shennian', isDefault: true },
|
|
29
|
+
{ id: 'claude', name: 'Claude Code', provider: 'shennian' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
22
32
|
];
|
|
23
33
|
for (const agent of AGENTS) {
|
|
24
34
|
const spec = resolveBuiltinCommand(agent);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentAdapter } from './adapter.js';
|
|
2
|
+
export declare class ManagerAdapter extends AgentAdapter {
|
|
3
|
+
readonly type: "manager";
|
|
4
|
+
private inner;
|
|
5
|
+
private sessionId;
|
|
6
|
+
private workDir;
|
|
7
|
+
private agentSessionId;
|
|
8
|
+
private modelId;
|
|
9
|
+
start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
10
|
+
send(text: string, modelId?: string): Promise<void>;
|
|
11
|
+
resume(agentSessionId: string): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
private bindInner;
|
|
14
|
+
private wrapEvent;
|
|
15
|
+
private withManagerEnv;
|
|
16
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @arch docs/features/manager-agent.md
|
|
2
|
+
// @test src/__tests__/manager-runtime.test.ts
|
|
3
|
+
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
4
|
+
import { CodexAdapter } from './codex.js';
|
|
5
|
+
import { ClaudeAdapter } from './claude.js';
|
|
6
|
+
import { MANAGER_SYSTEM_PROMPT, buildManagerPrompt } from '../manager/prompt.js';
|
|
7
|
+
import { getManagerRuntimeService } from '../manager/runtime.js';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
function normalizeManagerModel(modelId) {
|
|
11
|
+
return modelId === 'claude' ? 'claude' : 'codex';
|
|
12
|
+
}
|
|
13
|
+
function safeSessionDirName(sessionId) {
|
|
14
|
+
return sessionId.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
15
|
+
}
|
|
16
|
+
function readProjectAgentsMd(workDir) {
|
|
17
|
+
const agentsPath = path.join(workDir, 'AGENTS.md');
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(agentsPath, 'utf8').trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function managerInstructionsPath(workDir, sessionId) {
|
|
26
|
+
return path.join(workDir, '.shennian', 'runtime', 'manager', safeSessionDirName(sessionId), 'instructions.md');
|
|
27
|
+
}
|
|
28
|
+
function buildStableManagerInstructions(workDir, managerSessionId) {
|
|
29
|
+
const projectInstructions = readProjectAgentsMd(workDir);
|
|
30
|
+
const channelInstructions = getManagerRuntimeService()?.getManagerExternalChannelSystemPrompt(managerSessionId) ?? '';
|
|
31
|
+
const sections = [
|
|
32
|
+
'# Shennian Manager Instructions',
|
|
33
|
+
'This file is generated by Shennian for a Manager Agent substrate session. Do not edit it by hand.',
|
|
34
|
+
projectInstructions
|
|
35
|
+
? `## Project Instructions\n\n${projectInstructions}`
|
|
36
|
+
: '',
|
|
37
|
+
`## Manager Instructions\n\n${MANAGER_SYSTEM_PROMPT}`,
|
|
38
|
+
channelInstructions
|
|
39
|
+
? `## External Message Channel Instructions\n\n${channelInstructions}`
|
|
40
|
+
: '',
|
|
41
|
+
].filter(Boolean);
|
|
42
|
+
return `${sections.join('\n\n')}\n`;
|
|
43
|
+
}
|
|
44
|
+
function ensureManagerInstructionsFile(workDir, sessionId) {
|
|
45
|
+
const filePath = managerInstructionsPath(workDir, sessionId);
|
|
46
|
+
const content = buildStableManagerInstructions(workDir, sessionId);
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
+
try {
|
|
49
|
+
if (fs.readFileSync(filePath, 'utf8') === content)
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Write the generated instruction file below.
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(filePath, content);
|
|
56
|
+
return filePath;
|
|
57
|
+
}
|
|
58
|
+
export class ManagerAdapter extends AgentAdapter {
|
|
59
|
+
type = 'manager';
|
|
60
|
+
inner = null;
|
|
61
|
+
sessionId = '';
|
|
62
|
+
workDir = '';
|
|
63
|
+
agentSessionId = null;
|
|
64
|
+
modelId = 'codex';
|
|
65
|
+
async start(sessionId, workDir, agentSessionId) {
|
|
66
|
+
this.sessionId = sessionId;
|
|
67
|
+
this.workDir = workDir;
|
|
68
|
+
this.agentSessionId = agentSessionId ?? null;
|
|
69
|
+
getManagerRuntimeService()?.registerManager({
|
|
70
|
+
sessionId,
|
|
71
|
+
agentSessionId: this.agentSessionId,
|
|
72
|
+
workDir,
|
|
73
|
+
modelId: this.modelId,
|
|
74
|
+
status: 'idle',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async send(text, modelId) {
|
|
78
|
+
const nextModel = normalizeManagerModel(modelId);
|
|
79
|
+
if (!this.inner || nextModel !== this.modelId) {
|
|
80
|
+
await this.inner?.stop().catch(() => { });
|
|
81
|
+
this.inner?.removeAllListeners();
|
|
82
|
+
this.modelId = nextModel;
|
|
83
|
+
const instructionsFile = ensureManagerInstructionsFile(this.workDir, this.sessionId);
|
|
84
|
+
const systemPrompt = buildStableManagerInstructions(this.workDir, this.sessionId);
|
|
85
|
+
this.inner = nextModel === 'claude'
|
|
86
|
+
? new ClaudeAdapter({ systemPrompt, hidden: true })
|
|
87
|
+
: new CodexAdapter({ modelInstructionsFile: instructionsFile, hidden: true });
|
|
88
|
+
this.bindInner(this.inner);
|
|
89
|
+
await this.inner.start(this.sessionId, this.workDir, this.agentSessionId);
|
|
90
|
+
getManagerRuntimeService()?.bindManagerAdapterEvents(this.sessionId, this.inner);
|
|
91
|
+
}
|
|
92
|
+
getManagerRuntimeService()?.registerManager({
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
agentSessionId: this.agentSessionId,
|
|
95
|
+
workDir: this.workDir,
|
|
96
|
+
modelId: this.modelId,
|
|
97
|
+
status: 'running',
|
|
98
|
+
});
|
|
99
|
+
await getManagerRuntimeService()?.ready();
|
|
100
|
+
await this.withManagerEnv(() => this.inner.send(buildManagerPrompt(text)));
|
|
101
|
+
}
|
|
102
|
+
async resume(agentSessionId) {
|
|
103
|
+
this.agentSessionId = agentSessionId;
|
|
104
|
+
if (this.inner)
|
|
105
|
+
await this.inner.resume(agentSessionId);
|
|
106
|
+
}
|
|
107
|
+
async stop() {
|
|
108
|
+
await this.inner?.stop();
|
|
109
|
+
}
|
|
110
|
+
bindInner(inner) {
|
|
111
|
+
inner.on('agentEvent', (event) => {
|
|
112
|
+
if (event.agentSessionId)
|
|
113
|
+
this.agentSessionId = event.agentSessionId;
|
|
114
|
+
this.emit('agentEvent', this.wrapEvent(event));
|
|
115
|
+
});
|
|
116
|
+
inner.on('error', (error) => this.emit('error', error));
|
|
117
|
+
}
|
|
118
|
+
wrapEvent(event) {
|
|
119
|
+
return {
|
|
120
|
+
...event,
|
|
121
|
+
source: event.source ?? this.modelId,
|
|
122
|
+
agentSessionId: event.agentSessionId ?? this.agentSessionId ?? undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async withManagerEnv(work) {
|
|
126
|
+
const env = getManagerRuntimeService()?.getInjectedEnv(this.sessionId, this.agentSessionId, this.workDir, this.modelId) ?? {};
|
|
127
|
+
const previous = new Map();
|
|
128
|
+
for (const [key, value] of Object.entries(env)) {
|
|
129
|
+
previous.set(key, process.env[key]);
|
|
130
|
+
process.env[key] = value;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return await work();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
for (const [key, value] of previous) {
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
delete process.env[key];
|
|
139
|
+
else
|
|
140
|
+
process.env[key] = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
registerAgent('manager', () => new ManagerAdapter());
|
|
@@ -126,6 +126,10 @@ const fallbackPiModels = [
|
|
|
126
126
|
{ id: 'qwen3.6-plus', name: 'Qwen 3.6 Plus', provider: 'dashscope', isDefault: true },
|
|
127
127
|
{ id: 'qwen3.6-flash', name: 'Qwen 3.6 Flash', provider: 'dashscope' },
|
|
128
128
|
];
|
|
129
|
+
const managerModels = [
|
|
130
|
+
{ id: 'codex', name: 'Codex', provider: 'shennian', isDefault: true },
|
|
131
|
+
{ id: 'claude', name: 'Claude Code', provider: 'shennian' },
|
|
132
|
+
];
|
|
129
133
|
async function discoverPiModels(context) {
|
|
130
134
|
if (!context.serverUrl || !context.authToken)
|
|
131
135
|
return fallbackPiModels;
|
|
@@ -167,6 +171,8 @@ async function discoverBuiltinModels(type, context) {
|
|
|
167
171
|
return discoverOpenCodeModels();
|
|
168
172
|
case 'pi':
|
|
169
173
|
return discoverPiModels(context);
|
|
174
|
+
case 'manager':
|
|
175
|
+
return managerModels;
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
export async function discoverModelsForAgent(agent, context) {
|
package/dist/src/agents/pi.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class PiAdapter extends AgentAdapter {
|
|
|
26
26
|
private lastSummaryMsgCount;
|
|
27
27
|
private pendingBaseMessages;
|
|
28
28
|
private finalizePromise;
|
|
29
|
+
private sendGeneration;
|
|
29
30
|
private pendingSendStart;
|
|
30
31
|
start(sessionId: string, workDir: string, _agentSessionId?: string | null): Promise<void>;
|
|
31
32
|
send(text: string, modelId?: string): Promise<void>;
|
package/dist/src/agents/pi.js
CHANGED
|
@@ -394,6 +394,7 @@ export class PiAdapter extends AgentAdapter {
|
|
|
394
394
|
lastSummaryMsgCount = 0;
|
|
395
395
|
pendingBaseMessages = [];
|
|
396
396
|
finalizePromise = Promise.resolve();
|
|
397
|
+
sendGeneration = 0;
|
|
397
398
|
pendingSendStart = null;
|
|
398
399
|
async start(sessionId, workDir, _agentSessionId) {
|
|
399
400
|
this.sessionId = sessionId;
|
|
@@ -407,7 +408,16 @@ export class PiAdapter extends AgentAdapter {
|
|
|
407
408
|
this.loadSummary();
|
|
408
409
|
}
|
|
409
410
|
async send(text, modelId) {
|
|
410
|
-
this.
|
|
411
|
+
const generation = ++this.sendGeneration;
|
|
412
|
+
const interruptedMessages = this.agent && this.terminalState === 'open'
|
|
413
|
+
? cloneMessages(this.pendingBaseMessages)
|
|
414
|
+
: null;
|
|
415
|
+
if (interruptedMessages) {
|
|
416
|
+
this.agent?.abort();
|
|
417
|
+
this.agent = null;
|
|
418
|
+
this.restoredMessages = interruptedMessages;
|
|
419
|
+
this.finalizePromise = Promise.resolve();
|
|
420
|
+
}
|
|
411
421
|
const config = loadConfig();
|
|
412
422
|
const authToken = config.machineToken ?? config.accessToken;
|
|
413
423
|
if (!authToken) {
|
|
@@ -420,7 +430,7 @@ export class PiAdapter extends AgentAdapter {
|
|
|
420
430
|
return;
|
|
421
431
|
}
|
|
422
432
|
this.authToken = authToken;
|
|
423
|
-
this.proxyUrl = (config.serverUrl ?? SERVERS.
|
|
433
|
+
this.proxyUrl = (config.serverUrl ?? SERVERS.cn.url).replace(/\/$/, '');
|
|
424
434
|
this.runId = randomUUID();
|
|
425
435
|
this.seq = 0;
|
|
426
436
|
this.emittedLengths.clear();
|
|
@@ -438,10 +448,14 @@ export class PiAdapter extends AgentAdapter {
|
|
|
438
448
|
this.emit('agentEvent', { state: 'init', runId, seq: ++this.seq });
|
|
439
449
|
void this.agent.prompt(text)
|
|
440
450
|
.then(async () => {
|
|
451
|
+
if (generation !== this.sendGeneration)
|
|
452
|
+
return;
|
|
441
453
|
this.resolvePendingSendStart(runId);
|
|
442
454
|
await this.finalizePromise.catch(() => { });
|
|
443
455
|
})
|
|
444
456
|
.catch((err) => {
|
|
457
|
+
if (generation !== this.sendGeneration)
|
|
458
|
+
return;
|
|
445
459
|
this.rejectPendingSendStart(runId, err);
|
|
446
460
|
if (this.terminalState !== 'open')
|
|
447
461
|
return;
|
|
@@ -500,6 +514,8 @@ export class PiAdapter extends AgentAdapter {
|
|
|
500
514
|
});
|
|
501
515
|
}
|
|
502
516
|
agent.subscribe((evt) => {
|
|
517
|
+
if (this.agent !== agent)
|
|
518
|
+
return;
|
|
503
519
|
if (this.terminalState !== 'open')
|
|
504
520
|
return;
|
|
505
521
|
const seq = ++this.seq;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type ExternalChannelType = 'wecom' | 'websocket';
|
|
2
|
+
export type ExternalChannelConfig = {
|
|
3
|
+
id: string;
|
|
4
|
+
type: ExternalChannelType;
|
|
5
|
+
name: string;
|
|
6
|
+
managerSessionId: string;
|
|
7
|
+
workDir: string;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
secretRef: string;
|
|
10
|
+
};
|
|
11
|
+
export type ExternalChannelView = {
|
|
12
|
+
id: string;
|
|
13
|
+
type: ExternalChannelType;
|
|
14
|
+
name: string;
|
|
15
|
+
managerSessionId: string;
|
|
16
|
+
workDir: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
wsUrl?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
tokenConfigured?: boolean;
|
|
21
|
+
canReply?: boolean;
|
|
22
|
+
systemPrompt?: string;
|
|
23
|
+
};
|
|
24
|
+
export type ExternalMessageEvent = {
|
|
25
|
+
type: 'external.message';
|
|
26
|
+
channelId: string;
|
|
27
|
+
channelType: ExternalChannelType;
|
|
28
|
+
conversationId: string;
|
|
29
|
+
messageId: string;
|
|
30
|
+
sender: {
|
|
31
|
+
id: string;
|
|
32
|
+
name?: string | null;
|
|
33
|
+
};
|
|
34
|
+
text: string;
|
|
35
|
+
attachments: Array<{
|
|
36
|
+
type: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
mimeType?: string;
|
|
40
|
+
size?: number;
|
|
41
|
+
}>;
|
|
42
|
+
receivedAt: string;
|
|
43
|
+
replyTarget: string;
|
|
44
|
+
rawRef?: string | null;
|
|
45
|
+
};
|
|
46
|
+
export type ExternalReply = {
|
|
47
|
+
channelId: string;
|
|
48
|
+
conversationId: string;
|
|
49
|
+
text: string;
|
|
50
|
+
messageId?: string;
|
|
51
|
+
idempotencyKey?: string;
|
|
52
|
+
};
|
|
53
|
+
export interface ExternalChannelAdapter {
|
|
54
|
+
readonly type: ExternalChannelType;
|
|
55
|
+
connect(config: ExternalChannelConfig): Promise<void>;
|
|
56
|
+
disconnect(config: ExternalChannelConfig): Promise<void>;
|
|
57
|
+
send(config: ExternalChannelConfig, reply: ExternalReply): Promise<void>;
|
|
58
|
+
health(config: ExternalChannelConfig): Promise<{
|
|
59
|
+
ok: boolean;
|
|
60
|
+
message?: string;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExternalChannelConfig } from './base.js';
|
|
2
|
+
export declare class ChannelConfigRegistry {
|
|
3
|
+
list(): ExternalChannelConfig[];
|
|
4
|
+
get(channelId: string): ExternalChannelConfig | undefined;
|
|
5
|
+
upsert(config: ExternalChannelConfig): void;
|
|
6
|
+
replaceAll(channels: ExternalChannelConfig[]): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @arch docs/features/manager-agent.md
|
|
2
|
+
// @test src/__tests__/manager-runtime.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
+
const CHANNELS_PATH = resolveShennianPath('channels.json');
|
|
7
|
+
export class ChannelConfigRegistry {
|
|
8
|
+
list() {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(fs.readFileSync(CHANNELS_PATH, 'utf-8'));
|
|
11
|
+
return parsed.channels ?? [];
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
get(channelId) {
|
|
18
|
+
return this.list().find((channel) => channel.id === channelId);
|
|
19
|
+
}
|
|
20
|
+
upsert(config) {
|
|
21
|
+
const channels = this.list().filter((channel) => channel.id !== config.id);
|
|
22
|
+
channels.push(config);
|
|
23
|
+
fs.mkdirSync(path.dirname(CHANNELS_PATH), { recursive: true });
|
|
24
|
+
fs.writeFileSync(CHANNELS_PATH, JSON.stringify({ channels }, null, 2));
|
|
25
|
+
}
|
|
26
|
+
replaceAll(channels) {
|
|
27
|
+
fs.mkdirSync(path.dirname(CHANNELS_PATH), { recursive: true });
|
|
28
|
+
fs.writeFileSync(CHANNELS_PATH, JSON.stringify({ channels }, null, 2));
|
|
29
|
+
}
|
|
30
|
+
}
|