@zhin.js/core 1.0.32 → 1.0.34

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 (113) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/ai/agent.d.ts.map +1 -1
  3. package/lib/ai/agent.js +15 -2
  4. package/lib/ai/agent.js.map +1 -1
  5. package/lib/ai/bootstrap.d.ts +11 -2
  6. package/lib/ai/bootstrap.d.ts.map +1 -1
  7. package/lib/ai/bootstrap.js +46 -2
  8. package/lib/ai/bootstrap.js.map +1 -1
  9. package/lib/ai/builtin-tools.d.ts +28 -6
  10. package/lib/ai/builtin-tools.d.ts.map +1 -1
  11. package/lib/ai/builtin-tools.js +265 -76
  12. package/lib/ai/builtin-tools.js.map +1 -1
  13. package/lib/ai/index.d.ts +9 -1
  14. package/lib/ai/index.d.ts.map +1 -1
  15. package/lib/ai/index.js +8 -0
  16. package/lib/ai/index.js.map +1 -1
  17. package/lib/ai/init.d.ts.map +1 -1
  18. package/lib/ai/init.js +84 -3
  19. package/lib/ai/init.js.map +1 -1
  20. package/lib/ai/providers/anthropic.d.ts +7 -0
  21. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  22. package/lib/ai/providers/anthropic.js +3 -0
  23. package/lib/ai/providers/anthropic.js.map +1 -1
  24. package/lib/ai/providers/ollama.d.ts +10 -0
  25. package/lib/ai/providers/ollama.d.ts.map +1 -1
  26. package/lib/ai/providers/ollama.js +11 -3
  27. package/lib/ai/providers/ollama.js.map +1 -1
  28. package/lib/ai/providers/openai.d.ts +7 -0
  29. package/lib/ai/providers/openai.d.ts.map +1 -1
  30. package/lib/ai/providers/openai.js +3 -0
  31. package/lib/ai/providers/openai.js.map +1 -1
  32. package/lib/ai/service.d.ts +4 -0
  33. package/lib/ai/service.d.ts.map +1 -1
  34. package/lib/ai/service.js +7 -0
  35. package/lib/ai/service.js.map +1 -1
  36. package/lib/ai/subagent.d.ts +50 -0
  37. package/lib/ai/subagent.d.ts.map +1 -0
  38. package/lib/ai/subagent.js +144 -0
  39. package/lib/ai/subagent.js.map +1 -0
  40. package/lib/ai/types.d.ts +25 -5
  41. package/lib/ai/types.d.ts.map +1 -1
  42. package/lib/ai/zhin-agent-builtin-tools.d.ts +17 -0
  43. package/lib/ai/zhin-agent-builtin-tools.d.ts.map +1 -0
  44. package/lib/ai/zhin-agent-builtin-tools.js +220 -0
  45. package/lib/ai/zhin-agent-builtin-tools.js.map +1 -0
  46. package/lib/ai/zhin-agent-config.d.ts +54 -0
  47. package/lib/ai/zhin-agent-config.d.ts.map +1 -0
  48. package/lib/ai/zhin-agent-config.js +76 -0
  49. package/lib/ai/zhin-agent-config.js.map +1 -0
  50. package/lib/ai/zhin-agent-exec-policy.d.ts +20 -0
  51. package/lib/ai/zhin-agent-exec-policy.d.ts.map +1 -0
  52. package/lib/ai/zhin-agent-exec-policy.js +71 -0
  53. package/lib/ai/zhin-agent-exec-policy.js.map +1 -0
  54. package/lib/ai/zhin-agent-prompt.d.ts +21 -0
  55. package/lib/ai/zhin-agent-prompt.d.ts.map +1 -0
  56. package/lib/ai/zhin-agent-prompt.js +116 -0
  57. package/lib/ai/zhin-agent-prompt.js.map +1 -0
  58. package/lib/ai/zhin-agent-tool-collector.d.ts +22 -0
  59. package/lib/ai/zhin-agent-tool-collector.d.ts.map +1 -0
  60. package/lib/ai/zhin-agent-tool-collector.js +218 -0
  61. package/lib/ai/zhin-agent-tool-collector.js.map +1 -0
  62. package/lib/ai/zhin-agent.d.ts +11 -155
  63. package/lib/ai/zhin-agent.d.ts.map +1 -1
  64. package/lib/ai/zhin-agent.js +84 -684
  65. package/lib/ai/zhin-agent.js.map +1 -1
  66. package/lib/component.d.ts.map +1 -1
  67. package/lib/component.js +19 -19
  68. package/lib/component.js.map +1 -1
  69. package/lib/index.d.ts +1 -0
  70. package/lib/index.d.ts.map +1 -1
  71. package/lib/index.js +1 -0
  72. package/lib/index.js.map +1 -1
  73. package/lib/scheduler/index.d.ts +10 -0
  74. package/lib/scheduler/index.d.ts.map +1 -0
  75. package/lib/scheduler/index.js +12 -0
  76. package/lib/scheduler/index.js.map +1 -0
  77. package/lib/scheduler/scheduler.d.ts +49 -0
  78. package/lib/scheduler/scheduler.d.ts.map +1 -0
  79. package/lib/scheduler/scheduler.js +352 -0
  80. package/lib/scheduler/scheduler.js.map +1 -0
  81. package/lib/scheduler/types.d.ts +71 -0
  82. package/lib/scheduler/types.d.ts.map +1 -0
  83. package/lib/scheduler/types.js +8 -0
  84. package/lib/scheduler/types.js.map +1 -0
  85. package/lib/tool-zod.d.ts +28 -0
  86. package/lib/tool-zod.d.ts.map +1 -0
  87. package/lib/tool-zod.js +98 -0
  88. package/lib/tool-zod.js.map +1 -0
  89. package/package.json +9 -4
  90. package/src/ai/agent.ts +15 -2
  91. package/src/ai/bootstrap.ts +48 -2
  92. package/src/ai/builtin-tools.ts +283 -75
  93. package/src/ai/index.ts +19 -1
  94. package/src/ai/init.ts +85 -3
  95. package/src/ai/providers/anthropic.ts +3 -0
  96. package/src/ai/providers/ollama.ts +13 -3
  97. package/src/ai/providers/openai.ts +3 -0
  98. package/src/ai/service.ts +8 -0
  99. package/src/ai/subagent.ts +209 -0
  100. package/src/ai/types.ts +29 -2
  101. package/src/ai/zhin-agent-builtin-tools.ts +247 -0
  102. package/src/ai/zhin-agent-config.ts +113 -0
  103. package/src/ai/zhin-agent-exec-policy.ts +78 -0
  104. package/src/ai/zhin-agent-prompt.ts +136 -0
  105. package/src/ai/zhin-agent-tool-collector.ts +243 -0
  106. package/src/ai/zhin-agent.ts +113 -791
  107. package/src/component.ts +29 -28
  108. package/src/index.ts +1 -0
  109. package/src/scheduler/index.ts +28 -0
  110. package/src/scheduler/scheduler.ts +372 -0
  111. package/src/scheduler/types.ts +74 -0
  112. package/src/tool-zod.ts +115 -0
  113. package/tests/ai/subagent.test.ts +270 -0
