verybot 0.1.3
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/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { loadConfig, injectSecretsIntoEnv } from "../config.js";
|
|
2
|
+
import { INTEGRATIONS_DIR, MEMORY_DB_PATH } from "../paths.js";
|
|
3
|
+
import { Session } from "./session.js";
|
|
4
|
+
import { SessionStore } from "./session-store.js";
|
|
5
|
+
import { MessageQueue } from "./queue.js";
|
|
6
|
+
import { buildSystemPrompt } from "./context.js";
|
|
7
|
+
import { runLoop } from "./loop.js";
|
|
8
|
+
import { getModel } from "./providers.js";
|
|
9
|
+
import { adaptTools } from "./mcp-adapter.js";
|
|
10
|
+
import { resolveModelDef } from "../config/model-catalog.js";
|
|
11
|
+
import { isContextOverflowError, estimateTokens, estimateStringTokens, compactSchedulerRuns, SCHEDULED_TASK_PREFIX } from "./compaction.js";
|
|
12
|
+
import { DEFAULT_SAFE_BINS } from "../security/command-validator.js";
|
|
13
|
+
import { IntegrationRegistry } from "../integrations/registry.js";
|
|
14
|
+
import { ChannelStore } from "./channel-store.js";
|
|
15
|
+
import { DelegationStore } from "./delegation-store.js";
|
|
16
|
+
import { TeamRegistry, parseModel } from "./agent-registry.js";
|
|
17
|
+
import { DEFAULT_TEAM_ID } from "../config/agent-config.js";
|
|
18
|
+
import { buildChannelSpecs } from "../channels/specs.js";
|
|
19
|
+
import { logger } from "../logger.js";
|
|
20
|
+
import { emit, on } from "../events.js";
|
|
21
|
+
import { buildSessionKey, parseSessionKey, deriveMemoryTeamId } from "./session-key.js";
|
|
22
|
+
import { setsEqual, friendlyError } from "./utils.js";
|
|
23
|
+
import { SessionStateMap } from "./session-state.js";
|
|
24
|
+
import { MemoryExtractor } from "./memory-extractor.js";
|
|
25
|
+
import { WorkerCoordinator } from "./worker-coordinator.js";
|
|
26
|
+
import { buildRunTools } from "./run-tools.js";
|
|
27
|
+
/** Max compaction retries on context overflow before re-throwing. */
|
|
28
|
+
const MAX_COMPACTION_RETRIES = 3;
|
|
29
|
+
/** Skip compaction when estimated total tokens are below this fraction of the context window. */
|
|
30
|
+
const COMPACTION_SKIP_THRESHOLD = 0.6;
|
|
31
|
+
/** Session key suffix that identifies the team's shared scheduler session. */
|
|
32
|
+
const SCHEDULER_SESSION_SUFFIX = ":scheduler:main";
|
|
33
|
+
export class Agent {
|
|
34
|
+
sessions = new SessionStateMap();
|
|
35
|
+
sessionStore;
|
|
36
|
+
queue;
|
|
37
|
+
model;
|
|
38
|
+
modelId;
|
|
39
|
+
identity;
|
|
40
|
+
language;
|
|
41
|
+
tools;
|
|
42
|
+
modelDef;
|
|
43
|
+
memoryStore;
|
|
44
|
+
embeddingProvider;
|
|
45
|
+
memoryMaxResults;
|
|
46
|
+
config;
|
|
47
|
+
configStore;
|
|
48
|
+
sandbox;
|
|
49
|
+
skillManager;
|
|
50
|
+
integrationRegistry;
|
|
51
|
+
scheduleStore;
|
|
52
|
+
taskStore;
|
|
53
|
+
desktopAdapter;
|
|
54
|
+
browserManager;
|
|
55
|
+
teamRegistry = null;
|
|
56
|
+
delegationStore;
|
|
57
|
+
channelStore;
|
|
58
|
+
teamStore;
|
|
59
|
+
channelManager;
|
|
60
|
+
connectedChannels;
|
|
61
|
+
lastConfigMtime = null;
|
|
62
|
+
teamRegistryDirty = false;
|
|
63
|
+
newSessionPending = false;
|
|
64
|
+
unsubTeamChange = null;
|
|
65
|
+
memoryExtractor = null;
|
|
66
|
+
workerCoordinator;
|
|
67
|
+
/** Cleanup functions for HTTP MCP servers (keyed by session key). */
|
|
68
|
+
mcpCleanups = new Map();
|
|
69
|
+
constructor(deps) {
|
|
70
|
+
this.tools = deps.tools;
|
|
71
|
+
this.sessionStore = new SessionStore(deps.dataDir);
|
|
72
|
+
this.memoryStore = deps.memoryStore ?? null;
|
|
73
|
+
this.embeddingProvider = deps.embeddingProvider ?? null;
|
|
74
|
+
this.config = deps.config;
|
|
75
|
+
this.configStore = deps.configStore;
|
|
76
|
+
this.sandbox = deps.sandbox ?? null;
|
|
77
|
+
this.skillManager = deps.skillManager ?? { systemPrompt: "", readTool: null };
|
|
78
|
+
this.integrationRegistry = deps.integrationRegistry ?? new IntegrationRegistry();
|
|
79
|
+
this.scheduleStore = deps.scheduleStore ?? null;
|
|
80
|
+
this.taskStore = deps.taskStore ?? null;
|
|
81
|
+
this.desktopAdapter = deps.desktopAdapter ?? null;
|
|
82
|
+
this.browserManager = deps.browserManager ?? null;
|
|
83
|
+
this.delegationStore = deps.delegationStore ?? null;
|
|
84
|
+
this.channelStore = deps.channelStore ?? null;
|
|
85
|
+
this.teamStore = deps.teamStore ?? null;
|
|
86
|
+
this.channelManager = deps.channelManager ?? null;
|
|
87
|
+
this.connectedChannels = deps.connectedChannels ?? null;
|
|
88
|
+
// Apply config synchronously so fields are initialized immediately (no `!` assertions)
|
|
89
|
+
this.model = getModel(deps.config.model.provider, deps.config.model.id);
|
|
90
|
+
this.modelId = deps.config.model.id;
|
|
91
|
+
this.modelDef = resolveModelDef(deps.config.model.id, deps.config.model.contextWindow);
|
|
92
|
+
this.identity = deps.config.identity;
|
|
93
|
+
this.language = deps.config.language;
|
|
94
|
+
this.memoryMaxResults = deps.config.memory.maxResults;
|
|
95
|
+
if (this.memoryStore) {
|
|
96
|
+
this.memoryExtractor = new MemoryExtractor(this.model, this.memoryStore, this.embeddingProvider);
|
|
97
|
+
}
|
|
98
|
+
// Build team registry eagerly so getTeams() works before first message
|
|
99
|
+
this.rebuildTeamRegistry();
|
|
100
|
+
// Mark dirty when teams change so next main() call rebuilds the registry
|
|
101
|
+
this.unsubTeamChange = on("teamChange", () => { this.teamRegistryDirty = true; });
|
|
102
|
+
this.queue = new MessageQueue({
|
|
103
|
+
mode: "collect",
|
|
104
|
+
processMessage: (sessionKey, text, _signal) => this.main(sessionKey, text),
|
|
105
|
+
});
|
|
106
|
+
this.workerCoordinator = new WorkerCoordinator(this.sessions, this.queue);
|
|
107
|
+
}
|
|
108
|
+
/** Force an immediate config reload + channel reconciliation. */
|
|
109
|
+
async forceConfigReload() {
|
|
110
|
+
this.lastConfigMtime = null;
|
|
111
|
+
await this.reloadConfig();
|
|
112
|
+
}
|
|
113
|
+
getSession(key) {
|
|
114
|
+
return this.sessions.get(key)?.session;
|
|
115
|
+
}
|
|
116
|
+
getStore() {
|
|
117
|
+
return this.sessionStore;
|
|
118
|
+
}
|
|
119
|
+
/** Called from channels (Telegram, Discord, etc.) */
|
|
120
|
+
async handleMessage(msg, channel, agentId) {
|
|
121
|
+
const sessionKey = buildSessionKey(agentId
|
|
122
|
+
? (this.teamRegistry?.resolveTeam(agentId)?.teamId ?? DEFAULT_TEAM_ID)
|
|
123
|
+
: (msg.teamId ?? DEFAULT_TEAM_ID), msg.channelType, msg.channelId);
|
|
124
|
+
try {
|
|
125
|
+
const teamId = agentId
|
|
126
|
+
? (this.teamRegistry?.resolveTeam(agentId)?.teamId ?? DEFAULT_TEAM_ID)
|
|
127
|
+
: (msg.teamId ?? DEFAULT_TEAM_ID);
|
|
128
|
+
const text = msg.text ?? "";
|
|
129
|
+
// Eagerly create session so agentId + replyCallback are set before the queue runs
|
|
130
|
+
const state = await this.getOrCreateSession(sessionKey);
|
|
131
|
+
state.teamId = teamId;
|
|
132
|
+
state.channelType = msg.channelType;
|
|
133
|
+
state.channelId = msg.channelId;
|
|
134
|
+
this.applyAgentBinding(state, sessionKey, agentId);
|
|
135
|
+
if (!state.replyCallback) {
|
|
136
|
+
state.replyCallback = (reply) => this.deliverReply(msg, channel, reply);
|
|
137
|
+
}
|
|
138
|
+
const reply = await this.queue.enqueue(sessionKey, text);
|
|
139
|
+
if (reply)
|
|
140
|
+
await this.deliverReply(msg, channel, reply);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
logger.error(`Agent error [${this.sessionLabel(sessionKey)}]: ${err}`);
|
|
144
|
+
const userMsg = friendlyError(err);
|
|
145
|
+
emit("chat", {
|
|
146
|
+
sessionKey,
|
|
147
|
+
state: "final",
|
|
148
|
+
message: { role: "assistant", content: userMsg },
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
await channel.send(msg.channelId, userMsg);
|
|
152
|
+
}
|
|
153
|
+
catch (sendErr) {
|
|
154
|
+
logger.error(`Failed to send error to user [${this.sessionLabel(sessionKey)}]: ${sendErr}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Deliver reply as text or voice based on TTS reply mode. */
|
|
159
|
+
async deliverReply(msg, channel, reply) {
|
|
160
|
+
const { replyMode } = this.config.tts;
|
|
161
|
+
const shouldVoice = this.config.tts.enabled &&
|
|
162
|
+
channel.sendVoice &&
|
|
163
|
+
(replyMode === "voice" || (replyMode === "inbound" && msg.isVoice));
|
|
164
|
+
if (shouldVoice) {
|
|
165
|
+
try {
|
|
166
|
+
const { synthesize } = await import("../tts/edge.js");
|
|
167
|
+
const audioPath = await synthesize(reply);
|
|
168
|
+
await channel.sendVoice(msg.channelId, audioPath);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
logger.error(`Voice reply failed, falling back to text: ${err instanceof Error ? err.message : err}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await channel.send(msg.channelId, reply);
|
|
175
|
+
}
|
|
176
|
+
/** Called from gateway RPC (WebSocket UI) */
|
|
177
|
+
async handleGatewayMessage(sessionKey, text, agentId) {
|
|
178
|
+
try {
|
|
179
|
+
const state = await this.getOrCreateSession(sessionKey);
|
|
180
|
+
// Team ID is encoded in the session key (teamId:gateway:random) — extract it
|
|
181
|
+
// directly instead of reverse-resolving from orchestrator ID (which can collide).
|
|
182
|
+
const colonIdx = sessionKey.indexOf(":");
|
|
183
|
+
const keyTeamId = colonIdx > 0 ? sessionKey.slice(0, colonIdx) : "";
|
|
184
|
+
state.teamId = keyTeamId || DEFAULT_TEAM_ID;
|
|
185
|
+
state.channelType = "gateway";
|
|
186
|
+
state.channelId = sessionKey;
|
|
187
|
+
this.applyAgentBinding(state, sessionKey, agentId);
|
|
188
|
+
return await this.queue.enqueue(sessionKey, text);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
logger.error(`Gateway error [${this.sessionLabel(sessionKey)}]: ${err}`);
|
|
192
|
+
const errorReply = friendlyError(err);
|
|
193
|
+
// Emit a "chat" final event so the WebSocket UI renders the error
|
|
194
|
+
// instead of hanging on a loading state.
|
|
195
|
+
emit("chat", {
|
|
196
|
+
sessionKey,
|
|
197
|
+
agentId,
|
|
198
|
+
state: "final",
|
|
199
|
+
message: { role: "assistant", content: errorReply },
|
|
200
|
+
});
|
|
201
|
+
return errorReply;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Validate and bind an agentId to an existing session state. */
|
|
205
|
+
applyAgentBinding(state, sessionKey, agentId) {
|
|
206
|
+
if (!agentId)
|
|
207
|
+
return;
|
|
208
|
+
// Validate using the team ID already set on the state (derived from session key),
|
|
209
|
+
// not via resolveTeam(agentId) which can collide across teams.
|
|
210
|
+
if (this.teamRegistry && state.teamId) {
|
|
211
|
+
const registry = this.teamRegistry.getTeamRegistry(state.teamId);
|
|
212
|
+
if (!registry) {
|
|
213
|
+
logger.warn(`[${this.sessionLabel(sessionKey)}] Unknown team "${this.teamLabel(state.teamId)}" — ignoring agentId`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Verify agentId actually matches this team's orchestrator
|
|
217
|
+
if (registry.getOrchestrator().id !== agentId) {
|
|
218
|
+
logger.warn(`[${this.sessionLabel(sessionKey)}] agentId "${agentId}" does not match team "${this.teamLabel(state.teamId)}" orchestrator — ignoring`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (state.agentId && state.agentId !== agentId) {
|
|
223
|
+
logger.warn(`[${this.sessionLabel(sessionKey)}] Ignoring agentId change from "${state.agentId}" to "${agentId}" — clear session first`);
|
|
224
|
+
}
|
|
225
|
+
else if (!state.agentId) {
|
|
226
|
+
state.agentId = agentId;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Return teams for the UI picker. Reads from TeamStore for immediate visibility. */
|
|
230
|
+
getTeams() {
|
|
231
|
+
const defaultTeam = {
|
|
232
|
+
id: DEFAULT_TEAM_ID,
|
|
233
|
+
name: "Default",
|
|
234
|
+
color: "",
|
|
235
|
+
orchestratorId: "main",
|
|
236
|
+
orchestratorIdentity: this.identity,
|
|
237
|
+
orchestratorModel: this.modelId,
|
|
238
|
+
workerCount: 0,
|
|
239
|
+
};
|
|
240
|
+
if (!this.teamStore)
|
|
241
|
+
return [defaultTeam];
|
|
242
|
+
const teamConfigs = this.teamStore.toTeamConfigs();
|
|
243
|
+
const dbTeams = teamConfigs.map((t) => ({
|
|
244
|
+
id: t.id,
|
|
245
|
+
name: t.name ?? t.id,
|
|
246
|
+
color: t.color ?? "",
|
|
247
|
+
orchestratorId: t.orchestrator.id,
|
|
248
|
+
orchestratorIdentity: t.orchestrator.identity,
|
|
249
|
+
orchestratorModel: parseModel(t.orchestrator.model).modelId,
|
|
250
|
+
workerCount: t.workers.length,
|
|
251
|
+
}));
|
|
252
|
+
return [defaultTeam, ...dbTeams];
|
|
253
|
+
}
|
|
254
|
+
/** Abort current run for a session. */
|
|
255
|
+
abortSession(sessionKey) {
|
|
256
|
+
return this.queue.abort(sessionKey);
|
|
257
|
+
}
|
|
258
|
+
/** Clear conversation history (memories are preserved across clears). */
|
|
259
|
+
async clearSession(sessionKey) {
|
|
260
|
+
logger.info(`Clearing session ${sessionKey}`);
|
|
261
|
+
// Extract any remaining facts in background — don't block the clear
|
|
262
|
+
const state = this.sessions.get(sessionKey);
|
|
263
|
+
if (this.memoryExtractor && state && state.messagesSinceExtraction > 0) {
|
|
264
|
+
const messages = state.session.getMessages();
|
|
265
|
+
this.memoryExtractor.extractAndSaveFacts(sessionKey, messages, deriveMemoryTeamId(sessionKey)).catch((err) => {
|
|
266
|
+
logger.warn(`Pre-clear extraction failed: ${err instanceof Error ? err.message : err}`);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
this.sessions.delete(sessionKey);
|
|
270
|
+
this.queue.deleteLane(sessionKey);
|
|
271
|
+
await this.sessionStore.clear(sessionKey);
|
|
272
|
+
// Shut down any HTTP MCP server for this session
|
|
273
|
+
const cleanup = this.mcpCleanups.get(sessionKey);
|
|
274
|
+
if (cleanup) {
|
|
275
|
+
this.mcpCleanups.delete(sessionKey);
|
|
276
|
+
await cleanup();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Run a scheduled task in a persistent scheduler session. Returns the raw LLM reply.
|
|
281
|
+
* Serialized through the message queue to avoid races with human messages.
|
|
282
|
+
* The caller (Scheduler) handles [SKIP] detection and channel delivery.
|
|
283
|
+
*/
|
|
284
|
+
async runScheduledTask(opts) {
|
|
285
|
+
const schedulerKey = `${opts.teamId}${SCHEDULER_SESSION_SUFFIX}`;
|
|
286
|
+
const state = await this.getOrCreateSession(schedulerKey);
|
|
287
|
+
state.teamId = opts.teamId;
|
|
288
|
+
state.channelType = "scheduler";
|
|
289
|
+
state.channelId = "main";
|
|
290
|
+
// Store integrations for this scheduled run so main() can pick them up
|
|
291
|
+
state.scheduledIntegrations = opts.integrations;
|
|
292
|
+
return this.queue.enqueue(schedulerKey, `${SCHEDULED_TASK_PREFIX} ${opts.prompt}`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Handle a human message sent to the scheduler session.
|
|
296
|
+
* Serialized via the message queue to avoid overlap with scheduled task executions.
|
|
297
|
+
*/
|
|
298
|
+
async handleSchedulerMessage(teamId, text, senderInfo) {
|
|
299
|
+
const schedulerKey = `${teamId}${SCHEDULER_SESSION_SUFFIX}`;
|
|
300
|
+
const state = await this.getOrCreateSession(schedulerKey);
|
|
301
|
+
state.teamId = teamId;
|
|
302
|
+
state.channelType = "scheduler";
|
|
303
|
+
state.channelId = "main";
|
|
304
|
+
const prefixed = senderInfo ? `[${senderInfo}] ${text}` : text;
|
|
305
|
+
return this.queue.enqueue(schedulerKey, prefixed);
|
|
306
|
+
}
|
|
307
|
+
/** Get the scheduler session for a team (for history display). */
|
|
308
|
+
getSchedulerSession(teamId) {
|
|
309
|
+
const schedulerKey = `${teamId}${SCHEDULER_SESSION_SUFFIX}`;
|
|
310
|
+
return this.sessions.get(schedulerKey)?.session;
|
|
311
|
+
}
|
|
312
|
+
/** Start initial channels based on current config. Called once at boot. */
|
|
313
|
+
async initChannels() {
|
|
314
|
+
await this.reconcileChannels(this.config);
|
|
315
|
+
}
|
|
316
|
+
/** Clean up sandbox containers. */
|
|
317
|
+
cleanupSandbox() {
|
|
318
|
+
this.sandbox?.cleanupAll();
|
|
319
|
+
}
|
|
320
|
+
/** Extract remaining facts from all active sessions (call before shutdown). */
|
|
321
|
+
async flushMemories() {
|
|
322
|
+
if (this.memoryExtractor) {
|
|
323
|
+
await this.memoryExtractor.flushAll(this.sessions);
|
|
324
|
+
}
|
|
325
|
+
this.workerCoordinator.clearAllTimers();
|
|
326
|
+
this.unsubTeamChange?.();
|
|
327
|
+
// Shut down all HTTP MCP servers
|
|
328
|
+
const cleanups = [...this.mcpCleanups.values()];
|
|
329
|
+
this.mcpCleanups.clear();
|
|
330
|
+
await Promise.allSettled(cleanups.map((fn) => fn()));
|
|
331
|
+
}
|
|
332
|
+
async getOrCreateSession(key) {
|
|
333
|
+
const existing = this.sessions.get(key);
|
|
334
|
+
if (existing)
|
|
335
|
+
return existing;
|
|
336
|
+
this.newSessionPending = true;
|
|
337
|
+
const session = await this.sessionStore.load(key) ?? new Session(key);
|
|
338
|
+
const state = this.sessions.getOrCreate(key, session);
|
|
339
|
+
// Seed token estimate from restored messages so compaction skip check is accurate
|
|
340
|
+
if (session.messageCount > 0) {
|
|
341
|
+
state.estimatedMsgTokens = estimateTokens(session.getMessages());
|
|
342
|
+
}
|
|
343
|
+
return state;
|
|
344
|
+
}
|
|
345
|
+
/** Build tools, adapt for MCP providers, and run the inference loop. */
|
|
346
|
+
async buildAdaptAndRun(opts) {
|
|
347
|
+
const label = this.sessionLabel(opts.sessionKey);
|
|
348
|
+
const deps = this.buildRunToolsDeps(opts.teamScopedRegistry, `${opts.runProvider}:${opts.runModelId}`);
|
|
349
|
+
let tools = buildRunTools(deps, opts.sessionKey, opts.activeIntegrations, opts.channelInfo, opts.effectiveAgentId, opts.taskTeamId, opts.scheduleTeamId, label);
|
|
350
|
+
// Clean up any previous MCP server, then adapt for MCP-based providers
|
|
351
|
+
const prevCleanup = this.mcpCleanups.get(opts.sessionKey);
|
|
352
|
+
if (prevCleanup) {
|
|
353
|
+
this.mcpCleanups.delete(opts.sessionKey);
|
|
354
|
+
await prevCleanup();
|
|
355
|
+
}
|
|
356
|
+
const adapted = await adaptTools(opts.runProvider, opts.runModelId, opts.runModel, tools, {
|
|
357
|
+
sandboxEnabled: !!this.sandbox,
|
|
358
|
+
});
|
|
359
|
+
if (adapted.cleanup)
|
|
360
|
+
this.mcpCleanups.set(opts.sessionKey, adapted.cleanup);
|
|
361
|
+
tools = adapted.tools;
|
|
362
|
+
return this.runLoopWithRetry(opts.session, opts.system, tools, opts.sessionKey, adapted.model, opts.contextWindow, opts.maxSteps);
|
|
363
|
+
}
|
|
364
|
+
/** Build the RunToolsDeps from current agent state (agentRegistry filled per-call). */
|
|
365
|
+
buildRunToolsDeps(agentRegistry, effectiveModel) {
|
|
366
|
+
return {
|
|
367
|
+
baseTools: this.tools,
|
|
368
|
+
config: this.config,
|
|
369
|
+
memoryStore: this.memoryStore,
|
|
370
|
+
embeddingProvider: this.embeddingProvider,
|
|
371
|
+
memoryMaxResults: this.memoryMaxResults,
|
|
372
|
+
sandbox: this.sandbox,
|
|
373
|
+
skillManager: this.skillManager,
|
|
374
|
+
integrationRegistry: this.integrationRegistry,
|
|
375
|
+
scheduleStore: this.scheduleStore,
|
|
376
|
+
taskStore: this.taskStore,
|
|
377
|
+
teamStore: this.teamStore,
|
|
378
|
+
desktopAdapter: this.desktopAdapter,
|
|
379
|
+
browserManager: this.browserManager,
|
|
380
|
+
effectiveModel: effectiveModel ?? `${this.config.model.provider}:${this.config.model.id}`,
|
|
381
|
+
agentRegistry,
|
|
382
|
+
delegationStore: this.delegationStore,
|
|
383
|
+
channelStore: this.channelStore,
|
|
384
|
+
channelManager: this.channelManager,
|
|
385
|
+
sessionStore: this.sessionStore,
|
|
386
|
+
modelId: this.modelId,
|
|
387
|
+
onWorkerComplete: this.workerCoordinator.onWorkerComplete.bind(this.workerCoordinator),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/** Return all tool names available in the current config (for UI discovery). */
|
|
391
|
+
getToolNames() {
|
|
392
|
+
const deps = this.buildRunToolsDeps(null);
|
|
393
|
+
const tools = buildRunTools(deps, "__discovery__", new Set());
|
|
394
|
+
return Object.keys(tools);
|
|
395
|
+
}
|
|
396
|
+
/** Run the loop with compaction retries on context overflow. */
|
|
397
|
+
async runLoopWithRetry(session, system, tools, sessionKey, model, contextWindow, maxSteps) {
|
|
398
|
+
const effectiveModel = model ?? this.model;
|
|
399
|
+
const effectiveContextWindow = contextWindow ?? this.modelDef.contextWindow;
|
|
400
|
+
const effectiveMaxSteps = maxSteps && maxSteps > 0 ? maxSteps : this.config.model.maxSteps;
|
|
401
|
+
for (let attempt = 1; attempt <= MAX_COMPACTION_RETRIES + 1; attempt++) {
|
|
402
|
+
try {
|
|
403
|
+
const { text } = await runLoop({
|
|
404
|
+
model: effectiveModel,
|
|
405
|
+
system,
|
|
406
|
+
messages: session.getMessages(),
|
|
407
|
+
tools,
|
|
408
|
+
sessionKey,
|
|
409
|
+
sessionLabel: this.sessionLabel(sessionKey),
|
|
410
|
+
maxSteps: effectiveMaxSteps,
|
|
411
|
+
});
|
|
412
|
+
return text;
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
if (!isContextOverflowError(err) || attempt > MAX_COMPACTION_RETRIES) {
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
logger.warn(`Context overflow on attempt ${attempt}/${MAX_COMPACTION_RETRIES}, forcing compaction`);
|
|
419
|
+
const compacted = await session.compact(effectiveModel, effectiveContextWindow, system);
|
|
420
|
+
if (!compacted) {
|
|
421
|
+
logger.warn("Compaction had no effect, emergency truncating to last 10 messages");
|
|
422
|
+
session.truncate(10);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
throw new Error("Unreachable: all compaction retries exhausted");
|
|
427
|
+
}
|
|
428
|
+
/** (Re)build team registry from TeamStore (or skip if no store). */
|
|
429
|
+
rebuildTeamRegistry() {
|
|
430
|
+
if (!this.teamStore) {
|
|
431
|
+
this.teamRegistry = null;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const teamConfigs = this.teamStore.toTeamConfigs();
|
|
435
|
+
if (teamConfigs.length === 0) {
|
|
436
|
+
this.teamRegistry = null;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const hasMultipleTeams = teamConfigs.length > 1;
|
|
440
|
+
const hasWorkers = teamConfigs.some((t) => t.workers.length > 0);
|
|
441
|
+
if (hasMultipleTeams || hasWorkers) {
|
|
442
|
+
this.teamRegistry = new TeamRegistry(teamConfigs, {
|
|
443
|
+
memoryStore: this.memoryStore,
|
|
444
|
+
embeddingProvider: this.embeddingProvider,
|
|
445
|
+
baseTools: this.tools,
|
|
446
|
+
config: this.config,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
this.teamRegistry = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/** Reload config from disk so API key / model changes take effect immediately. */
|
|
454
|
+
async reloadConfig() {
|
|
455
|
+
// Skip reload if config file hasn't changed (stat is ~microseconds vs full parse + rebuild)
|
|
456
|
+
const mtime = this.configStore.mtime();
|
|
457
|
+
if (mtime !== null && mtime === this.lastConfigMtime)
|
|
458
|
+
return;
|
|
459
|
+
this.lastConfigMtime = mtime;
|
|
460
|
+
logger.info(`Reloading config`);
|
|
461
|
+
injectSecretsIntoEnv(this.configStore);
|
|
462
|
+
const fresh = loadConfig(this.configStore);
|
|
463
|
+
this.model = getModel(fresh.model.provider, fresh.model.id);
|
|
464
|
+
this.modelId = fresh.model.id;
|
|
465
|
+
this.modelDef = resolveModelDef(fresh.model.id, fresh.model.contextWindow);
|
|
466
|
+
this.identity = fresh.identity;
|
|
467
|
+
this.language = fresh.language;
|
|
468
|
+
this.memoryMaxResults = fresh.memory.maxResults;
|
|
469
|
+
this.config = fresh;
|
|
470
|
+
// Update memory extractor model
|
|
471
|
+
this.memoryExtractor?.setModel(this.model);
|
|
472
|
+
// Push browser config changes so next launch uses fresh values
|
|
473
|
+
await this.browserManager?.updateConfig({
|
|
474
|
+
headless: fresh.browserHeadless,
|
|
475
|
+
userAgent: fresh.browserUserAgent || undefined,
|
|
476
|
+
});
|
|
477
|
+
// Hot-reload channels in background (non-blocking)
|
|
478
|
+
this.reconcileChannels(fresh).catch((err) => {
|
|
479
|
+
logger.error(`Channel reconcile failed: ${err instanceof Error ? err.message : err}`);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Build channel specs from config and reconcile with ChannelManager.
|
|
484
|
+
* Handlers use dynamic orchestrator lookup (reads this.config at call time)
|
|
485
|
+
* so orchestrator-only changes don't require a channel restart.
|
|
486
|
+
*/
|
|
487
|
+
async reconcileChannels(config) {
|
|
488
|
+
if (!this.channelManager)
|
|
489
|
+
return;
|
|
490
|
+
const specs = buildChannelSpecs(config, {
|
|
491
|
+
onMessage: (msg, ch) => this.handleMessage(msg, ch),
|
|
492
|
+
onClear: (channelType, channelId, teamId) => this.clearSession(buildSessionKey(teamId ?? DEFAULT_TEAM_ID, channelType, channelId)),
|
|
493
|
+
listTeams: () => this.getTeams().map((t) => ({ id: t.id, name: t.name })),
|
|
494
|
+
connectedChannels: this.connectedChannels,
|
|
495
|
+
getChannelTableConfig: (channel) => this.config.channels[channel]
|
|
496
|
+
?.markdown?.tables,
|
|
497
|
+
});
|
|
498
|
+
await this.channelManager.reconcile(specs);
|
|
499
|
+
}
|
|
500
|
+
/** Resolve a human-readable team label for logs/console (name preferred, falls back to ID). */
|
|
501
|
+
teamLabel(teamId) {
|
|
502
|
+
const team = this.teamStore?.getTeamById(teamId);
|
|
503
|
+
return team?.name ?? teamId;
|
|
504
|
+
}
|
|
505
|
+
/** Build a human-readable session label for logs: replaces team UUID with team name. */
|
|
506
|
+
sessionLabel(sessionKey) {
|
|
507
|
+
const colonIdx = sessionKey.indexOf(":");
|
|
508
|
+
if (colonIdx <= 0)
|
|
509
|
+
return sessionKey;
|
|
510
|
+
const teamId = sessionKey.slice(0, colonIdx);
|
|
511
|
+
return this.teamLabel(teamId) + sessionKey.slice(colonIdx);
|
|
512
|
+
}
|
|
513
|
+
/** Enrich session index entry with team/agent name metadata for UI display. */
|
|
514
|
+
enrichSessionMetadata(sessionKey, teamId) {
|
|
515
|
+
const meta = { teamId };
|
|
516
|
+
if (this.teamStore) {
|
|
517
|
+
const team = this.teamStore.getTeamById(teamId);
|
|
518
|
+
if (team)
|
|
519
|
+
meta.teamName = team.name;
|
|
520
|
+
const parsed = parseSessionKey(sessionKey);
|
|
521
|
+
meta.channelType = parsed.channelType;
|
|
522
|
+
if (parsed.isWorker && parsed.agentName) {
|
|
523
|
+
meta.agentName = parsed.agentName;
|
|
524
|
+
const agent = this.teamStore.getAgentByName(teamId, parsed.agentName);
|
|
525
|
+
if (agent)
|
|
526
|
+
meta.agentId = agent.id;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
this.sessionStore.updateMetadata(sessionKey, meta);
|
|
530
|
+
}
|
|
531
|
+
async main(sessionKey, text) {
|
|
532
|
+
await this.reloadConfig();
|
|
533
|
+
// Rebuild team registry when teams were modified or a new session started
|
|
534
|
+
if (this.teamRegistryDirty || this.newSessionPending) {
|
|
535
|
+
this.teamRegistryDirty = false;
|
|
536
|
+
this.newSessionPending = false;
|
|
537
|
+
this.rebuildTeamRegistry();
|
|
538
|
+
if (this.teamRegistry) {
|
|
539
|
+
if (!this.delegationStore) {
|
|
540
|
+
this.delegationStore = await DelegationStore.create(MEMORY_DB_PATH);
|
|
541
|
+
}
|
|
542
|
+
if (!this.channelStore) {
|
|
543
|
+
this.channelStore = await ChannelStore.create(MEMORY_DB_PATH);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Session is guaranteed to exist (created eagerly in handleMessage/handleGatewayMessage)
|
|
548
|
+
const state = this.sessions.get(sessionKey);
|
|
549
|
+
if (!state) {
|
|
550
|
+
logger.warn(`[${this.sessionLabel(sessionKey)}] Session was cleared mid-queue, skipping`);
|
|
551
|
+
return "";
|
|
552
|
+
}
|
|
553
|
+
const { session } = state;
|
|
554
|
+
// Resolve per-session orchestrator via team registry
|
|
555
|
+
const effectiveAgentId = state.agentId;
|
|
556
|
+
const effectiveTeamId = state.teamId ?? DEFAULT_TEAM_ID;
|
|
557
|
+
let runModel = this.model;
|
|
558
|
+
let runModelDef = this.modelDef;
|
|
559
|
+
let runIdentity = this.identity;
|
|
560
|
+
let runModelId = this.modelId;
|
|
561
|
+
let runProvider = this.config.model.provider;
|
|
562
|
+
let runMaxSteps = 0; // 0 = inherit global default
|
|
563
|
+
let teamScopedRegistry = null;
|
|
564
|
+
if (this.teamRegistry) {
|
|
565
|
+
// Resolve by team ID (source of truth from session key), not orchestrator ID
|
|
566
|
+
const registry = this.teamRegistry.getTeamRegistry(effectiveTeamId);
|
|
567
|
+
if (registry) {
|
|
568
|
+
teamScopedRegistry = registry;
|
|
569
|
+
const resolved = registry.resolveOrchestrator();
|
|
570
|
+
runModel = resolved.model;
|
|
571
|
+
runModelDef = resolved.modelDef;
|
|
572
|
+
runIdentity = resolved.agentConfig.identity;
|
|
573
|
+
const parsed = parseModel(resolved.agentConfig.model);
|
|
574
|
+
runModelId = parsed.modelId;
|
|
575
|
+
runProvider = parsed.provider;
|
|
576
|
+
runMaxSteps = resolved.agentConfig.maxSteps;
|
|
577
|
+
logger.info(`[${this.sessionLabel(sessionKey)}] Using team "${this.teamLabel(effectiveTeamId)}" orchestrator "${resolved.agentConfig.id}"`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Track delegation depth via worker coordinator
|
|
581
|
+
this.workerCoordinator.trackDelegationDepth(sessionKey, text);
|
|
582
|
+
// Detect scheduler session from key pattern
|
|
583
|
+
const isSchedulerSession = sessionKey.endsWith(SCHEDULER_SESSION_SUFFIX);
|
|
584
|
+
const isScheduledTask = text.startsWith(SCHEDULED_TASK_PREFIX);
|
|
585
|
+
// For scheduled tasks: compact previous runs before appending
|
|
586
|
+
if (isSchedulerSession && isScheduledTask) {
|
|
587
|
+
compactSchedulerRuns(session);
|
|
588
|
+
}
|
|
589
|
+
session.append({ role: "user", content: text });
|
|
590
|
+
state.estimatedMsgTokens += estimateStringTokens(text);
|
|
591
|
+
// Re-discover integrations at the start of each new session.
|
|
592
|
+
if (!state.integrations) {
|
|
593
|
+
await this.integrationRegistry.refresh(this.configStore, INTEGRATIONS_DIR);
|
|
594
|
+
state.integrations = new Set();
|
|
595
|
+
}
|
|
596
|
+
// For scheduled tasks, merge in the scheduled integrations then consume them
|
|
597
|
+
let activeIntegrations = state.integrations;
|
|
598
|
+
if (isScheduledTask && state.scheduledIntegrations) {
|
|
599
|
+
const merged = new Set(activeIntegrations);
|
|
600
|
+
for (const name of state.scheduledIntegrations) {
|
|
601
|
+
if (this.integrationRegistry.has(name))
|
|
602
|
+
merged.add(name);
|
|
603
|
+
}
|
|
604
|
+
activeIntegrations = merged;
|
|
605
|
+
state.scheduledIntegrations = undefined;
|
|
606
|
+
}
|
|
607
|
+
// Build system prompt first so compaction can account for its token cost
|
|
608
|
+
const system = buildSystemPrompt({
|
|
609
|
+
identity: runIdentity,
|
|
610
|
+
modelId: runModelId,
|
|
611
|
+
language: this.language,
|
|
612
|
+
hasMemory: this.memoryStore !== null,
|
|
613
|
+
bashMode: this.config.bash.security,
|
|
614
|
+
bashSafeBins: [...DEFAULT_SAFE_BINS, ...this.config.bash.safeBins],
|
|
615
|
+
hasDesktop: this.config.desktop.enabled,
|
|
616
|
+
skillListing: this.skillManager.systemPrompt,
|
|
617
|
+
integrationListing: this.integrationRegistry.buildListing(activeIntegrations),
|
|
618
|
+
activeIntegrationPrompts: this.integrationRegistry.getPromptsFor(activeIntegrations),
|
|
619
|
+
hasScheduler: this.scheduleStore !== null,
|
|
620
|
+
scheduledTask: isScheduledTask,
|
|
621
|
+
schedulerSession: isSchedulerSession,
|
|
622
|
+
hasTTS: this.config.tts.enabled,
|
|
623
|
+
channelType: state.channelType,
|
|
624
|
+
delegationWorkers: teamScopedRegistry
|
|
625
|
+
? teamScopedRegistry.delegatableWorkers().map((name) => {
|
|
626
|
+
const w = teamScopedRegistry.getWorker(name);
|
|
627
|
+
return { name, identity: w?.identity ?? "" };
|
|
628
|
+
})
|
|
629
|
+
: undefined,
|
|
630
|
+
});
|
|
631
|
+
// Skip compaction when clearly under budget (avoids O(n) token scan on every message)
|
|
632
|
+
const sysTokens = estimateStringTokens(system);
|
|
633
|
+
if (state.estimatedMsgTokens + sysTokens > runModelDef.contextWindow * COMPACTION_SKIP_THRESHOLD) {
|
|
634
|
+
const compacted = await session.compact(runModel, runModelDef.contextWindow, system);
|
|
635
|
+
if (compacted)
|
|
636
|
+
state.estimatedMsgTokens = estimateTokens(session.getMessages());
|
|
637
|
+
}
|
|
638
|
+
// Snapshot active integrations before the run to detect mid-run changes
|
|
639
|
+
const activeBefore = new Set(activeIntegrations);
|
|
640
|
+
// Build per-run tools, adapt for MCP providers, and execute the loop
|
|
641
|
+
// Task tools: default team sees all tasks; non-default teams are scoped to their own tasks
|
|
642
|
+
// Schedule tools: always pass effectiveTeamId so schedules work for all teams
|
|
643
|
+
const runOpts = {
|
|
644
|
+
session,
|
|
645
|
+
sessionKey,
|
|
646
|
+
system,
|
|
647
|
+
teamScopedRegistry,
|
|
648
|
+
activeIntegrations,
|
|
649
|
+
channelInfo: { channelType: state.channelType ?? "unknown", channelId: state.channelId ?? sessionKey },
|
|
650
|
+
effectiveAgentId,
|
|
651
|
+
taskTeamId: effectiveTeamId !== DEFAULT_TEAM_ID ? effectiveTeamId : undefined,
|
|
652
|
+
scheduleTeamId: effectiveTeamId,
|
|
653
|
+
runProvider,
|
|
654
|
+
runModelId,
|
|
655
|
+
runModel,
|
|
656
|
+
contextWindow: runModelDef.contextWindow,
|
|
657
|
+
maxSteps: runMaxSteps,
|
|
658
|
+
};
|
|
659
|
+
let reply = await this.buildAdaptAndRun(runOpts);
|
|
660
|
+
// If integrations were enabled/disabled during the run, re-run with updated tools
|
|
661
|
+
if (!setsEqual(activeIntegrations, activeBefore)) {
|
|
662
|
+
logger.info(`[${this.sessionLabel(sessionKey)}] Integrations changed mid-run, re-running with updated tools`);
|
|
663
|
+
reply = await this.buildAdaptAndRun(runOpts);
|
|
664
|
+
}
|
|
665
|
+
session.append({ role: "assistant", content: reply });
|
|
666
|
+
// Refresh running token estimate (used by compaction skip check on next message)
|
|
667
|
+
const msgTokens = estimateTokens(session.getMessages());
|
|
668
|
+
state.estimatedMsgTokens = msgTokens;
|
|
669
|
+
logger.info(`[${this.sessionLabel(sessionKey)}] Context: ~${msgTokens + sysTokens} tokens (system: ${sysTokens}, messages: ${msgTokens}, budget: ${runModelDef.contextWindow})`);
|
|
670
|
+
await this.sessionStore.save(session);
|
|
671
|
+
// Enrich session metadata with team/agent names for UI display
|
|
672
|
+
this.enrichSessionMetadata(sessionKey, effectiveTeamId);
|
|
673
|
+
// Track and maybe extract facts via memory extractor
|
|
674
|
+
if (this.memoryExtractor) {
|
|
675
|
+
const memoryTeamId = effectiveTeamId !== DEFAULT_TEAM_ID ? effectiveTeamId : undefined;
|
|
676
|
+
this.memoryExtractor.trackAndMaybeExtract(sessionKey, this.sessions, session.getMessages(), memoryTeamId);
|
|
677
|
+
}
|
|
678
|
+
return reply;
|
|
679
|
+
}
|
|
680
|
+
}
|