@lemoncat7/dsh-partner 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +38 -0
  2. package/docs/architecture.md +99 -0
  3. package/lib/agent-runtime.d.ts +31 -8
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +230 -48
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/api/features/workspace-api.d.ts +18 -0
  8. package/lib/api/features/workspace-api.d.ts.map +1 -0
  9. package/lib/api/features/workspace-api.js +188 -0
  10. package/lib/api/features/workspace-api.js.map +1 -0
  11. package/lib/api/http.d.ts +12 -0
  12. package/lib/api/http.d.ts.map +1 -0
  13. package/lib/api/http.js +53 -0
  14. package/lib/api/http.js.map +1 -0
  15. package/lib/api.d.ts +10 -0
  16. package/lib/api.d.ts.map +1 -1
  17. package/lib/api.js +48 -63
  18. package/lib/api.js.map +1 -1
  19. package/lib/channels/manager.d.ts +4 -1
  20. package/lib/channels/manager.d.ts.map +1 -1
  21. package/lib/channels/manager.js +20 -3
  22. package/lib/channels/manager.js.map +1 -1
  23. package/lib/client-api.d.ts +157 -1
  24. package/lib/client-api.d.ts.map +1 -1
  25. package/lib/client-api.js.map +1 -1
  26. package/lib/client-controller.d.ts +15 -0
  27. package/lib/client-controller.d.ts.map +1 -0
  28. package/lib/client-controller.js +69 -0
  29. package/lib/client-controller.js.map +1 -0
  30. package/lib/client.d.ts.map +1 -1
  31. package/lib/client.js +2702 -885
  32. package/lib/client.js.map +4 -4
  33. package/lib/collaboration/composition.d.ts +27 -0
  34. package/lib/collaboration/composition.d.ts.map +1 -0
  35. package/lib/collaboration/composition.js +345 -0
  36. package/lib/collaboration/composition.js.map +1 -0
  37. package/lib/collaboration/domain.d.ts +38 -0
  38. package/lib/collaboration/domain.d.ts.map +1 -0
  39. package/lib/collaboration/domain.js +3 -0
  40. package/lib/collaboration/domain.js.map +1 -0
  41. package/lib/collaboration/retry-policy.d.ts +4 -0
  42. package/lib/collaboration/retry-policy.d.ts.map +1 -0
  43. package/lib/collaboration/retry-policy.js +18 -0
  44. package/lib/collaboration/retry-policy.js.map +1 -0
  45. package/lib/collaboration/service.d.ts +76 -0
  46. package/lib/collaboration/service.d.ts.map +1 -0
  47. package/lib/collaboration/service.js +370 -0
  48. package/lib/collaboration/service.js.map +1 -0
  49. package/lib/companions/service.d.ts +13 -0
  50. package/lib/companions/service.d.ts.map +1 -0
  51. package/lib/companions/service.js +51 -0
  52. package/lib/companions/service.js.map +1 -0
  53. package/lib/concern-domain.d.ts +8 -1
  54. package/lib/concern-domain.d.ts.map +1 -1
  55. package/lib/concern-domain.js +25 -1
  56. package/lib/concern-domain.js.map +1 -1
  57. package/lib/concern-store.d.ts.map +1 -1
  58. package/lib/concern-store.js +2 -1
  59. package/lib/concern-store.js.map +1 -1
  60. package/lib/concern-tool.d.ts +6 -0
  61. package/lib/concern-tool.d.ts.map +1 -1
  62. package/lib/concern-tool.js +35 -9
  63. package/lib/concern-tool.js.map +1 -1
  64. package/lib/core/collections.d.ts +2 -0
  65. package/lib/core/collections.d.ts.map +1 -0
  66. package/lib/core/collections.js +6 -0
  67. package/lib/core/collections.js.map +1 -0
  68. package/lib/core/semaphore.d.ts +10 -0
  69. package/lib/core/semaphore.d.ts.map +1 -0
  70. package/lib/core/semaphore.js +32 -0
  71. package/lib/core/semaphore.js.map +1 -0
  72. package/lib/core/validation.d.ts +7 -0
  73. package/lib/core/validation.d.ts.map +1 -0
  74. package/lib/core/validation.js +40 -0
  75. package/lib/core/validation.js.map +1 -0
  76. package/lib/domain.d.ts +28 -2
  77. package/lib/domain.d.ts.map +1 -1
  78. package/lib/domain.js +3 -3
  79. package/lib/domain.js.map +1 -1
  80. package/lib/execution/agent-support.d.ts +12 -0
  81. package/lib/execution/agent-support.d.ts.map +1 -0
  82. package/lib/execution/agent-support.js +48 -0
  83. package/lib/execution/agent-support.js.map +1 -0
  84. package/lib/execution/domain.d.ts +17 -0
  85. package/lib/execution/domain.d.ts.map +1 -0
  86. package/lib/execution/domain.js +2 -0
  87. package/lib/execution/domain.js.map +1 -0
  88. package/lib/execution/service.d.ts +52 -0
  89. package/lib/execution/service.d.ts.map +1 -0
  90. package/lib/execution/service.js +175 -0
  91. package/lib/execution/service.js.map +1 -0
  92. package/lib/index.d.ts.map +1 -1
  93. package/lib/index.js +42 -3
  94. package/lib/index.js.map +1 -1
  95. package/lib/memory-reflection.js +2 -2
  96. package/lib/memory-reflection.js.map +1 -1
  97. package/lib/scheduler/domain.d.ts +25 -0
  98. package/lib/scheduler/domain.d.ts.map +1 -0
  99. package/lib/scheduler/domain.js +2 -0
  100. package/lib/scheduler/domain.js.map +1 -0
  101. package/lib/scheduler/service.d.ts +26 -0
  102. package/lib/scheduler/service.d.ts.map +1 -0
  103. package/lib/scheduler/service.js +177 -0
  104. package/lib/scheduler/service.js.map +1 -0
  105. package/lib/skills/builtin.d.ts +7 -0
  106. package/lib/skills/builtin.d.ts.map +1 -0
  107. package/lib/skills/builtin.js +16 -0
  108. package/lib/skills/builtin.js.map +1 -0
  109. package/lib/skills/domain.d.ts +52 -0
  110. package/lib/skills/domain.d.ts.map +1 -0
  111. package/lib/skills/domain.js +2 -0
  112. package/lib/skills/domain.js.map +1 -0
  113. package/lib/skills/loader.d.ts +18 -0
  114. package/lib/skills/loader.d.ts.map +1 -0
  115. package/lib/skills/loader.js +134 -0
  116. package/lib/skills/loader.js.map +1 -0
  117. package/lib/skills/markets/adapters.d.ts +8 -0
  118. package/lib/skills/markets/adapters.d.ts.map +1 -0
  119. package/lib/skills/markets/adapters.js +72 -0
  120. package/lib/skills/markets/adapters.js.map +1 -0
  121. package/lib/skills/markets/builtin.d.ts +10 -0
  122. package/lib/skills/markets/builtin.d.ts.map +1 -0
  123. package/lib/skills/markets/builtin.js +19 -0
  124. package/lib/skills/markets/builtin.js.map +1 -0
  125. package/lib/skills/network.d.ts +14 -0
  126. package/lib/skills/network.d.ts.map +1 -0
  127. package/lib/skills/network.js +174 -0
  128. package/lib/skills/network.js.map +1 -0
  129. package/lib/skills/repository.d.ts +17 -0
  130. package/lib/skills/repository.d.ts.map +1 -0
  131. package/lib/skills/repository.js +76 -0
  132. package/lib/skills/repository.js.map +1 -0
  133. package/lib/skills/service.d.ts +48 -0
  134. package/lib/skills/service.d.ts.map +1 -0
  135. package/lib/skills/service.js +248 -0
  136. package/lib/skills/service.js.map +1 -0
  137. package/lib/skills/zip.d.ts +3 -0
  138. package/lib/skills/zip.d.ts.map +1 -0
  139. package/lib/skills/zip.js +59 -0
  140. package/lib/skills/zip.js.map +1 -0
  141. package/lib/store.d.ts.map +1 -1
  142. package/lib/store.js +69 -3
  143. package/lib/store.js.map +1 -1
  144. package/lib/tasks/domain.d.ts +39 -0
  145. package/lib/tasks/domain.d.ts.map +1 -0
  146. package/lib/tasks/domain.js +3 -0
  147. package/lib/tasks/domain.js.map +1 -0
  148. package/lib/tasks/result.d.ts +14 -0
  149. package/lib/tasks/result.d.ts.map +1 -0
  150. package/lib/tasks/result.js +109 -0
  151. package/lib/tasks/result.js.map +1 -0
  152. package/lib/tasks/service.d.ts +39 -0
  153. package/lib/tasks/service.d.ts.map +1 -0
  154. package/lib/tasks/service.js +334 -0
  155. package/lib/tasks/service.js.map +1 -0
  156. package/lib/ui/companion-access-panel.d.ts +4 -0
  157. package/lib/ui/companion-access-panel.d.ts.map +1 -0
  158. package/lib/ui/companion-access-panel.js +46 -0
  159. package/lib/ui/companion-access-panel.js.map +1 -0
  160. package/lib/ui/companion-create.d.ts +11 -0
  161. package/lib/ui/companion-create.d.ts.map +1 -0
  162. package/lib/ui/companion-create.js +30 -0
  163. package/lib/ui/companion-create.js.map +1 -0
  164. package/lib/ui/partner-components.d.ts +36 -0
  165. package/lib/ui/partner-components.d.ts.map +1 -0
  166. package/lib/ui/partner-components.js +32 -0
  167. package/lib/ui/partner-components.js.map +1 -0
  168. package/lib/ui/schedule-panel.d.ts +5 -0
  169. package/lib/ui/schedule-panel.d.ts.map +1 -0
  170. package/lib/ui/schedule-panel.js +86 -0
  171. package/lib/ui/schedule-panel.js.map +1 -0
  172. package/lib/ui/skills-panel.d.ts +5 -0
  173. package/lib/ui/skills-panel.d.ts.map +1 -0
  174. package/lib/ui/skills-panel.js +236 -0
  175. package/lib/ui/skills-panel.js.map +1 -0
  176. package/lib/ui/task-board-panel.d.ts +2 -0
  177. package/lib/ui/task-board-panel.d.ts.map +1 -0
  178. package/lib/ui/task-board-panel.js +231 -0
  179. package/lib/ui/task-board-panel.js.map +1 -0
  180. package/lib/ui/workspace-components.d.ts +40 -0
  181. package/lib/ui/workspace-components.d.ts.map +1 -0
  182. package/lib/ui/workspace-components.js +61 -0
  183. package/lib/ui/workspace-components.js.map +1 -0
  184. package/package.json +2 -1