package/src/ai/index.ts CHANGED
@@ -9,12 +9,13 @@ export type {
9
9
  AIConfig,
10
10
  AIProvider,
11
11
  ProviderConfig,
12
+ ProviderCapabilities,
13
+ OllamaProviderConfig,
12
14
  ChatMessage,
13
15
  ChatCompletionRequest,
14
16
  ChatCompletionResponse,
15
17
  ChatCompletionChunk,
16
18
  ContentPart,
17
- // 注意:ToolDefinition 已从 core/types.ts 导出,此处不重复导出
18
19
  ToolCall,
19
20
  MessageRole,
20
21
  AgentTool,
@@ -52,6 +53,14 @@ export type {
52
53
  export { ZhinAgent } from './zhin-agent.js';
53
54
  export type { ZhinAgentConfig, OnChunkCallback } from './zhin-agent.js';
54
55
 
56
+ // ── ZhinAgent sub-modules (for advanced consumers) ──
57
+ export { PERM_MAP, DEFAULT_CONFIG as ZHIN_AGENT_DEFAULT_CONFIG, SECTION_SEP } from './zhin-agent-config.js';
58
+ export { checkExecPolicy, applyExecPolicyToTools, resolveExecAllowlist, EXEC_PRESETS } from './zhin-agent-exec-policy.js';
59
+ export { collectRelevantTools, toAgentTool } from './zhin-agent-tool-collector.js';
60
+ export { buildRichSystemPrompt, buildContextHint, buildEnhancedPersona, buildUserMessageWithHistory, contentToText } from './zhin-agent-prompt.js';
61
+ export type { RichSystemPromptContext } from './zhin-agent-prompt.js';
62
+ export { createChatHistoryTool, createUserProfileTool, createScheduleFollowUpTool, createSpawnTaskTool } from './zhin-agent-builtin-tools.js';
63
+
55
64
  // ── Conversation Memory ──
56
65
  export {
57
66
  ConversationMemory,
@@ -74,6 +83,15 @@ export type { RateLimitConfig, RateLimitResult } from './rate-limiter.js';
74
83
  export { FollowUpManager, AI_FOLLOWUP_MODEL } from './follow-up.js';
75
84
  export type { FollowUpRecord, FollowUpSender } from './follow-up.js';
76
85
 
86
+ // ── Subagent ──
87
+ export { SubagentManager } from './subagent.js';
88
+ export type {
89
+ SubagentOrigin,
90
+ SubagentResultSender,
91
+ SpawnOptions,
92
+ SubagentManagerOptions,
93
+ } from './subagent.js';
94
+
77
95
  // ── 持久化定时任务引擎 ──
78
96
  export {
79
97
  PersistentCronEngine,
package/src/ai/init.ts CHANGED
@@ -29,7 +29,8 @@ import type { MessageDispatcherService } from '../built/dispatcher.js';
29
29
  import type { SkillFeature } from '../built/skill.js';
30
30
  import { AIService } from './service.js';
31
31
  import { ZhinAgent } from './zhin-agent.js';
32
- import { createBuiltinTools, discoverWorkspaceSkills, loadSoulPersona } from './builtin-tools.js';
32
+ import { createBuiltinTools, discoverWorkspaceSkills, loadSoulPersona, loadAlwaysSkillsContent, buildSkillsSummaryXML } from './builtin-tools.js';
33
+ import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from './zhin-agent-config.js';
33
34
  import { loadBootstrapFiles, buildContextFiles, buildBootstrapContextSection, loadToolsGuide } from './bootstrap.js';
34
35
  import { triggerAIHook, createAIHookEvent } from './hooks.js';
35
36
  import { SessionManager, createDatabaseSessionManager } from './session.js';
@@ -44,6 +45,7 @@ import { AI_MESSAGE_MODEL, AI_SUMMARY_MODEL } from './conversation-memory.js';
44
45
  import { AI_USER_PROFILE_MODEL } from './user-profile.js';
45
46
  import { AI_FOLLOWUP_MODEL } from './follow-up.js';
46
47
  import { PersistentCronEngine, setCronManager, createCronTools } from './cron-engine.js';
48
+ import { Scheduler, getScheduler, setScheduler } from '../scheduler/index.js';
47
49
  import { renderToPlainText, type OutputElement } from './output.js';
48
50
  import type { AIConfig, ContentPart } from './types.js';
49
51
 
@@ -204,6 +206,38 @@ export function initAIModule(): void {
204
206
  });
205
207
  });
206
208
 
209
+ // 子任务管理器:让 AI 可以 spawn 后台子 agent 异步执行复杂任务
210
+ agent.initSubagentManager(() => {
211
+ const modelName = provider.models[0] || '';
212
+ const fullConfig = { ...DEFAULT_CONFIG, ...agentConfig } as Required<import('./zhin-agent-config.js').ZhinAgentConfig>;
213
+ const zhinTools = createBuiltinTools({ skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullConfig, modelName) });
214
+ return zhinTools.map(zt => {
215
+ const t = zt.toTool();
216
+ return {
217
+ name: t.name,
218
+ description: t.description,
219
+ parameters: t.parameters as any,
220
+ execute: t.execute as (args: Record<string, any>) => Promise<any>,
221
+ tags: t.tags,
222
+ keywords: t.keywords,
223
+ };
224
+ });
225
+ });
226
+ agent.setSubagentSender(async (origin, content) => {
227
+ const adapter = root.inject(origin.platform as any) as any;
228
+ if (!adapter || typeof adapter.sendMessage !== 'function') {
229
+ logger.warn(`[子任务] 找不到适配器: ${origin.platform}`);
230
+ return;
231
+ }
232
+ await adapter.sendMessage({
233
+ context: origin.platform,
234
+ bot: origin.botId,
235
+ id: origin.sceneId,
236
+ type: origin.sceneType as any,
237
+ content,
238
+ });
239
+ });
240
+
207
241
  // 持久化定时任务引擎:加载 data/cron-jobs.json,到点用 prompt 调用 Agent;并暴露给 AI 管理(list/add/remove/pause/resume)
