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.
Files changed (49) hide show
  1. package/dist/src/agents/claude.d.ts +5 -0
  2. package/dist/src/agents/claude.js +17 -0
  3. package/dist/src/agents/codex.d.ts +5 -0
  4. package/dist/src/agents/codex.js +12 -1
  5. package/dist/src/agents/command-spec.js +74 -3
  6. package/dist/src/agents/detect.js +12 -2
  7. package/dist/src/agents/manager.d.ts +16 -0
  8. package/dist/src/agents/manager.js +145 -0
  9. package/dist/src/agents/model-registry/discovery.js +6 -0
  10. package/dist/src/agents/pi.d.ts +1 -0
  11. package/dist/src/agents/pi.js +18 -2
  12. package/dist/src/channels/base.d.ts +62 -0
  13. package/dist/src/channels/base.js +3 -0
  14. package/dist/src/channels/registry.d.ts +7 -0
  15. package/dist/src/channels/registry.js +30 -0
  16. package/dist/src/channels/runtime.d.ts +60 -0
  17. package/dist/src/channels/runtime.js +158 -0
  18. package/dist/src/channels/secret-registry.d.ts +17 -0
  19. package/dist/src/channels/secret-registry.js +44 -0
  20. package/dist/src/channels/websocket.d.ts +48 -0
  21. package/dist/src/channels/websocket.js +314 -0
  22. package/dist/src/channels/wecom.d.ts +48 -0
  23. package/dist/src/channels/wecom.js +315 -0
  24. package/dist/src/commands/manager.d.ts +2 -0
  25. package/dist/src/commands/manager.js +168 -0
  26. package/dist/src/config/index.js +5 -1
  27. package/dist/src/index.js +3 -1
  28. package/dist/src/manager/prompt.d.ts +2 -0
  29. package/dist/src/manager/prompt.js +46 -0
  30. package/dist/src/manager/registry.d.ts +92 -0
  31. package/dist/src/manager/registry.js +267 -0
  32. package/dist/src/manager/runtime.d.ts +59 -0
  33. package/dist/src/manager/runtime.js +534 -0
  34. package/dist/src/native-fusion/parsers.js +6 -0
  35. package/dist/src/native-fusion/service.d.ts +1 -0
  36. package/dist/src/native-fusion/service.js +5 -3
  37. package/dist/src/native-fusion/types.d.ts +2 -1
  38. package/dist/src/region.js +7 -19
  39. package/dist/src/session/handlers/agents.js +1 -1
  40. package/dist/src/session/handlers/chat.js +121 -2
  41. package/dist/src/session/handlers/control.js +1 -1
  42. package/dist/src/session/manager.d.ts +2 -0
  43. package/dist/src/session/manager.js +16 -0
  44. package/dist/src/session/projection.d.ts +3 -0
  45. package/dist/src/session/projection.js +54 -0
  46. package/dist/src/session/store.d.ts +24 -1
  47. package/dist/src/session/store.js +66 -3
  48. package/dist/src/session/types.d.ts +2 -0
  49. 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>;
@@ -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 proc = spawnResolvedCommand(spec, ['app-server', '--listen', 'stdio://'], {
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 lookup = getProcessPlatform() === 'win32' ? 'where' : 'which';
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
- pathLookupCache.set(command, paths);
59
- return paths;
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 agent is always available (built-in, platform-powered)
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) {
@@ -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>;
@@ -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.agent?.abort();
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.global.url).replace(/\/$/, '');
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,3 @@
1
+ // @arch docs/features/manager-agent.md
2
+ // @test src/__tests__/manager-runtime.test.ts
3
+ export {};
@@ -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
+ }