shennian 0.2.51 → 0.2.53

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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { AgentType } from '@shennian/wire';
2
+ import type { AgentType, ExternalChannelSessionStatus } from '@shennian/wire';
3
3
  export type AgentEvent = {
4
4
  state: string;
5
5
  runId: string;
@@ -24,8 +24,12 @@ export interface AgentAdapterEvents {
24
24
  }
25
25
  export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEvents> {
26
26
  abstract readonly type: AgentType;
27
+ configure?(options: {
28
+ externalChannel?: ExternalChannelSessionStatus | null;
29
+ env?: NodeJS.ProcessEnv;
30
+ }): void;
27
31
  abstract start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
28
- abstract send(text: string, modelId?: string): Promise<void>;
32
+ abstract send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
29
33
  abstract resume(agentSessionId: string): Promise<void>;
30
34
  abstract stop(): Promise<void>;
31
35
  }
@@ -1,5 +1,7 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
+ import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
3
  export declare function normalizeClaudeModelId(modelId?: string | null): string;
4
+ export declare function normalizeClaudeReasoningEffort(reasoningEffort?: string | null): string | undefined;
3
5
  export declare class ClaudeAdapter extends AgentAdapter {
4
6
  private readonly options;
5
7
  readonly type: "claude";
@@ -15,8 +17,14 @@ export declare class ClaudeAdapter extends AgentAdapter {
15
17
  systemPrompt?: string;
16
18
  hidden?: boolean;
17
19
  });
20
+ private externalChannel;
21
+ private extraEnv;
22
+ configure(options: {
23
+ externalChannel?: ExternalChannelSessionStatus | null;
24
+ env?: NodeJS.ProcessEnv;
25
+ }): void;
18
26
  start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
19
- send(text: string, modelId?: string): Promise<void>;
27
+ send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
20
28
  resume(agentSessionId: string): Promise<void>;
21
29
  stop(): Promise<void>;
22
30
  private spawnAndParse;
@@ -8,6 +8,15 @@ export function normalizeClaudeModelId(modelId) {
8
8
  const trimmed = modelId?.trim();
9
9
  return trimmed || 'default';
10
10
  }
11
+ const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
12
+ export function normalizeClaudeReasoningEffort(reasoningEffort) {
13
+ const trimmed = reasoningEffort?.trim();
14
+ if (!trimmed)
15
+ return undefined;
16
+ if (CLAUDE_REASONING_EFFORTS.has(trimmed))
17
+ return trimmed;
18
+ throw new Error(`Unsupported Claude reasoning effort "${trimmed}". Supported values: low, medium, high, xhigh, max.`);
19
+ }
11
20
  export class ClaudeAdapter extends AgentAdapter {
12
21
  options;
13
22
  type = 'claude';
@@ -23,6 +32,12 @@ export class ClaudeAdapter extends AgentAdapter {
23
32
  super();
24
33
  this.options = options;
25
34
  }
35
+ externalChannel = null;
36
+ extraEnv = {};
37
+ configure(options) {
38
+ this.externalChannel = options.externalChannel ?? null;
39
+ this.extraEnv = options.env ?? {};
40
+ }
26
41
  async start(sessionId, workDir, agentSessionId) {
27
42
  this.sessionId = sessionId;
28
43
  this.workDir = workDir;
@@ -30,12 +45,12 @@ export class ClaudeAdapter extends AgentAdapter {
30
45
  if (agentSessionId)
31
46
  this.agentSessionId = agentSessionId;
32
47
  }
33
- async send(text, modelId) {
48
+ async send(text, modelId, reasoningEffort) {
34
49
  await this.killProcess();
35
50
  this.runId = randomUUID();
36
51
  this.resetRunState();
37
52
  const args = ['-p', text, '--output-format', 'stream-json', '--verbose'];
38
- const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd());
53
+ const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel);
39
54
  if (systemPrompt) {
40
55
  args.push('--append-system-prompt', systemPrompt);
41
56
  }
@@ -46,6 +61,10 @@ export class ClaudeAdapter extends AgentAdapter {
46
61
  args.push('--dangerously-skip-permissions');
47
62
  }
48
63
  args.push('--model', normalizeClaudeModelId(modelId));
64
+ const effort = normalizeClaudeReasoningEffort(reasoningEffort);
65
+ if (effort) {
66
+ args.push('--effort', effort);
67
+ }
49
68
  if (this.agentSessionId) {
50
69
  args.push('--resume', this.agentSessionId);
51
70
  }
@@ -57,7 +76,7 @@ export class ClaudeAdapter extends AgentAdapter {
57
76
  this.runId = randomUUID();
58
77
  this.resetRunState();
59
78
  const resumeArgs = ['--resume', agentSessionId, '--output-format', 'stream-json', '--verbose'];
60
- const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd());
79
+ const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel);
61
80
  if (systemPrompt) {
62
81
  resumeArgs.push('--append-system-prompt', systemPrompt);
63
82
  }
@@ -81,7 +100,7 @@ export class ClaudeAdapter extends AgentAdapter {
81
100
  const proc = spawnResolvedCommand(spec, args, {
82
101
  cwd: this.workDir ?? undefined,
83
102
  stdio: ['ignore', 'pipe', 'pipe'],
84
- env: buildAgentProcessEnv(),
103
+ env: buildAgentProcessEnv(this.extraEnv),
85
104
  });
86
105
  this.process = proc;
87
106
  const rl = createInterface({ input: proc.stdout });
@@ -1,4 +1,5 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
+ import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
3
  export declare class CodexAdapter extends AgentAdapter {
3
4
  private readonly options;
4
5
  readonly type: "codex";
@@ -25,8 +26,14 @@ export declare class CodexAdapter extends AgentAdapter {
25
26
  modelInstructionsFile?: string;
26
27
  hidden?: boolean;
27
28
  });
29
+ private externalChannel;
30
+ private extraEnv;
31
+ configure(options: {
32
+ externalChannel?: ExternalChannelSessionStatus | null;
33
+ env?: NodeJS.ProcessEnv;
34
+ }): void;
28
35
  start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
29
- send(text: string, modelId?: string): Promise<void>;
36
+ send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
30
37
  resume(agentSessionId: string): Promise<void>;
31
38
  stop(): Promise<void>;
32
39
  private spawnCodex;
@@ -58,4 +65,6 @@ export declare class CodexAdapter extends AgentAdapter {
58
65
  private clearForceCloseTimer;
59
66
  }
60
67
  export declare function normalizeCodexModelId(modelId?: string | null): string | undefined;
68
+ export declare function normalizeCodexReasoningEffort(reasoningEffort?: string | null): string | undefined;
61
69
  export declare function isMissingCodexRolloutError(error: unknown): boolean;
70
+ export declare function isCodexUnsupportedEffortError(error: unknown): boolean;
@@ -32,18 +32,25 @@ export class CodexAdapter extends AgentAdapter {
32
32
  super();
33
33
  this.options = options;
34
34
  }
35
+ externalChannel = null;
36
+ extraEnv = {};
37
+ configure(options) {
38
+ this.externalChannel = options.externalChannel ?? null;
39
+ this.extraEnv = options.env ?? {};
40
+ }
35
41
  async start(_sessionId, workDir, agentSessionId) {
36
42
  this.workDir = workDir;
37
43
  this.seq = 0;
38
44
  if (agentSessionId)
39
45
  this.agentSessionId = agentSessionId;
40
46
  }
41
- async send(text, modelId) {
47
+ async send(text, modelId, reasoningEffort) {
42
48
  if (this.activeTurnId) {
43
49
  await this.interruptActiveTurn().catch(() => { });
44
50
  await this.killProcess();
45
51
  }
46
52
  const codexModelId = normalizeCodexModelId(modelId);
53
+ const codexReasoningEffort = normalizeCodexReasoningEffort(reasoningEffort);
47
54
  this.runId = randomUUID();
48
55
  this.seq = 0;
49
56
  this.resetRunState();
@@ -59,7 +66,7 @@ export class CodexAdapter extends AgentAdapter {
59
66
  }).catch(() => { });
60
67
  this.namedThread = true;
61
68
  }
62
- const response = await this.startTurnWithRecovery(threadId, text, codexModelId);
69
+ const response = await this.startTurnWithRecovery(threadId, text, codexModelId, codexReasoningEffort);
63
70
  this.activeTurnId = response.turn?.id ?? null;
64
71
  }
65
72
  async resume(agentSessionId) {
@@ -81,7 +88,7 @@ export class CodexAdapter extends AgentAdapter {
81
88
  const proc = spawnResolvedCommand(spec, args, {
82
89
  cwd: this.workDir ?? undefined,
83
90
  stdio: ['ignore', 'pipe', 'pipe'],
84
- env: buildAgentProcessEnv(),
91
+ env: buildAgentProcessEnv(this.extraEnv),
85
92
  });
86
93
  this.process = proc;
87
94
  const rl = createInterface({ input: proc.stdout });
@@ -126,14 +133,14 @@ export class CodexAdapter extends AgentAdapter {
126
133
  return;
127
134
  }
128
135
  const args = ['app-server', '--listen', 'stdio://'];
129
- const modelInstructionsFile = this.options.modelInstructionsFile ?? ensurePlatformInstructionsFile(this.workDir ?? process.cwd());
136
+ const modelInstructionsFile = this.options.modelInstructionsFile ?? ensurePlatformInstructionsFile(this.workDir ?? process.cwd(), this.externalChannel);
130
137
  if (modelInstructionsFile) {
131
138
  args.push('-c', `model_instructions_file=${JSON.stringify(modelInstructionsFile)}`);
132
139
  }
133
140
  const proc = spawnResolvedCommand(spec, args, {
134
141
  cwd: this.workDir ?? undefined,
135
142
  stdio: ['pipe', 'pipe', 'pipe'],
136
- env: buildAgentProcessEnv({ NO_COLOR: '1' }),
143
+ env: buildAgentProcessEnv({ NO_COLOR: '1', ...this.extraEnv }),
137
144
  });
138
145
  this.process = proc;
139
146
  this.stderrBuf = '';
@@ -227,26 +234,35 @@ export class CodexAdapter extends AgentAdapter {
227
234
  this.agentSessionId = threadId;
228
235
  this.namedThread = !!response.thread?.name;
229
236
  }
230
- async startTurnWithRecovery(threadId, text, codexModelId) {
237
+ async startTurnWithRecovery(threadId, text, codexModelId, reasoningEffort) {
231
238
  try {
232
- return await this.startTurn(threadId, text, codexModelId);
239
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
233
240
  }
234
241
  catch (error) {
235
242
  if (!isMissingCodexRolloutError(error))
236
243
  throw error;
237
244
  await this.killProcess();
238
245
  await this.ensureAppServer(codexModelId);
239
- return await this.startTurn(threadId, text, codexModelId);
246
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
240
247
  }
241
248
  }
242
- async startTurn(threadId, text, codexModelId) {
243
- return await this.sendRpc('turn/start', {
244
- threadId,
245
- input: [{ type: 'text', text, text_elements: [] }],
246
- approvalPolicy: 'never',
247
- sandboxPolicy: { type: 'dangerFullAccess' },
248
- ...(codexModelId ? { model: codexModelId } : {}),
249
- });
249
+ async startTurn(threadId, text, codexModelId, reasoningEffort) {
250
+ try {
251
+ return await this.sendRpc('turn/start', {
252
+ threadId,
253
+ input: [{ type: 'text', text, text_elements: [] }],
254
+ approvalPolicy: 'never',
255
+ sandboxPolicy: { type: 'dangerFullAccess' },
256
+ ...(codexModelId ? { model: codexModelId } : {}),
257
+ ...(reasoningEffort ? { effort: reasoningEffort } : {}),
258
+ });
259
+ }
260
+ catch (error) {
261
+ if (reasoningEffort && isCodexUnsupportedEffortError(error)) {
262
+ throw new Error(`Codex app-server does not accept reasoning effort "${reasoningEffort}" for this turn. Refresh models or upgrade Codex CLI, then retry.`, { cause: error });
263
+ }
264
+ throw error;
265
+ }
250
266
  }
251
267
  async interruptActiveTurn() {
252
268
  const threadId = this.agentSessionId;
@@ -793,10 +809,21 @@ export function normalizeCodexModelId(modelId) {
793
809
  return undefined;
794
810
  return trimmed.toLowerCase() === 'openai' ? undefined : trimmed;
795
811
  }
812
+ export function normalizeCodexReasoningEffort(reasoningEffort) {
813
+ const trimmed = reasoningEffort?.trim();
814
+ return trimmed || undefined;
815
+ }
796
816
  export function isMissingCodexRolloutError(error) {
797
817
  const message = error instanceof Error ? error.message : String(error ?? '');
798
818
  return /\bno rollout found for thread id\b/i.test(message);
799
819
  }
820
+ export function isCodexUnsupportedEffortError(error) {
821
+ const message = error instanceof Error ? error.message : String(error ?? '');
822
+ return (/\bunknown field\b.*\beffort\b/i.test(message) ||
823
+ /\binvalid.*\beffort\b/i.test(message) ||
824
+ /\bunsupported.*\beffort\b/i.test(message) ||
825
+ /\breasoning effort\b/i.test(message));
826
+ }
800
827
  function extractAppServerErrorMessage(params) {
801
828
  if (typeof params.message === 'string' && params.message.trim())
802
829
  return params.message.trim();
@@ -0,0 +1,2 @@
1
+ import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
+ export declare function buildExternalChannelInstructions(channel?: ExternalChannelSessionStatus | null): string;
@@ -0,0 +1,22 @@
1
+ // @arch docs/features/wecom-managed-channel.md
2
+ // @test src/__tests__/platform-instructions.test.ts
3
+ export function buildExternalChannelInstructions(channel) {
4
+ if (!channel?.configured && !channel?.connected)
5
+ return '';
6
+ const channelName = channel.name?.trim() || '外部消息通道';
7
+ const customPrompt = channel.systemPrompt?.trim();
8
+ const sections = [
9
+ `当前对话已接入外部消息通道:${channelName}。`,
10
+ `外部消息会以如下格式进入对话:\n外部消息\n<时间> <用户昵称>: <内容>`,
11
+ channel.canReply === false
12
+ ? '当前通道只允许接收消息,不要尝试向外部通道发送回复。'
13
+ : [
14
+ '当用户明确要求你向外部消息通道发送内容,或你需要回复一条外部消息时,调用:',
15
+ 'shennian external send --text "<要发送的消息>"',
16
+ '只发送用户可见的最终内容,不要发送内部推理、工具日志或实现细节。',
17
+ ].join('\n'),
18
+ '如果外部消息和当前任务无关,可以忽略或简短说明无需处理;如果处理需要时间,先简短确认,再继续完成任务。',
19
+ customPrompt ? `本通道附加约束:${customPrompt}` : '',
20
+ ].filter(Boolean);
21
+ return sections.join('\n\n');
22
+ }
@@ -7,6 +7,7 @@ import { MANAGER_SYSTEM_PROMPT, buildManagerPrompt } from '../manager/prompt.js'
7
7
  import { getManagerRuntimeService } from '../manager/runtime.js';
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
+ import { PLATFORM_OUTPUT_INSTRUCTIONS } from './platform-instructions.js';
10
11
  function normalizeManagerModel(modelId) {
11
12
  return modelId === 'claude' ? 'claude' : 'codex';
12
13
  }
@@ -38,6 +39,7 @@ function buildStableManagerInstructions(workDir, managerSessionId) {
38
39
  channelInstructions
39
40
  ? `## External Message Channel Instructions\n\n${channelInstructions}`
40
41
  : '',
42
+ PLATFORM_OUTPUT_INSTRUCTIONS,
41
43
  ].filter(Boolean);
42
44
  return `${sections.join('\n\n')}\n`;
43
45
  }
@@ -123,13 +123,13 @@ export function parseClaudeModels(raw) {
123
123
  for (const [pattern, id, name] of patterns) {
124
124
  const match = rawClean.match(pattern);
125
125
  if (match) {
126
- models.push({
126
+ models.push(withClaudeReasoningEfforts({
127
127
  id,
128
128
  name,
129
129
  description: `v${match[1]}`,
130
130
  provider: 'anthropic',
131
131
  isDefault: id === 'default',
132
- });
132
+ }));
133
133
  }
134
134
  }
135
135
  if (models.length > 0)
@@ -144,12 +144,12 @@ export function parseClaudeModels(raw) {
144
144
  continue;
145
145
  const version = match[2] ?? '';
146
146
  const isDefault = defaultMatch && defaultMatch[1]?.toLowerCase() === family && defaultMatch[2] === version;
147
- fallback.push({
147
+ fallback.push(withClaudeReasoningEfforts({
148
148
  id: alias,
149
149
  name: `${titleCaseSegment(family)} ${version}`,
150
150
  provider: 'anthropic',
151
151
  isDefault: Boolean(isDefault),
152
- });
152
+ }));
153
153
  }
154
154
  return uniqueModels(fallback);
155
155
  }
@@ -193,6 +193,20 @@ const CLAUDE_ALIAS_MODEL_ENV = {
193
193
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
194
194
  haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
195
195
  };
196
+ const CLAUDE_REASONING_EFFORTS = [
197
+ { id: 'low', name: 'Low' },
198
+ { id: 'medium', name: 'Medium' },
199
+ { id: 'high', name: 'High' },
200
+ { id: 'xhigh', name: 'Extra High' },
201
+ { id: 'max', name: 'Max' },
202
+ ];
203
+ function withClaudeReasoningEfforts(model) {
204
+ return {
205
+ ...model,
206
+ supportedReasoningEfforts: CLAUDE_REASONING_EFFORTS,
207
+ defaultReasoningEffort: 'medium',
208
+ };
209
+ }
196
210
  function readEnvValue(env, key) {
197
211
  const value = env[key]?.trim();
198
212
  return value || null;
@@ -244,13 +258,13 @@ export function parseClaudeBinaryModels(raw) {
244
258
  if (!new RegExp(`\\b${alias}\\b`, 'i').test(clean))
245
259
  continue;
246
260
  const version = (alias === 'default' ? familyVersions.get('sonnet') : familyVersions.get(alias)) ?? null;
247
- models.push({
261
+ models.push(withClaudeReasoningEfforts({
248
262
  id: alias,
249
263
  name: CLAUDE_ALIAS_LABELS[alias],
250
264
  description: formatClaudeVersion(version),
251
265
  provider: 'anthropic',
252
266
  isDefault: alias === 'default',
253
- });
267
+ }));
254
268
  }
255
269
  return uniqueModels(models);
256
270
  }
@@ -260,7 +274,7 @@ export function fallbackClaudeAliasModels() {
260
274
  { id: 'sonnet', name: 'Sonnet', provider: 'anthropic' },
261
275
  { id: 'opus', name: 'Opus', provider: 'anthropic' },
262
276
  { id: 'haiku', name: 'Haiku', provider: 'anthropic' },
263
- ]);
277
+ ]).map(withClaudeReasoningEfforts);
264
278
  }
265
279
  export function parseCodexModels(raw) {
266
280
  const clean = stripAnsi(raw);
@@ -1,4 +1,5 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
+ import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
3
  type ShellCommandSpec = {
3
4
  file: string;
4
5
  args: string[];
@@ -27,7 +28,13 @@ export declare class PiAdapter extends AgentAdapter {
27
28
  private pendingBaseMessages;
28
29
  private finalizePromise;
29
30
  private sendGeneration;
31
+ private externalChannel;
32
+ private extraEnv;
30
33
  private pendingSendStart;
34
+ configure(options: {
35
+ externalChannel?: ExternalChannelSessionStatus | null;
36
+ env?: NodeJS.ProcessEnv;
37
+ }): void;
31
38
  start(sessionId: string, workDir: string, _agentSessionId?: string | null): Promise<void>;
32
39
  send(text: string, modelId?: string): Promise<void>;
33
40
  private initAgent;
@@ -8,6 +8,7 @@ import { promisify } from 'node:util';
8
8
  import { Agent, streamProxy } from '@mariozechner/pi-agent-core';
9
9
  import { Type } from '@sinclair/typebox';
10
10
  import { AgentAdapter, registerAgent } from './adapter.js';
11
+ import { buildExternalChannelInstructions } from './external-channel-instructions.js';
11
12
  import { loadConfig, resolveShennianPath } from '../config/index.js';
12
13
  import { SERVERS } from '../region.js';
13
14
  const execFileAsync = promisify(execFile);
@@ -186,7 +187,7 @@ async function requestProxySummary(proxyUrl, authToken, prompt) {
186
187
  }
187
188
  // ── Local tools ───────────────────────────────────────────────────────────────
188
189
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
- function makeTools(workDir) {
190
+ function makeTools(workDir, extraEnv = {}) {
190
191
  return [
191
192
  {
192
193
  name: 'read_file',
@@ -292,6 +293,7 @@ function makeTools(workDir) {
292
293
  try {
293
294
  const { stdout, stderr } = await execFileAsync(spec.file, spec.args, {
294
295
  cwd: workDir,
296
+ env: { ...process.env, ...extraEnv },
295
297
  timeout: 30_000,
296
298
  signal,
297
299
  maxBuffer: 1024 * 1024,
@@ -395,7 +397,13 @@ export class PiAdapter extends AgentAdapter {
395
397
  pendingBaseMessages = [];
396
398
  finalizePromise = Promise.resolve();
397
399
  sendGeneration = 0;
400
+ externalChannel = null;
401
+ extraEnv = {};
398
402
  pendingSendStart = null;
403
+ configure(options) {
404
+ this.externalChannel = options.externalChannel ?? null;
405
+ this.extraEnv = options.env ?? {};
406
+ }
399
407
  async start(sessionId, workDir, _agentSessionId) {
400
408
  this.sessionId = sessionId;
401
409
  this.workDir = workDir;
@@ -483,10 +491,14 @@ export class PiAdapter extends AgentAdapter {
483
491
  // ── Agent lifecycle ──────────────────────────────────────────────────────────
484
492
  initAgent() {
485
493
  const workDir = this.workDir ?? process.cwd();
486
- const tools = makeTools(workDir);
494
+ const tools = makeTools(workDir, this.extraEnv);
487
495
  const agent = new Agent({
488
496
  initialState: {
489
- systemPrompt: SYSTEM_PROMPT + `\n\n当前工作目录:${workDir}`,
497
+ systemPrompt: [
498
+ SYSTEM_PROMPT,
499
+ `当前工作目录:${workDir}`,
500
+ buildExternalChannelInstructions(this.externalChannel),
501
+ ].filter(Boolean).join('\n\n'),
490
502
  model: createPiModel(),
491
503
  tools,
492
504
  },
@@ -1,3 +1,4 @@
1
+ import type { ExternalChannelSessionStatus } from '@shennian/wire';
1
2
  export declare const PLATFORM_OUTPUT_INSTRUCTIONS = "## Shennian Output Instructions\n\nWhen you mention a local file that the user may want to open in Shennian, format it as a Markdown link using the exact local path:\n\n- Use [filename.ext](</absolute/path/to/filename.ext>) for absolute Unix, macOS, or Windows paths.\n- Use [filename.ext](<relative/path/to/filename.ext>) for paths relative to the current working directory.\n- Do not use file:// URLs for local files.\n- Do not put user-openable file paths only inside code blocks.\n- Keep normal http:// and https:// links unchanged.";
2
- export declare function buildPlatformInstructions(workDir: string): string;
3
- export declare function ensurePlatformInstructionsFile(workDir: string): string;
3
+ export declare function buildPlatformInstructions(workDir: string, externalChannel?: ExternalChannelSessionStatus | null): string;
4
+ export declare function ensurePlatformInstructionsFile(workDir: string, externalChannel?: ExternalChannelSessionStatus | null): string;
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { resolveShennianPath } from '../config/index.js';
7
+ import { buildExternalChannelInstructions } from './external-channel-instructions.js';
7
8
  export const PLATFORM_OUTPUT_INSTRUCTIONS = `## Shennian Output Instructions
8
9
 
9
10
  When you mention a local file that the user may want to open in Shennian, format it as a Markdown link using the exact local path:
@@ -22,21 +23,28 @@ function readProjectAgentsMd(workDir) {
22
23
  return '';
23
24
  }
24
25
  }
25
- export function buildPlatformInstructions(workDir) {
26
+ export function buildPlatformInstructions(workDir, externalChannel) {
26
27
  const projectInstructions = readProjectAgentsMd(workDir);
28
+ const channelInstructions = buildExternalChannelInstructions(externalChannel);
27
29
  const sections = [
28
30
  '# Shennian Agent Instructions',
29
31
  projectInstructions
30
32
  ? `## Project Instructions\n\n${projectInstructions}`
31
33
  : '',
32
34
  PLATFORM_OUTPUT_INSTRUCTIONS,
35
+ channelInstructions
36
+ ? `## External Message Channel Instructions\n\n${channelInstructions}`
37
+ : '',
33
38
  ].filter(Boolean);
34
39
  return `${sections.join('\n\n')}\n`;
35
40
  }
36
- export function ensurePlatformInstructionsFile(workDir) {
37
- const key = createHash('sha256').update(path.resolve(workDir)).digest('hex').slice(0, 16);
41
+ export function ensurePlatformInstructionsFile(workDir, externalChannel) {
42
+ const key = createHash('sha256')
43
+ .update(`${path.resolve(workDir)}:${JSON.stringify(externalChannel ?? null)}`)
44
+ .digest('hex')
45
+ .slice(0, 16);
38
46
  const filePath = resolveShennianPath('runtime', 'agent-instructions', `${key}.md`);
39
- const content = buildPlatformInstructions(workDir);
47
+ const content = buildPlatformInstructions(workDir, externalChannel);
40
48
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
49
  try {
42
50
  if (fs.readFileSync(filePath, 'utf8') === content)
@@ -3,8 +3,12 @@ export type ExternalChannelConfig = {
3
3
  id: string;
4
4
  type: ExternalChannelType;
5
5
  name: string;
6
+ sessionId?: string;
6
7
  managerSessionId: string;
7
8
  workDir: string;
9
+ agentType?: string;
10
+ agentSessionId?: string | null;
11
+ modelId?: string | null;
8
12
  enabled: boolean;
9
13
  secretRef: string;
10
14
  };
@@ -12,8 +16,12 @@ export type ExternalChannelView = {
12
16
  id: string;
13
17
  type: ExternalChannelType;
14
18
  name: string;
19
+ sessionId?: string;
15
20
  managerSessionId: string;
16
21
  workDir: string;
22
+ agentType?: string;
23
+ agentSessionId?: string | null;
24
+ modelId?: string | null;
17
25
  enabled: boolean;
18
26
  wsUrl?: string;
19
27
  token?: string;
@@ -5,7 +5,7 @@ export declare class ChannelRuntime {
5
5
  private configs;
6
6
  private secrets;
7
7
  private adapters;
8
- constructor(onExternalMessage: (managerSessionId: string, event: ExternalMessageEvent) => void, createReplyTarget: (input: {
8
+ constructor(onExternalMessage: (sessionId: string, event: ExternalMessageEvent) => void, createReplyTarget: (input: {
9
9
  managerSessionId: string;
10
10
  channelId: string;
11
11
  conversationId: string;
@@ -24,19 +24,24 @@ export declare class ChannelRuntime {
24
24
  ok: false;
25
25
  error: string;
26
26
  }>;
27
- getDefaultReplyTarget(managerSessionId: string): Promise<{
27
+ getDefaultReplyTarget(sessionId: string): Promise<{
28
28
  channelId: string;
29
29
  conversationId: string;
30
30
  }>;
31
31
  getManagerChannel(managerSessionId: string, type: ExternalChannelConfig['type'], opts?: {
32
32
  includeSecret?: boolean;
33
33
  }): ExternalChannelView | null;
34
+ getChannelById(channelId: string, opts?: {
35
+ includeSecret?: boolean;
36
+ }): ExternalChannelView | null;
34
37
  getManagerChannelStatus(managerSessionId: string): {
35
38
  configured: boolean;
36
39
  connected: boolean;
37
40
  type?: string;
38
41
  channelId?: string;
39
42
  name?: string;
43
+ canReply?: boolean;
44
+ systemPrompt?: string;
40
45
  } | null;
41
46
  listManagerChannelStatuses(): Array<{
42
47
  managerSessionId: string;
@@ -52,9 +57,13 @@ export declare class ChannelRuntime {
52
57
  upsertManagerChannel(input: {
53
58
  id: string;
54
59
  managerSessionId: string;
60
+ sessionId?: string;
55
61
  workDir: string;
56
62
  type: 'websocket';
57
63
  name?: string;
64
+ agentType?: string;
65
+ agentSessionId?: string | null;
66
+ modelId?: string | null;
58
67
  enabled: boolean;
59
68
  wsUrl?: string;
60
69
  token?: string;