208
242
  let cronEngine: PersistentCronEngine | null = null;
209
243
  const cronFeature = root.inject('cron' as any);
@@ -223,6 +257,26 @@ export function initAIModule(): void {
223
257
  setCronManager({ cronFeature, engine: cronEngine });
224
258
  }
225
259
 
260
+ // 统一调度器(at/every/cron + Heartbeat),持久化到 data/scheduler-jobs.json
261
+ const dataDir = path.join(process.cwd(), 'data');
262
+ const workspace = process.cwd();
263
+ const scheduler = new Scheduler({
264
+ storePath: path.join(dataDir, 'scheduler-jobs.json'),
265
+ workspace,
266
+ onJob: async (job) => {
267
+ if (!zhinAgentInstance) return;
268
+ await zhinAgentInstance.process(job.payload.message, {
269
+ platform: 'cron',
270
+ senderId: 'system',
271
+ sceneId: 'scheduler',
272
+ });
273
+ },
274
+ heartbeatEnabled: true,
275
+ heartbeatIntervalMs: 30 * 60 * 1000,
276
+ });
277
+ setScheduler(scheduler);
278
+ scheduler.start().catch((e) => logger.warn('Scheduler start failed: ' + (e as Error).message));
279
+
226
280
  logger.debug('ZhinAgent created');
