@borgee/agents-host 0.2.2 → 0.2.26
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/README.md +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +89 -15
- package/dist/agents-host.js +2099 -141
- package/dist/chat/chat-control-plane.d.ts +13 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.js +54 -2
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +35 -0
- package/dist/compatibility-gates.js +127 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/injection.d.ts +109 -0
- package/dist/context/injection.js +350 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +170 -1
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +106 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +151 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +1 -0
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +66 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +1 -0
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +13 -1
- package/dist/state-paths.js +84 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +174 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- package/skills/borgee-agent/borgee-agent.py +409 -0
package/dist/agents-host.js
CHANGED
|
@@ -1,33 +1,277 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
4
|
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
5
|
+
import { HostLogger, summarizeError } from './debug.js';
|
|
3
6
|
import { createDurableCursorStore } from './durable-cursor-store.js';
|
|
7
|
+
import { COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, TOKEN_BINDING_COMPATIBILITY_GATE, } from './compatibility-gates.js';
|
|
8
|
+
import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS, } from './gateway/localhost-gateway.js';
|
|
9
|
+
import { extractTaskIdFromTaskAssignmentContent } from './context/injection.js';
|
|
10
|
+
import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
|
|
4
11
|
import { createProvider } from './providers/create-provider.js';
|
|
12
|
+
import { resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
|
|
13
|
+
import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
|
|
14
|
+
import { waitForTaskForThread } from './task-thread-resolution.js';
|
|
5
15
|
const PRIVATE_STATE_ROOT_MODE = 0o700;
|
|
6
16
|
const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
|
|
17
|
+
const PROTOCOL_STATE_DIRNAME = 'protocol-state';
|
|
18
|
+
const PROTOCOL_PROVIDER_TURN_TIMEOUT_MS = 120_000;
|
|
19
|
+
const PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS = PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
|
|
20
|
+
const PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS = 50;
|
|
21
|
+
const PROTOCOL_KICKOFF_ROUND_LIMIT = 20;
|
|
22
|
+
const COLLABORATION_LATE_SEND_GRACE_MS = 5_000;
|
|
7
23
|
async function ensurePrivateStateRoot(path) {
|
|
8
24
|
await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
|
|
9
25
|
await chmod(path, PRIVATE_STATE_ROOT_MODE);
|
|
10
26
|
}
|
|
27
|
+
function matchesProtocolStateIdentity(left, right) {
|
|
28
|
+
return (left.anchorMessageId === right.anchorMessageId &&
|
|
29
|
+
left.issuerId === right.issuerId &&
|
|
30
|
+
left.participantIds[0] === right.participantIds[0] &&
|
|
31
|
+
left.participantIds[1] === right.participantIds[1]);
|
|
32
|
+
}
|
|
33
|
+
function shouldAdoptPeerProtocolState(current, candidate, selfAgentId) {
|
|
34
|
+
if (!matchesProtocolStateIdentity(current, candidate)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (candidate.currentTurnIndex !== current.currentTurnIndex) {
|
|
38
|
+
return candidate.currentTurnIndex > current.currentTurnIndex;
|
|
39
|
+
}
|
|
40
|
+
if ((candidate.finished && !current.finished) || (!candidate.active && current.active)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (!current.lastProtocolMessageId && candidate.lastProtocolMessageId) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (current.expectedNextSpeakerId !== selfAgentId &&
|
|
47
|
+
candidate.expectedNextSpeakerId === selfAgentId) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
async function writePrivateJsonFile(path, value, options) {
|
|
53
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
54
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
55
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
56
|
+
await writeFile(temporaryPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
57
|
+
try {
|
|
58
|
+
if (options?.exclusive) {
|
|
59
|
+
await link(temporaryPath, path);
|
|
60
|
+
await unlink(temporaryPath);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await rename(temporaryPath, path);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
await unlink(temporaryPath).catch(() => { });
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class DefaultHostRuntime {
|
|
71
|
+
config;
|
|
72
|
+
provider;
|
|
73
|
+
controlPlane;
|
|
74
|
+
ensureStateRoot;
|
|
75
|
+
gateway;
|
|
76
|
+
authorizationAuditSink;
|
|
77
|
+
logger;
|
|
78
|
+
constructor(config, deps) {
|
|
79
|
+
this.config = config;
|
|
80
|
+
this.logger = deps.logger;
|
|
81
|
+
const compatibilityGates = resolveInternalCompatibilityGates();
|
|
82
|
+
const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
|
|
83
|
+
const skillRuntimeGateEnabled = compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
|
|
84
|
+
const localhostGatewayGateEnabled = contextInjectionGateEnabled &&
|
|
85
|
+
skillRuntimeGateEnabled &&
|
|
86
|
+
compatibilityGates.has(LOCALHOST_GATEWAY_COMPATIBILITY_GATE);
|
|
87
|
+
const tokenBindingGateEnabled = compatibilityGates.has(TOKEN_BINDING_COMPATIBILITY_GATE);
|
|
88
|
+
const policyAuditGateEnabled = compatibilityGates.has(POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE);
|
|
89
|
+
const policyMode = resolveInternalPolicyMode(policyAuditGateEnabled);
|
|
90
|
+
const collaborationEnabled = localhostGatewayGateEnabled &&
|
|
91
|
+
compatibilityGates.has(COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE) &&
|
|
92
|
+
policyAuditGateEnabled &&
|
|
93
|
+
policyMode === 'enforce';
|
|
94
|
+
this.authorizationAuditSink = policyAuditGateEnabled
|
|
95
|
+
? new AuthorizationAuditSink({
|
|
96
|
+
stateRootDir: config.stateRootDir,
|
|
97
|
+
logger: deps.logger,
|
|
98
|
+
})
|
|
99
|
+
: undefined;
|
|
100
|
+
this.controlPlane =
|
|
101
|
+
deps.borgee ??
|
|
102
|
+
new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
|
|
103
|
+
cursorStore: createDurableCursorStore({
|
|
104
|
+
stateRootDir: config.stateRootDir,
|
|
105
|
+
}),
|
|
106
|
+
pluginId: `agents-host:${config.agent.provider}`,
|
|
107
|
+
});
|
|
108
|
+
this.gateway = createLocalhostGatewayController({
|
|
109
|
+
gateEnabled: localhostGatewayGateEnabled,
|
|
110
|
+
collaborationEnabled,
|
|
111
|
+
tokenBindingGateEnabled,
|
|
112
|
+
policyAuditGateEnabled,
|
|
113
|
+
policyMode,
|
|
114
|
+
controlPlane: this.controlPlane,
|
|
115
|
+
stateRootDir: config.stateRootDir,
|
|
116
|
+
resolveStableAgentId: deps.resolveStableAgentId,
|
|
117
|
+
logger: deps.logger,
|
|
118
|
+
auditSink: this.authorizationAuditSink,
|
|
119
|
+
authorizeCollaborationSend: deps.authorizeCollaborationSend,
|
|
120
|
+
readCollaborationDraft: deps.readCollaborationDraft,
|
|
121
|
+
});
|
|
122
|
+
this.provider =
|
|
123
|
+
deps.provider ??
|
|
124
|
+
createProvider({
|
|
125
|
+
provider: config.agent.provider,
|
|
126
|
+
stateRootDir: config.stateRootDir,
|
|
127
|
+
resolveStableAgentId: deps.resolveStableAgentId,
|
|
128
|
+
claudeCommand: config.claudeCommand,
|
|
129
|
+
claudeArgs: config.claudeArgs,
|
|
130
|
+
codexCommand: config.codexCommand,
|
|
131
|
+
codexArgs: config.codexArgs,
|
|
132
|
+
copilotCommand: config.copilotCommand,
|
|
133
|
+
copilotArgs: config.copilotArgs,
|
|
134
|
+
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
135
|
+
}, deps.logger, {
|
|
136
|
+
compatibilityGates,
|
|
137
|
+
authorizationAuditSink: this.authorizationAuditSink,
|
|
138
|
+
localhostGateway: localhostGatewayGateEnabled ? this.gateway.contextPublisher : undefined,
|
|
139
|
+
});
|
|
140
|
+
this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
|
|
141
|
+
}
|
|
142
|
+
async start(onMessage) {
|
|
143
|
+
await this.ensureStateRoot(this.config.stateRootDir);
|
|
144
|
+
await this.gateway.start();
|
|
145
|
+
try {
|
|
146
|
+
await this.controlPlane.connect(onMessage);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
await this.gateway.stop().catch(() => { });
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async stop() {
|
|
154
|
+
let thrown;
|
|
155
|
+
try {
|
|
156
|
+
await this.controlPlane.close();
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
thrown = error;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
await this.gateway.stop();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
thrown ??= error;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await this.provider.dispose?.();
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
thrown ??= error;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
await this.authorizationAuditSink?.drain();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
this.logger.error('authorization audit drain failed during shutdown', {
|
|
178
|
+
error: summarizeError(error),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (thrown) {
|
|
182
|
+
throw thrown;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function createDefaultHostRuntime(config, deps) {
|
|
187
|
+
return new DefaultHostRuntime(config, deps);
|
|
188
|
+
}
|
|
11
189
|
function hasVisibleText(value) {
|
|
12
190
|
return value.trim().length > 0;
|
|
13
191
|
}
|
|
192
|
+
function extractDispatchMentionIds(content) {
|
|
193
|
+
return [
|
|
194
|
+
...content.matchAll(/(?:^|[^A-Za-z0-9_])@([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9A-HJKMNP-TV-Z]{26})/gu),
|
|
195
|
+
]
|
|
196
|
+
.map((match) => match[1]?.trim())
|
|
197
|
+
.filter((value) => Boolean(value));
|
|
198
|
+
}
|
|
199
|
+
function hasCanonicalBodyMention(content, participantId) {
|
|
200
|
+
return extractCanonicalMentionIds(content).includes(participantId);
|
|
201
|
+
}
|
|
202
|
+
function hasRelayBodyMention(content, participantId) {
|
|
203
|
+
return (hasCanonicalBodyMention(content, participantId) ||
|
|
204
|
+
extractDispatchMentionIds(content).includes(participantId));
|
|
205
|
+
}
|
|
206
|
+
function encodeStatePathSegment(value) {
|
|
207
|
+
const encoded = Buffer.from(value, 'utf8').toString('hex');
|
|
208
|
+
return encoded.length > 0 ? encoded : 'empty';
|
|
209
|
+
}
|
|
210
|
+
function decodeStatePathSegment(value) {
|
|
211
|
+
if (value === 'empty') {
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
if (!/^(?:[0-9a-f]{2})+$/u.test(value)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return Buffer.from(value, 'hex').toString('utf8');
|
|
218
|
+
}
|
|
219
|
+
function summarizeAnchorContent(content) {
|
|
220
|
+
const collapsed = content.replace(/\s+/gu, ' ').trim();
|
|
221
|
+
if (collapsed.length <= 160) {
|
|
222
|
+
return collapsed;
|
|
223
|
+
}
|
|
224
|
+
return `${collapsed.slice(0, 157)}...`;
|
|
225
|
+
}
|
|
226
|
+
function nextProtocolFallbackDeadline(now = Date.now()) {
|
|
227
|
+
return now + PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
|
|
228
|
+
}
|
|
229
|
+
function nextProtocolKickoffDecisionDeadline(now = Date.now()) {
|
|
230
|
+
return now + PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS;
|
|
231
|
+
}
|
|
232
|
+
function delay(ms) {
|
|
233
|
+
return new Promise((resolveDelay) => {
|
|
234
|
+
setTimeout(resolveDelay, ms);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function isReplyTriggeringEventType(eventType) {
|
|
238
|
+
if (eventType == null) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
const normalized = eventType.trim();
|
|
242
|
+
// The SDK forwards lowercase inbound kind literals into ChannelMessageEvent.type.
|
|
243
|
+
return normalized === '' || normalized === 'message' || normalized === 'mention';
|
|
244
|
+
}
|
|
245
|
+
function resolvePublicReplyText(reply) {
|
|
246
|
+
if (reply.controlMalformed) {
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
if (hasVisibleText(reply.text)) {
|
|
250
|
+
return reply.text;
|
|
251
|
+
}
|
|
252
|
+
return reply.awaitingUser?.question ?? '';
|
|
253
|
+
}
|
|
14
254
|
function providerUsesDraftProgress(provider) {
|
|
15
|
-
return provider === 'claude' || provider === 'copilot';
|
|
255
|
+
return provider === 'claude' || provider === 'codex' || provider === 'copilot';
|
|
16
256
|
}
|
|
17
257
|
class DraftMessageController {
|
|
18
258
|
borgee;
|
|
19
259
|
channelId;
|
|
20
260
|
canWrite;
|
|
261
|
+
logger;
|
|
262
|
+
creationMetadata;
|
|
21
263
|
messageId = null;
|
|
22
264
|
currentBody = '';
|
|
23
265
|
pendingBody = null;
|
|
24
266
|
timer = null;
|
|
25
267
|
sealed = false;
|
|
26
268
|
writeChain = Promise.resolve();
|
|
27
|
-
constructor(borgee, channelId, canWrite) {
|
|
269
|
+
constructor(borgee, channelId, canWrite, logger, creationMetadata) {
|
|
28
270
|
this.borgee = borgee;
|
|
29
271
|
this.channelId = channelId;
|
|
30
272
|
this.canWrite = canWrite;
|
|
273
|
+
this.logger = logger;
|
|
274
|
+
this.creationMetadata = creationMetadata;
|
|
31
275
|
}
|
|
32
276
|
update(text) {
|
|
33
277
|
if (this.sealed || !hasVisibleText(text)) {
|
|
@@ -40,7 +284,7 @@ class DraftMessageController {
|
|
|
40
284
|
this.timer = setTimeout(() => {
|
|
41
285
|
this.timer = null;
|
|
42
286
|
void this.flushPending().catch((error) => {
|
|
43
|
-
|
|
287
|
+
this.logger.error('failed to flush draft progress', error);
|
|
44
288
|
});
|
|
45
289
|
}, STREAM_PROGRESS_EDIT_THROTTLE_MS);
|
|
46
290
|
}
|
|
@@ -60,7 +304,7 @@ class DraftMessageController {
|
|
|
60
304
|
if (hasVisibleText(finalText)) {
|
|
61
305
|
this.pendingBody = finalText;
|
|
62
306
|
await this.flushPending();
|
|
63
|
-
return;
|
|
307
|
+
return this.messageId;
|
|
64
308
|
}
|
|
65
309
|
await this.enqueueWrite(async () => {
|
|
66
310
|
if (!this.canWrite() || !this.messageId) {
|
|
@@ -71,6 +315,7 @@ class DraftMessageController {
|
|
|
71
315
|
this.currentBody = '';
|
|
72
316
|
this.pendingBody = null;
|
|
73
317
|
});
|
|
318
|
+
return null;
|
|
74
319
|
}
|
|
75
320
|
async discard() {
|
|
76
321
|
this.sealed = true;
|
|
@@ -80,7 +325,7 @@ class DraftMessageController {
|
|
|
80
325
|
}
|
|
81
326
|
this.pendingBody = null;
|
|
82
327
|
await this.enqueueWrite(async () => {
|
|
83
|
-
if (!this.
|
|
328
|
+
if (!this.messageId) {
|
|
84
329
|
return;
|
|
85
330
|
}
|
|
86
331
|
await this.borgee.deleteMessage(this.messageId);
|
|
@@ -98,7 +343,11 @@ class DraftMessageController {
|
|
|
98
343
|
await this.borgee.editMessage(this.messageId, body);
|
|
99
344
|
}
|
|
100
345
|
else {
|
|
101
|
-
const posted = await this.borgee.postMessage(
|
|
346
|
+
const posted = await this.borgee.postMessage({
|
|
347
|
+
channelId: this.channelId,
|
|
348
|
+
body,
|
|
349
|
+
replyToId: this.creationMetadata?.replyToId,
|
|
350
|
+
});
|
|
102
351
|
this.messageId = posted.messageId;
|
|
103
352
|
}
|
|
104
353
|
this.currentBody = body;
|
|
@@ -109,201 +358,1910 @@ class DraftMessageController {
|
|
|
109
358
|
return this.writeChain;
|
|
110
359
|
}
|
|
111
360
|
}
|
|
361
|
+
const BLOCKED_AUTHOR_REFRESH_BACKOFF_MS = 1_000;
|
|
362
|
+
const MAX_QUEUED_MESSAGES_PER_CHANNEL = 8;
|
|
363
|
+
const COLLABORATION_SENDS_PER_TURN = 1;
|
|
112
364
|
/**
|
|
113
365
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
114
366
|
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
115
|
-
*
|
|
116
|
-
* Conversation memory is handled entirely by each provider's native
|
|
117
|
-
* per-channel CLI session (Claude `--resume`, Copilot `--session-id`) — see
|
|
118
|
-
* the cli-client.ts file under each provider's folder. This class does not
|
|
119
|
-
* keep any message history itself.
|
|
120
|
-
*
|
|
121
|
-
* Out of scope for this single-agent runner (see README): execution/remote-command
|
|
122
|
-
* dispatch, node provisioning, systemd install, and scheduled (periodic)
|
|
123
|
-
* prompts. Multi-agent local hosting is handled one level up by the local-config
|
|
124
|
-
* supervisor, which creates one isolated `AgentsHost` per effective agent key.
|
|
125
|
-
*
|
|
126
|
-
* Message gating (mention-only / DM rules) is NOT done here: the server
|
|
127
|
-
* enforces per-agent + per-channel `require_mention` at BPP fan-out, so this
|
|
128
|
-
* host simply replies to every message it is handed (minus its own).
|
|
129
367
|
*/
|
|
130
368
|
export class AgentsHost {
|
|
131
369
|
config;
|
|
370
|
+
runtime;
|
|
132
371
|
provider;
|
|
133
372
|
borgee;
|
|
373
|
+
logger;
|
|
374
|
+
collaborationEnabled;
|
|
134
375
|
activeTurns = new Set();
|
|
376
|
+
awaitingUserByChannel = new Map();
|
|
135
377
|
progressChannelQueues = new Map();
|
|
136
378
|
progressDrafts = new Map();
|
|
379
|
+
collaborationDrafts = new Map();
|
|
380
|
+
collaborationChannels = new Map();
|
|
381
|
+
participantDirectoryByUserId = new Map();
|
|
382
|
+
participantKindsByUserId = new Map();
|
|
383
|
+
participantRefreshBackoffByUserId = new Map();
|
|
384
|
+
participantRefreshPromise = null;
|
|
385
|
+
participantDirectorySupported = true;
|
|
137
386
|
selfAgentId = null;
|
|
387
|
+
selfAgentProfile = null;
|
|
138
388
|
selfAgentIdPromise = null;
|
|
139
389
|
started = false;
|
|
140
390
|
controlPlaneClosed = false;
|
|
141
391
|
constructor(config, deps) {
|
|
142
392
|
this.config = config;
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
393
|
+
this.logger = new HostLogger(deps?.runtimeOptions);
|
|
394
|
+
const compatibilityGates = resolveInternalCompatibilityGates();
|
|
395
|
+
const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
|
|
396
|
+
const skillRuntimeGateEnabled = compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
|
|
397
|
+
const localhostGatewayGateEnabled = contextInjectionGateEnabled &&
|
|
398
|
+
skillRuntimeGateEnabled &&
|
|
399
|
+
compatibilityGates.has(LOCALHOST_GATEWAY_COMPATIBILITY_GATE);
|
|
400
|
+
const policyAuditGateEnabled = compatibilityGates.has(POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE);
|
|
401
|
+
this.collaborationEnabled =
|
|
402
|
+
localhostGatewayGateEnabled &&
|
|
403
|
+
compatibilityGates.has(COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE) &&
|
|
404
|
+
policyAuditGateEnabled &&
|
|
405
|
+
resolveInternalPolicyMode(policyAuditGateEnabled) === 'enforce';
|
|
406
|
+
this.runtime = createDefaultHostRuntime(config, {
|
|
407
|
+
logger: this.logger,
|
|
408
|
+
provider: deps?.provider,
|
|
409
|
+
borgee: deps?.borgee,
|
|
146
410
|
resolveStableAgentId: () => this.selfAgentId ?? undefined,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
copilotCommand: config.copilotCommand,
|
|
150
|
-
copilotArgs: config.copilotArgs,
|
|
151
|
-
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
152
|
-
});
|
|
153
|
-
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
|
|
154
|
-
cursorStore: createDurableCursorStore({
|
|
155
|
-
stateRootDir: config.stateRootDir,
|
|
156
|
-
}),
|
|
157
|
-
// Lets the server know which runtime/provider actually connected
|
|
158
|
-
// (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
|
|
159
|
-
// the web UI can show real state instead of a client-side guess.
|
|
160
|
-
pluginId: `agents-host:${config.agent.provider}`,
|
|
411
|
+
readCollaborationDraft: (input) => this.readCollaborationDraft(input),
|
|
412
|
+
authorizeCollaborationSend: (input) => this.authorizeCollaborationSend(input),
|
|
161
413
|
});
|
|
414
|
+
this.provider = this.runtime.provider;
|
|
415
|
+
this.borgee = this.runtime.controlPlane;
|
|
162
416
|
}
|
|
163
417
|
async start() {
|
|
164
418
|
if (this.started)
|
|
165
419
|
return;
|
|
166
420
|
this.started = true;
|
|
167
421
|
this.controlPlaneClosed = false;
|
|
168
|
-
|
|
169
|
-
await this.borgee.connect((message) => {
|
|
170
|
-
if (!this.started) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const task = providerUsesDraftProgress(this.config.agent.provider)
|
|
174
|
-
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
|
|
175
|
-
: this.handleMessage(message);
|
|
176
|
-
this.trackActiveTurn(task);
|
|
177
|
-
});
|
|
178
|
-
const agentId = await this.ensureSelfAgentId();
|
|
179
|
-
console.log('[agents-host] connected', {
|
|
180
|
-
agentId,
|
|
181
|
-
agentName: this.config.agent.agentName,
|
|
422
|
+
this.logger.debug('starting host', {
|
|
182
423
|
provider: this.config.agent.provider,
|
|
424
|
+
agentName: this.config.agent.agentName,
|
|
425
|
+
stateRootDir: this.config.stateRootDir,
|
|
426
|
+
collaborationEnabled: this.collaborationEnabled,
|
|
183
427
|
});
|
|
428
|
+
const bufferedMessages = [];
|
|
429
|
+
let bufferingMessages = true;
|
|
430
|
+
try {
|
|
431
|
+
await this.runtime.start((message) => {
|
|
432
|
+
if (!this.started) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (bufferingMessages) {
|
|
436
|
+
bufferedMessages.push(message);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.dispatchMessage(message);
|
|
440
|
+
});
|
|
441
|
+
const agentId = await this.ensureSelfAgentId();
|
|
442
|
+
bufferingMessages = false;
|
|
443
|
+
while (bufferedMessages.length > 0) {
|
|
444
|
+
const pendingMessages = bufferedMessages.splice(0, bufferedMessages.length);
|
|
445
|
+
for (const message of pendingMessages) {
|
|
446
|
+
this.dispatchMessage(message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this.logger.debug('connected host', {
|
|
450
|
+
agentId,
|
|
451
|
+
agentName: this.config.agent.agentName,
|
|
452
|
+
provider: this.config.agent.provider,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
bufferingMessages = false;
|
|
457
|
+
bufferedMessages.length = 0;
|
|
458
|
+
this.started = false;
|
|
459
|
+
this.controlPlaneClosed = true;
|
|
460
|
+
await this.runtime.stop().catch(() => { });
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
184
463
|
}
|
|
185
464
|
async stop() {
|
|
186
465
|
if (!this.started)
|
|
187
466
|
return;
|
|
188
467
|
this.started = false;
|
|
468
|
+
this.logger.debug('stopping host', {
|
|
469
|
+
activeTurnCount: this.activeTurns.size,
|
|
470
|
+
provider: this.config.agent.provider,
|
|
471
|
+
agentName: this.config.agent.agentName,
|
|
472
|
+
});
|
|
473
|
+
for (const channelId of this.collaborationChannels.keys()) {
|
|
474
|
+
this.invalidateChannelTurn(channelId);
|
|
475
|
+
this.clearProtocolFallback(this.collaborationChannels.get(channelId));
|
|
476
|
+
}
|
|
189
477
|
for (const draft of this.progressDrafts.values()) {
|
|
190
478
|
draft.cancelProgress();
|
|
191
479
|
}
|
|
480
|
+
for (const channelId of [...this.collaborationDrafts.keys()]) {
|
|
481
|
+
this.clearCollaborationDraft(channelId);
|
|
482
|
+
}
|
|
483
|
+
this.awaitingUserByChannel.clear();
|
|
192
484
|
this.controlPlaneClosed = true;
|
|
193
|
-
await this.
|
|
194
|
-
await this.provider.dispose?.();
|
|
485
|
+
await this.runtime.stop();
|
|
195
486
|
await Promise.allSettled([...this.activeTurns]);
|
|
196
487
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
agentName: this.config.agent.agentName,
|
|
202
|
-
channelId: msg.channel_id,
|
|
488
|
+
trackActiveTurn(task) {
|
|
489
|
+
this.activeTurns.add(task);
|
|
490
|
+
void task.finally(() => {
|
|
491
|
+
this.activeTurns.delete(task);
|
|
203
492
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
493
|
+
}
|
|
494
|
+
dispatchMessage(message) {
|
|
495
|
+
const task = this.collaborationEnabled
|
|
496
|
+
? this.handleCollaborationMessage(message)
|
|
497
|
+
: providerUsesDraftProgress(this.config.agent.provider)
|
|
498
|
+
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleLegacyMessage(message))
|
|
499
|
+
: this.handleLegacyMessage(message);
|
|
500
|
+
this.trackActiveTurn(task);
|
|
501
|
+
}
|
|
502
|
+
async handleLegacyMessage(msg) {
|
|
503
|
+
const normalized = await this.normalizeMessage(msg);
|
|
504
|
+
if (!normalized) {
|
|
207
505
|
return;
|
|
208
506
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
507
|
+
if (msg.message_type === 'task_assignment') {
|
|
508
|
+
await this.markAssignedTaskInProgress(msg.channel_id, normalized.content);
|
|
509
|
+
}
|
|
510
|
+
await this.runUngatedTurn(msg, normalized.authorId, normalized.content);
|
|
511
|
+
}
|
|
512
|
+
async handleCollaborationMessage(msg) {
|
|
513
|
+
const normalized = await this.normalizeMessage(msg);
|
|
514
|
+
if (!normalized) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (msg.message_type === 'task_assignment') {
|
|
518
|
+
await this.markAssignedTaskInProgress(msg.channel_id, normalized.content);
|
|
519
|
+
}
|
|
520
|
+
const channelState = this.ensureCollaborationChannelState(msg.channel_id);
|
|
521
|
+
await this.ensureProtocolStateLoaded(msg.channel_id, channelState);
|
|
522
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
523
|
+
const authorKind = await this.classifyAuthor(normalized.authorId, false);
|
|
524
|
+
if (channelState.protocol?.active) {
|
|
525
|
+
if (msg.message_id && authorKind === 'agent') {
|
|
526
|
+
await this.handleActiveProtocolParticipantMessage(msg, normalized, channelState, channelState.protocol, selfAgentId);
|
|
527
|
+
if (channelState.protocol?.active ||
|
|
528
|
+
channelState.queue.some((message) => message.kind === 'protocol-turn')) {
|
|
529
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
530
|
+
return;
|
|
233
531
|
}
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
await draft?.discard().catch(() => { });
|
|
239
|
-
this.progressDrafts.delete(msg.channel_id);
|
|
532
|
+
}
|
|
533
|
+
if (authorKind !== 'agent') {
|
|
534
|
+
if (msg.message_id === channelState.protocol.anchorMessageId) {
|
|
535
|
+
await this.handleActiveProtocolAnchorReplay(msg, normalized, channelState, channelState.protocol, selfAgentId);
|
|
240
536
|
return;
|
|
241
537
|
}
|
|
242
|
-
|
|
243
|
-
|
|
538
|
+
this.cancelProtocolState(msg.channel_id, channelState);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (channelState.blocked) {
|
|
542
|
+
const blockedAuthorKind = await this.classifyBlockedWakeAuthor(normalized.authorId, channelState.blocked);
|
|
543
|
+
if (blockedAuthorKind === 'agent') {
|
|
544
|
+
channelState.blocked.suppressedAgentMessages += 1;
|
|
545
|
+
return;
|
|
244
546
|
}
|
|
245
|
-
|
|
246
|
-
|
|
547
|
+
if (blockedAuthorKind === 'unknown') {
|
|
548
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
549
|
+
raw: msg,
|
|
550
|
+
authorId: normalized.authorId,
|
|
551
|
+
authorKind: 'unknown',
|
|
552
|
+
content: normalized.content,
|
|
553
|
+
});
|
|
554
|
+
const resumed = await this.tryResumeBlockedFromQueue(msg.channel_id, channelState);
|
|
555
|
+
if (resumed) {
|
|
556
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
247
559
|
}
|
|
560
|
+
const wakeBlockedState = channelState.blocked.awaitingUser;
|
|
561
|
+
channelState.blocked = undefined;
|
|
562
|
+
this.awaitingUserByChannel.delete(msg.channel_id);
|
|
563
|
+
channelState.queue = [
|
|
564
|
+
{
|
|
565
|
+
raw: msg,
|
|
566
|
+
authorId: normalized.authorId,
|
|
567
|
+
authorKind: 'human',
|
|
568
|
+
content: normalized.content,
|
|
569
|
+
wakeBlockedState,
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
573
|
+
return;
|
|
248
574
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.
|
|
575
|
+
if (authorKind !== 'agent') {
|
|
576
|
+
const protocolCandidate = this.detectProtocolKickoffCandidate(msg.message_id, normalized.content, selfAgentId);
|
|
577
|
+
if (protocolCandidate) {
|
|
578
|
+
const protocolRequest = await this.resolveProtocolKickoffRequest(msg, normalized, protocolCandidate, selfAgentId);
|
|
579
|
+
if (protocolRequest) {
|
|
580
|
+
await this.startProtocolForAnchor(msg, normalized, channelState, protocolRequest, selfAgentId);
|
|
581
|
+
return;
|
|
255
582
|
}
|
|
256
583
|
}
|
|
257
|
-
console.error('[agents-host] failed to generate or send reply:', {
|
|
258
|
-
provider: this.config.agent.provider,
|
|
259
|
-
agentName: this.config.agent.agentName,
|
|
260
|
-
error,
|
|
261
|
-
});
|
|
262
584
|
}
|
|
263
|
-
|
|
264
|
-
|
|
585
|
+
if (authorKind !== 'agent' && channelState.activeTurn) {
|
|
586
|
+
this.supersedeActiveTurn(msg.channel_id, channelState.activeTurn);
|
|
265
587
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
588
|
+
if (authorKind !== 'agent') {
|
|
589
|
+
channelState.recentTurn = undefined;
|
|
590
|
+
}
|
|
591
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
592
|
+
raw: msg,
|
|
593
|
+
authorId: normalized.authorId,
|
|
594
|
+
authorKind,
|
|
595
|
+
content: normalized.content,
|
|
271
596
|
});
|
|
597
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
272
598
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
599
|
+
detectProtocolKickoffCandidate(anchorMessageId, content, selfAgentId) {
|
|
600
|
+
if (!anchorMessageId) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const mentions = extractCanonicalMentionIds(content);
|
|
604
|
+
if (mentions.length !== 2) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const unique = [...new Set(mentions)].sort();
|
|
608
|
+
if (unique.length !== 2 || !unique.includes(selfAgentId)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
anchorMessageId,
|
|
613
|
+
participantIds: [unique[0], unique[1]],
|
|
614
|
+
issuerId: unique[0],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async validateProtocolKickoffCandidate(anchorAuthorId, protocolCandidate, selfAgentId) {
|
|
618
|
+
const anchorAuthorKind = await this.classifyAuthor(anchorAuthorId, true);
|
|
619
|
+
if (anchorAuthorKind === 'agent') {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
if (protocolCandidate.participantIds.includes(anchorAuthorId)) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
for (const participantId of protocolCandidate.participantIds) {
|
|
626
|
+
if (participantId === selfAgentId) {
|
|
627
|
+
continue;
|
|
281
628
|
}
|
|
629
|
+
const participantKind = await this.classifyAuthor(participantId, true);
|
|
630
|
+
if (participantKind === 'human') {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return protocolCandidate;
|
|
635
|
+
}
|
|
636
|
+
async resolveProtocolKickoffRequest(msg, normalized, protocolCandidate, selfAgentId) {
|
|
637
|
+
const validatedCandidate = await this.validateProtocolKickoffCandidate(normalized.authorId, protocolCandidate, selfAgentId);
|
|
638
|
+
if (!validatedCandidate) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
const pendingDecision = await this.ensureProtocolKickoffDecisionRecord(msg.channel_id, validatedCandidate);
|
|
642
|
+
if (!pendingDecision) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const decision = pendingDecision.status === 'pending'
|
|
646
|
+
? validatedCandidate.issuerId === selfAgentId
|
|
647
|
+
? await this.evaluateIssuerProtocolKickoff(msg, normalized, validatedCandidate, pendingDecision)
|
|
648
|
+
: await this.waitForProtocolKickoffDecision(msg.channel_id, validatedCandidate, pendingDecision.deadlineAt)
|
|
649
|
+
: pendingDecision;
|
|
650
|
+
if (!decision || decision.status !== 'start') {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
const rounds = this.validateProtocolKickoffRounds(decision.rounds);
|
|
654
|
+
if (!rounds) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
participantIds: decision.participantIds,
|
|
659
|
+
issuerId: decision.issuerId,
|
|
660
|
+
maxTurnCount: rounds * 2,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
validateProtocolKickoffRounds(rounds) {
|
|
664
|
+
if (!Number.isFinite(rounds) ||
|
|
665
|
+
typeof rounds !== 'number' ||
|
|
666
|
+
!Number.isInteger(rounds) ||
|
|
667
|
+
rounds < 1) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
return Math.min(rounds, PROTOCOL_KICKOFF_ROUND_LIMIT);
|
|
671
|
+
}
|
|
672
|
+
protocolRequestFromKickoffDecision(decision) {
|
|
673
|
+
const rounds = this.validateProtocolKickoffRounds(decision.rounds);
|
|
674
|
+
if (!rounds) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
participantIds: decision.participantIds,
|
|
679
|
+
issuerId: decision.issuerId,
|
|
680
|
+
maxTurnCount: rounds * 2,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async evaluateIssuerProtocolKickoff(msg, normalized, protocolCandidate, pendingDecision) {
|
|
684
|
+
const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
|
|
685
|
+
kickoff: protocolCandidate,
|
|
686
|
+
silent: true,
|
|
282
687
|
});
|
|
283
|
-
|
|
284
|
-
|
|
688
|
+
if (executed.stale) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const reply = executed.reply;
|
|
692
|
+
const rounds = reply?.control?.kind === 'start-protocol'
|
|
693
|
+
? this.validateProtocolKickoffRounds(reply.control.rounds)
|
|
694
|
+
: null;
|
|
695
|
+
const finalizedDecision = await this.finalizeProtocolKickoffDecision(msg.channel_id, protocolCandidate, pendingDecision.deadlineAt, {
|
|
696
|
+
status: rounds ? 'start' : 'decline',
|
|
697
|
+
...(rounds ? { rounds } : {}),
|
|
698
|
+
});
|
|
699
|
+
this.logger.debug('completed silent kickoff evaluation', {
|
|
700
|
+
channelId: msg.channel_id,
|
|
701
|
+
messageId: msg.message_id,
|
|
702
|
+
anchorMessageId: protocolCandidate.anchorMessageId,
|
|
703
|
+
provider: this.config.agent.provider,
|
|
704
|
+
agentName: this.config.agent.agentName,
|
|
705
|
+
controlKind: reply?.control?.kind ?? null,
|
|
706
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
707
|
+
outcome: rounds ? 'start' : reply?.controlMalformed ? 'malformed-control' : 'no-control',
|
|
708
|
+
persistedStatus: finalizedDecision?.status ?? null,
|
|
709
|
+
persistedRounds: finalizedDecision?.rounds ?? null,
|
|
710
|
+
replyLength: reply ? resolvePublicReplyText(reply).length : 0,
|
|
711
|
+
});
|
|
712
|
+
return finalizedDecision;
|
|
285
713
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
714
|
+
async waitForProtocolKickoffDecision(channelId, protocolCandidate, deadlineAt) {
|
|
715
|
+
for (;;) {
|
|
716
|
+
const decision = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
717
|
+
if (decision &&
|
|
718
|
+
this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, decision)) {
|
|
719
|
+
if (decision.status !== 'pending') {
|
|
720
|
+
return decision;
|
|
721
|
+
}
|
|
722
|
+
const now = Date.now();
|
|
723
|
+
if (decision.deadlineAt <= now) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
await delay(Math.min(PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS, decision.deadlineAt - now));
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
if (deadlineAt <= now) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
await delay(Math.min(PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS, deadlineAt - now));
|
|
291
734
|
}
|
|
292
|
-
return draft;
|
|
293
735
|
}
|
|
294
|
-
async
|
|
295
|
-
|
|
296
|
-
|
|
736
|
+
async ensureProtocolKickoffDecisionRecord(channelId, protocolCandidate) {
|
|
737
|
+
const existing = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
738
|
+
if (existing) {
|
|
739
|
+
return this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, existing)
|
|
740
|
+
? existing
|
|
741
|
+
: null;
|
|
297
742
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
743
|
+
const record = {
|
|
744
|
+
channelId,
|
|
745
|
+
...protocolCandidate,
|
|
746
|
+
deadlineAt: nextProtocolKickoffDecisionDeadline(),
|
|
747
|
+
status: 'pending',
|
|
748
|
+
};
|
|
749
|
+
try {
|
|
750
|
+
await this.writeProtocolKickoffDecisionRecord(record, 'wx');
|
|
751
|
+
return record;
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
const code = error.code;
|
|
755
|
+
if (code !== 'EEXIST') {
|
|
304
756
|
throw error;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const persisted = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
760
|
+
return persisted &&
|
|
761
|
+
this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, persisted)
|
|
762
|
+
? persisted
|
|
763
|
+
: null;
|
|
764
|
+
}
|
|
765
|
+
async finalizeProtocolKickoffDecision(channelId, protocolCandidate, deadlineAt, decision) {
|
|
766
|
+
const current = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
767
|
+
if (current &&
|
|
768
|
+
!this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, current)) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const effectiveDeadlineAt = current?.deadlineAt ?? deadlineAt;
|
|
772
|
+
if (effectiveDeadlineAt <= Date.now()) {
|
|
773
|
+
if (!current || current.status === 'pending') {
|
|
774
|
+
await this.writeProtocolKickoffDecisionRecord({
|
|
775
|
+
channelId,
|
|
776
|
+
...protocolCandidate,
|
|
777
|
+
deadlineAt: effectiveDeadlineAt,
|
|
778
|
+
status: 'decline',
|
|
779
|
+
decidedAt: Date.now(),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
if (current?.status === 'start') {
|
|
785
|
+
return current;
|
|
786
|
+
}
|
|
787
|
+
if (current?.status === 'decline') {
|
|
788
|
+
return current;
|
|
789
|
+
}
|
|
790
|
+
const finalized = {
|
|
791
|
+
channelId,
|
|
792
|
+
...protocolCandidate,
|
|
793
|
+
deadlineAt: effectiveDeadlineAt,
|
|
794
|
+
status: decision.status,
|
|
795
|
+
...(decision.rounds ? { rounds: decision.rounds } : {}),
|
|
796
|
+
decidedAt: Date.now(),
|
|
797
|
+
};
|
|
798
|
+
await this.writeProtocolKickoffDecisionRecord(finalized);
|
|
799
|
+
return finalized;
|
|
800
|
+
}
|
|
801
|
+
matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, record) {
|
|
802
|
+
return (record.channelId === channelId &&
|
|
803
|
+
record.anchorMessageId === protocolCandidate.anchorMessageId &&
|
|
804
|
+
record.issuerId === protocolCandidate.issuerId &&
|
|
805
|
+
record.participantIds[0] === protocolCandidate.participantIds[0] &&
|
|
806
|
+
record.participantIds[1] === protocolCandidate.participantIds[1]);
|
|
807
|
+
}
|
|
808
|
+
resolveProtocolKickoffDecisionPath(channelId, anchorMessageId) {
|
|
809
|
+
return resolveSharedProtocolKickoffDecisionPath(this.config.stateRootDir, channelId, anchorMessageId);
|
|
810
|
+
}
|
|
811
|
+
resolveSharedProtocolStatusPath(channelId, anchorMessageId) {
|
|
812
|
+
return resolveSharedProtocolStatusPath(this.config.stateRootDir, channelId, anchorMessageId);
|
|
813
|
+
}
|
|
814
|
+
async readProtocolKickoffDecisionRecord(channelId, anchorMessageId) {
|
|
815
|
+
try {
|
|
816
|
+
const content = await readFile(this.resolveProtocolKickoffDecisionPath(channelId, anchorMessageId), 'utf8');
|
|
817
|
+
return JSON.parse(content);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
const code = error.code;
|
|
821
|
+
if (code === 'ENOENT') {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async writeProtocolKickoffDecisionRecord(record, flag = 'w') {
|
|
828
|
+
const path = this.resolveProtocolKickoffDecisionPath(record.channelId, record.anchorMessageId);
|
|
829
|
+
await writePrivateJsonFile(path, record, { exclusive: flag === 'wx' });
|
|
830
|
+
}
|
|
831
|
+
async handleActiveProtocolAnchorReplay(msg, normalized, channelState, protocolState, selfAgentId) {
|
|
832
|
+
if (protocolState.currentTurnIndex > 0) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (selfAgentId === protocolState.issuerId) {
|
|
836
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
837
|
+
kind: 'protocol-turn',
|
|
838
|
+
raw: msg,
|
|
839
|
+
authorId: normalized.authorId,
|
|
840
|
+
authorKind: 'human',
|
|
841
|
+
content: normalized.content,
|
|
842
|
+
});
|
|
843
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
847
|
+
}
|
|
848
|
+
async startProtocolForAnchor(msg, normalized, channelState, protocolRequest, selfAgentId) {
|
|
849
|
+
this.cancelProtocolState(msg.channel_id, channelState);
|
|
850
|
+
const protocolState = {
|
|
851
|
+
ownerAgentId: selfAgentId,
|
|
852
|
+
anchorAuthorId: normalized.authorId,
|
|
853
|
+
anchorContent: normalized.content,
|
|
854
|
+
anchorMessageId: msg.message_id ?? randomUUID(),
|
|
855
|
+
participantIds: protocolRequest.participantIds,
|
|
856
|
+
issuerId: protocolRequest.issuerId,
|
|
857
|
+
expectedNextSpeakerId: protocolRequest.issuerId,
|
|
858
|
+
currentTurnIndex: 0,
|
|
859
|
+
maxTurnCount: protocolRequest.maxTurnCount,
|
|
860
|
+
active: true,
|
|
861
|
+
finished: false,
|
|
862
|
+
fallbackDeadlineAt: nextProtocolFallbackDeadline(),
|
|
863
|
+
fallbackTimer: null,
|
|
864
|
+
};
|
|
865
|
+
channelState.protocol = protocolState;
|
|
866
|
+
channelState.protocolLoaded = true;
|
|
867
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
868
|
+
await this.persistSharedProtocolStatus(msg.channel_id, protocolState);
|
|
869
|
+
if (selfAgentId === protocolRequest.issuerId) {
|
|
870
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
871
|
+
kind: 'protocol-turn',
|
|
872
|
+
raw: msg,
|
|
873
|
+
authorId: normalized.authorId,
|
|
874
|
+
authorKind: 'human',
|
|
875
|
+
content: normalized.content,
|
|
305
876
|
});
|
|
877
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
878
|
+
return;
|
|
306
879
|
}
|
|
307
|
-
|
|
880
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
881
|
+
}
|
|
882
|
+
async deliverDeferredTurnReply(channelId, activeTurn, executed) {
|
|
883
|
+
if (!executed.completed ||
|
|
884
|
+
executed.stale ||
|
|
885
|
+
!this.canAcceptTurnOutput(channelId, activeTurn.generation)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const publicReplyText = executed.publicReplyText ?? '';
|
|
889
|
+
if (!this.started || this.controlPlaneClosed || !hasVisibleText(publicReplyText)) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const posted = await this.borgee.postMessage({
|
|
893
|
+
channelId,
|
|
894
|
+
body: executed.visibleReplyBody ?? publicReplyText,
|
|
895
|
+
replyToId: activeTurn.delivery?.replyToId,
|
|
896
|
+
});
|
|
897
|
+
executed.deliveredMessageId = posted.messageId;
|
|
898
|
+
}
|
|
899
|
+
async handleActiveProtocolParticipantMessage(msg, normalized, channelState, protocolState, selfAgentId) {
|
|
900
|
+
if (!msg.message_id || normalized.authorId !== protocolState.expectedNextSpeakerId) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const expectedReplyToId = protocolState.lastProtocolMessageId ?? protocolState.anchorMessageId;
|
|
904
|
+
const confirmed = await this.confirmProtocolMessage(msg.channel_id, msg.message_id, normalized.authorId, expectedReplyToId, msg.created_at);
|
|
905
|
+
if (!confirmed) {
|
|
906
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
907
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.clearProtocolFallback(channelState);
|
|
912
|
+
protocolState.currentTurnIndex += 1;
|
|
913
|
+
protocolState.lastProtocolMessageId = msg.message_id;
|
|
914
|
+
const continuesProtocol = msg.type === 'mention' || hasRelayBodyMention(confirmed.body, selfAgentId);
|
|
915
|
+
if (!continuesProtocol) {
|
|
916
|
+
protocolState.active = false;
|
|
917
|
+
protocolState.finished = true;
|
|
918
|
+
protocolState.expectedNextSpeakerId = normalized.authorId;
|
|
919
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (protocolState.currentTurnIndex >= protocolState.maxTurnCount) {
|
|
923
|
+
protocolState.active = false;
|
|
924
|
+
protocolState.finished = true;
|
|
925
|
+
protocolState.expectedNextSpeakerId = normalized.authorId;
|
|
926
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
protocolState.expectedNextSpeakerId = this.otherProtocolParticipant(protocolState, normalized.authorId);
|
|
930
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
931
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
932
|
+
if (protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
933
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
934
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
939
|
+
kind: 'protocol-turn',
|
|
940
|
+
raw: msg,
|
|
941
|
+
authorId: normalized.authorId,
|
|
942
|
+
authorKind: 'agent',
|
|
943
|
+
content: normalized.content,
|
|
944
|
+
});
|
|
945
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
946
|
+
}
|
|
947
|
+
enqueueCollaborationMessage(channelState, message) {
|
|
948
|
+
channelState.queue.push(message);
|
|
949
|
+
while (channelState.queue.length > MAX_QUEUED_MESSAGES_PER_CHANNEL) {
|
|
950
|
+
channelState.queue.shift();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
ensureCollaborationChannelState(channelId) {
|
|
954
|
+
let state = this.collaborationChannels.get(channelId);
|
|
955
|
+
if (!state) {
|
|
956
|
+
state = {
|
|
957
|
+
generation: 0,
|
|
958
|
+
queue: [],
|
|
959
|
+
running: false,
|
|
960
|
+
protocolLoaded: false,
|
|
961
|
+
};
|
|
962
|
+
this.collaborationChannels.set(channelId, state);
|
|
963
|
+
}
|
|
964
|
+
return state;
|
|
965
|
+
}
|
|
966
|
+
async ensureCollaborationProcessor(channelId, channelState) {
|
|
967
|
+
if (channelState.processor) {
|
|
968
|
+
await channelState.processor;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const processor = this.processCollaborationQueue(channelId, channelState).finally(() => {
|
|
972
|
+
if (channelState.processor === processor) {
|
|
973
|
+
channelState.processor = undefined;
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
channelState.processor = processor;
|
|
977
|
+
await processor;
|
|
978
|
+
}
|
|
979
|
+
async processCollaborationQueue(channelId, channelState) {
|
|
980
|
+
while (this.started && !this.controlPlaneClosed && !channelState.blocked) {
|
|
981
|
+
const next = channelState.queue.shift();
|
|
982
|
+
if (!next) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
channelState.running = true;
|
|
986
|
+
try {
|
|
987
|
+
await this.runCollaborationTurn(channelId, channelState, next);
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
channelState.running = false;
|
|
991
|
+
}
|
|
992
|
+
if (channelState.blocked) {
|
|
993
|
+
channelState.queue = channelState.queue.filter((queued) => queued.authorKind !== 'agent');
|
|
994
|
+
const resumed = await this.tryResumeBlockedFromQueue(channelId, channelState);
|
|
995
|
+
if (resumed) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async tryResumeBlockedFromQueue(channelId, channelState) {
|
|
1002
|
+
const blocked = channelState.blocked;
|
|
1003
|
+
if (!blocked || channelState.queue.length === 0) {
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
const retained = [];
|
|
1007
|
+
for (const [index, queued] of channelState.queue.entries()) {
|
|
1008
|
+
const classified = queued.authorKind === 'human'
|
|
1009
|
+
? 'human'
|
|
1010
|
+
: await this.classifyQueuedBlockedWakeCandidate(queued, blocked);
|
|
1011
|
+
if (classified === 'agent') {
|
|
1012
|
+
blocked.suppressedAgentMessages += 1;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (classified === 'unknown') {
|
|
1016
|
+
retained.push(queued);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const wakeBlockedState = blocked.awaitingUser;
|
|
1020
|
+
channelState.blocked = undefined;
|
|
1021
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1022
|
+
channelState.queue = [
|
|
1023
|
+
{
|
|
1024
|
+
...queued,
|
|
1025
|
+
authorKind: 'human',
|
|
1026
|
+
wakeBlockedState,
|
|
1027
|
+
},
|
|
1028
|
+
...channelState.queue.slice(index + 1),
|
|
1029
|
+
];
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
channelState.queue = retained;
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
async runCollaborationTurn(channelId, channelState, message) {
|
|
1036
|
+
if (message.kind === 'protocol-turn') {
|
|
1037
|
+
await this.runProtocolManagedTurn(channelId, channelState, message);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
channelState.generation += 1;
|
|
1041
|
+
const activeTurn = {
|
|
1042
|
+
generation: channelState.generation,
|
|
1043
|
+
turnExecutionId: randomUUID(),
|
|
1044
|
+
superseded: false,
|
|
1045
|
+
collaborationMessagesSent: 0,
|
|
1046
|
+
targetCooldowns: new Map(),
|
|
1047
|
+
};
|
|
1048
|
+
this.clearCollaborationDraft(channelId);
|
|
1049
|
+
channelState.recentTurn = undefined;
|
|
1050
|
+
channelState.activeTurn = activeTurn;
|
|
1051
|
+
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1052
|
+
activeTurn,
|
|
1053
|
+
incomingAuthorKind: message.authorKind,
|
|
1054
|
+
});
|
|
1055
|
+
channelState.activeTurn = undefined;
|
|
1056
|
+
if (executed.stale) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (executed.completed) {
|
|
1060
|
+
this.rememberRecentTurn(channelState, activeTurn);
|
|
1061
|
+
}
|
|
1062
|
+
if (executed.reply?.awaitingUser) {
|
|
1063
|
+
channelState.blocked = {
|
|
1064
|
+
awaitingUser: executed.reply.awaitingUser,
|
|
1065
|
+
suppressedAgentMessages: 0,
|
|
1066
|
+
};
|
|
1067
|
+
this.awaitingUserByChannel.set(channelId, executed.reply.awaitingUser);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
channelState.blocked = undefined;
|
|
1071
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1072
|
+
}
|
|
1073
|
+
async runProtocolManagedTurn(channelId, channelState, message) {
|
|
1074
|
+
const protocolState = channelState.protocol;
|
|
1075
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1076
|
+
if (!protocolState?.active || protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (!(await this.canContinueSharedProtocol(channelId, channelState, protocolState))) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
channelState.generation += 1;
|
|
1083
|
+
const currentTurnIndex = protocolState.currentTurnIndex + 1;
|
|
1084
|
+
const peerId = this.otherProtocolParticipant(protocolState, selfAgentId);
|
|
1085
|
+
const isFinalTurn = currentTurnIndex >= protocolState.maxTurnCount;
|
|
1086
|
+
const activeTurn = {
|
|
1087
|
+
generation: channelState.generation,
|
|
1088
|
+
turnExecutionId: randomUUID(),
|
|
1089
|
+
superseded: false,
|
|
1090
|
+
collaborationMessagesSent: 0,
|
|
1091
|
+
targetCooldowns: new Map(),
|
|
1092
|
+
delivery: {
|
|
1093
|
+
replyToId: protocolState.lastProtocolMessageId ?? protocolState.anchorMessageId,
|
|
1094
|
+
},
|
|
1095
|
+
protocol: {
|
|
1096
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1097
|
+
anchorSummary: summarizeAnchorContent(protocolState.anchorContent),
|
|
1098
|
+
issuerId: protocolState.issuerId,
|
|
1099
|
+
role: protocolState.currentTurnIndex === 0 ? 'issuer' : isFinalTurn ? 'final' : 'intermediate',
|
|
1100
|
+
targetPeerId: peerId,
|
|
1101
|
+
turnIndex: currentTurnIndex,
|
|
1102
|
+
maxTurnCount: protocolState.maxTurnCount,
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
this.clearCollaborationDraft(channelId);
|
|
1106
|
+
channelState.recentTurn = undefined;
|
|
1107
|
+
channelState.activeTurn = activeTurn;
|
|
1108
|
+
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1109
|
+
activeTurn,
|
|
1110
|
+
incomingAuthorKind: message.authorKind,
|
|
1111
|
+
});
|
|
1112
|
+
channelState.activeTurn = undefined;
|
|
1113
|
+
if (executed.stale || !protocolState.active) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (executed.completed) {
|
|
1117
|
+
this.rememberRecentTurn(channelState, activeTurn);
|
|
1118
|
+
}
|
|
1119
|
+
const deliveredMessageId = executed.deliveredMessageId ?? null;
|
|
1120
|
+
const visibleMessageId = deliveredMessageId ?? protocolState.lastProtocolMessageId;
|
|
1121
|
+
if (executed.reply?.controlMalformed) {
|
|
1122
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (!executed.reply?.control) {
|
|
1126
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (executed.reply.control.kind === 'awaiting-user') {
|
|
1130
|
+
channelState.blocked = {
|
|
1131
|
+
awaitingUser: executed.reply.awaitingUser ?? {
|
|
1132
|
+
question: executed.reply.control.question,
|
|
1133
|
+
...(executed.reply.control.reason ? { reason: executed.reply.control.reason } : {}),
|
|
1134
|
+
},
|
|
1135
|
+
suppressedAgentMessages: 0,
|
|
1136
|
+
};
|
|
1137
|
+
this.awaitingUserByChannel.set(channelId, channelState.blocked.awaitingUser);
|
|
1138
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (executed.reply.control.kind === 'start-protocol') {
|
|
1142
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (!deliveredMessageId) {
|
|
1146
|
+
this.finishProtocolState(channelId, channelState, protocolState);
|
|
1147
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1148
|
+
channelState.blocked = undefined;
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
protocolState.currentTurnIndex = currentTurnIndex;
|
|
1152
|
+
if (deliveredMessageId) {
|
|
1153
|
+
protocolState.lastProtocolMessageId = deliveredMessageId;
|
|
1154
|
+
}
|
|
1155
|
+
if (executed.reply.control.kind === 'conclude-locally') {
|
|
1156
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1157
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1158
|
+
channelState.blocked = undefined;
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (currentTurnIndex >= protocolState.maxTurnCount) {
|
|
1162
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1163
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1164
|
+
channelState.blocked = undefined;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
protocolState.expectedNextSpeakerId = peerId;
|
|
1168
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
1169
|
+
await this.persistProtocolState(channelId, protocolState);
|
|
1170
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
1171
|
+
this.armProtocolFallback(channelId, channelState, protocolState);
|
|
1172
|
+
}
|
|
1173
|
+
channelState.blocked = undefined;
|
|
1174
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1175
|
+
}
|
|
1176
|
+
buildIncomingContent(message) {
|
|
1177
|
+
if (!message.wakeBlockedState) {
|
|
1178
|
+
return message.content;
|
|
1179
|
+
}
|
|
1180
|
+
const lines = [
|
|
1181
|
+
'[Internal note: a previously blocked human reply has arrived.]',
|
|
1182
|
+
`Pending question: ${message.wakeBlockedState.question}`,
|
|
1183
|
+
...(message.wakeBlockedState.reason
|
|
1184
|
+
? [`Blocked reason: ${message.wakeBlockedState.reason}`]
|
|
1185
|
+
: []),
|
|
1186
|
+
`Human reply from ${message.authorId}: ${message.content}`,
|
|
1187
|
+
'',
|
|
1188
|
+
message.content,
|
|
1189
|
+
];
|
|
1190
|
+
return lines.join('\n');
|
|
1191
|
+
}
|
|
1192
|
+
otherProtocolParticipant(protocolState, currentAgentId) {
|
|
1193
|
+
return protocolState.participantIds[0] === currentAgentId
|
|
1194
|
+
? protocolState.participantIds[1]
|
|
1195
|
+
: protocolState.participantIds[0];
|
|
1196
|
+
}
|
|
1197
|
+
resolveProtocolStatePath(channelId, ownerAgentId) {
|
|
1198
|
+
return resolve(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME, `${encodeStatePathSegment(channelId)}--${encodeStatePathSegment(ownerAgentId)}.json`);
|
|
1199
|
+
}
|
|
1200
|
+
async ensureProtocolStateLoaded(channelId, channelState) {
|
|
1201
|
+
if (channelState.protocolLoaded) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (channelState.protocol) {
|
|
1205
|
+
channelState.protocolLoaded = true;
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
channelState.protocolLoaded = true;
|
|
1209
|
+
try {
|
|
1210
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1211
|
+
const content = await readFile(this.resolveProtocolStatePath(channelId, selfAgentId), 'utf8');
|
|
1212
|
+
const parsed = JSON.parse(content);
|
|
1213
|
+
if ((parsed.ownerAgentId && parsed.ownerAgentId !== selfAgentId) ||
|
|
1214
|
+
!parsed.participantIds.includes(selfAgentId)) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const resolvedProtocolState = await this.reconcileOwnProtocolStateFromPeers(channelId, {
|
|
1218
|
+
...parsed,
|
|
1219
|
+
ownerAgentId: parsed.ownerAgentId ?? selfAgentId,
|
|
1220
|
+
}, selfAgentId);
|
|
1221
|
+
channelState.protocol = {
|
|
1222
|
+
...resolvedProtocolState,
|
|
1223
|
+
fallbackTimer: null,
|
|
1224
|
+
};
|
|
1225
|
+
if (resolvedProtocolState.currentTurnIndex !== parsed.currentTurnIndex ||
|
|
1226
|
+
resolvedProtocolState.expectedNextSpeakerId !== parsed.expectedNextSpeakerId ||
|
|
1227
|
+
resolvedProtocolState.active !== parsed.active ||
|
|
1228
|
+
resolvedProtocolState.finished !== parsed.finished ||
|
|
1229
|
+
resolvedProtocolState.lastProtocolMessageId !== parsed.lastProtocolMessageId) {
|
|
1230
|
+
await this.persistProtocolState(channelId, channelState.protocol);
|
|
1231
|
+
}
|
|
1232
|
+
if (resolvedProtocolState.active) {
|
|
1233
|
+
this.armProtocolFallback(channelId, channelState, channelState.protocol);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
const code = error.code;
|
|
1238
|
+
if (code !== 'ENOENT') {
|
|
1239
|
+
this.logger.error('failed to load protocol state', {
|
|
1240
|
+
channelId,
|
|
1241
|
+
error: summarizeError(error),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async reconcileOwnProtocolStateFromPeers(channelId, protocolState, selfAgentId) {
|
|
1247
|
+
const directoryPath = join(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME);
|
|
1248
|
+
const channelPrefix = `${encodeStatePathSegment(channelId)}--`;
|
|
1249
|
+
const ownSuffix = `--${encodeStatePathSegment(selfAgentId)}.json`;
|
|
1250
|
+
let entries;
|
|
1251
|
+
try {
|
|
1252
|
+
entries = await readdir(directoryPath);
|
|
1253
|
+
}
|
|
1254
|
+
catch (error) {
|
|
1255
|
+
const code = error.code;
|
|
1256
|
+
if (code === 'ENOENT') {
|
|
1257
|
+
return protocolState;
|
|
1258
|
+
}
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
let resolved = protocolState;
|
|
1262
|
+
for (const entry of entries) {
|
|
1263
|
+
if (!entry.startsWith(channelPrefix) || entry.endsWith(ownSuffix)) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
const candidate = JSON.parse(await readFile(join(directoryPath, entry), 'utf8'));
|
|
1268
|
+
if (!candidate.participantIds.includes(selfAgentId)) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!shouldAdoptPeerProtocolState(resolved, candidate, selfAgentId)) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
resolved = {
|
|
1275
|
+
...candidate,
|
|
1276
|
+
ownerAgentId: selfAgentId,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
const code = error.code;
|
|
1281
|
+
if (code === 'ENOENT') {
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
throw error;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return resolved;
|
|
1288
|
+
}
|
|
1289
|
+
async loadPersistedProtocolStates() {
|
|
1290
|
+
if (!this.collaborationEnabled) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1294
|
+
const ownSuffix = `--${encodeStatePathSegment(selfAgentId)}.json`;
|
|
1295
|
+
const directoryPath = join(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME);
|
|
1296
|
+
let entries;
|
|
1297
|
+
try {
|
|
1298
|
+
entries = await readdir(directoryPath);
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
const code = error.code;
|
|
1302
|
+
if (code !== 'ENOENT') {
|
|
1303
|
+
this.logger.error('failed to enumerate protocol state directory', {
|
|
1304
|
+
directoryPath,
|
|
1305
|
+
error: summarizeError(error),
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
for (const entry of entries) {
|
|
1311
|
+
if (!entry.endsWith(ownSuffix)) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const encodedChannelId = entry.slice(0, -ownSuffix.length);
|
|
1315
|
+
const channelId = decodeStatePathSegment(encodedChannelId);
|
|
1316
|
+
if (channelId == null) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const channelState = this.ensureCollaborationChannelState(channelId);
|
|
1320
|
+
await this.ensureProtocolStateLoaded(channelId, channelState);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async resumeOwnedProtocolTurns() {
|
|
1324
|
+
if (!this.collaborationEnabled) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1328
|
+
for (const [channelId, channelState] of this.collaborationChannels.entries()) {
|
|
1329
|
+
const protocolState = channelState.protocol;
|
|
1330
|
+
if (!protocolState?.active || protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (channelState.activeTurn ||
|
|
1334
|
+
channelState.queue.some((message) => message.kind === 'protocol-turn')) {
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
const resumed = await this.buildResumedProtocolMessage(channelId, protocolState);
|
|
1338
|
+
if (!resumed) {
|
|
1339
|
+
this.cancelProtocolState(channelId, channelState);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
this.enqueueCollaborationMessage(channelState, resumed);
|
|
1343
|
+
await this.ensureCollaborationProcessor(channelId, channelState);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async buildResumedProtocolMessage(channelId, protocolState) {
|
|
1347
|
+
if (protocolState.currentTurnIndex === 0) {
|
|
1348
|
+
return {
|
|
1349
|
+
kind: 'protocol-turn',
|
|
1350
|
+
raw: {
|
|
1351
|
+
channel_id: channelId,
|
|
1352
|
+
message_id: protocolState.anchorMessageId,
|
|
1353
|
+
user_id: protocolState.anchorAuthorId,
|
|
1354
|
+
content: protocolState.anchorContent,
|
|
1355
|
+
body: protocolState.anchorContent,
|
|
1356
|
+
type: 'message',
|
|
1357
|
+
},
|
|
1358
|
+
authorId: protocolState.anchorAuthorId,
|
|
1359
|
+
authorKind: 'human',
|
|
1360
|
+
content: protocolState.anchorContent,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
if (!protocolState.lastProtocolMessageId) {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
let before;
|
|
1367
|
+
for (;;) {
|
|
1368
|
+
const history = await this.borgee.readChannelHistory({
|
|
1369
|
+
channelId,
|
|
1370
|
+
before,
|
|
1371
|
+
limit: 200,
|
|
1372
|
+
});
|
|
1373
|
+
const priorMessage = history.find((entry) => entry.id === protocolState.lastProtocolMessageId);
|
|
1374
|
+
if (priorMessage) {
|
|
1375
|
+
return {
|
|
1376
|
+
kind: 'protocol-turn',
|
|
1377
|
+
raw: {
|
|
1378
|
+
channel_id: channelId,
|
|
1379
|
+
message_id: priorMessage.id,
|
|
1380
|
+
user_id: priorMessage.authorId,
|
|
1381
|
+
content: priorMessage.body,
|
|
1382
|
+
body: priorMessage.body,
|
|
1383
|
+
type: 'message',
|
|
1384
|
+
created_at: priorMessage.createdAt,
|
|
1385
|
+
},
|
|
1386
|
+
authorId: priorMessage.authorId,
|
|
1387
|
+
authorKind: 'agent',
|
|
1388
|
+
content: priorMessage.body,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
if (history.length < 200) {
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
const oldestCreatedAt = history.reduce((oldest, entry) => {
|
|
1395
|
+
return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
|
|
1396
|
+
}, null);
|
|
1397
|
+
if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
|
|
1398
|
+
break;
|
|
1399
|
+
}
|
|
1400
|
+
before = oldestCreatedAt;
|
|
1401
|
+
}
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
async persistProtocolState(channelId, protocolState) {
|
|
1405
|
+
const ownerAgentId = protocolState.ownerAgentId ?? (await this.ensureSelfAgentId());
|
|
1406
|
+
protocolState.ownerAgentId = ownerAgentId;
|
|
1407
|
+
await writePrivateJsonFile(this.resolveProtocolStatePath(channelId, ownerAgentId), {
|
|
1408
|
+
ownerAgentId,
|
|
1409
|
+
anchorAuthorId: protocolState.anchorAuthorId,
|
|
1410
|
+
anchorContent: protocolState.anchorContent,
|
|
1411
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1412
|
+
participantIds: protocolState.participantIds,
|
|
1413
|
+
issuerId: protocolState.issuerId,
|
|
1414
|
+
expectedNextSpeakerId: protocolState.expectedNextSpeakerId,
|
|
1415
|
+
currentTurnIndex: protocolState.currentTurnIndex,
|
|
1416
|
+
maxTurnCount: protocolState.maxTurnCount,
|
|
1417
|
+
lastProtocolMessageId: protocolState.lastProtocolMessageId,
|
|
1418
|
+
active: protocolState.active,
|
|
1419
|
+
finished: protocolState.finished,
|
|
1420
|
+
fallbackDeadlineAt: protocolState.fallbackDeadlineAt,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
async persistSharedProtocolStatus(channelId, protocolState) {
|
|
1424
|
+
await writePrivateJsonFile(this.resolveSharedProtocolStatusPath(channelId, protocolState.anchorMessageId), {
|
|
1425
|
+
channelId,
|
|
1426
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1427
|
+
active: protocolState.active,
|
|
1428
|
+
finished: protocolState.finished,
|
|
1429
|
+
updatedAt: Date.now(),
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
async readSharedProtocolStatus(channelId, anchorMessageId) {
|
|
1433
|
+
try {
|
|
1434
|
+
const content = await readFile(this.resolveSharedProtocolStatusPath(channelId, anchorMessageId), 'utf8');
|
|
1435
|
+
const parsed = JSON.parse(content);
|
|
1436
|
+
if (typeof parsed.active !== 'boolean' || typeof parsed.finished !== 'boolean') {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
return { active: parsed.active, finished: parsed.finished };
|
|
1440
|
+
}
|
|
1441
|
+
catch (error) {
|
|
1442
|
+
const code = error.code;
|
|
1443
|
+
if (code === 'ENOENT') {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
clearProtocolFallback(channelState) {
|
|
1450
|
+
const timer = channelState?.protocol?.fallbackTimer;
|
|
1451
|
+
if (timer) {
|
|
1452
|
+
clearTimeout(timer);
|
|
1453
|
+
if (channelState?.protocol) {
|
|
1454
|
+
channelState.protocol.fallbackTimer = null;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
armProtocolFallback(channelId, channelState, protocolState) {
|
|
1459
|
+
this.clearProtocolFallback(channelState);
|
|
1460
|
+
const delay = Math.max(0, protocolState.fallbackDeadlineAt - Date.now());
|
|
1461
|
+
protocolState.fallbackTimer = setTimeout(() => {
|
|
1462
|
+
void this.runProtocolFallback(channelId).catch((error) => {
|
|
1463
|
+
this.logger.error('protocol fallback failed', {
|
|
1464
|
+
channelId,
|
|
1465
|
+
error: summarizeError(error),
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
}, delay);
|
|
1469
|
+
}
|
|
1470
|
+
async runProtocolFallback(channelId) {
|
|
1471
|
+
const channelState = this.collaborationChannels.get(channelId);
|
|
1472
|
+
const protocolState = channelState?.protocol;
|
|
1473
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1474
|
+
if (!channelState ||
|
|
1475
|
+
!protocolState?.active ||
|
|
1476
|
+
selfAgentId === protocolState.issuerId ||
|
|
1477
|
+
protocolState.expectedNextSpeakerId !== protocolState.issuerId ||
|
|
1478
|
+
protocolState.fallbackDeadlineAt > Date.now()) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
await this.handleProtocolFallbackFailure(channelId, channelState, protocolState, selfAgentId);
|
|
1482
|
+
}
|
|
1483
|
+
async handleProtocolFallbackFailure(channelId, channelState, protocolState, selfAgentId) {
|
|
1484
|
+
if (selfAgentId === protocolState.issuerId) {
|
|
1485
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
1486
|
+
await this.persistProtocolState(channelId, protocolState);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const fallbackMessage = {
|
|
1490
|
+
channel_id: channelId,
|
|
1491
|
+
message_id: protocolState.anchorMessageId,
|
|
1492
|
+
user_id: protocolState.anchorAuthorId,
|
|
1493
|
+
content: protocolState.anchorContent,
|
|
1494
|
+
body: protocolState.anchorContent,
|
|
1495
|
+
type: 'message',
|
|
1496
|
+
};
|
|
1497
|
+
this.cancelProtocolState(channelId, channelState);
|
|
1498
|
+
await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, protocolState.anchorContent);
|
|
1499
|
+
}
|
|
1500
|
+
finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
|
|
1501
|
+
protocolState.active = false;
|
|
1502
|
+
protocolState.finished = true;
|
|
1503
|
+
if (lastMessageId) {
|
|
1504
|
+
protocolState.lastProtocolMessageId = lastMessageId;
|
|
1505
|
+
}
|
|
1506
|
+
this.clearProtocolFallback(channelState);
|
|
1507
|
+
void this.persistProtocolState(channelId, protocolState);
|
|
1508
|
+
void this.persistSharedProtocolStatus(channelId, protocolState);
|
|
1509
|
+
}
|
|
1510
|
+
cancelProtocolState(channelId, channelState) {
|
|
1511
|
+
if (channelState.activeTurn) {
|
|
1512
|
+
this.supersedeActiveTurn(channelId, channelState.activeTurn);
|
|
1513
|
+
}
|
|
1514
|
+
if (!channelState.protocol) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
channelState.protocol.active = false;
|
|
1518
|
+
channelState.protocol.finished = false;
|
|
1519
|
+
this.clearProtocolFallback(channelState);
|
|
1520
|
+
void this.persistProtocolState(channelId, channelState.protocol);
|
|
1521
|
+
void this.persistSharedProtocolStatus(channelId, channelState.protocol);
|
|
1522
|
+
}
|
|
1523
|
+
async confirmProtocolMessage(channelId, messageId, authorId, expectedReplyToId, createdAt) {
|
|
1524
|
+
const history = await this.borgee.readChannelHistory({
|
|
1525
|
+
channelId,
|
|
1526
|
+
before: createdAt != null ? createdAt + 1 : undefined,
|
|
1527
|
+
limit: 20,
|
|
1528
|
+
});
|
|
1529
|
+
const matched = history.find((entry) => entry.id === messageId);
|
|
1530
|
+
if (!matched) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
if (matched.authorId !== authorId || matched.replyToId !== expectedReplyToId) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
return matched;
|
|
1537
|
+
}
|
|
1538
|
+
async runUngatedTurn(msg, authorId, content) {
|
|
1539
|
+
const executed = await this.executeTurn(msg, authorId, content);
|
|
1540
|
+
if (executed.reply) {
|
|
1541
|
+
this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async executeTurn(msg, authorId, content, options) {
|
|
1545
|
+
const channelId = msg.channel_id;
|
|
1546
|
+
const activeTurn = options?.activeTurn;
|
|
1547
|
+
const silentTurn = options?.silent === true;
|
|
1548
|
+
const deferDelivery = options?.deferDelivery === true;
|
|
1549
|
+
const collaborationTurnMode = activeTurn?.protocol
|
|
1550
|
+
? 'protocol-managed'
|
|
1551
|
+
: options?.kickoff
|
|
1552
|
+
? 'silent-kickoff'
|
|
1553
|
+
: undefined;
|
|
1554
|
+
const turnExecutionId = activeTurn?.turnExecutionId ?? null;
|
|
1555
|
+
const stopTyping = silentTurn ? () => { } : this.borgee.startTyping(channelId);
|
|
1556
|
+
const collaborationGrounding = await this.buildCollaborationGrounding(authorId, options?.incomingAuthorKind, activeTurn, options?.kickoff);
|
|
1557
|
+
const usePublicDraftProgress = !silentTurn &&
|
|
1558
|
+
!deferDelivery &&
|
|
1559
|
+
!this.collaborationEnabled &&
|
|
1560
|
+
options?.incomingAuthorKind !== 'agent' &&
|
|
1561
|
+
providerUsesDraftProgress(this.config.agent.provider) &&
|
|
1562
|
+
activeTurn?.protocol === undefined;
|
|
1563
|
+
const usePrivateCollaborationDraftProgress = !silentTurn &&
|
|
1564
|
+
!deferDelivery &&
|
|
1565
|
+
this.collaborationEnabled &&
|
|
1566
|
+
turnExecutionId != null &&
|
|
1567
|
+
providerUsesDraftProgress(this.config.agent.provider) &&
|
|
1568
|
+
activeTurn?.protocol === undefined;
|
|
1569
|
+
const turnContext = {
|
|
1570
|
+
eventType: msg.type,
|
|
1571
|
+
channelId,
|
|
1572
|
+
messageId: msg.message_id,
|
|
1573
|
+
authorId,
|
|
1574
|
+
contentLength: content.length,
|
|
1575
|
+
provider: this.config.agent.provider,
|
|
1576
|
+
agentName: this.config.agent.agentName,
|
|
1577
|
+
generation: activeTurn?.generation,
|
|
1578
|
+
};
|
|
1579
|
+
const turnStartedAt = Date.now();
|
|
1580
|
+
this.logger.debug('starting turn', turnContext);
|
|
1581
|
+
try {
|
|
1582
|
+
const reply = await this.provider.generateReply({
|
|
1583
|
+
agentName: this.config.agent.agentName,
|
|
1584
|
+
provider: this.config.agent.provider,
|
|
1585
|
+
channelId,
|
|
1586
|
+
incomingAuthorId: authorId,
|
|
1587
|
+
incomingContent: content,
|
|
1588
|
+
incomingEventKind: msg.type,
|
|
1589
|
+
incomingMessageType: msg.message_type,
|
|
1590
|
+
collaboration: turnExecutionId || options?.kickoff
|
|
1591
|
+
? {
|
|
1592
|
+
enabled: this.collaborationEnabled,
|
|
1593
|
+
...(turnExecutionId ? { turnExecutionId } : {}),
|
|
1594
|
+
turnMode: collaborationTurnMode ?? 'ordinary',
|
|
1595
|
+
...(options?.kickoff ? { kickoff: options.kickoff } : {}),
|
|
1596
|
+
...(activeTurn?.protocol ? { protocol: activeTurn.protocol } : {}),
|
|
1597
|
+
...(collaborationGrounding ? { grounding: collaborationGrounding } : {}),
|
|
1598
|
+
}
|
|
1599
|
+
: undefined,
|
|
1600
|
+
}, usePublicDraftProgress || usePrivateCollaborationDraftProgress
|
|
1601
|
+
? {
|
|
1602
|
+
onProgress: (update) => {
|
|
1603
|
+
if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (usePublicDraftProgress) {
|
|
1607
|
+
this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery).update(update.text);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
this.publishCollaborationDraft(channelId, activeTurn, update.text);
|
|
1611
|
+
},
|
|
1612
|
+
}
|
|
1613
|
+
: undefined);
|
|
1614
|
+
const publicReplyText = resolvePublicReplyText(reply);
|
|
1615
|
+
const visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
|
|
1616
|
+
if (!(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
|
|
1617
|
+
await this.discardDraft(channelId);
|
|
1618
|
+
if (activeTurn?.turnExecutionId) {
|
|
1619
|
+
this.clearCollaborationDraft(channelId, {
|
|
1620
|
+
generation: activeTurn.generation,
|
|
1621
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return { reply, stale: true, completed: false };
|
|
1625
|
+
}
|
|
1626
|
+
if (silentTurn) {
|
|
1627
|
+
this.logger.debug('completed turn', {
|
|
1628
|
+
...turnContext,
|
|
1629
|
+
durationMs: Date.now() - turnStartedAt,
|
|
1630
|
+
replyLength: publicReplyText.length,
|
|
1631
|
+
controlKind: reply?.control?.kind ?? null,
|
|
1632
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
1633
|
+
delivery: 'silent',
|
|
1634
|
+
});
|
|
1635
|
+
return {
|
|
1636
|
+
reply,
|
|
1637
|
+
deliveredMessageId: null,
|
|
1638
|
+
publicReplyText,
|
|
1639
|
+
visibleReplyBody,
|
|
1640
|
+
stale: false,
|
|
1641
|
+
completed: true,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
if (deferDelivery) {
|
|
1645
|
+
this.logger.debug('completed turn', {
|
|
1646
|
+
...turnContext,
|
|
1647
|
+
durationMs: Date.now() - turnStartedAt,
|
|
1648
|
+
replyLength: publicReplyText.length,
|
|
1649
|
+
controlKind: reply?.control?.kind ?? null,
|
|
1650
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
1651
|
+
delivery: 'deferred',
|
|
1652
|
+
});
|
|
1653
|
+
return {
|
|
1654
|
+
reply,
|
|
1655
|
+
deliveredMessageId: null,
|
|
1656
|
+
publicReplyText,
|
|
1657
|
+
visibleReplyBody,
|
|
1658
|
+
stale: false,
|
|
1659
|
+
completed: true,
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
if (usePublicDraftProgress) {
|
|
1663
|
+
const draft = this.progressDrafts.get(channelId);
|
|
1664
|
+
if (!this.started) {
|
|
1665
|
+
await draft?.discard().catch(() => { });
|
|
1666
|
+
this.progressDrafts.delete(channelId);
|
|
1667
|
+
return { reply, stale: true, completed: false };
|
|
1668
|
+
}
|
|
1669
|
+
const deliveredMessageId = await (draft ?? this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery)).finalize(publicReplyText);
|
|
1670
|
+
this.progressDrafts.delete(channelId);
|
|
1671
|
+
this.logger.debug('completed turn', {
|
|
1672
|
+
...turnContext,
|
|
1673
|
+
durationMs: Date.now() - turnStartedAt,
|
|
1674
|
+
replyLength: publicReplyText.length,
|
|
1675
|
+
controlKind: reply?.control?.kind ?? null,
|
|
1676
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
1677
|
+
delivery: 'draft',
|
|
1678
|
+
});
|
|
1679
|
+
return {
|
|
1680
|
+
reply,
|
|
1681
|
+
deliveredMessageId,
|
|
1682
|
+
publicReplyText,
|
|
1683
|
+
visibleReplyBody,
|
|
1684
|
+
stale: false,
|
|
1685
|
+
completed: true,
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
else if (this.started && !this.controlPlaneClosed && hasVisibleText(publicReplyText)) {
|
|
1689
|
+
const posted = await this.borgee.postMessage({
|
|
1690
|
+
channelId,
|
|
1691
|
+
body: visibleReplyBody,
|
|
1692
|
+
replyToId: activeTurn?.delivery?.replyToId,
|
|
1693
|
+
});
|
|
1694
|
+
this.logger.debug('completed turn', {
|
|
1695
|
+
...turnContext,
|
|
1696
|
+
durationMs: Date.now() - turnStartedAt,
|
|
1697
|
+
replyLength: publicReplyText.length,
|
|
1698
|
+
controlKind: reply?.control?.kind ?? null,
|
|
1699
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
1700
|
+
delivery: 'post',
|
|
1701
|
+
});
|
|
1702
|
+
if (usePrivateCollaborationDraftProgress) {
|
|
1703
|
+
this.finalizeCollaborationDraft(channelId, activeTurn, posted.messageId, visibleReplyBody);
|
|
1704
|
+
}
|
|
1705
|
+
return {
|
|
1706
|
+
reply,
|
|
1707
|
+
deliveredMessageId: posted.messageId,
|
|
1708
|
+
publicReplyText,
|
|
1709
|
+
visibleReplyBody,
|
|
1710
|
+
stale: false,
|
|
1711
|
+
completed: true,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
this.logger.debug('completed turn', {
|
|
1715
|
+
...turnContext,
|
|
1716
|
+
durationMs: Date.now() - turnStartedAt,
|
|
1717
|
+
replyLength: publicReplyText.length,
|
|
1718
|
+
delivery: usePublicDraftProgress ? 'draft' : 'post',
|
|
1719
|
+
});
|
|
1720
|
+
if (usePrivateCollaborationDraftProgress && activeTurn?.turnExecutionId) {
|
|
1721
|
+
this.clearCollaborationDraft(channelId, {
|
|
1722
|
+
generation: activeTurn.generation,
|
|
1723
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
reply,
|
|
1728
|
+
deliveredMessageId: null,
|
|
1729
|
+
publicReplyText,
|
|
1730
|
+
visibleReplyBody,
|
|
1731
|
+
stale: false,
|
|
1732
|
+
completed: true,
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
catch (error) {
|
|
1736
|
+
await this.discardDraft(channelId);
|
|
1737
|
+
if (activeTurn?.turnExecutionId) {
|
|
1738
|
+
this.clearCollaborationDraft(channelId, {
|
|
1739
|
+
generation: activeTurn.generation,
|
|
1740
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
this.logger.debugError('failed to generate or send reply', {
|
|
1744
|
+
...turnContext,
|
|
1745
|
+
error,
|
|
1746
|
+
});
|
|
1747
|
+
this.logger.error('failed to generate or send reply', {
|
|
1748
|
+
...turnContext,
|
|
1749
|
+
error: summarizeError(error),
|
|
1750
|
+
});
|
|
1751
|
+
return { stale: activeTurn?.superseded === true, completed: false };
|
|
1752
|
+
}
|
|
1753
|
+
finally {
|
|
1754
|
+
stopTyping();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
canAcceptTurnOutput(channelId, generation) {
|
|
1758
|
+
if (!this.started || this.controlPlaneClosed) {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
if (generation == null) {
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
1765
|
+
return (activeTurn?.generation === generation &&
|
|
1766
|
+
!activeTurn.superseded &&
|
|
1767
|
+
activeTurn.turnExecutionId != null);
|
|
1768
|
+
}
|
|
1769
|
+
async canAcceptCompletedTurnOutput(channelId, activeTurn) {
|
|
1770
|
+
if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
if (!activeTurn?.protocol) {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
const channelState = this.collaborationChannels.get(channelId);
|
|
1777
|
+
if (!channelState?.protocol) {
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
if ((await this.canContinueSharedProtocol(channelId, channelState, channelState.protocol)) &&
|
|
1781
|
+
channelState.protocol.anchorMessageId === activeTurn.protocol.anchorMessageId) {
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
this.supersedeActiveTurn(channelId, activeTurn);
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
async canContinueSharedProtocol(channelId, channelState, protocolState) {
|
|
1788
|
+
const sharedStatus = await this.readSharedProtocolStatus(channelId, protocolState.anchorMessageId);
|
|
1789
|
+
if (sharedStatus?.active !== false) {
|
|
1790
|
+
return protocolState.active;
|
|
1791
|
+
}
|
|
1792
|
+
protocolState.active = false;
|
|
1793
|
+
protocolState.finished = sharedStatus.finished;
|
|
1794
|
+
channelState.queue = channelState.queue.filter((message) => message.kind !== 'protocol-turn');
|
|
1795
|
+
channelState.recentTurn = undefined;
|
|
1796
|
+
this.clearProtocolFallback(channelState);
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
enqueueProgressChannelTurn(channelId, task) {
|
|
1800
|
+
const previous = this.progressChannelQueues.get(channelId) ?? Promise.resolve();
|
|
1801
|
+
const next = previous
|
|
1802
|
+
.catch(() => { })
|
|
1803
|
+
.then(task)
|
|
1804
|
+
.finally(() => {
|
|
1805
|
+
if (this.progressChannelQueues.get(channelId) === next) {
|
|
1806
|
+
this.progressChannelQueues.delete(channelId);
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
this.progressChannelQueues.set(channelId, next);
|
|
1810
|
+
return next;
|
|
1811
|
+
}
|
|
1812
|
+
ensureProgressDraft(channelId, generation, creationMetadata) {
|
|
1813
|
+
let draft = this.progressDrafts.get(channelId);
|
|
1814
|
+
if (!draft) {
|
|
1815
|
+
draft = new DraftMessageController(this.borgee, channelId, () => !this.controlPlaneClosed && this.canAcceptTurnOutput(channelId, generation), this.logger, creationMetadata);
|
|
1816
|
+
this.progressDrafts.set(channelId, draft);
|
|
1817
|
+
}
|
|
1818
|
+
return draft;
|
|
1819
|
+
}
|
|
1820
|
+
publishCollaborationDraft(channelId, activeTurn, body) {
|
|
1821
|
+
if (!activeTurn?.turnExecutionId || !hasVisibleText(body)) {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const generation = activeTurn.generation;
|
|
1825
|
+
const existing = this.collaborationDrafts.get(channelId);
|
|
1826
|
+
if (existing &&
|
|
1827
|
+
(existing.generation !== generation || existing.turnExecutionId !== activeTurn.turnExecutionId)) {
|
|
1828
|
+
this.clearCollaborationDraft(channelId);
|
|
1829
|
+
}
|
|
1830
|
+
const next = this.collaborationDrafts.get(channelId);
|
|
1831
|
+
if (next?.clearTimer) {
|
|
1832
|
+
clearTimeout(next.clearTimer);
|
|
1833
|
+
next.clearTimer = null;
|
|
1834
|
+
}
|
|
1835
|
+
const snapshot = next?.snapshot ?? {
|
|
1836
|
+
draftId: randomUUID(),
|
|
1837
|
+
channelId,
|
|
1838
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1839
|
+
replyToId: activeTurn.delivery?.replyToId,
|
|
1840
|
+
body,
|
|
1841
|
+
status: 'streaming',
|
|
1842
|
+
updatedAt: Date.now(),
|
|
1843
|
+
};
|
|
1844
|
+
snapshot.body = body;
|
|
1845
|
+
snapshot.status = 'streaming';
|
|
1846
|
+
snapshot.updatedAt = Date.now();
|
|
1847
|
+
delete snapshot.finalMessageId;
|
|
1848
|
+
this.collaborationDrafts.set(channelId, {
|
|
1849
|
+
generation,
|
|
1850
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1851
|
+
snapshot,
|
|
1852
|
+
clearTimer: null,
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
finalizeCollaborationDraft(channelId, activeTurn, finalMessageId, finalBody) {
|
|
1856
|
+
if (!activeTurn?.turnExecutionId) {
|
|
1857
|
+
this.clearCollaborationDraft(channelId);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (!finalMessageId || !hasVisibleText(finalBody)) {
|
|
1861
|
+
this.clearCollaborationDraft(channelId, {
|
|
1862
|
+
generation: activeTurn.generation,
|
|
1863
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1864
|
+
});
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const draft = this.collaborationDrafts.get(channelId);
|
|
1868
|
+
if (!draft ||
|
|
1869
|
+
draft.generation !== activeTurn.generation ||
|
|
1870
|
+
draft.turnExecutionId !== activeTurn.turnExecutionId) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (draft.clearTimer) {
|
|
1874
|
+
clearTimeout(draft.clearTimer);
|
|
1875
|
+
}
|
|
1876
|
+
draft.snapshot.body = finalBody;
|
|
1877
|
+
draft.snapshot.status = 'finalized';
|
|
1878
|
+
draft.snapshot.updatedAt = Date.now();
|
|
1879
|
+
draft.snapshot.finalMessageId = finalMessageId;
|
|
1880
|
+
draft.clearTimer = setTimeout(() => {
|
|
1881
|
+
this.clearCollaborationDraft(channelId, {
|
|
1882
|
+
generation: activeTurn.generation,
|
|
1883
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1884
|
+
});
|
|
1885
|
+
}, COLLABORATION_LATE_SEND_GRACE_MS);
|
|
1886
|
+
}
|
|
1887
|
+
clearCollaborationDraft(channelId, expected) {
|
|
1888
|
+
const draft = this.collaborationDrafts.get(channelId);
|
|
1889
|
+
if (!draft) {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
if (expected &&
|
|
1893
|
+
(draft.generation !== expected.generation || draft.turnExecutionId !== expected.turnExecutionId)) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (draft.clearTimer) {
|
|
1897
|
+
clearTimeout(draft.clearTimer);
|
|
1898
|
+
}
|
|
1899
|
+
this.collaborationDrafts.delete(channelId);
|
|
1900
|
+
}
|
|
1901
|
+
readCollaborationDraft(input) {
|
|
1902
|
+
const draft = this.collaborationDrafts.get(input.channelId);
|
|
1903
|
+
if (!draft || draft.turnExecutionId !== input.turnExecutionId) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
return { ...draft.snapshot };
|
|
1907
|
+
}
|
|
1908
|
+
async discardDraft(channelId) {
|
|
1909
|
+
const draft = this.progressDrafts.get(channelId);
|
|
1910
|
+
if (!draft) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
this.progressDrafts.delete(channelId);
|
|
1914
|
+
await draft.discard().catch(() => { });
|
|
1915
|
+
}
|
|
1916
|
+
supersedeActiveTurn(channelId, activeTurn) {
|
|
1917
|
+
if (activeTurn.superseded) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (activeTurn.turnExecutionId) {
|
|
1921
|
+
this.clearCollaborationDraft(channelId, {
|
|
1922
|
+
generation: activeTurn.generation,
|
|
1923
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
activeTurn.superseded = true;
|
|
1927
|
+
activeTurn.turnExecutionId = null;
|
|
1928
|
+
void this.discardDraft(channelId);
|
|
1929
|
+
}
|
|
1930
|
+
invalidateChannelTurn(channelId) {
|
|
1931
|
+
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
1932
|
+
if (activeTurn) {
|
|
1933
|
+
activeTurn.turnExecutionId = null;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
rememberRecentTurn(channelState, activeTurn) {
|
|
1937
|
+
if (!activeTurn.turnExecutionId) {
|
|
1938
|
+
channelState.recentTurn = undefined;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
channelState.recentTurn = {
|
|
1942
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
1943
|
+
superseded: activeTurn.superseded,
|
|
1944
|
+
collaborationMessagesSent: activeTurn.collaborationMessagesSent,
|
|
1945
|
+
targetCooldowns: new Map(activeTurn.targetCooldowns),
|
|
1946
|
+
...(activeTurn.protocol ? { protocol: activeTurn.protocol } : {}),
|
|
1947
|
+
expiresAt: Date.now() + COLLABORATION_LATE_SEND_GRACE_MS,
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
resolveAuthorizedCollaborationTurn(channelState, turnExecutionId) {
|
|
1951
|
+
const activeTurn = channelState?.activeTurn;
|
|
1952
|
+
if (activeTurn &&
|
|
1953
|
+
!activeTurn.superseded &&
|
|
1954
|
+
activeTurn.turnExecutionId != null &&
|
|
1955
|
+
activeTurn.turnExecutionId === turnExecutionId) {
|
|
1956
|
+
return activeTurn;
|
|
1957
|
+
}
|
|
1958
|
+
const recentTurn = channelState?.recentTurn;
|
|
1959
|
+
if (!recentTurn) {
|
|
1960
|
+
return undefined;
|
|
1961
|
+
}
|
|
1962
|
+
if (recentTurn.expiresAt <= Date.now()) {
|
|
1963
|
+
channelState.recentTurn = undefined;
|
|
1964
|
+
return undefined;
|
|
1965
|
+
}
|
|
1966
|
+
if (!recentTurn.superseded && recentTurn.turnExecutionId === turnExecutionId) {
|
|
1967
|
+
return recentTurn;
|
|
1968
|
+
}
|
|
1969
|
+
return undefined;
|
|
1970
|
+
}
|
|
1971
|
+
updateAwaitingUserState(channelId, awaitingUser) {
|
|
1972
|
+
if (awaitingUser) {
|
|
1973
|
+
this.awaitingUserByChannel.set(channelId, awaitingUser);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1977
|
+
}
|
|
1978
|
+
resolveVisibleTurnBody(activeTurn, reply, publicReplyText) {
|
|
1979
|
+
if (!activeTurn?.protocol || !this.shouldRelayProtocolReply(activeTurn, reply)) {
|
|
1980
|
+
return publicReplyText;
|
|
1981
|
+
}
|
|
1982
|
+
return appendVisibleBodyMention(publicReplyText, activeTurn.protocol.targetPeerId);
|
|
1983
|
+
}
|
|
1984
|
+
shouldRelayProtocolReply(activeTurn, reply) {
|
|
1985
|
+
const protocol = activeTurn.protocol;
|
|
1986
|
+
return (protocol != null &&
|
|
1987
|
+
reply.control?.kind === 'continue-to-peer' &&
|
|
1988
|
+
protocol.turnIndex < protocol.maxTurnCount);
|
|
1989
|
+
}
|
|
1990
|
+
async normalizeMessage(msg) {
|
|
1991
|
+
this.logger.debug('received channel event', {
|
|
1992
|
+
eventType: msg.type,
|
|
1993
|
+
messageType: msg.message_type,
|
|
1994
|
+
provider: this.config.agent.provider,
|
|
1995
|
+
agentName: this.config.agent.agentName,
|
|
1996
|
+
channelId: msg.channel_id,
|
|
1997
|
+
messageId: msg.message_id,
|
|
1998
|
+
});
|
|
1999
|
+
if (!isReplyTriggeringEventType(msg.type)) {
|
|
2000
|
+
this.logger.debug('ignored non-triggering channel event', {
|
|
2001
|
+
eventType: msg.type,
|
|
2002
|
+
messageType: msg.message_type,
|
|
2003
|
+
channelId: msg.channel_id,
|
|
2004
|
+
messageId: msg.message_id,
|
|
2005
|
+
});
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2009
|
+
const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
|
|
2010
|
+
if (authorId === selfAgentId && msg.message_type !== 'task_assignment') {
|
|
2011
|
+
this.logger.debug('ignored self-authored message', {
|
|
2012
|
+
channelId: msg.channel_id,
|
|
2013
|
+
messageId: msg.message_id,
|
|
2014
|
+
});
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
const content = String(msg.content ?? msg.body ?? '').trim();
|
|
2018
|
+
if (!content) {
|
|
2019
|
+
this.logger.debug('ignored empty message', {
|
|
2020
|
+
channelId: msg.channel_id,
|
|
2021
|
+
messageId: msg.message_id,
|
|
2022
|
+
});
|
|
2023
|
+
return null;
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
authorId,
|
|
2027
|
+
content,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
async markAssignedTaskInProgress(threadId, content) {
|
|
2031
|
+
try {
|
|
2032
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2033
|
+
const task = await waitForTaskForThread(this.borgee, threadId, {
|
|
2034
|
+
preferredTaskId: extractTaskIdFromTaskAssignmentContent(content),
|
|
2035
|
+
attempts: 10,
|
|
2036
|
+
delayMs: 250,
|
|
2037
|
+
});
|
|
2038
|
+
if (!task || task.status !== 'open' || task.assigneeId !== selfAgentId) {
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
await this.borgee.updateTask({
|
|
2042
|
+
taskId: task.id,
|
|
2043
|
+
status: 'in_progress',
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
catch (error) {
|
|
2047
|
+
this.logger.debugError('best-effort task auto-start failed', {
|
|
2048
|
+
channelId: threadId,
|
|
2049
|
+
error,
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async ensureSelfAgentId() {
|
|
2054
|
+
if (this.selfAgentId) {
|
|
2055
|
+
return this.selfAgentId;
|
|
2056
|
+
}
|
|
2057
|
+
if (!this.selfAgentIdPromise) {
|
|
2058
|
+
this.selfAgentIdPromise = this.borgee
|
|
2059
|
+
.getMe()
|
|
2060
|
+
.then((me) => {
|
|
2061
|
+
this.selfAgentId = me.id;
|
|
2062
|
+
this.selfAgentProfile = me;
|
|
2063
|
+
this.participantKindsByUserId.set(me.id, 'agent');
|
|
2064
|
+
this.participantDirectoryByUserId.set(me.id, {
|
|
2065
|
+
id: me.id,
|
|
2066
|
+
display_name: me.display_name,
|
|
2067
|
+
kind: 'agent',
|
|
2068
|
+
});
|
|
2069
|
+
return me.id;
|
|
2070
|
+
})
|
|
2071
|
+
.catch((error) => {
|
|
2072
|
+
this.selfAgentIdPromise = null;
|
|
2073
|
+
throw error;
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return this.selfAgentIdPromise;
|
|
2077
|
+
}
|
|
2078
|
+
async classifyAuthor(authorId, forceRefreshOnMiss) {
|
|
2079
|
+
const cached = this.participantKindsByUserId.get(authorId);
|
|
2080
|
+
if (cached) {
|
|
2081
|
+
return cached === 'agent' ? 'agent' : 'human';
|
|
2082
|
+
}
|
|
2083
|
+
if (!this.participantDirectorySupported) {
|
|
2084
|
+
return this.inferAuthorKind(authorId);
|
|
2085
|
+
}
|
|
2086
|
+
const now = Date.now();
|
|
2087
|
+
const nextRefreshAt = this.participantRefreshBackoffByUserId.get(authorId) ?? 0;
|
|
2088
|
+
if (forceRefreshOnMiss || now >= nextRefreshAt) {
|
|
2089
|
+
await this.refreshParticipantDirectory();
|
|
2090
|
+
const refreshed = this.participantKindsByUserId.get(authorId);
|
|
2091
|
+
if (refreshed) {
|
|
2092
|
+
this.participantRefreshBackoffByUserId.delete(authorId);
|
|
2093
|
+
return refreshed === 'agent' ? 'agent' : 'human';
|
|
2094
|
+
}
|
|
2095
|
+
this.participantRefreshBackoffByUserId.set(authorId, now + BLOCKED_AUTHOR_REFRESH_BACKOFF_MS);
|
|
2096
|
+
}
|
|
2097
|
+
return 'unknown';
|
|
2098
|
+
}
|
|
2099
|
+
async refreshParticipantDirectory() {
|
|
2100
|
+
if (!this.participantDirectorySupported) {
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (!this.participantRefreshPromise) {
|
|
2104
|
+
this.participantRefreshPromise = this.borgee
|
|
2105
|
+
.listUsers()
|
|
2106
|
+
.then((users) => {
|
|
2107
|
+
const nextKinds = new Map();
|
|
2108
|
+
const nextDirectory = new Map();
|
|
2109
|
+
for (const user of users) {
|
|
2110
|
+
nextKinds.set(user.id, user.kind);
|
|
2111
|
+
nextDirectory.set(user.id, user);
|
|
2112
|
+
}
|
|
2113
|
+
if (this.selfAgentId) {
|
|
2114
|
+
nextKinds.set(this.selfAgentId, 'agent');
|
|
2115
|
+
nextDirectory.set(this.selfAgentId, {
|
|
2116
|
+
id: this.selfAgentId,
|
|
2117
|
+
display_name: this.selfAgentProfile?.display_name,
|
|
2118
|
+
kind: 'agent',
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
this.participantKindsByUserId.clear();
|
|
2122
|
+
this.participantDirectoryByUserId.clear();
|
|
2123
|
+
for (const [userId, kind] of nextKinds) {
|
|
2124
|
+
this.participantKindsByUserId.set(userId, kind);
|
|
2125
|
+
}
|
|
2126
|
+
for (const [userId, user] of nextDirectory) {
|
|
2127
|
+
this.participantDirectoryByUserId.set(userId, user);
|
|
2128
|
+
}
|
|
2129
|
+
})
|
|
2130
|
+
.catch((error) => {
|
|
2131
|
+
if (this.isUnsupportedSemanticOperation(error, 'list_users')) {
|
|
2132
|
+
this.participantDirectorySupported = false;
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
this.logger.error('failed to refresh participant directory', {
|
|
2136
|
+
error: summarizeError(error),
|
|
2137
|
+
});
|
|
2138
|
+
})
|
|
2139
|
+
.finally(() => {
|
|
2140
|
+
this.participantRefreshPromise = null;
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
await this.participantRefreshPromise;
|
|
2144
|
+
}
|
|
2145
|
+
async classifyBlockedWakeAuthor(authorId, blockedState) {
|
|
2146
|
+
void blockedState;
|
|
2147
|
+
return this.classifyAuthor(authorId, true);
|
|
2148
|
+
}
|
|
2149
|
+
async classifyQueuedBlockedWakeCandidate(message, blockedState) {
|
|
2150
|
+
void blockedState;
|
|
2151
|
+
return this.classifyAuthor(message.authorId, true);
|
|
2152
|
+
}
|
|
2153
|
+
inferAuthorKind(authorId) {
|
|
2154
|
+
if (this.selfAgentId === authorId) {
|
|
2155
|
+
return 'agent';
|
|
2156
|
+
}
|
|
2157
|
+
if (authorId.includes('@')) {
|
|
2158
|
+
return 'human';
|
|
2159
|
+
}
|
|
2160
|
+
if (authorId.startsWith('user-') || authorId.startsWith('human-')) {
|
|
2161
|
+
return 'human';
|
|
2162
|
+
}
|
|
2163
|
+
if (authorId.startsWith('agent-')) {
|
|
2164
|
+
return 'agent';
|
|
2165
|
+
}
|
|
2166
|
+
return 'unknown';
|
|
2167
|
+
}
|
|
2168
|
+
async buildCollaborationGrounding(authorId, incomingAuthorKind, activeTurn, kickoff) {
|
|
2169
|
+
if (!activeTurn?.protocol && !kickoff) {
|
|
2170
|
+
return undefined;
|
|
2171
|
+
}
|
|
2172
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2173
|
+
const peerId = activeTurn?.protocol?.targetPeerId ??
|
|
2174
|
+
kickoff?.participantIds.find((participantId) => participantId !== selfAgentId);
|
|
2175
|
+
const participantIds = kickoff?.participantIds ?? (peerId ? [selfAgentId, peerId] : [selfAgentId]);
|
|
2176
|
+
await this.hydratePromptIdentityCache([selfAgentId, authorId, ...participantIds]);
|
|
2177
|
+
return {
|
|
2178
|
+
self: this.resolvePromptIdentity(selfAgentId, 'agent'),
|
|
2179
|
+
incomingAuthor: this.resolvePromptIdentity(authorId, incomingAuthorKind ?? 'unknown'),
|
|
2180
|
+
...(peerId ? { peer: this.resolvePromptIdentity(peerId, 'agent') } : {}),
|
|
2181
|
+
participants: participantIds.map((participantId) => this.resolvePromptIdentity(participantId, 'agent')),
|
|
2182
|
+
...(kickoff ? { hostRecognizedKickoffCandidate: true } : {}),
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
async hydratePromptIdentityCache(userIds) {
|
|
2186
|
+
if (!this.participantDirectorySupported) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const unresolved = [...new Set(userIds)].filter((userId) => !this.participantDirectoryByUserId.has(userId));
|
|
2190
|
+
if (unresolved.length === 0) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
await this.refreshParticipantDirectory();
|
|
2194
|
+
}
|
|
2195
|
+
resolvePromptIdentity(userId, fallbackKind) {
|
|
2196
|
+
const directoryUser = this.participantDirectoryByUserId.get(userId);
|
|
2197
|
+
if (this.selfAgentId === userId) {
|
|
2198
|
+
return {
|
|
2199
|
+
id: userId,
|
|
2200
|
+
displayName: this.selfAgentProfile?.display_name ??
|
|
2201
|
+
directoryUser?.display_name ??
|
|
2202
|
+
this.config.agent.agentName,
|
|
2203
|
+
kind: 'agent',
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
return {
|
|
2207
|
+
id: userId,
|
|
2208
|
+
displayName: directoryUser?.display_name,
|
|
2209
|
+
kind: (directoryUser?.kind === 'user' ? 'human' : directoryUser?.kind) ?? fallbackKind,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
isUnsupportedSemanticOperation(error, op) {
|
|
2213
|
+
if (!error || typeof error !== 'object') {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
const code = 'code' in error ? error.code : undefined;
|
|
2217
|
+
const message = 'message' in error ? error.message : undefined;
|
|
2218
|
+
return (code === 'bpp.semantic_op_unknown' && typeof message === 'string' && message.includes(op));
|
|
2219
|
+
}
|
|
2220
|
+
authorizeCollaborationSend(input) {
|
|
2221
|
+
if (!this.collaborationEnabled) {
|
|
2222
|
+
return { ok: false, statusCode: 403, error: 'collaboration_not_enabled' };
|
|
2223
|
+
}
|
|
2224
|
+
const channelState = this.collaborationChannels.get(input.channelId);
|
|
2225
|
+
const authorizedTurn = this.resolveAuthorizedCollaborationTurn(channelState, input.turnExecutionId);
|
|
2226
|
+
if (!authorizedTurn) {
|
|
2227
|
+
return { ok: false, statusCode: 409, error: 'stale_turn_execution_id' };
|
|
2228
|
+
}
|
|
2229
|
+
if (authorizedTurn.protocol) {
|
|
2230
|
+
return { ok: false, statusCode: 403, error: 'protocol_managed_turn' };
|
|
2231
|
+
}
|
|
2232
|
+
if (authorizedTurn.collaborationMessagesSent >= COLLABORATION_SENDS_PER_TURN) {
|
|
2233
|
+
return { ok: false, statusCode: 429, error: 'collaboration_quota_exceeded' };
|
|
2234
|
+
}
|
|
2235
|
+
const targetKey = [
|
|
2236
|
+
input.replyToId ? `reply:${input.replyToId}` : '',
|
|
2237
|
+
input.mentions.length > 0 ? `mentions:${[...new Set(input.mentions)].sort().join(',')}` : '',
|
|
2238
|
+
]
|
|
2239
|
+
.filter((value) => value.length > 0)
|
|
2240
|
+
.join('|');
|
|
2241
|
+
const existingCooldownUntil = authorizedTurn.targetCooldowns.get(targetKey) ?? 0;
|
|
2242
|
+
if (existingCooldownUntil > Date.now()) {
|
|
2243
|
+
return { ok: false, statusCode: 429, error: 'collaboration_target_cooldown' };
|
|
2244
|
+
}
|
|
2245
|
+
const cooldownUntil = Date.now() + LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS;
|
|
2246
|
+
authorizedTurn.collaborationMessagesSent += 1;
|
|
2247
|
+
authorizedTurn.targetCooldowns.set(targetKey, cooldownUntil);
|
|
2248
|
+
let settled = false;
|
|
2249
|
+
return {
|
|
2250
|
+
ok: true,
|
|
2251
|
+
statusCode: 200,
|
|
2252
|
+
commit: () => {
|
|
2253
|
+
settled = true;
|
|
2254
|
+
},
|
|
2255
|
+
rollback: () => {
|
|
2256
|
+
if (settled) {
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
settled = true;
|
|
2260
|
+
authorizedTurn.collaborationMessagesSent = Math.max(0, authorizedTurn.collaborationMessagesSent - 1);
|
|
2261
|
+
if (authorizedTurn.targetCooldowns.get(targetKey) === cooldownUntil) {
|
|
2262
|
+
authorizedTurn.targetCooldowns.delete(targetKey);
|
|
2263
|
+
}
|
|
2264
|
+
},
|
|
2265
|
+
};
|
|
308
2266
|
}
|
|
309
2267
|
}
|