dsh-bot-team 0.0.0-stage → 1.3.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.
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +249 -2
- package/client.js +13558 -0
- package/cordis.patch.yml +68 -0
- package/host/agents.js +542 -0
- package/host/background-interactions.js +213 -0
- package/host/chat-settings.js +24 -0
- package/host/conversations.js +45 -0
- package/host/discussion-flow.js +179 -0
- package/host/discussion-runner.js +491 -0
- package/host/domain.js +975 -0
- package/host/engine.js +1698 -0
- package/host/execution.js +471 -0
- package/host/layout.js +95 -0
- package/host/message-metrics.js +99 -0
- package/host/native-activity.js +225 -0
- package/host/native-session.js +144 -0
- package/host/pdf.js +895 -0
- package/host/personas.js +120 -0
- package/host/prompt.js +668 -0
- package/host/routes.js +135 -0
- package/host/service.js +3209 -0
- package/host/session-export.js +22 -0
- package/host/session-visibility.js +147 -0
- package/host/storage-layout.js +109 -0
- package/host/tools.js +900 -0
- package/host/update-check.js +351 -0
- package/host/util.js +149 -0
- package/host/workspace.js +228 -0
- package/icon.svg +6 -0
- package/index.js +20 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +80 -4
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Bot Team bundle patch: the plugin row + the full-capability preset its bots mount.
|
|
2
|
+
#
|
|
3
|
+
# Presets are ordinary @deepseek-ai/dsh-agent-preset declarations. A bot agent is
|
|
4
|
+
# created by the Host half with meta.agentPreset and mounts the preset in its
|
|
5
|
+
# setup callback (same mechanism the Web session controller uses).
|
|
6
|
+
- insert:
|
|
7
|
+
- id: bot-team
|
|
8
|
+
name: 'dsh-bot-team'
|
|
9
|
+
config:
|
|
10
|
+
# Empty ⇒ $DSH_HOME/dsh-bot-team (plugin data root).
|
|
11
|
+
dataDir: ''
|
|
12
|
+
# Empty ⇒ <dataDir>/bots (BOT.md persona files).
|
|
13
|
+
botsDir: ''
|
|
14
|
+
# Legacy fallback only: new groups default to <dataDir>/groups/<groupId>,
|
|
15
|
+
# direct chats to <dataDir>/direct/<botId>. Empty ⇒ the Host process cwd.
|
|
16
|
+
defaultCwd: ''
|
|
17
|
+
maxParallelTurns: 3
|
|
18
|
+
maxContextMessages: 40
|
|
19
|
+
# Zero lets native goals and human approvals finish on their own schedule.
|
|
20
|
+
turnTimeoutMs: 0
|
|
21
|
+
# M1 verification finished: stop writing demo/smoke data on activation.
|
|
22
|
+
# Flip back to true only when re-running the end-to-end smoke checks.
|
|
23
|
+
smokeTest: false
|
|
24
|
+
# M4 hardening defaults
|
|
25
|
+
botSandbox: true
|
|
26
|
+
# ask: surface approval requests in the group and answer in the bot session.
|
|
27
|
+
# never: deterministic headless deny (unattended/CI).
|
|
28
|
+
botApprovalPolicy: ask
|
|
29
|
+
# PDF export discovers a system CJK TrueType font. Set pdfFontPath to
|
|
30
|
+
# override it with an explicit .ttf/.ttc file.
|
|
31
|
+
pdfFontPath: ''
|
|
32
|
+
dailyTokenBudget: 5000000
|
|
33
|
+
# smokeResultsDir is intentionally unset: it defaults to <cwd>/.bot-team.
|
|
34
|
+
# Set it per profile only when re-running the smoke checks.
|
|
35
|
+
|
|
36
|
+
- id: preset-bot-team-tools
|
|
37
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
38
|
+
config:
|
|
39
|
+
id: bot-team-tools
|
|
40
|
+
name: 'Bot Team · 全能'
|
|
41
|
+
description: 'Bot Team bot:文件读写、搜索、联网、命令执行、Skill,workspace-write 沙箱。'
|
|
42
|
+
order: 40
|
|
43
|
+
plugins:
|
|
44
|
+
- id: tool-fs
|
|
45
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
46
|
+
- id: tool-fs-search
|
|
47
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
48
|
+
config:
|
|
49
|
+
sampleOverCapGlobResults: false
|
|
50
|
+
- id: tool-web
|
|
51
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
52
|
+
config:
|
|
53
|
+
fetch: true
|
|
54
|
+
searchTimeoutMs: 60000
|
|
55
|
+
# The skill registry itself is host-plane (dsh-base); the preset owns
|
|
56
|
+
# local discovery and the model-facing catalog/loader. Both are needed
|
|
57
|
+
# so a user-picked /skill-name gesture resolves inside a bot turn.
|
|
58
|
+
- id: skill-filesystem
|
|
59
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
60
|
+
- id: tool-skill
|
|
61
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
62
|
+
- id: tool-bash
|
|
63
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
64
|
+
disabled: !!js process.platform === 'win32'
|
|
65
|
+
- id: tool-todo
|
|
66
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
67
|
+
config:
|
|
68
|
+
allowParallelInProgress: true
|
package/host/agents.js
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One hidden background DSH Session per Bot membership. The plugin owns
|
|
3
|
+
* factory-created/resumed handles and mounts the native default Agent preset.
|
|
4
|
+
*/
|
|
5
|
+
import { BOT_TEAM_TOOL_NAMES } from './tools.js';
|
|
6
|
+
import { newId } from './util.js';
|
|
7
|
+
import { currentConversationId, conversationFloor, isCurrentMemberSession } from './conversations.js';
|
|
8
|
+
import { defaultWorkspaceDir, ensureWorkspaceDir, sessionCwdMatches, validateProjectWorkspace } from './workspace.js';
|
|
9
|
+
import { botModel, inspectSessionHeader, installBotModel, submitNativeInput } from './native-session.js';
|
|
10
|
+
|
|
11
|
+
const RECURSIVE_TEAM_TOOLS = BOT_TEAM_TOOL_NAMES.filter(name => !['bot_team_list', 'bot_team_task_list'].includes(name));
|
|
12
|
+
// Native reminders deliver through SessionController, which rejects hidden
|
|
13
|
+
// origins. Leave list/delete available for old ordinary-session reminders.
|
|
14
|
+
const BACKGROUND_UNDELIVERABLE_TOOLS = ['schedule_create', 'schedule_update'];
|
|
15
|
+
const backgroundHeader = header => header?.origin === 'subagent'
|
|
16
|
+
&& header.parentSession === undefined && header.parentSessionId === undefined
|
|
17
|
+
&& header.parent === undefined && (header.delegationDepth === undefined || header.delegationDepth === 0);
|
|
18
|
+
const presetOf = (header, events) => {
|
|
19
|
+
const selected = events.findLast(event => event.type === 'agent-preset/selected');
|
|
20
|
+
if (!selected) return header.agentPreset;
|
|
21
|
+
if (typeof selected.data?.agentPreset !== 'string' || !selected.data.agentPreset) throw new Error('Invalid stored Bot preset selection');
|
|
22
|
+
return selected.data.agentPreset;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class AgentPool {
|
|
26
|
+
constructor({ ctx, repo, personas, config, logger }) {
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
this.repo = repo;
|
|
29
|
+
this.personas = personas;
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
this.entries = new Map();
|
|
33
|
+
this.owned = new Map();
|
|
34
|
+
this.preparing = new Map();
|
|
35
|
+
this.preparationAborts = new Map();
|
|
36
|
+
this.overlays = new Map();
|
|
37
|
+
this.overlayPreparing = new Map();
|
|
38
|
+
this.versions = new Map();
|
|
39
|
+
this.modelUpdates = new Map();
|
|
40
|
+
this.recordingModel = new WeakSet();
|
|
41
|
+
this.submitting = new Map();
|
|
42
|
+
this.modelWrites = new Map();
|
|
43
|
+
this.drainedSessions = new Set();
|
|
44
|
+
this.disposed = false;
|
|
45
|
+
// Observe factory publication and old ordinary-session activations. Owned
|
|
46
|
+
// setup already installs its overlay; repeated hooks remain idempotent.
|
|
47
|
+
this.offCreated = ctx.on('agent/created', async ({ agent }) => {
|
|
48
|
+
if (this.disposed) return;
|
|
49
|
+
const member = this.memberForSession(agent.id);
|
|
50
|
+
if (member && this.canOverlay(agent)) {
|
|
51
|
+
await this.makeSetup(member.membershipId)(agent.ctx, agent);
|
|
52
|
+
}
|
|
53
|
+
}, { global: true });
|
|
54
|
+
this.offDisposed = ctx.on('agent/disposed', ({ agent }) => {
|
|
55
|
+
this.detachOverlay(agent);
|
|
56
|
+
this.owned.delete(agent);
|
|
57
|
+
for (const [id, entry] of this.entries) if (entry.agent === agent) this.entries.delete(id);
|
|
58
|
+
}, { global: true });
|
|
59
|
+
this.offSelection = ctx.on('session/event', (session, event) => this.onModelSelection(session, event), { global: true });
|
|
60
|
+
this.ready = Promise.all([...(this.repo.members?.values() || [])].map(member => {
|
|
61
|
+
const agent = isCurrentMemberSession(this.repo, member) && this.ctx.agents.get(member.sessionId);
|
|
62
|
+
return agent && this.canOverlay(agent) ? this.makeSetup(member.membershipId)(agent.ctx, agent) : undefined;
|
|
63
|
+
})).then(() => undefined);
|
|
64
|
+
// Keep the rejection observable to service.ready / ensure without an
|
|
65
|
+
// unhandled rejection while the host is still constructing the service.
|
|
66
|
+
this.ready.catch(error => this.logger?.warn?.('bot-team: restore live overlays failed: ' + String(error)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sessionIdOf(member) { return member.sessionId; }
|
|
70
|
+
|
|
71
|
+
memberForSession(sessionId) {
|
|
72
|
+
// The durable repository is the single mapping authority, including after
|
|
73
|
+
// restart and before create publishes agent/created. Never infer from IDs.
|
|
74
|
+
for (const member of this.repo.members?.values() || []) {
|
|
75
|
+
if (isCurrentMemberSession(this.repo, member, sessionId)) return member;
|
|
76
|
+
}
|
|
77
|
+
// Small embedders can expose only getMember; entries still stay verified.
|
|
78
|
+
for (const memberId of new Set([...this.preparing.keys(), ...this.entries.keys()])) {
|
|
79
|
+
const member = this.repo.getMember(memberId);
|
|
80
|
+
if (isCurrentMemberSession(this.repo, member, sessionId)) return member;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
requireMember(memberId) {
|
|
86
|
+
const member = this.repo.getMember(memberId);
|
|
87
|
+
if (!member) throw new Error('membership ' + memberId + ' does not exist');
|
|
88
|
+
const bot = this.repo.getBot(member.botId);
|
|
89
|
+
if (!bot) throw new Error('bot ' + member.botId + ' of membership ' + memberId + ' does not exist');
|
|
90
|
+
const group = this.repo.getGroup(member.groupId);
|
|
91
|
+
if (!group) throw new Error('group ' + member.groupId + ' does not exist');
|
|
92
|
+
return { member, bot, group };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async ensure(memberId) {
|
|
96
|
+
if (this.disposed) throw new Error('Bot AgentPool is disposed');
|
|
97
|
+
const { member } = this.requireMember(memberId);
|
|
98
|
+
this.assertAdmission?.(member.groupId);
|
|
99
|
+
const expected = this.generation(member);
|
|
100
|
+
if (this.preparing.has(memberId)) return this.preparing.get(memberId);
|
|
101
|
+
// Defer prepare until this map is populated: synchronous lifecycle hooks
|
|
102
|
+
// and callers joining in the same tick see the same in-flight operation.
|
|
103
|
+
const abort = new AbortController();
|
|
104
|
+
const version = this.versions.get(memberId);
|
|
105
|
+
this.preparationAborts.set(memberId, abort);
|
|
106
|
+
const pending = Promise.resolve().then(() => this.prepare(memberId, version, abort.signal, expected));
|
|
107
|
+
this.preparing.set(memberId, pending);
|
|
108
|
+
try { return await pending; }
|
|
109
|
+
finally {
|
|
110
|
+
if (this.preparing.get(memberId) === pending) this.preparing.delete(memberId);
|
|
111
|
+
if (this.preparationAborts.get(memberId) === abort) this.preparationAborts.delete(memberId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
isPreparingGroup(groupId) {
|
|
116
|
+
return [...this.preparing.keys(), ...this.submitting.keys(), ...[...this.overlayPreparing.values()].map(value => value.membershipId)]
|
|
117
|
+
.some(id => this.repo.getMember(id)?.groupId === groupId) || [...this.modelWrites.values()].includes(groupId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
generation(member) {
|
|
121
|
+
return { membershipId: member.membershipId, groupId: member.groupId, botId: member.botId,
|
|
122
|
+
conversationId: currentConversationId(this.repo, member.groupId), sessionId: member.sessionId };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assertGeneration(expected, { current = false, admission = true } = {}) {
|
|
126
|
+
const member = this.repo.getMember(expected.membershipId);
|
|
127
|
+
if (this.disposed || !member || member.groupId !== expected.groupId || member.botId !== expected.botId ||
|
|
128
|
+
member.sessionId !== expected.sessionId || currentConversationId(this.repo, expected.groupId) !== expected.conversationId ||
|
|
129
|
+
(current && !isCurrentMemberSession(this.repo, member, expected.sessionId))) {
|
|
130
|
+
throw Object.assign(new Error('Bot session belongs to a stale conversation or was disposed'), { code: 'conversation-stale' });
|
|
131
|
+
}
|
|
132
|
+
if (admission) this.assertAdmission?.(expected.groupId);
|
|
133
|
+
return member;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
assertPreparing(memberId, version, expected) {
|
|
137
|
+
if (this.disposed || this.versions.get(memberId) !== version) throw new Error('Bot session preparation was disposed');
|
|
138
|
+
if (expected) this.assertGeneration(expected);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
owns(agent) { return this.owned.has(agent); }
|
|
142
|
+
|
|
143
|
+
canOverlay(agent) {
|
|
144
|
+
const header = agent.session?.header;
|
|
145
|
+
if (header?.origin !== undefined && header.origin !== 'subagent') return false;
|
|
146
|
+
if (!header || agent.parentAgent || header.parentSession !== undefined || header.parentSessionId !== undefined || header.parent !== undefined || (header.delegationDepth !== undefined && header.delegationDepth !== 0)) return false;
|
|
147
|
+
if (header.origin === 'subagent' && !backgroundHeader(header)) return false;
|
|
148
|
+
// Durable origin does not define runtime parentage. A malformed real child
|
|
149
|
+
// must not be borrowed just because it happens to match a membership id.
|
|
150
|
+
if (this.ctx.agents.get(agent.id) === agent && typeof this.ctx.agents.roots === 'function' && !this.ctx.agents.roots().includes(agent)) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async storedPreset(sessionId, header) {
|
|
155
|
+
const live = this.ctx.agents.get(sessionId);
|
|
156
|
+
if (live) return presetOf(header, live.session.snapshotEvents());
|
|
157
|
+
const observation = await this.ctx.get('sessionQuery').observeSession(sessionId, { projectionMode: 'none' });
|
|
158
|
+
try {
|
|
159
|
+
if (observation.header?.id !== sessionId || !Array.isArray(observation.events)) throw new Error('Invalid stored Bot preset history: ' + sessionId);
|
|
160
|
+
return presetOf(observation.header, observation.events);
|
|
161
|
+
} finally { observation[Symbol.dispose]?.(); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async releaseOwned(agent) {
|
|
165
|
+
const owned = this.owned.get(agent);
|
|
166
|
+
if (!owned) return;
|
|
167
|
+
owned.releasing ||= Promise.resolve().then(async () => {
|
|
168
|
+
// The native factory owns the root only; the subagent manager owns its
|
|
169
|
+
// descendants. Close that tree's admission and await its exact handles.
|
|
170
|
+
if (this.ctx.agents.get(agent.id) === agent) await this.ctx.get('subagents')?.drainDescendants?.([agent]);
|
|
171
|
+
await owned.handle.dispose();
|
|
172
|
+
this.drainedSessions.add(agent.id);
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
await owned.releasing;
|
|
176
|
+
if (this.owned.get(agent) === owned) this.owned.delete(agent);
|
|
177
|
+
this.detachOverlay(agent);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
owned.releasing = undefined;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async prepare(memberId, version, signal, expected) {
|
|
185
|
+
const check = () => { signal.throwIfAborted(); this.assertPreparing(memberId, version, expected); };
|
|
186
|
+
check();
|
|
187
|
+
await this.ready;
|
|
188
|
+
check();
|
|
189
|
+
const { member, group } = this.requireMember(memberId);
|
|
190
|
+
const cwd = group.workspaceCwd || defaultWorkspaceDir(this.config, group);
|
|
191
|
+
if (group.projectWorkspaceCwd) await validateProjectWorkspace(cwd);
|
|
192
|
+
else if (!(await ensureWorkspaceDir(cwd, this.logger))) throw new Error('无法准备默认工作目录');
|
|
193
|
+
check();
|
|
194
|
+
const presets = this.ctx.agentPresets || this.ctx.get('agentPresets');
|
|
195
|
+
if (typeof presets?.resolve !== 'function' || typeof presets?.mount !== 'function') throw new Error('Native default Agent preset registry is unavailable');
|
|
196
|
+
const staleConversation = expected.conversationId !== null && member.sessionConversationId !== expected.conversationId;
|
|
197
|
+
let sessionId = member.sessionId;
|
|
198
|
+
// A stale mapping never causes native observation/resume or id reuse, even
|
|
199
|
+
// when its old log was never written or several conversations were skipped.
|
|
200
|
+
let header = !staleConversation && sessionId ? await inspectSessionHeader(this.ctx, sessionId) : undefined;
|
|
201
|
+
check();
|
|
202
|
+
const live = !staleConversation && sessionId && this.ctx.agents.get(sessionId);
|
|
203
|
+
let compatible = false;
|
|
204
|
+
let preset;
|
|
205
|
+
let storedId;
|
|
206
|
+
if (header && backgroundHeader(header) && sessionCwdMatches(header.cwd, cwd) && (!live || this.canOverlay(live))) {
|
|
207
|
+
storedId = await this.storedPreset(sessionId, header);
|
|
208
|
+
check();
|
|
209
|
+
if (typeof storedId !== 'string' || !storedId) throw new Error('Invalid stored Bot preset identity');
|
|
210
|
+
if (storedId !== 'bot-team-tools') {
|
|
211
|
+
preset = await presets.resolve(storedId);
|
|
212
|
+
check();
|
|
213
|
+
if (preset?.id !== storedId || preset.broken) throw new Error('Invalid stored Bot preset: ' + storedId);
|
|
214
|
+
compatible = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!compatible) {
|
|
218
|
+
preset = await presets.resolve(undefined);
|
|
219
|
+
check();
|
|
220
|
+
if (typeof preset?.id !== 'string' || !preset.id || preset.broken) throw new Error('Native default Agent preset is invalid');
|
|
221
|
+
compatible = !staleConversation && storedId === preset.id;
|
|
222
|
+
}
|
|
223
|
+
const rotated = staleConversation || Boolean(header && !compatible);
|
|
224
|
+
const next = { ...member };
|
|
225
|
+
if (rotated) {
|
|
226
|
+
if (sessionId) {
|
|
227
|
+
if (typeof this.repo.bindSession === 'function') {
|
|
228
|
+
await this.repo.bindSession(member, { sessionId, conversationId: member.sessionConversationId, reason: 'retained-history' });
|
|
229
|
+
check();
|
|
230
|
+
}
|
|
231
|
+
next.previousSessionIds = [...new Set([...(member.previousSessionIds || []), sessionId])];
|
|
232
|
+
}
|
|
233
|
+
sessionId = newId('session-botteam');
|
|
234
|
+
header = undefined;
|
|
235
|
+
next.lastSeenSeq = conversationFloor(this.repo, group.groupId);
|
|
236
|
+
}
|
|
237
|
+
if (!sessionId) sessionId = newId('session-botteam');
|
|
238
|
+
next.sessionId = sessionId;
|
|
239
|
+
if (expected.conversationId !== null) next.sessionConversationId = expected.conversationId;
|
|
240
|
+
if (rotated) {
|
|
241
|
+
// Release all old handles owned by this membership, including detached
|
|
242
|
+
// cache entries. Historical borrowed agents retain their original owner.
|
|
243
|
+
this.entries.delete(memberId);
|
|
244
|
+
for (const [agent, owned] of [...this.owned]) if (owned.membershipId === memberId) {
|
|
245
|
+
await this.releaseOwned(agent);
|
|
246
|
+
check();
|
|
247
|
+
}
|
|
248
|
+
for (const [agent, overlay] of [...this.overlays]) if (overlay.membershipId === memberId) this.detachOverlay(agent);
|
|
249
|
+
}
|
|
250
|
+
if (typeof this.repo.bindSession === 'function') {
|
|
251
|
+
await this.repo.bindSession(next, { sessionId, conversationId: expected.conversationId, reason: rotated ? 'session-rotation' : 'session-activation' });
|
|
252
|
+
check();
|
|
253
|
+
}
|
|
254
|
+
if (!header || next.sessionConversationId !== member.sessionConversationId) {
|
|
255
|
+
// The new mapping is published only after strict persistence succeeds.
|
|
256
|
+
if (typeof this.repo.putMemberStrict === 'function') {
|
|
257
|
+
await this.repo.putMemberStrict(next, { expectedConversationId: expected.conversationId, expectedSessionId: expected.sessionId });
|
|
258
|
+
} else {
|
|
259
|
+
// Legacy repository doubles preserve object identity. Real repositories
|
|
260
|
+
// use the strict copy/write/publish path above.
|
|
261
|
+
const before = { ...member };
|
|
262
|
+
Object.assign(member, next);
|
|
263
|
+
try { await this.repo.putMember(member); }
|
|
264
|
+
catch (error) { Object.assign(member, before); throw error; }
|
|
265
|
+
}
|
|
266
|
+
expected = { ...expected, sessionId };
|
|
267
|
+
check();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let handle;
|
|
271
|
+
let agent = compatible ? live : undefined;
|
|
272
|
+
try {
|
|
273
|
+
if (!agent) {
|
|
274
|
+
const setup = async (scope, prepared) => {
|
|
275
|
+
check();
|
|
276
|
+
if (!backgroundHeader(prepared.session.header) || !sessionCwdMatches(prepared.session.header.cwd, cwd)) throw new Error('Background Bot session metadata changed during activation: ' + sessionId);
|
|
277
|
+
await presets.mount(scope, preset.id);
|
|
278
|
+
check();
|
|
279
|
+
await this.makeSetup(memberId)(scope, prepared);
|
|
280
|
+
check();
|
|
281
|
+
};
|
|
282
|
+
const options = { agentOptions: botModel(this.requireMember(memberId).bot), setup, signal };
|
|
283
|
+
handle = header
|
|
284
|
+
? await this.ctx.agents.resume({ ...options, resumeSessionId: sessionId })
|
|
285
|
+
: await this.ctx.agents.create({ ...options, sessionId, meta: { cwd, origin: 'subagent', agentPreset: preset.id } });
|
|
286
|
+
agent = handle?.agent;
|
|
287
|
+
if (agent) this.owned.set(agent, { membershipId: memberId, conversationId: expected.conversationId, handle });
|
|
288
|
+
if (!agent || agent.id !== sessionId || typeof handle.dispose !== 'function') throw new Error('Background Agent factory returned an unexpected Bot session: ' + sessionId);
|
|
289
|
+
}
|
|
290
|
+
check();
|
|
291
|
+
if (!backgroundHeader(agent.session.header) || !this.canOverlay(agent) || !sessionCwdMatches(agent.session.header.cwd, cwd)) throw new Error('Background Bot session metadata changed during activation: ' + sessionId);
|
|
292
|
+
await this.makeSetup(memberId)(agent.ctx, agent);
|
|
293
|
+
check();
|
|
294
|
+
const workingDirectory = this.ctx.get('workingDirectory');
|
|
295
|
+
if (typeof workingDirectory?.set === 'function') {
|
|
296
|
+
try { await workingDirectory.set(agent, cwd); }
|
|
297
|
+
catch (error) { throw new Error('无法设置 Bot 工作目录 ' + cwd + ': ' + String(error?.message || error), { cause: error }); }
|
|
298
|
+
}
|
|
299
|
+
check();
|
|
300
|
+
if (this.ctx.agents.get(sessionId) !== agent) throw new Error('Bot session was disposed during activation');
|
|
301
|
+
this.assertGeneration(expected, { current: true });
|
|
302
|
+
this.entries.set(memberId, { membershipId: memberId, sessionId, conversationId: expected.conversationId, agent });
|
|
303
|
+
return agent;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (handle && agent) await this.releaseOwned(agent);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async applySandboxMode(agent) {
|
|
311
|
+
if (!this.config.botSandbox) return;
|
|
312
|
+
const policy = this.ctx.get('sandboxPolicy');
|
|
313
|
+
const current = policy?.overrideOf?.(agent.session)
|
|
314
|
+
?? agent.session.snapshotEvents?.().findLast(event => event.type === 'sandbox/mode')?.data.mode;
|
|
315
|
+
if (current !== 'workspace-write') agent.session.append('sandbox/mode', { mode: 'workspace-write' });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
current(memberId) {
|
|
319
|
+
if (this.disposed) return undefined;
|
|
320
|
+
const member = this.repo.getMember(memberId);
|
|
321
|
+
const agent = isCurrentMemberSession(this.repo, member) && this.ctx.agents.get(member.sessionId);
|
|
322
|
+
const entry = this.entries.get(memberId);
|
|
323
|
+
if (entry && (entry.sessionId !== member?.sessionId || entry.agent !== agent)) this.entries.delete(memberId);
|
|
324
|
+
// Borrowed background roots and legacy native activations may bypass ensure().
|
|
325
|
+
const overlay = agent && this.overlays.get(agent);
|
|
326
|
+
return overlay?.membershipId === memberId && overlay.conversationId === currentConversationId(this.repo, member.groupId) ? agent : undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
makeSetup(membershipId) {
|
|
330
|
+
return async (agentCtx, agent) => {
|
|
331
|
+
if (this.disposed || !this.canOverlay(agent) || !isCurrentMemberSession(this.repo, this.repo.getMember(membershipId), agent.id)) return;
|
|
332
|
+
const pending = this.overlayPreparing.get(agent);
|
|
333
|
+
if (pending) return pending.promise;
|
|
334
|
+
const operation = {
|
|
335
|
+
membershipId,
|
|
336
|
+
promise: Promise.resolve().then(() => this.installOverlay(membershipId, agentCtx, agent)),
|
|
337
|
+
};
|
|
338
|
+
this.overlayPreparing.set(agent, operation);
|
|
339
|
+
try { return await operation.promise; }
|
|
340
|
+
finally { if (this.overlayPreparing.get(agent) === operation) this.overlayPreparing.delete(agent); }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async installOverlay(membershipId, agentCtx, agent) {
|
|
345
|
+
if (this.disposed) return;
|
|
346
|
+
const { member } = this.requireMember(membershipId);
|
|
347
|
+
if (!isCurrentMemberSession(this.repo, member, agent.id)) return;
|
|
348
|
+
const expected = this.generation(member);
|
|
349
|
+
this.assertGeneration(expected, { current: true });
|
|
350
|
+
if (this.overlays.has(agent)) return;
|
|
351
|
+
const overlay = { membershipId, conversationId: expected.conversationId, disposers: [] };
|
|
352
|
+
const check = () => {
|
|
353
|
+
this.assertGeneration(expected, { current: true });
|
|
354
|
+
if (this.overlays.get(agent) !== overlay) throw new Error('Bot overlay was disposed');
|
|
355
|
+
};
|
|
356
|
+
const retain = disposer => { if (typeof disposer === 'function') overlay.disposers.push(disposer); };
|
|
357
|
+
this.overlays.set(agent, overlay);
|
|
358
|
+
try {
|
|
359
|
+
// Independent section: keep the harness identity, deployment persona,
|
|
360
|
+
// default preset, and the preset's entire native capability composition.
|
|
361
|
+
retain(agentCtx.systemPrompt.section({
|
|
362
|
+
name: 'bot-team:persona', order: 100,
|
|
363
|
+
text: context => {
|
|
364
|
+
if (context?.agent && context.agent !== agent) return '';
|
|
365
|
+
const member = this.repo.getMember(membershipId);
|
|
366
|
+
return isCurrentMemberSession(this.repo, member, agent.id) && expected.conversationId === currentConversationId(this.repo, member.groupId) ? this.personaText(member.botId, member.groupId) : '';
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
retain(agentCtx.systemPrompt.context({
|
|
370
|
+
name: 'bot-team:group', order: 600,
|
|
371
|
+
text: context => {
|
|
372
|
+
if (context?.agent && context.agent !== agent) return '';
|
|
373
|
+
const member = this.repo.getMember(membershipId);
|
|
374
|
+
return isCurrentMemberSession(this.repo, member, agent.id) && expected.conversationId === currentConversationId(this.repo, member.groupId) ? this.groupContextText(member.botId, member.groupId) : '';
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
const tools = this.ctx.get('tools');
|
|
378
|
+
const forbidden = [...RECURSIVE_TEAM_TOOLS, ...(backgroundHeader(agent.session.header) ? BACKGROUND_UNDELIVERABLE_TOOLS : [])];
|
|
379
|
+
const deny = forbidden.filter(name => tools?.get?.(name, agent) !== undefined);
|
|
380
|
+
if (deny.length) {
|
|
381
|
+
if (typeof agentCtx.tools?.restrict !== 'function') throw new Error('Native scoped tool restrictions are unavailable');
|
|
382
|
+
retain(agentCtx.tools.restrict({ deny }));
|
|
383
|
+
}
|
|
384
|
+
// A monotonic native guard also covers tools registered after this
|
|
385
|
+
// overlay (for example while the plugin is hot-reloading).
|
|
386
|
+
if (typeof agentCtx.tools?.guard === 'function') {
|
|
387
|
+
retain(agentCtx.tools.guard(exec => RECURSIVE_TEAM_TOOLS.includes(exec.name)
|
|
388
|
+
? 'Bot Team members cannot recursively drive Bot Team write tools'
|
|
389
|
+
: backgroundHeader(agent.session.header) && BACKGROUND_UNDELIVERABLE_TOOLS.includes(exec.name)
|
|
390
|
+
? 'Native reminders cannot deliver to hidden Bot Team sessions' : undefined));
|
|
391
|
+
}
|
|
392
|
+
await installBotModel(this.ctx, agentCtx, agent, () => this.modelFor(membershipId, expected), selected => this.publishModelSelection(agent, selected, expected), retain, check);
|
|
393
|
+
check();
|
|
394
|
+
retain(agentCtx.on('agent/assistant-stream', payload => {
|
|
395
|
+
if (payload.agent && payload.agent !== agent) return;
|
|
396
|
+
if (this.current(membershipId) !== agent || expected.conversationId !== currentConversationId(this.repo, expected.groupId)) return;
|
|
397
|
+
this.streamHandler?.({ ...payload, agent });
|
|
398
|
+
}));
|
|
399
|
+
retain(agentCtx.on('agent/inbox/claimed', payload => {
|
|
400
|
+
if (payload.agent && payload.agent !== agent) return;
|
|
401
|
+
if (this.current(membershipId) !== agent || expected.conversationId !== currentConversationId(this.repo, expected.groupId)) return;
|
|
402
|
+
this.claimHandler?.({ ...payload, agent });
|
|
403
|
+
}));
|
|
404
|
+
const approval = this.ctx.get('approval');
|
|
405
|
+
approval?.setPolicy?.(agent, this.config.botApprovalPolicy === 'never' ? 'never' : 'ask');
|
|
406
|
+
await this.applySandboxMode(agent);
|
|
407
|
+
check();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.detachOverlay(agent);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
detachOverlay(agent) {
|
|
415
|
+
const overlay = this.overlays.get(agent);
|
|
416
|
+
if (!overlay) return;
|
|
417
|
+
this.overlays.delete(agent);
|
|
418
|
+
for (const dispose of overlay.disposers.reverse()) {
|
|
419
|
+
try { dispose(); }
|
|
420
|
+
catch (error) { this.logger?.warn?.('bot-team: detach overlay failed: ' + String(error)); }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
onModelSelection(session, event) {
|
|
425
|
+
if (this.disposed || event.type !== 'model/selection' || this.recordingModel.has(session)) return;
|
|
426
|
+
const member = this.memberForSession(session.id);
|
|
427
|
+
if (!member) return;
|
|
428
|
+
const expected = this.generation(member);
|
|
429
|
+
const selected = { ...event.data };
|
|
430
|
+
const previous = this.modelUpdates.get(member.botId);
|
|
431
|
+
// A new native selection can retry a failed profile write. Within one Bot
|
|
432
|
+
// these writes stay ordered, including selections from different Sessions.
|
|
433
|
+
const write = (previous ? previous.catch(() => undefined) : Promise.resolve()).then(async () => {
|
|
434
|
+
this.assertGeneration(expected, { current: true });
|
|
435
|
+
const current = this.repo.getBot(member.botId);
|
|
436
|
+
if (!current) throw new Error('Bot no longer exists for native model selection');
|
|
437
|
+
const next = { ...current, provider: selected.provider, model: selected.model };
|
|
438
|
+
if (selected.reasoningEffort === undefined) delete next.reasoningEffort;
|
|
439
|
+
else next.reasoningEffort = selected.reasoningEffort;
|
|
440
|
+
await this.repo.putBot(next);
|
|
441
|
+
this.assertGeneration(expected, { current: true });
|
|
442
|
+
});
|
|
443
|
+
this.modelUpdates.set(member.botId, write);
|
|
444
|
+
this.modelWrites.set(write, member.groupId);
|
|
445
|
+
write.then(() => this.modelWrites.delete(write), () => this.modelWrites.delete(write));
|
|
446
|
+
write.catch(error => this.logger?.warn?.('bot-team: persist native model selection failed: ' + String(error)));
|
|
447
|
+
return write;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async modelFor(membershipId, expected) {
|
|
451
|
+
const { member } = this.requireMember(membershipId);
|
|
452
|
+
// New UI selections can arrive while a previous write is settling.
|
|
453
|
+
for (;;) {
|
|
454
|
+
const pending = this.modelUpdates.get(member.botId);
|
|
455
|
+
if (pending) await pending;
|
|
456
|
+
if (expected) this.assertGeneration(expected, { current: true });
|
|
457
|
+
if (pending === this.modelUpdates.get(member.botId)) return this.requireMember(membershipId).bot;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
publishModelSelection(agent, selected, expected) {
|
|
462
|
+
if (expected) this.assertGeneration(expected, { current: true });
|
|
463
|
+
const projections = this.ctx.get('sessionProjections');
|
|
464
|
+
if (typeof projections?.stateOf !== 'function') throw new Error('Native modelSelection projection is unavailable');
|
|
465
|
+
const state = projections.stateOf(agent.session, 'modelSelection');
|
|
466
|
+
if (!state) throw new Error('Native modelSelection projection is not registered');
|
|
467
|
+
const next = state.pending ?? state.lastUsed;
|
|
468
|
+
const { maxTokens, ...intent } = selected;
|
|
469
|
+
if (next?.provider === intent.provider && next.model === intent.model && next.reasoningEffort === intent.reasoningEffort) return;
|
|
470
|
+
// Session.append dispatches synchronously. Ignore only this plugin's own
|
|
471
|
+
// publication; native UI selectModel events update the durable Bot profile.
|
|
472
|
+
this.recordingModel.add(agent.session);
|
|
473
|
+
try { agent.session.append('model/selection', intent); }
|
|
474
|
+
finally { this.recordingModel.delete(agent.session); }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
personaText(botId, groupId) { return this.prompt?.renderPersona(botId, groupId) || ''; }
|
|
478
|
+
groupContextText(botId, groupId) { return this.prompt?.renderGroupContext(botId, groupId) || ''; }
|
|
479
|
+
attachPrompt(promptApi) { this.prompt = promptApi; }
|
|
480
|
+
attachStreamHandler(handler) { this.streamHandler = handler; }
|
|
481
|
+
attachClaimHandler(handler) {
|
|
482
|
+
this.claimHandler = handler;
|
|
483
|
+
return () => { if (this.claimHandler === handler) this.claimHandler = undefined; };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async submit(agent, input, assertActive = () => {}) {
|
|
487
|
+
if (this.disposed) throw new Error('Bot AgentPool is disposed');
|
|
488
|
+
const member = this.memberForSession(agent.id);
|
|
489
|
+
if (!member) throw new Error('Bot input targets an unmapped or stale conversation native session');
|
|
490
|
+
const expected = this.generation(member);
|
|
491
|
+
const version = this.versions.get(member.membershipId);
|
|
492
|
+
const check = () => {
|
|
493
|
+
this.assertPreparing(member.membershipId, version, expected);
|
|
494
|
+
this.assertGeneration(expected, { current: true });
|
|
495
|
+
if (this.ctx.agents.get(agent.id) !== agent) throw new Error('Bot session was disposed during input admission');
|
|
496
|
+
assertActive();
|
|
497
|
+
};
|
|
498
|
+
check();
|
|
499
|
+
const pending = Promise.resolve().then(async () => {
|
|
500
|
+
const model = botModel(await this.modelFor(member.membershipId, expected));
|
|
501
|
+
check();
|
|
502
|
+
await submitNativeInput(this.ctx, agent, input, model, check);
|
|
503
|
+
check();
|
|
504
|
+
});
|
|
505
|
+
const operations = this.submitting.get(member.membershipId) || new Set();
|
|
506
|
+
operations.add(pending);
|
|
507
|
+
this.submitting.set(member.membershipId, operations);
|
|
508
|
+
try { return await pending; }
|
|
509
|
+
finally {
|
|
510
|
+
operations.delete(pending);
|
|
511
|
+
if (operations.size === 0) this.submitting.delete(member.membershipId);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async disposeMember(membershipId) {
|
|
516
|
+
this.versions.set(membershipId, (this.versions.get(membershipId) || 0) + 1);
|
|
517
|
+
this.preparationAborts.get(membershipId)?.abort(new Error('Bot session preparation was disposed'));
|
|
518
|
+
this.entries.delete(membershipId);
|
|
519
|
+
const pending = this.preparing.get(membershipId);
|
|
520
|
+
const overlays = [...this.overlayPreparing.values()].filter(operation => operation.membershipId === membershipId);
|
|
521
|
+
await Promise.allSettled([pending, ...overlays.map(operation => operation.promise), ...(this.submitting.get(membershipId) || [])]);
|
|
522
|
+
for (const [agent, owned] of [...this.owned]) if (owned.membershipId === membershipId) await this.releaseOwned(agent);
|
|
523
|
+
for (const [agent, overlay] of this.overlays) if (overlay.membershipId === membershipId) this.detachOverlay(agent);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async disposeAll() {
|
|
527
|
+
this.disposed = true;
|
|
528
|
+
for (const abort of this.preparationAborts.values()) abort.abort(new Error('Bot AgentPool is disposed'));
|
|
529
|
+
this.offCreated?.();
|
|
530
|
+
this.offDisposed?.();
|
|
531
|
+
this.offSelection?.();
|
|
532
|
+
await Promise.allSettled([this.ready, ...this.modelUpdates.values(), ...this.preparing.values(), ...[...this.overlayPreparing.values()].map(operation => operation.promise), ...[...this.submitting.values()].flatMap(operations => [...operations])]);
|
|
533
|
+
const releases = await Promise.allSettled([...this.owned.keys()].map(agent => this.releaseOwned(agent)));
|
|
534
|
+
for (const result of releases) if (result.status === 'rejected') this.logger?.warn?.('bot-team: release background Agent failed: ' + String(result.reason));
|
|
535
|
+
this.entries.clear();
|
|
536
|
+
for (const agent of this.overlays.keys()) this.detachOverlay(agent);
|
|
537
|
+
this.streamHandler = undefined;
|
|
538
|
+
this.claimHandler = undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
list() { return [...new Set([...this.entries.keys(), ...[...this.overlays.values()].map(overlay => overlay.membershipId)])].filter(id => this.current(id)); }
|
|
542
|
+
}
|