let-them-talk 5.2.5 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7214
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
package/api-agents.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// API Agent Engine — registration, heartbeat, canonical message polling, provider dispatch
|
|
2
|
+
// API agents run inside the dashboard process and poll canonical branch/channel projections for requests
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const { OllamaProvider } = require('./providers/ollama');
|
|
9
|
+
const { DalleProvider } = require('./providers/dalle');
|
|
10
|
+
const { ReplicateProvider } = require('./providers/replicate');
|
|
11
|
+
const { GeminiProvider } = require('./providers/gemini');
|
|
12
|
+
const { ComfyUIProvider } = require('./providers/comfyui');
|
|
13
|
+
const { ZaiProvider } = require('./providers/zai');
|
|
14
|
+
const {
|
|
15
|
+
PROVIDER_COLORS,
|
|
16
|
+
createApiAgentRuntimeDescriptor,
|
|
17
|
+
resolveAgentRuntimeMetadata,
|
|
18
|
+
validateExplicitRuntimeDescriptor,
|
|
19
|
+
} = require('./runtime-descriptor');
|
|
20
|
+
const { createCanonicalState } = require('./state/canonical');
|
|
21
|
+
|
|
22
|
+
const PROVIDERS = {
|
|
23
|
+
ollama: OllamaProvider,
|
|
24
|
+
dalle: DalleProvider,
|
|
25
|
+
replicate: ReplicateProvider,
|
|
26
|
+
gemini: GeminiProvider,
|
|
27
|
+
comfyui: ComfyUIProvider,
|
|
28
|
+
zai: ZaiProvider,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class ApiAgentEngine {
|
|
32
|
+
constructor(dataDir) {
|
|
33
|
+
this.dataDir = dataDir;
|
|
34
|
+
this.agents = {}; // name -> { config, provider, pollInterval, running, stats }
|
|
35
|
+
this._configFile = path.join(dataDir, 'api-agents.json');
|
|
36
|
+
this._mediaFile = path.join(dataDir, 'media.jsonl');
|
|
37
|
+
this._mediaDir = path.join(dataDir, 'media');
|
|
38
|
+
this._canonicalState = createCanonicalState({ dataDir, processPid: process.pid });
|
|
39
|
+
this._loadConfigs();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_loadConfigs() {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(this._configFile)) {
|
|
45
|
+
const configs = JSON.parse(fs.readFileSync(this._configFile, 'utf8'));
|
|
46
|
+
for (const cfg of configs) {
|
|
47
|
+
const normalizedConfig = this._normalizeAgentConfig(cfg.name, cfg);
|
|
48
|
+
this.agents[cfg.name] = this._createAgentState(normalizedConfig);
|
|
49
|
+
this._registerInAgentsJson(cfg.name, normalizedConfig.provider);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_saveConfigs() {
|
|
56
|
+
const configs = Object.values(this.agents).map(a => a.config);
|
|
57
|
+
fs.writeFileSync(this._configFile, JSON.stringify(configs, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_createProvider(config) {
|
|
61
|
+
const ProviderClass = PROVIDERS[config.provider];
|
|
62
|
+
if (!ProviderClass) return null;
|
|
63
|
+
return new ProviderClass({
|
|
64
|
+
endpoint: config.endpoint,
|
|
65
|
+
model: config.model,
|
|
66
|
+
apiKey: config.apiKey,
|
|
67
|
+
...config.options,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_createAgentState(config) {
|
|
72
|
+
return {
|
|
73
|
+
config,
|
|
74
|
+
provider: this._createProvider(config),
|
|
75
|
+
pollInterval: null,
|
|
76
|
+
heartbeatInterval: null,
|
|
77
|
+
running: false,
|
|
78
|
+
stats: { requests: 0, completed: 0, errors: 0, lastActivity: null },
|
|
79
|
+
lastReadOffset: 0,
|
|
80
|
+
seenMessageIds: new Set(),
|
|
81
|
+
branchSessions: {},
|
|
82
|
+
activeBranches: new Set(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_normalizeAgentConfig(name, config = {}) {
|
|
87
|
+
const normalized = {
|
|
88
|
+
...config,
|
|
89
|
+
options: config.options || {},
|
|
90
|
+
};
|
|
91
|
+
const descriptor = createApiAgentRuntimeDescriptor({
|
|
92
|
+
name,
|
|
93
|
+
provider_id: normalized.provider,
|
|
94
|
+
model_id: normalized.model,
|
|
95
|
+
capabilities: normalized.capabilities,
|
|
96
|
+
});
|
|
97
|
+
const validation = validateExplicitRuntimeDescriptor(descriptor);
|
|
98
|
+
|
|
99
|
+
if (!validation.valid) return normalized;
|
|
100
|
+
|
|
101
|
+
normalized.provider = descriptor.provider_id;
|
|
102
|
+
normalized.model = descriptor.model_id;
|
|
103
|
+
normalized.capabilities = descriptor.capabilities;
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_getAgentRuntimeMetadata(name) {
|
|
108
|
+
const agent = this.agents[name];
|
|
109
|
+
const config = agent ? this._normalizeAgentConfig(name, agent.config) : null;
|
|
110
|
+
if (agent && config !== agent.config) agent.config = config;
|
|
111
|
+
|
|
112
|
+
return resolveAgentRuntimeMetadata({
|
|
113
|
+
name,
|
|
114
|
+
is_api_agent: true,
|
|
115
|
+
runtime_type: 'api',
|
|
116
|
+
provider_id: config ? config.provider : null,
|
|
117
|
+
model_id: config ? config.model : null,
|
|
118
|
+
capabilities: config ? config.capabilities : null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Register a new API agent
|
|
123
|
+
create(name, provider, options = {}) {
|
|
124
|
+
if (!name || !/^[a-zA-Z0-9_-]{1,20}$/.test(name)) {
|
|
125
|
+
return { error: 'Invalid name (1-20 alphanumeric/underscore/dash)' };
|
|
126
|
+
}
|
|
127
|
+
if (this.agents[name]) {
|
|
128
|
+
return { error: 'API agent already exists: ' + name };
|
|
129
|
+
}
|
|
130
|
+
if (!PROVIDERS[provider]) {
|
|
131
|
+
return { error: 'Unknown provider: ' + provider + '. Available: ' + Object.keys(PROVIDERS).join(', ') };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const config = this._normalizeAgentConfig(name, {
|
|
135
|
+
name,
|
|
136
|
+
provider,
|
|
137
|
+
model: options.model || 'sdxl',
|
|
138
|
+
capabilities: options.capabilities,
|
|
139
|
+
endpoint: options.endpoint || 'http://localhost:11434',
|
|
140
|
+
apiKey: options.apiKey || '',
|
|
141
|
+
options: options.providerOptions || {},
|
|
142
|
+
created: new Date().toISOString(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const descriptor = createApiAgentRuntimeDescriptor({
|
|
146
|
+
name,
|
|
147
|
+
provider_id: config.provider,
|
|
148
|
+
model_id: config.model,
|
|
149
|
+
capabilities: config.capabilities,
|
|
150
|
+
});
|
|
151
|
+
const validation = validateExplicitRuntimeDescriptor(descriptor);
|
|
152
|
+
if (!validation.valid) {
|
|
153
|
+
return { error: 'Invalid API agent runtime descriptor: ' + validation.errors.join('; ') };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.agents[name] = this._createAgentState(config);
|
|
157
|
+
|
|
158
|
+
this._saveConfigs();
|
|
159
|
+
this._registerInAgentsJson(name, provider);
|
|
160
|
+
return { ok: true, name, provider };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Register API agent in agents.json so it appears in the dashboard + 3D Hub
|
|
164
|
+
_registerInAgentsJson(name, provider) {
|
|
165
|
+
const runtimeMetadata = this._getAgentRuntimeMetadata(name);
|
|
166
|
+
this._canonicalState.registerApiAgent({
|
|
167
|
+
name,
|
|
168
|
+
agent: {
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
last_activity: new Date().toISOString(),
|
|
171
|
+
status: 'sleeping',
|
|
172
|
+
role: 'api-agent',
|
|
173
|
+
is_api_agent: true,
|
|
174
|
+
runtime_type: runtimeMetadata.runtime_type,
|
|
175
|
+
provider_id: runtimeMetadata.provider_id,
|
|
176
|
+
model_id: runtimeMetadata.model_id,
|
|
177
|
+
capabilities: runtimeMetadata.capabilities,
|
|
178
|
+
provider: runtimeMetadata.provider || provider,
|
|
179
|
+
provider_color: runtimeMetadata.provider_color || PROVIDER_COLORS[provider] || '#666',
|
|
180
|
+
bot_capability: runtimeMetadata.bot_capability,
|
|
181
|
+
},
|
|
182
|
+
profile: {
|
|
183
|
+
display_name: name,
|
|
184
|
+
role: 'api-agent',
|
|
185
|
+
bio: `${provider} API agent — generates media on request`,
|
|
186
|
+
avatar: 'robot',
|
|
187
|
+
is_api_agent: true,
|
|
188
|
+
provider: runtimeMetadata.provider || provider,
|
|
189
|
+
},
|
|
190
|
+
createProfileIfMissing: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Remove API agent from agents.json
|
|
195
|
+
_unregisterFromAgentsJson(name) {
|
|
196
|
+
this._canonicalState.unregisterApiAgent(name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Delete an API agent
|
|
200
|
+
remove(name) {
|
|
201
|
+
if (!this.agents[name]) return { error: 'API agent not found: ' + name };
|
|
202
|
+
this.stop(name);
|
|
203
|
+
this._unregisterFromAgentsJson(name);
|
|
204
|
+
delete this.agents[name];
|
|
205
|
+
this._saveConfigs();
|
|
206
|
+
return { ok: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Start polling for messages
|
|
210
|
+
start(name) {
|
|
211
|
+
const agent = this.agents[name];
|
|
212
|
+
if (!agent) return { error: 'API agent not found: ' + name };
|
|
213
|
+
if (agent.running) return { ok: true, message: 'Already running' };
|
|
214
|
+
|
|
215
|
+
agent.running = true;
|
|
216
|
+
this._primeSeenMessages(name);
|
|
217
|
+
this._updateAgentStatus(name, 'active');
|
|
218
|
+
|
|
219
|
+
// Poll every 2 seconds
|
|
220
|
+
agent.pollInterval = setInterval(() => {
|
|
221
|
+
this._pollMessages(name);
|
|
222
|
+
}, 2000);
|
|
223
|
+
|
|
224
|
+
// Heartbeat every 10 seconds
|
|
225
|
+
agent.heartbeatInterval = setInterval(() => {
|
|
226
|
+
this._updateHeartbeat(name);
|
|
227
|
+
}, 10000);
|
|
228
|
+
|
|
229
|
+
return { ok: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Stop polling
|
|
233
|
+
stop(name) {
|
|
234
|
+
const agent = this.agents[name];
|
|
235
|
+
if (!agent) return { error: 'API agent not found: ' + name };
|
|
236
|
+
|
|
237
|
+
agent.running = false;
|
|
238
|
+
if (agent.pollInterval) { clearInterval(agent.pollInterval); agent.pollInterval = null; }
|
|
239
|
+
if (agent.heartbeatInterval) { clearInterval(agent.heartbeatInterval); agent.heartbeatInterval = null; }
|
|
240
|
+
this._interruptAgentSessions(name);
|
|
241
|
+
this._updateAgentStatus(name, 'sleeping');
|
|
242
|
+
return { ok: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// List all API agents with status
|
|
246
|
+
list() {
|
|
247
|
+
return Object.values(this.agents).map(a => {
|
|
248
|
+
const runtimeMetadata = this._getAgentRuntimeMetadata(a.config.name);
|
|
249
|
+
return {
|
|
250
|
+
...runtimeMetadata,
|
|
251
|
+
name: a.config.name,
|
|
252
|
+
provider: a.config.provider,
|
|
253
|
+
model: a.config.model,
|
|
254
|
+
endpoint: a.config.endpoint,
|
|
255
|
+
hasApiKey: !!a.config.apiKey,
|
|
256
|
+
running: a.running,
|
|
257
|
+
stats: a.stats,
|
|
258
|
+
color: runtimeMetadata.provider_color || PROVIDER_COLORS[a.config.provider] || '#666',
|
|
259
|
+
created: a.config.created,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Poll branch-scoped conversation history for new messages addressed to this API agent
|
|
265
|
+
_pollMessages(name) {
|
|
266
|
+
const agent = this.agents[name];
|
|
267
|
+
if (!agent || !agent.running) return;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const pendingMessages = this._collectPendingMessages(name);
|
|
271
|
+
for (const entry of pendingMessages) {
|
|
272
|
+
Promise.resolve(this._processMessage(name, entry.message, { branch: entry.branch }))
|
|
273
|
+
.then((handled) => {
|
|
274
|
+
if (handled) {
|
|
275
|
+
this._markConsumedMessage(name, entry.branch, entry.message.id);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
agent.seenMessageIds.delete(entry.message.id);
|
|
280
|
+
})
|
|
281
|
+
.catch(() => {
|
|
282
|
+
agent.seenMessageIds.delete(entry.message.id);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_listKnownBranches() {
|
|
289
|
+
try {
|
|
290
|
+
const branches = this._canonicalState.listMarkdownBranches();
|
|
291
|
+
const names = Array.isArray(branches)
|
|
292
|
+
? branches.map((entry) => entry && entry.branch).filter(Boolean)
|
|
293
|
+
: [];
|
|
294
|
+
return names.length > 0 ? names : ['main'];
|
|
295
|
+
} catch {
|
|
296
|
+
return ['main'];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_getConversationMessages(branch) {
|
|
301
|
+
try {
|
|
302
|
+
return this._canonicalState.getConversationMessages({ branch });
|
|
303
|
+
} catch {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_primeSeenMessages(name) {
|
|
309
|
+
const agent = this.agents[name];
|
|
310
|
+
if (!agent) return;
|
|
311
|
+
|
|
312
|
+
const seenMessageIds = new Set();
|
|
313
|
+
for (const branch of this._listKnownBranches()) {
|
|
314
|
+
const consumedIds = this._canonicalState.readConsumedMessageIds(name, { branch });
|
|
315
|
+
for (const messageId of consumedIds) seenMessageIds.add(messageId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
agent.seenMessageIds = seenMessageIds;
|
|
319
|
+
agent.lastReadOffset = seenMessageIds.size;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_readConsumedMessages(name, branch) {
|
|
323
|
+
return this._canonicalState.readConsumedMessageIds(name, { branch });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_markConsumedMessage(name, branch, messageId) {
|
|
327
|
+
const agent = this.agents[name];
|
|
328
|
+
if (!agent || !messageId) return;
|
|
329
|
+
|
|
330
|
+
const consumedIds = this._readConsumedMessages(name, branch);
|
|
331
|
+
consumedIds.add(messageId);
|
|
332
|
+
this._canonicalState.writeConsumedMessageIds(name, consumedIds, { branch });
|
|
333
|
+
this._markSeenMessage(agent, messageId);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_markSeenMessage(agent, messageId) {
|
|
337
|
+
if (!agent || !messageId) return;
|
|
338
|
+
agent.seenMessageIds.add(messageId);
|
|
339
|
+
agent.lastReadOffset = agent.seenMessageIds.size;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_collectPendingMessages(name) {
|
|
343
|
+
const agent = this.agents[name];
|
|
344
|
+
if (!agent) return [];
|
|
345
|
+
|
|
346
|
+
const pending = [];
|
|
347
|
+
for (const branch of this._listKnownBranches()) {
|
|
348
|
+
const consumedIds = this._readConsumedMessages(name, branch);
|
|
349
|
+
for (const message of this._getConversationMessages(branch)) {
|
|
350
|
+
if (!message || !message.id || agent.seenMessageIds.has(message.id) || consumedIds.has(message.id)) continue;
|
|
351
|
+
if (message.to !== name || !message.content) continue;
|
|
352
|
+
agent.seenMessageIds.add(message.id);
|
|
353
|
+
pending.push({ branch, message });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
pending.sort((left, right) => {
|
|
358
|
+
const leftTime = Date.parse(left.message.timestamp || '') || 0;
|
|
359
|
+
const rightTime = Date.parse(right.message.timestamp || '') || 0;
|
|
360
|
+
if (leftTime !== rightTime) return leftTime - rightTime;
|
|
361
|
+
return String(left.message.id).localeCompare(String(right.message.id));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return pending;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_ensureAgentBranchSession(name, branch) {
|
|
368
|
+
const agent = this.agents[name];
|
|
369
|
+
if (!agent) return null;
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const activation = this._canonicalState.ensureAgentSession({
|
|
373
|
+
agentName: name,
|
|
374
|
+
branchName: branch,
|
|
375
|
+
sessionId: agent.branchSessions[branch] || null,
|
|
376
|
+
provider: agent.config.provider,
|
|
377
|
+
reason: 'api_agent_message',
|
|
378
|
+
});
|
|
379
|
+
const session = activation && activation.session ? activation.session : null;
|
|
380
|
+
if (session && session.session_id) {
|
|
381
|
+
agent.branchSessions[branch] = session.session_id;
|
|
382
|
+
agent.activeBranches.add(branch);
|
|
383
|
+
}
|
|
384
|
+
try { this._canonicalState.updateAgentBranch(name, branch); } catch {}
|
|
385
|
+
return session;
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_interruptAgentSessions(name) {
|
|
392
|
+
const agent = this.agents[name];
|
|
393
|
+
if (!agent) return;
|
|
394
|
+
|
|
395
|
+
for (const branch of agent.activeBranches) {
|
|
396
|
+
try {
|
|
397
|
+
this._canonicalState.transitionLatestSessionForAgent({
|
|
398
|
+
agentName: name,
|
|
399
|
+
branchName: branch,
|
|
400
|
+
state: 'interrupted',
|
|
401
|
+
reason: 'api_agent_stop',
|
|
402
|
+
});
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
agent.activeBranches = new Set();
|
|
407
|
+
agent.branchSessions = {};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_buildReplyContext(msg, options = {}) {
|
|
411
|
+
const branch = options.branch || 'main';
|
|
412
|
+
const channel = msg && msg.channel && msg.channel !== 'general' ? msg.channel : null;
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
branch,
|
|
416
|
+
channel,
|
|
417
|
+
replyTo: msg && msg.id ? msg.id : null,
|
|
418
|
+
threadId: msg && (msg.thread_id || msg.id) ? (msg.thread_id || msg.id) : null,
|
|
419
|
+
sessionId: options.sessionId || null,
|
|
420
|
+
commandId: msg && msg.command_id ? msg.command_id : null,
|
|
421
|
+
causationId: msg && msg.id ? msg.id : null,
|
|
422
|
+
correlationId: msg && (msg.correlation_id || msg.command_id || msg.thread_id || msg.id)
|
|
423
|
+
? (msg.correlation_id || msg.command_id || msg.thread_id || msg.id)
|
|
424
|
+
: null,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Process an incoming message
|
|
429
|
+
async _processMessage(name, msg, context = {}) {
|
|
430
|
+
const agent = this.agents[name];
|
|
431
|
+
if (!agent || !agent.provider) return false;
|
|
432
|
+
|
|
433
|
+
const branch = context.branch || 'main';
|
|
434
|
+
const session = this._ensureAgentBranchSession(name, branch);
|
|
435
|
+
const replyContext = this._buildReplyContext(msg, {
|
|
436
|
+
branch,
|
|
437
|
+
sessionId: session && session.session_id ? session.session_id : null,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
agent.stats.requests++;
|
|
441
|
+
agent.stats.lastActivity = new Date().toISOString();
|
|
442
|
+
|
|
443
|
+
const content = msg.content || '';
|
|
444
|
+
// Extract prompt — support "Generate: <prompt>" or just raw text
|
|
445
|
+
let prompt = content;
|
|
446
|
+
const genMatch = content.match(/^(?:Generate|Create|Make|Draw|Render):\s*(.+)/i);
|
|
447
|
+
if (genMatch) prompt = genMatch[1].trim();
|
|
448
|
+
|
|
449
|
+
// Detect media category from prompt keywords
|
|
450
|
+
// texture: seamless patterns, materials, surfaces for 3D/game use
|
|
451
|
+
// video: animations, motion, mp4 requests
|
|
452
|
+
// image: everything else (concept art, photos, illustrations)
|
|
453
|
+
let mediaCategory = 'image';
|
|
454
|
+
const lowerPrompt = prompt.toLowerCase();
|
|
455
|
+
if (/\b(texture|tileable|seamless|material|pbr|normal.?map|roughness.?map|diffuse|bump.?map|2d.?texture|surface.?pattern)\b/.test(lowerPrompt)) {
|
|
456
|
+
mediaCategory = 'texture';
|
|
457
|
+
} else if (/\b(video|animation|animate|mp4|motion|clip|footage)\b/.test(lowerPrompt)) {
|
|
458
|
+
mediaCategory = 'video';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Extract image attachments (base64)
|
|
462
|
+
var imageAttachments = [];
|
|
463
|
+
if (msg.attachments && Array.isArray(msg.attachments)) {
|
|
464
|
+
for (const att of msg.attachments) {
|
|
465
|
+
if (att.base64 && att.mimeType && att.mimeType.startsWith('image/')) {
|
|
466
|
+
imageAttachments.push({ mimeType: att.mimeType, base64: att.base64 });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
// Send "processing" response
|
|
473
|
+
var attachNote = imageAttachments.length > 0 ? ' [+' + imageAttachments.length + ' image(s)]' : '';
|
|
474
|
+
this._sendMessage(
|
|
475
|
+
name,
|
|
476
|
+
msg.from,
|
|
477
|
+
`Processing: "${prompt.substring(0, 60)}${prompt.length > 60 ? '...' : ''}"${attachNote}`,
|
|
478
|
+
msg.id,
|
|
479
|
+
replyContext
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const result = await agent.provider.generate(prompt, { images: imageAttachments });
|
|
483
|
+
agent.stats.completed++;
|
|
484
|
+
|
|
485
|
+
if (result.type === 'image' && result.data) {
|
|
486
|
+
// Save media file
|
|
487
|
+
const mediaId = crypto.randomUUID();
|
|
488
|
+
const ext = result.format === 'url' ? 'png' : (result.format || 'png');
|
|
489
|
+
const filename = `${mediaId}.${ext}`;
|
|
490
|
+
|
|
491
|
+
// Ensure media directory exists
|
|
492
|
+
if (!fs.existsSync(this._mediaDir)) fs.mkdirSync(this._mediaDir, { recursive: true });
|
|
493
|
+
|
|
494
|
+
if (result.format === 'url') {
|
|
495
|
+
// Download from URL
|
|
496
|
+
await this._downloadFile(result.data, path.join(this._mediaDir, filename));
|
|
497
|
+
} else {
|
|
498
|
+
// Save base64 data
|
|
499
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
500
|
+
fs.writeFileSync(path.join(this._mediaDir, filename), buffer);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Also save a named copy to generated-images/ in the project folder
|
|
504
|
+
// so other agents can reference them by readable name
|
|
505
|
+
const projectImgDir = path.join(this.dataDir, '..', 'generated-images');
|
|
506
|
+
try {
|
|
507
|
+
if (!fs.existsSync(projectImgDir)) fs.mkdirSync(projectImgDir, { recursive: true });
|
|
508
|
+
const safeName = prompt.substring(0, 60).replace(/[^a-zA-Z0-9 _-]/g, '').trim().replace(/\s+/g, '_') || 'image';
|
|
509
|
+
const namedFile = safeName + '_' + mediaId.substring(0, 8) + '.' + ext;
|
|
510
|
+
fs.copyFileSync(path.join(this._mediaDir, filename), path.join(projectImgDir, namedFile));
|
|
511
|
+
} catch (e) { /* non-critical — media dir copy is the source of truth */ }
|
|
512
|
+
|
|
513
|
+
// Log media metadata
|
|
514
|
+
const mediaEntry = {
|
|
515
|
+
id: mediaId,
|
|
516
|
+
type: mediaCategory,
|
|
517
|
+
prompt: prompt,
|
|
518
|
+
agent: name,
|
|
519
|
+
provider: agent.config.provider,
|
|
520
|
+
model: agent.config.model,
|
|
521
|
+
filename: filename,
|
|
522
|
+
timestamp: new Date().toISOString(),
|
|
523
|
+
requestedBy: msg.from,
|
|
524
|
+
};
|
|
525
|
+
fs.appendFileSync(this._mediaFile, JSON.stringify(mediaEntry) + '\n');
|
|
526
|
+
|
|
527
|
+
// Send response with media reference
|
|
528
|
+
this._sendMessage(name, msg.from,
|
|
529
|
+
`Generated image: "${prompt.substring(0, 80)}"\nMedia ID: ${mediaId}\nModel: ${result.model || agent.config.model}${result.revised_prompt ? '\nRevised: ' + result.revised_prompt : ''}`,
|
|
530
|
+
msg.id,
|
|
531
|
+
replyContext
|
|
532
|
+
);
|
|
533
|
+
} else if (result.type === 'text') {
|
|
534
|
+
this._sendMessage(name, msg.from, result.data, msg.id, replyContext);
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
agent.stats.errors++;
|
|
538
|
+
this._sendMessage(name, msg.from, `Error: ${err.message}`, msg.id, replyContext);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Send a message from the API agent
|
|
545
|
+
_sendMessage(from, to, content, replyTo) {
|
|
546
|
+
const context = arguments[4] || {};
|
|
547
|
+
const channel = context.channel && context.channel !== 'general' ? context.channel : null;
|
|
548
|
+
const msg = {
|
|
549
|
+
id: crypto.randomUUID(),
|
|
550
|
+
from,
|
|
551
|
+
to,
|
|
552
|
+
content,
|
|
553
|
+
timestamp: new Date().toISOString(),
|
|
554
|
+
reply_to: replyTo || null,
|
|
555
|
+
system: false,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
if (channel) msg.channel = channel;
|
|
559
|
+
if (context.threadId) msg.thread_id = context.threadId;
|
|
560
|
+
if (context.sessionId) msg.session_id = context.sessionId;
|
|
561
|
+
if (context.commandId) msg.command_id = context.commandId;
|
|
562
|
+
if (context.causationId) msg.causation_id = context.causationId;
|
|
563
|
+
if (context.correlationId) msg.correlation_id = context.correlationId;
|
|
564
|
+
|
|
565
|
+
if (!context.branch && !channel && !context.sessionId && !context.commandId && !context.causationId && !context.correlationId) {
|
|
566
|
+
this._canonicalState.appendMessage(msg);
|
|
567
|
+
return msg;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this._canonicalState.appendScopedMessage(msg, {
|
|
571
|
+
branch: context.branch,
|
|
572
|
+
channel,
|
|
573
|
+
actorAgent: from,
|
|
574
|
+
sessionId: context.sessionId || null,
|
|
575
|
+
commandId: context.commandId || null,
|
|
576
|
+
causationId: context.causationId || null,
|
|
577
|
+
correlationId: context.correlationId || null,
|
|
578
|
+
});
|
|
579
|
+
return msg;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Update heartbeat in agents.json
|
|
583
|
+
_updateHeartbeat(name) {
|
|
584
|
+
this._canonicalState.updateAgentHeartbeat(name);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_updateAgentStatus(name, status) {
|
|
588
|
+
this._canonicalState.updateAgentStatus(name, status);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
_getMessageCount() {
|
|
592
|
+
return Object.values(this.agents).reduce((total, agent) => total + (agent.lastReadOffset || 0), 0);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
_downloadFile(url, dest) {
|
|
596
|
+
const transport = url.startsWith('https') ? require('https') : require('http');
|
|
597
|
+
return new Promise((resolve, reject) => {
|
|
598
|
+
const file = fs.createWriteStream(dest);
|
|
599
|
+
transport.get(url, { timeout: 60000 }, (response) => {
|
|
600
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
601
|
+
file.close();
|
|
602
|
+
fs.unlinkSync(dest);
|
|
603
|
+
return this._downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
604
|
+
}
|
|
605
|
+
response.pipe(file);
|
|
606
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
607
|
+
}).on('error', (e) => {
|
|
608
|
+
file.close();
|
|
609
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
610
|
+
reject(e);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Get media list (paginated, filterable)
|
|
616
|
+
// Scans generated-images/ folder as the single source of truth
|
|
617
|
+
getMedia(options = {}) {
|
|
618
|
+
let items = [];
|
|
619
|
+
const imageExts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
620
|
+
const genImgDir = path.join(this.dataDir, '..', 'generated-images');
|
|
621
|
+
|
|
622
|
+
// Build a lookup from media.jsonl for metadata (prompt, agent, provider, model)
|
|
623
|
+
const metaByShortId = {};
|
|
624
|
+
if (fs.existsSync(this._mediaFile)) {
|
|
625
|
+
try {
|
|
626
|
+
const content = fs.readFileSync(this._mediaFile, 'utf8').trim();
|
|
627
|
+
if (content) {
|
|
628
|
+
const parsed = content.split(/\r?\n/).map(line => {
|
|
629
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
630
|
+
}).filter(Boolean);
|
|
631
|
+
for (const item of parsed) {
|
|
632
|
+
// Match by short ID suffix in filename (e.g. "happy_pitbull_40e85ec7.png" matches id starting with "40e85ec7")
|
|
633
|
+
const shortId = (item.id || '').substring(0, 8);
|
|
634
|
+
if (shortId) metaByShortId[shortId] = item;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Scan generated-images/ folder
|
|
641
|
+
if (fs.existsSync(genImgDir)) {
|
|
642
|
+
try {
|
|
643
|
+
const files = fs.readdirSync(genImgDir);
|
|
644
|
+
for (const file of files) {
|
|
645
|
+
const ext = path.extname(file).toLowerCase();
|
|
646
|
+
if (!imageExts.has(ext)) continue;
|
|
647
|
+
const filePath = path.join(genImgDir, file);
|
|
648
|
+
const stat = fs.statSync(filePath);
|
|
649
|
+
const baseName = path.basename(file, ext);
|
|
650
|
+
// Extract short ID from end of filename (e.g. "happy_pitbull_40e85ec7" -> "40e85ec7")
|
|
651
|
+
const idMatch = baseName.match(/_([a-f0-9]{8})$/);
|
|
652
|
+
const shortId = idMatch ? idMatch[1] : null;
|
|
653
|
+
const meta = shortId ? metaByShortId[shortId] : null;
|
|
654
|
+
const name = baseName.replace(/_/g, ' ').replace(/\s+[a-f0-9]{8}$/, '');
|
|
655
|
+
|
|
656
|
+
items.push({
|
|
657
|
+
id: meta ? meta.id : ('file:gen:' + file),
|
|
658
|
+
type: meta ? meta.type : 'image',
|
|
659
|
+
prompt: meta ? meta.prompt : name,
|
|
660
|
+
agent: meta ? meta.agent : 'imported',
|
|
661
|
+
provider: meta ? meta.provider : 'file',
|
|
662
|
+
model: meta ? meta.model : '',
|
|
663
|
+
filename: file,
|
|
664
|
+
timestamp: meta ? meta.timestamp : stat.mtime.toISOString(),
|
|
665
|
+
_source: 'generated-images',
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Filter by type
|
|
672
|
+
if (options.type) items = items.filter(i => i.type === options.type);
|
|
673
|
+
// Filter by agent
|
|
674
|
+
if (options.agent) items = items.filter(i => i.agent === options.agent);
|
|
675
|
+
|
|
676
|
+
// Sort newest first
|
|
677
|
+
items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
678
|
+
|
|
679
|
+
// Paginate
|
|
680
|
+
const page = options.page || 1;
|
|
681
|
+
const limit = options.limit || 20;
|
|
682
|
+
const start = (page - 1) * limit;
|
|
683
|
+
return items.slice(start, start + limit);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Get media file path — serves from generated-images/ first, falls back to .agent-bridge/media/
|
|
687
|
+
getMediaFilePath(id) {
|
|
688
|
+
const genImgDir = path.join(this.dataDir, '..', 'generated-images');
|
|
689
|
+
|
|
690
|
+
// Handle virtual file IDs from folder scan
|
|
691
|
+
if (id.startsWith('file:gen:')) {
|
|
692
|
+
const filename = id.slice('file:gen:'.length);
|
|
693
|
+
const filePath = path.join(genImgDir, filename);
|
|
694
|
+
return fs.existsSync(filePath) ? filePath : null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Look up in media.jsonl — find the named copy in generated-images/
|
|
698
|
+
if (!fs.existsSync(this._mediaFile)) return null;
|
|
699
|
+
try {
|
|
700
|
+
const content = fs.readFileSync(this._mediaFile, 'utf8').trim();
|
|
701
|
+
const lines = content.split(/\r?\n/);
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
try {
|
|
704
|
+
const item = JSON.parse(line);
|
|
705
|
+
if (item.id === id) {
|
|
706
|
+
const shortId = item.id.substring(0, 8);
|
|
707
|
+
// Look for the named copy in generated-images/
|
|
708
|
+
if (fs.existsSync(genImgDir)) {
|
|
709
|
+
const files = fs.readdirSync(genImgDir);
|
|
710
|
+
const match = files.find(f => f.includes(shortId));
|
|
711
|
+
if (match) return path.join(genImgDir, match);
|
|
712
|
+
}
|
|
713
|
+
// Fallback to .agent-bridge/media/
|
|
714
|
+
const filePath = path.join(this._mediaDir, item.filename);
|
|
715
|
+
return fs.existsSync(filePath) ? filePath : null;
|
|
716
|
+
}
|
|
717
|
+
} catch {}
|
|
718
|
+
}
|
|
719
|
+
} catch {}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Delete a media item
|
|
724
|
+
deleteMedia(id) {
|
|
725
|
+
if (!fs.existsSync(this._mediaFile)) return { error: 'No media found' };
|
|
726
|
+
try {
|
|
727
|
+
const content = fs.readFileSync(this._mediaFile, 'utf8').trim();
|
|
728
|
+
const lines = content.split(/\r?\n/);
|
|
729
|
+
const remaining = [];
|
|
730
|
+
let found = false;
|
|
731
|
+
for (const line of lines) {
|
|
732
|
+
try {
|
|
733
|
+
const item = JSON.parse(line);
|
|
734
|
+
if (item.id === id) {
|
|
735
|
+
found = true;
|
|
736
|
+
// Delete actual file
|
|
737
|
+
const filePath = path.join(this._mediaDir, item.filename);
|
|
738
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
739
|
+
} else {
|
|
740
|
+
remaining.push(line);
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
remaining.push(line);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!found) return { error: 'Media not found' };
|
|
747
|
+
fs.writeFileSync(this._mediaFile, remaining.join('\n') + (remaining.length ? '\n' : ''));
|
|
748
|
+
return { ok: true };
|
|
749
|
+
} catch (e) { return { error: e.message }; }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Stop all agents (cleanup on shutdown)
|
|
753
|
+
stopAll() {
|
|
754
|
+
for (const name in this.agents) {
|
|
755
|
+
this.stop(name);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
module.exports = { ApiAgentEngine, PROVIDER_COLORS };
|