@@ -2,13 +2,15 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, realpath, stat, writeFile } from 'node:fs/promises';
3
3
  import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { BlockAssembler, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
5
- import { createScope, scopeOf } from '@deepseek-ai/dsh-scope';
5
+ import { createScope, scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
6
6
  import { concernObservationPrompt } from './autonomy.js';
7
7
  import { boundedConcernCheckMinutes } from './concern-domain.js';
8
8
  import { PARTNER_MEDIA_MAX_BYTES, safeMediaName } from './channel-message.js';
9
9
  import { listConcernFileSources } from './concern-sources.js';
10
10
  import { CONCERN_CREATED_NOTICE, renderConcernCreatedNotice } from './concern-notification.js';
11
11
  import { HEARTBEAT_LOCAL_COMMAND, heartbeatLocalCommandTool } from './heartbeat-command.js';
12
+ import { assistantTextAfter, renderPartnerPersona, renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
13
+ export { renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
12
14
  const HEARTBEAT_TIMEOUT_MS = 115_000;
13
15
  const HEARTBEAT_DISCOVERY_BUDGET_MS = 72_000;
14
16
  const HEARTBEAT_TOOL_TIMEOUT_MS = 18_000;
@@ -19,6 +21,7 @@ const HEARTBEAT_WEB_FETCH_RESULT_LIMIT = 40_000;
19
21
  const HEARTBEAT_WEB_SOURCE_RESULT_LIMIT = 125_000;
20
22
  const HEARTBEAT_AUDIT_INPUT_LIMIT = 320;
21
23
  const HEARTBEAT_AUDIT_OUTPUT_LIMIT = 520;
24
+ const PARTNER_TASK_TIMEOUT_MS = 30 * 60_000;
22
25
  const HEARTBEAT_READ_ONLY_TOOLS = new Set([
23
26
  'knowledge_base_search',
24
27
  'knowledge_search',
@@ -40,19 +43,34 @@ export class PartnerAgentRuntime {
40
43
  memory;
41
44
  reflection;
42
45
  concerns;
46
+ composer;
43
47
  handles = new Map();
44
48
  queues = new Map();
45
49
  heartbeatQueues = new Map();
50
+ taskQueues = new Map();
46
51
  steeringQueues = new Map();
47
52
  workspaces = new Map();
48
53
  profileVersions = new Map();
49
- constructor(ctx, store, defaultCwd, memory, reflection, concerns) {
54
+ restoredCompositions = new Map();
55
+ restoredCompositionJobs = new Map();
56
+ disposeAgentListener;
57
+ constructor(ctx, store, defaultCwd, memory, reflection, concerns, composer) {
50
58
  this.ctx = ctx;
51
59
  this.store = store;
52
60
  this.defaultCwd = defaultCwd;
53
61
  this.memory = memory;
54
62
  this.reflection = reflection;
55
63
  this.concerns = concerns;
64
+ this.composer = composer;
65
+ this.disposeAgentListener = typeof this.ctx.on === 'function'
66
+ ? this.ctx.on('agent/disposed', ({ agent }) => {
67
+ const restored = this.restoredCompositions.get(agent.session.id);
68
+ if (restored?.agent !== agent)
69
+ return;
70
+ restored.dispose();
71
+ this.restoredCompositions.delete(agent.session.id);
72
+ })
73
+ : () => { };
56
74
  }
57
75
  async reply(companion, channelId, userId, message) {
58
76
  const key = `${channelId}:${userId}`;
@@ -122,6 +140,114 @@ export class PartnerAgentRuntime {
122
140
  await this.ensureAgent(companion, route);
123
141
  return route;
124
142
  }
143
+ async ensureLocalSessionRecord(companionId) {
144
+ const companion = this.store.snapshot().companions.find(item => item.id === companionId);
145
+ if (companion === undefined)
146
+ throw new Error('伙伴身份不存在');
147
+ const existing = this.store.snapshot().sessions.find(item => item.companionId === companionId && item.kind === 'local');
148
+ if (existing !== undefined && canReuseSession(existing, companion.id, this.ctx.workspaceRegistry.archivedSessionIds)) {
149
+ return existing;
150
+ }
151
+ const now = Date.now();
152
+ const route = {
153
+ id: `route-${randomUUID()}`,
154
+ kind: 'local',
155
+ channelId: '@local',
156
+ userId: `owner:${companion.id}`,
157
+ companionId: companion.id,
158
+ sessionId: `session-${randomUUID()}`,
159
+ cwd: partnerCwd(this.defaultCwd, companion.id),
160
+ lastMessageAt: now,
161
+ };
162
+ await this.store.update(state => {
163
+ state.sessions = state.sessions.filter(item => !(item.companionId === companion.id && item.kind === 'local'));
164
+ state.sessions.push(route);
165
+ });
166
+ return route;
167
+ }
168
+ async createLocalSession(companionId) {
169
+ const companion = this.store.snapshot().companions.find(item => item.id === companionId);
170
+ if (companion === undefined)
171
+ throw new Error('伙伴身份不存在');
172
+ const route = await this.ensureLocalSessionRecord(companionId);
173
+ await this.ensureAgent(companion, route);
174
+ return route;
175
+ }
176
+ async notifyTaskProgress(task, previousStatus) {
177
+ if (!task.creatorCompanionId || (task.status !== 'review' && task.status !== 'done' && task.status !== 'blocked'))
178
+ return;
179
+ const companion = this.store.snapshot().companions.find(item => item.id === task.creatorCompanionId);
180
+ if (!companion)
181
+ return;
182
+ const state = this.store.snapshot();
183
+ const routes = state.sessions.filter(item => item.companionId === companion.id);
184
+ const downstream = state.tasks.filter(item => item.dependencyTaskIds.includes(task.id) && item.status !== 'done');
185
+ const route = selectTaskNotificationRoute(routes, task.creatorSessionId, item => this.isArchived(item))
186
+ ?? await this.createLocalSession(companion.id);
187
+ const agent = await this.ensureAgent(companion, route);
188
+ const isReviewer = task.status === 'review' && task.reviewerCompanionId === companion.id;
189
+ const instruction = isReviewer
190
+ ? '这是你创建并分配的看板任务,执行伙伴已提交结果,现在默认由你验收。请根据任务要求真实核验已有产出;通过后必须调用 partner_task_board accept,不通过则调用 reject 并写明原因。不要重新执行该任务。'
191
+ : task.status === 'review'
192
+ ? '这是你创建并分配的看板任务进度事件。执行结果已提交并等待指定伙伴验收;不要重新执行,请向用户简要说明当前进度。'
193
+ : task.status === 'done'
194
+ ? '这是你创建并分配的看板任务终态事件。不要重新执行已完成的工作。请检查依赖它的后续任务是否解锁,需要时继续在看板上分配。对用户的终态结果由系统直接投递,不要只改写成“已完成”或进度摘要。'
195
+ : '这是你创建并分配的看板任务受阻事件。请保留已有产出、检查是否能调整分工或解锁依赖;需要用户介入时,明确说明阻塞原因与所需动作。';
196
+ agent.followup(createUserMessage({
197
+ content: [{ type: 'text', text: [
198
+ instruction,
199
+ `任务:${task.title}`,
200
+ `状态:${previousStatus} → ${task.status}`,
201
+ task.resultSummary ? `结果:\n${task.resultSummary.slice(0, 3000)}` : '',
202
+ task.status === 'review' && task.reviewHandoff ? `验收交接:\n${task.reviewHandoff.slice(0, 2000)}` : '',
203
+ task.reviewSummary ? `验收:\n${task.reviewSummary.slice(0, 2000)}` : '',
204
+ downstream.length > 0 ? `后续任务:\n${downstream.map(item => `- ${item.title}(${item.status})`).join('\n')}` : '没有依赖此任务的后续任务。',
205
+ ].filter(Boolean).join('\n\n') }],
206
+ source: { kind: 'plugin', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: taskProgressNoticeSummary(task.status) },
207
+ }));
208
+ }
209
+ async executeTask(input) {
210
+ const route = await this.createLocalSession(input.companion.id);
211
+ const key = route.sessionId;
212
+ const previous = this.taskQueues.get(key) ?? Promise.resolve();
213
+ let output = '';
214
+ const current = previous.catch(() => { }).then(async () => {
215
+ const agent = await this.ensureAgent(input.companion, route);
216
+ if (agent.status !== 'idle')
217
+ await agent.whenIdle();
218
+ const startSeq = agent.session.seq;
219
+ agent.followup(createUserMessage({
220
+ content: [{ type: 'text', text: input.prompt }],
221
+ source: { kind: 'plugin', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: input.sourceId.startsWith('review:') ? '伙伴核验看板任务' : '伙伴执行看板任务' },
222
+ }));
223
+ let timedOut = false;
224
+ const timer = setTimeout(() => {
225
+ timedOut = true;
226
+ agent.cancel({ kind: 'hook', reason: 'partner task execution timed out' });
227
+ }, PARTNER_TASK_TIMEOUT_MS);
228
+ timer.unref?.();
229
+ try {
230
+ await agent.whenIdle();
231
+ }
232
+ finally {
233
+ clearTimeout(timer);
234
+ }
235
+ if (timedOut)
236
+ throw new Error('伙伴看板任务执行超时');
237
+ output = assistantTextAfter(agent, startSeq).trim();
238
+ if (!output)
239
+ throw new Error('伙伴会话没有产生任务结果');
240
+ });
241
+ this.taskQueues.set(key, current);
242
+ try {
243
+ await current;
244
+ return { run: { id: `session-task-${randomUUID()}` }, output };
245
+ }
246
+ finally {
247
+ if (this.taskQueues.get(key) === current)
248
+ this.taskQueues.delete(key);
249
+ }
250
+ }
125
251
  isArchived(route) {
126
252
  return this.ctx.workspaceRegistry.archivedSessionIds.includes(route.sessionId);
127
253
  }
@@ -131,10 +257,14 @@ export class PartnerAgentRuntime {
131
257
  throw new Error('伙伴会话不存在');
132
258
  if (!this.isArchived(previous))
133
259
  return previous;
260
+ const companion = this.store.snapshot().companions.find(item => item.id === previous.companionId);
261
+ if (companion === undefined)
262
+ throw new Error('伙伴身份不存在');
134
263
  const next = renewedSession(previous);
135
264
  await this.store.update(state => {
136
265
  state.sessions = state.sessions.map(item => item.id === routeId ? next : item);
137
266
  });
267
+ await this.ensureAgent(companion, next);
138
268
  return next;
139
269
  }
140
270
  async heartbeat(companion, route, concerns) {
@@ -351,6 +481,7 @@ export class PartnerAgentRuntime {
351
481
  const handle = this.handles.get(item.sessionId);
352
482
  this.handles.delete(item.sessionId);
353
483
  this.profileVersions.delete(item.sessionId);
484
+ await this.releaseRestoredComposition(item.sessionId);
354
485
  await handle?.dispose().catch(() => { });
355
486
  }));
356
487
  }
@@ -360,17 +491,31 @@ export class PartnerAgentRuntime {
360
491
  const handle = this.handles.get(item.sessionId);
361
492
  this.handles.delete(item.sessionId);
362
493
  this.profileVersions.delete(item.sessionId);
494
+ await this.releaseRestoredComposition(item.sessionId);
363
495
  await handle?.dispose().catch(() => { });
364
496
  }));
365
497
  await this.store.update(state => { state.sessions = state.sessions.filter(item => item.channelId !== channelId); });
366
498
  }
367
499
  async close() {
500
+ this.disposeAgentListener();
501
+ await Promise.allSettled(this.restoredCompositionJobs.values());
502
+ for (const restored of this.restoredCompositions.values())
503
+ restored.dispose();
504
+ this.restoredCompositions.clear();
505
+ this.restoredCompositionJobs.clear();
368
506
  await Promise.all([...this.handles.values()].map(handle => handle.dispose().catch(() => { })));
369
507
  this.handles.clear();
370
508
  this.profileVersions.clear();
371
509
  this.heartbeatQueues.clear();
510
+ this.taskQueues.clear();
372
511
  this.steeringQueues.clear();
373
512
  }
513
+ async releaseRestoredComposition(sessionId) {
514
+ await this.restoredCompositionJobs.get(sessionId)?.catch(() => { });
515
+ const restored = this.restoredCompositions.get(sessionId);
516
+ restored?.dispose();
517
+ this.restoredCompositions.delete(sessionId);
518
+ }
374
519
  async drive(companion, channelId, userId, message) {
375
520
  const session = await this.ensureSession(companion, channelId, userId);
376
521
  const agent = await this.ensureAgent(companion, session);
@@ -398,7 +543,7 @@ export class PartnerAgentRuntime {
398
543
  source: { kind: 'user' },
399
544
  }));
400
545
  await agent.whenIdle();
401
- const response = extractAssistantText(agent, startSeq);
546
+ const response = assistantTextAfter(agent, startSeq);
402
547
  await this.store.update(state => {
403
548
  const target = state.sessions.find(item => item.id === session.id);
404
549
  if (target)
@@ -443,6 +588,7 @@ export class PartnerAgentRuntime {
443
588
  const now = Date.now();
444
589
  const session = {
445
590
  id: `route-${randomUUID()}`,
591
+ kind: 'channel',
446
592
  channelId,
447
593
  userId,
448
594
  companionId: companion.id,
@@ -469,6 +615,7 @@ export class PartnerAgentRuntime {
469
615
  }
470
616
  const live = this.ctx.agents.get(route.sessionId);
471
617
  if (live !== undefined) {
618
+ await this.ensureRestoredComposition(live, companion, route, profile);
472
619
  await this.attachSession(companion, route);
473
620
  if (profile)
474
621
  this.injectProfileUpdate(live, route.sessionId, profile);
@@ -483,7 +630,7 @@ export class PartnerAgentRuntime {
483
630
  agentCtx.systemPrompt.section({
484
631
  name: 'partner-identity',
485
632
  order: -10,
486
- text: renderPersona(companion),
633
+ text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation'),
487
634
  });
488
635
  agentCtx.systemPrompt.section({
489
636
  name: 'partner-tool-routing',
@@ -496,6 +643,7 @@ export class PartnerAgentRuntime {
496
643
  order: -8,
497
644
  text: renderProfile(profile),
498
645
  });
646
+ await this.composer?.compose(agentCtx, companion);
499
647
  };
500
648
  let handle;
501
649
  try {
@@ -522,6 +670,68 @@ export class PartnerAgentRuntime {
522
670
  await this.attachSession(companion, route);
523
671
  return handle.agent;
524
672
  }
673
+ async ensureRestoredComposition(agent, companion, route, profile) {
674
+ const installed = this.restoredCompositions.get(route.sessionId);
675
+ if (installed?.agent === agent)
676
+ return;
677
+ const pending = this.restoredCompositionJobs.get(route.sessionId);
678
+ if (pending !== undefined)
679
+ return pending;
680
+ const job = this.composeRestoredAgent(agent, companion, route, profile);
681
+ this.restoredCompositionJobs.set(route.sessionId, job);
682
+ try {
683
+ await job;
684
+ }
685
+ finally {
686
+ if (this.restoredCompositionJobs.get(route.sessionId) === job)
687
+ this.restoredCompositionJobs.delete(route.sessionId);
688
+ }
689
+ }
690
+ async composeRestoredAgent(agent, companion, route, profile) {
691
+ await agent.whenIdle();
692
+ let dispose;
693
+ await agent.runMaintenance(async (signal) => {
694
+ if (signal.aborted)
695
+ throw signal.reason;
696
+ const agentCtx = agent.ctx;
697
+ const key = scopeOf(agentCtx);
698
+ if (key === undefined)
699
+ throw new Error('伙伴会话缺少 Agent 作用域');
700
+ if (scopeParentOf(key) === undefined) {
701
+ const presets = agentCtx.get('agentPresets');
702
+ if (presets === undefined)
703
+ throw new Error('伙伴会话缺少 Agent Presets 服务');
704
+ await presets.mount(agentCtx, companion.presetId);
705
+ }
706
+ const disposers = [];
707
+ try {
708
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-identity', order: -10, text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation') }));
709
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-tool-routing', order: -9, text: renderToolProtocol() }));
710
+ if (profile && profile.entries.length > 0)
711
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-user-profile', order: -8, text: renderProfile(profile) }));
712
+ if (this.composer)
713
+ disposers.push(await this.composer.compose(agentCtx, companion));
714
+ }
715
+ catch (error) {
716
+ for (let index = disposers.length - 1; index >= 0; index -= 1)
717
+ disposers[index]?.();
718
+ throw error;
719
+ }
720
+ let disposed = false;
721
+ dispose = () => {
722
+ if (disposed)
723
+ return;
724
+ disposed = true;
725
+ for (let index = disposers.length - 1; index >= 0; index -= 1)
726
+ disposers[index]?.();
727
+ };
728
+ });
729
+ if (dispose === undefined)
730
+ throw new Error('伙伴会话恢复组合未完成');
731
+ const previous = this.restoredCompositions.get(route.sessionId);
732
+ previous?.dispose();
733
+ this.restoredCompositions.set(route.sessionId, { agent, dispose });
734
+ }
525
735
  injectProfileUpdate(agent, sessionId, profile) {
526
736
  const previous = this.profileVersions.get(sessionId);
527
737
  if (previous === profile.version)
@@ -544,6 +754,21 @@ export class PartnerAgentRuntime {
544
754
  await (await workspace).attachSession(route.sessionId);
545
755
  }
546
756
  }
757
+ const TASK_PROGRESS_NOTICE_SUMMARIES = ['看板任务待验收', '看板任务已完成', '看板任务受阻'];
758
+ function taskProgressNoticeSummary(status) {
759
+ if (status === 'review')
760
+ return '看板任务待验收';
761
+ if (status === 'done')
762
+ return '看板任务已完成';
763
+ return '看板任务受阻';
764
+ }
765
+ export function selectTaskNotificationRoute(routes, creatorSessionId, archived) {
766
+ const available = routes.filter(route => !archived(route));
767
+ return available.find(route => route.sessionId === creatorSessionId)
768
+ ?? available.filter(route => route.kind === 'channel').sort((left, right) => right.lastMessageAt - left.lastMessageAt)[0]
769
+ ?? available.find(route => route.kind === 'local')
770
+ ?? available.sort((left, right) => right.lastMessageAt - left.lastMessageAt)[0];
771
+ }
547
772
  function delay(milliseconds) {
548
773
  return new Promise(resolve => setTimeout(resolve, milliseconds));
549
774
  }
@@ -660,13 +885,6 @@ function renderDeferredMentions(entries) {
660
885
  ...entries.map(item => `- ${item.event}${item.evidence ? `(依据:${item.evidence})` : ''}`),
661
886
  ].join('\n');
662
887
  }
663
- export function resolveAgentOptions(defaultModel, companion) {
664
- return {
665
- ...defaultModel.currentSelection(),
666
- ...(companion.provider ? { provider: companion.provider } : {}),
667
- ...(companion.model ? { model: companion.model } : {}),
668
- };
669
- }
670
888
  export function canReuseSession(route, companionId, archivedSessionIds) {
671
889
  return route.companionId === companionId && !archivedSessionIds.includes(route.sessionId);
672
890
  }
@@ -750,7 +968,7 @@ async function runHeartbeatInference(ctx, agent, companion, concerns, signal) {
750
968
  for await (const chunk of prepared.stream({
751
969
  ...prepared.config,
752
970
  messages,
753
- system: renderPersona(companion, 'heartbeat'),
971
+ system: renderPartnerPersona(companion, 'heartbeat'),
754
972
  tools: discoveryOpen ? tools : [],
755
973
  signal,
756
974
  }))
@@ -1001,42 +1219,6 @@ function formatDuration(milliseconds) {
1001
1219
  return `${(milliseconds / 1_000).toFixed(milliseconds < 10_000 ? 1 : 0)} 秒`;
1002
1220
  }
1003
1221
  function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
1004
- function renderPersona(companion, surface = 'conversation') {
1005
- const capabilities = companion.capabilities.length > 0 ? companion.capabilities.join('、') : '由当前 Agent Preset 提供的基础能力';
1006
- return [
1007
- `你当前以长期工作伙伴「${companion.name}」的身份工作。`,
1008
- `角色:${companion.role}`,
1009
- companion.description ? `定位:${companion.description}` : '',
1010
- companion.instructions ? `长期行为准则:\n${companion.instructions}` : '',
1011
- `用户为此伙伴启用的能力范围:${capabilities}。能力标识不等于授权;只能调用当前会话实际提供且已经通过权限校验的工具。`,
1012
- surface === 'heartbeat'
1013
- ? '这是一轮独立的伙伴心跳,不是用户聊天的延续。只根据本轮真实可读信息行动,最终结果由渠道适配器决定是否发送。'
1014
- : '这个会话由 DSH 网页与微信私聊渠道共同使用。保持同一上下文,不要假定每条消息都来自微信;渠道回传由适配器负责。回答兼顾网页与移动端阅读,执行外部操作前继续遵守工具自身的授权与审批边界。',
1015
- ].filter(Boolean).join('\n\n');
1016
- }
1017
- export function renderToolProtocol() {
1018
- return [
1019
- 'DSH 工具调用协议(必须遵守):工具 SDK 中出现某项能力,不代表它可以作为顶层函数直接调用。',
1020
- '先以当前请求真正暴露的顶层工具清单为准。若顶层只提供 `run_code`,则它是唯一允许直接调用的工具;`web_search`、知识库、SSH 等 SDK 能力必须放进 `run_code` 程序,通过生成的 `tools` SDK 按准确签名调用,例如 `await tools.web_search(...)`。绝不要直接发起名为 `web_search` 的顶层工具调用。',
1021
- '`run_code` 的程序只返回完成当前任务所需的精简结果。需要顺序依赖时逐个 `await`,互不依赖的只读调用才可并行。',
1022
- '若工具结果提示 “only `run_code` is callable directly”,说明路由方式错误;立即在同一轮改用 `run_code` 重试。只有规范重试也失败时,才向用户说明真正的失败原因。',
1023
- '若当前请求原生暴露了目标工具,则可以直接调用该原生工具;不要臆造未出现在顶层清单或生成 SDK 中的工具。',
1024
- '需要把生成的图片或文档交给用户时,最终回复必须用 Markdown 链接明确引用伙伴工作目录中的文件,例如 `[下载文件](/绝对路径/文件.pdf)`;不要只说“文件在这里”。渠道会在确认真实路径位于伙伴工作目录后发送附件。',
1025
- ].join('\n');
1026
- }
1027
- function extractAssistantText(agent, fromSeq) {
1028
- const messages = [];
1029
- for (const event of agent.session.events) {
1030
- if (event.seq < fromSeq || event.type !== 'assistant/message' || event.data.interrupted)
1031
- continue;
1032
- const text = event.data.message.content
1033
- .filter((block) => block.type === 'text')
1034
- .map(block => block.text.trim()).filter(Boolean).join('\n');
1035
- if (text)
1036
- messages.push(text);
1037
- }
1038
- return messages.join('\n\n').trim();
1039
- }
1040
1222
  function textContent(content) {
1041
1223
  return content.filter((block) => block.type === 'text' && typeof block.text === 'string')
1042
1224
  .map(block => block.text.trim()).filter(Boolean).join('\n');