227
281
  return () => {
228
282
  setCronManager(null);
@@ -230,6 +284,11 @@ export function initAIModule(): void {
230
284
  cronEngine.unload();
231
285
  cronEngine = null;
232
286
  }
287
+ const s = getScheduler();
288
+ if (s) {
289
+ s.stop();
290
+ setScheduler(null);
291
+ }
233
292
  agent.dispose();
234
293
  zhinAgentInstance = null;
235
294
  };
@@ -534,8 +593,11 @@ export function initAIModule(): void {
534
593
  useContext('ai', 'tool', (ai, toolService) => {
535
594
  if (!ai || !toolService) return;
536
595
 
537
- // 注册工具
538
- const builtinTools = createBuiltinTools();
596
+ const provider = ai.getProvider();
597
+ const agentCfg = ai.getAgentConfig();
598
+ const fullCfg = { ...DEFAULT_CONFIG, ...agentCfg } as Required<import('./zhin-agent-config.js').ZhinAgentConfig>;
599
+ const modelName = provider.models[0] || '';
600
+ const builtinTools = createBuiltinTools({ skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullCfg, modelName) });
539
601
  const disposers: (() => void)[] = [];
540
602
  for (const tool of builtinTools) disposers.push(toolService.addTool(tool, root.name));
541
603
  const cronTools = createCronTools();
@@ -632,6 +694,19 @@ export function initAIModule(): void {
632
694
  logger.debug(`Bootstrap files not loaded: ${e.message}`);
633
695
  }
634
696
 
697
+ // ── 第三步:常驻技能正文 + 技能 XML 摘要注入 Agent ──
698
+ try {
699
+ const skillsForContext = await discoverWorkspaceSkills();
700
+ const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
701
+ const skillsXml = buildSkillsSummaryXML(skillsForContext);
702
+ if (zhinAgentInstance) {
703
+ zhinAgentInstance.setActiveSkillsContext(alwaysContent);
704
+ zhinAgentInstance.setSkillsSummaryXML(skillsXml);
705
+ }
706
+ } catch (e: any) {
707
+ logger.debug(`Skills context not set: ${e.message}`);
708
+ }
709
+
635
710
  // 触发 agent:bootstrap Hook
636
711
  const skillFeature2 = (root as any).inject?.('skill') as SkillFeature | undefined;
637
712
  await triggerAIHook(createAIHookEvent('agent', 'bootstrap', undefined, {
@@ -650,6 +725,13 @@ export function initAIModule(): void {
650
725
  skillReloadDebounce = null;
651
726
  try {
652
727
  const count = await syncWorkspaceSkills();
728
+ const skillsForContext = await discoverWorkspaceSkills();
729
+ const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
730
+ const skillsXml = buildSkillsSummaryXML(skillsForContext);
731
+ if (zhinAgentInstance) {
732
+ zhinAgentInstance.setActiveSkillsContext(alwaysContent);
733
+ zhinAgentInstance.setSkillsSummaryXML(skillsXml);
734
+ }
653
735
  await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
654
736
  if (count >= 0) logger.info(`[技能热重载] 已更新,当前工作区技能数: ${count}`);
655
737
  } catch (e: any) {
@@ -178,12 +178,15 @@ export class AnthropicProvider extends BaseProvider {
178
178
  'claude-3-sonnet-20240229',
179
179
  'claude-3-haiku-20240307',
180
180
  ];
181
+ contextWindow: number;
182
+ capabilities = { vision: true, streaming: true, toolCalling: true, thinking: false };
181
183
 
182
184
  private baseUrl: string;
183
185
  private anthropicVersion: string;
184
186
 
185
187
  constructor(config: AnthropicConfig = {}) {
186
188
  super(config);
189
+ this.contextWindow = config.contextWindow ?? 200000;
187
190
  this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
188
191
  this.anthropicVersion = config.anthropicVersion || '2023-06-01';
189
192
  }
@@ -19,6 +19,8 @@ const logger = new Logger(null, 'Ollama');
19
19
  export interface OllamaConfig extends ProviderConfig {
20
20
  host?: string;
21
21
  models?: string[];
22
+ /** Ollama 上下文窗口大小(token 数),默认 32768。影响多轮对话和技能指令的保持能力 */
23
+ num_ctx?: number;
22
24
  }
23
25
 
24
26
  /**
@@ -81,13 +83,17 @@ function toOllamaTools(tools?: ToolDefinition[]): any[] | undefined {
81
83
  export class OllamaProvider extends BaseProvider {
82
84
  name = 'ollama';
83
85
  models: string[];
86
+ contextWindow: number;
87
+ capabilities = { vision: true, streaming: true, toolCalling: true, thinking: true };
84
88
 
85
89
  private host: string;
90
+ private numCtx: number;
86
91
 
87
92
  constructor(config: OllamaConfig = {}) {
88
93
  super(config);
89
94
  this.host = config.host || config.baseUrl || 'http://localhost:11434';
90
- // 使用配置中的模型列表,如果没有则使用默认列表
95
+ this.numCtx = config.contextWindow ?? config.num_ctx ?? 32768;
96
+ this.contextWindow = this.numCtx;
91
97
  this.models = config.models?.length ? config.models : [
92
98
  'llama3.3',
93
99
  'llama3.2',
@@ -113,7 +119,9 @@ export class OllamaProvider extends BaseProvider {
113
119
  model: request.model,
114
120
  messages,
115
121
  stream: false,
116
- options: {},
122
+ options: {
123
+ num_ctx: this.numCtx,
124
+ },
117
125
  };
118
126
 
119
127
  // think 参数:控制 qwen3 等模型的思考模式
@@ -186,7 +194,9 @@ export class OllamaProvider extends BaseProvider {
186
194
  model: request.model,
187
195
  messages,
188
196
  stream: true,
189
- options: {},
197
+ options: {
198
+ num_ctx: this.numCtx,
199
+ },
190
200
  };
191
201
 
192
202
  if (request.think !== undefined) {
@@ -28,11 +28,14 @@ export class OpenAIProvider extends BaseProvider {
28
28
  'o1-preview',
29
29
  'o3-mini',
30
30
  ];
31
+ contextWindow: number;
32
+ capabilities = { vision: true, streaming: true, toolCalling: true, thinking: false };
31
33
 
32
34
  private baseUrl: string;
33
35
 
34
36
  constructor(config: OpenAIConfig = {}) {
35
37
  super(config);
38
+ this.contextWindow = config.contextWindow ?? 128000;
36
39
  this.baseUrl = config.baseUrl || 'https://api.openai.com/v1';
37
40
 
38
41
  if (config.organization) {
package/src/ai/service.ts CHANGED
@@ -267,6 +267,14 @@ ${preExecutedData ? `\n已自动获取的数据:${preExecutedData}\n` : ''}
267
267
  }
268
268
  listProviders(): string[] { return Array.from(this.providers.keys()); }
269
269
 
270
+ getProviderCapabilities(name?: string): { contextWindow?: number; capabilities?: import('./types.js').ProviderCapabilities } {
271
+ const provider = this.getProvider(name);
272
+ return {
273
+ contextWindow: provider.contextWindow,
274
+ capabilities: provider.capabilities,
275
+ };
276
+ }
277
+
270
278
  async listModels(providerName?: string): Promise<{ provider: string; models: string[] }[]> {
271
279
  const result: { provider: string; models: string[] }[] = [];
272
280
  if (providerName) {
@@ -0,0 +1,209 @@
1
+ /**
2
+ * SubagentManager — 后台子任务执行管理器
3
+ *
4
+ * 职责:
5
+ * 1. 接收主 agent 发起的 spawn 请求,创建后台子 agent
6
+ * 2. 为子 agent 配备受限工具集(文件/Shell/网络,不含消息/spawn/技能)
7
+ * 3. 管理子 agent 生命周期(运行跟踪、完成/失败回告)
8
+ * 4. 完成后将结果投递回主流程,由主 agent 摘要后发送到原始频道
9
+ */
10
+
11
+ import { randomUUID } from 'crypto';
12
+ import { Logger } from '@zhin.js/logger';
13
+ import type { AIProvider, AgentTool } from './types.js';
14
+ import { createAgent } from './agent.js';
15
+ import type { ZhinAgentConfig } from './zhin-agent-config.js';
16
+ import { applyExecPolicyToTools } from './zhin-agent-exec-policy.js';
17
+
18
+ const logger = new Logger(null, 'Subagent');
19
+
20
+ // ============================================================================
21
+ // 类型
22
+ // ============================================================================
23
+
24
+ export interface SubagentOrigin {
25
+ platform: string;
26
+ botId: string;
27
+ sceneId: string;
28
+ senderId: string;
29
+ sceneType: string;
30
+ }
31
+
32
+ export interface SpawnOptions {
33
+ task: string;
34
+ label?: string;
35
+ origin: SubagentOrigin;
36
+ }
37
+
38
+ export type SubagentResultSender = (origin: SubagentOrigin, content: string) => Promise<void>;
39
+
40
+ export interface SubagentManagerOptions {
41
+ provider: AIProvider;
42
+ workspace: string;
43
+ createTools: () => AgentTool[];
44
+ maxIterations?: number;
45
+ /** Exec policy config to enforce on subagent bash tools */
46
+ execPolicyConfig?: Required<ZhinAgentConfig>;
47
+ }
48
+
49
+ // ============================================================================
50
+ // 子 agent 允许使用的工具名单
51
+ // ============================================================================
52
+
53
+ const SUBAGENT_ALLOWED_TOOLS = new Set([
54
+ 'read_file',
55
+ 'write_file',
56
+ 'edit_file',
57
+ 'list_dir',
58
+ 'glob',
59
+ 'grep',
60
+ 'bash',
61
+ 'web_search',
62
+ 'web_fetch',
63
+ ]);
64
+
65
+ // ============================================================================
66
+ // SubagentManager
67
+ // ============================================================================
68
+
69
+ export class SubagentManager {
70
+ private provider: AIProvider;
71
+ private workspace: string;
72
+ private createTools: () => AgentTool[];
73
+ private maxIterations: number;
74
+ private execPolicyConfig: Required<ZhinAgentConfig> | null;
75
+ private runningTasks: Map<string, AbortController> = new Map();
76
+ private resultSender: SubagentResultSender | null = null;
77
+
78
+ constructor(options: SubagentManagerOptions) {
79
+ this.provider = options.provider;
80
+ this.workspace = options.workspace;
81
+ this.createTools = options.createTools;
82
+ this.maxIterations = options.maxIterations ?? 15;
83
+ this.execPolicyConfig = options.execPolicyConfig ?? null;
84
+ }
85
+
86
+ setSender(sender: SubagentResultSender): void {
87
+ this.resultSender = sender;
88
+ }
89
+
90
+ async spawn(options: SpawnOptions): Promise<string> {
91
+ const taskId = randomUUID().slice(0, 8);
92
+ const displayLabel =
93
+ options.label ||
94
+ options.task.slice(0, 30) + (options.task.length > 30 ? '...' : '');
95
+
96
+ const abortController = new AbortController();
97
+ this.runningTasks.set(taskId, abortController);
98
+
99
+ this.runSubagent(taskId, options.task, displayLabel, options.origin)
100
+ .catch((error) => {
101
+ logger.error({ error, taskId }, 'Subagent failed');
102
+ })
103
+ .finally(() => {
104
+ this.runningTasks.delete(taskId);
105
+ });
106
+
107
+ logger.info({ taskId, label: displayLabel }, 'Spawned subagent');
108
+ return `子任务 [${displayLabel}] 已启动 (id: ${taskId}),完成后会自动通知你。`;
109
+ }
110
+
111
+ getRunningCount(): number {
112
+ return this.runningTasks.size;
113
+ }
114
+
115
+ // ── 内部方法 ──────────────────────────────────────────────────────
116
+
117
+ private async runSubagent(
118
+ taskId: string,
119
+ task: string,
120
+ label: string,
121
+ origin: SubagentOrigin,
122
+ ): Promise<void> {
123
+ logger.info({ taskId, label }, 'Subagent starting task');
124
+
125
+ try {
126
+ const allTools = this.createTools();
127
+ let tools = allTools.filter(t => SUBAGENT_ALLOWED_TOOLS.has(t.name));
128
+ if (this.execPolicyConfig) {
129
+ tools = applyExecPolicyToTools(this.execPolicyConfig, tools);
130
+ }
131
+
132
+ const systemPrompt = this.buildSubagentPrompt(task);
133
+ const agent = createAgent(this.provider, {
134
+ systemPrompt,
135
+ tools,
136
+ maxIterations: this.maxIterations,
137
+ });
138
+
139
+ const result = await agent.run(task);
140
+ const finalResult = result.content || '任务已完成,但未生成最终响应。';
141
+
142
+ logger.info({ taskId }, 'Subagent completed successfully');
143
+ await this.announceResult(taskId, label, task, finalResult, origin, 'ok');
144
+ } catch (error) {
145
+ const errorMsg = `Error: ${error}`;
146
+ logger.error({ taskId, error }, 'Subagent failed');
147
+ await this.announceResult(taskId, label, task, errorMsg, origin, 'error');
148
+ }
149
+ }
150
+
151
+ private async announceResult(
152
+ taskId: string,
153
+ label: string,
154
+ task: string,
155
+ result: string,
156
+ origin: SubagentOrigin,
157
+ status: 'ok' | 'error',
158
+ ): Promise<void> {
159
+ if (!this.resultSender) {
160
+ logger.warn({ taskId }, 'No result sender configured, discarding subagent result');
161
+ return;
162
+ }
163
+
164
+ const statusText = status === 'ok' ? '已完成' : '执行失败';
165
+ const announceContent = `[后台任务 '${label}' ${statusText}]\n\n任务: ${task}\n\n结果:\n${result}`;
166
+
167
+ try {
168
+ await this.resultSender(origin, announceContent);
169
+ logger.debug({ taskId, origin }, 'Subagent announced result');
170
+ } catch (e) {
171
+ logger.error({ taskId, error: e }, 'Failed to announce subagent result');
172
+ }
173
+ }
174
+
175
+ private buildSubagentPrompt(task: string): string {
176
+ return `# 子任务 Agent
177
+
178
+ 你是一个被主 agent 派生出来执行特定任务的子 agent。
179
+
180
+ ## 你的任务
181
+ ${task}
182
+
183
+ ## 规则
184
+ 1. 专注完成分配的任务,不做其他事情
185
+ 2. 你的最终回复会被报告给主 agent,并转达给用户
186
+ 3. 不要发起对话或承担额外任务
187
+ 4. 回复要简洁但信息充分
188
+
189
+ ## 你可以做的
190
+ - 读写工作区内的文件
191
+ - 执行 Shell 命令
192
+ - 搜索和抓取网页
193
+ - 彻底完成任务
194
+
195
+ ## 你不能做的
196
+ - 直接向用户发送消息
197
+ - 派生其他子任务
198
+ - 访问主 agent 的对话历史
199
+
200
+ ## 工作区
201
+ 你的工作区路径: ${this.workspace}
202
+
203
+ 完成任务后,请提供清晰的发现或操作摘要。`;
204
+ }
205
+
206
+ dispose(): void {
207
+ this.runningTasks.clear();
208
+ }
209
+ }
package/src/ai/types.ts CHANGED
@@ -128,6 +128,14 @@ export interface ChatCompletionChunkChoice {
128
128
  // Provider 类型
129
129
  // ============================================================================
130
130
 
131
+ /** Provider 能力声明 */
132
+ export interface ProviderCapabilities {
133
+ vision?: boolean;
134
+ streaming?: boolean;
135
+ toolCalling?: boolean;
136
+ thinking?: boolean;
137
+ }
138
+
131
139
  /** Provider 配置 */
132
140
  export interface ProviderConfig {
133
141
  apiKey?: string;
@@ -136,6 +144,10 @@ export interface ProviderConfig {
136
144
  timeout?: number;
137
145
  maxRetries?: number;
138
146
  headers?: Record<string, string>;
147
+ /** 上下文窗口大小(token 数),各 Provider 映射到自身参数 */
148
+ contextWindow?: number;
149
+ /** Provider 能力声明 */
150
+ capabilities?: ProviderCapabilities;
139
151
  }
140
152
 
141
153
  /** Provider 接口 */
@@ -154,6 +166,12 @@ export interface AIProvider {
154
166
 
155
167
  /** 检查连接 */
156
168
  healthCheck?(): Promise<boolean>;
169
+
170
+ /** 上下文窗口大小(token 数),由 Provider 实现暴露 */
171
+ contextWindow?: number;
172
+
173
+ /** Provider 能力声明 */
174
+ capabilities?: ProviderCapabilities;
157
175
  }
158
176
 
159
177
  // ============================================================================
@@ -241,6 +259,13 @@ export interface Session {
241
259
  // AI Service 配置
242
260
  // ============================================================================
243
261
 
262
+ /** Ollama-specific fields for AIConfig typing (mirrors OllamaConfig) */
263
+ export interface OllamaProviderConfig extends ProviderConfig {
264
+ host?: string;
265
+ models?: string[];
266
+ num_ctx?: number;
267
+ }
268
+
244
269
  /** AI 服务配置 */
245
270
  export interface AIConfig {
246
271
  enabled?: boolean;
@@ -251,7 +276,7 @@ export interface AIConfig {
251
276
  deepseek?: ProviderConfig;
252
277
  moonshot?: ProviderConfig;
253
278
  zhipu?: ProviderConfig;
254
- ollama?: ProviderConfig & { host?: string; models?: string[] };
279
+ ollama?: OllamaProviderConfig;
255
280
  custom?: ProviderConfig[];
256
281
  };
257
282
  sessions?: {
@@ -284,7 +309,9 @@ export interface AIConfig {
284
309
  allowedTools?: string[];
285
310
  /** bash 执行策略:deny=禁止执行,allowlist=仅允许列表内命令,full=不限制 */
286
311
  execSecurity?: 'deny' | 'allowlist' | 'full';
287
- /** allowlist 模式下允许的命令(支持正则字符串,如 "^ls "、"^cat ") */
312
+ /** 预设命令白名单模式:readonly / network / development / custom(默认 custom,使用自定义 execAllowlist) */
313
+ execPreset?: 'readonly' | 'network' | 'development' | 'custom';
314
+ /** allowlist 模式下允许的命令(支持正则字符串,如 "^ls "、"^cat "),与 preset 合并 */
288
315
  execAllowlist?: string[];
289
316
  /** allowlist 未命中时:true=需审批(当前实现为拒绝并提示),false=直接拒绝 */
290
317
  execAsk?: boolean;