groove-dev 0.27.172 → 0.27.174
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +2 -0
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +100 -0
- package/node_modules/@groove-dev/daemon/src/process.js +5 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +2 -1
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +34 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +2 -2
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +203 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BvJwMNAX.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BsCp-oqa.js → index-DZY-EWPs.js} +217 -217
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -0
- package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +25 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +14 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +9 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +2 -0
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/innerchat.js +100 -0
- package/packages/daemon/src/process.js +5 -0
- package/packages/daemon/src/providers/claude-code.js +2 -1
- package/packages/daemon/src/routes/innerchat.js +34 -0
- package/packages/daemon/src/validate.js +2 -2
- package/packages/gui/dist/assets/index-BvJwMNAX.css +1 -0
- package/packages/gui/dist/assets/{index-BsCp-oqa.js → index-DZY-EWPs.js} +217 -217
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-config.jsx +1 -0
- package/packages/gui/src/components/agents/recommended-team-card.jsx +1 -1
- package/packages/gui/src/components/chat/chat-messages.jsx +25 -1
- package/packages/gui/src/stores/groove.js +51 -0
- package/packages/gui/src/stores/slices/agents-slice.js +14 -0
- package/packages/gui/src/stores/slices/ui-slice.js +9 -0
- package/packages/gui/src/views/agents.jsx +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +0 -1
- package/packages/gui/dist/assets/index-BrMU-6gi.css +0 -1
|
@@ -27,6 +27,7 @@ import { registerIntegrationRoutes } from './routes/integrations.js';
|
|
|
27
27
|
import { registerFileRoutes, resetEditorRoot } from './routes/files.js';
|
|
28
28
|
import { registerNetworkRoutes } from './routes/network.js';
|
|
29
29
|
import { registerScheduleRoutes } from './routes/schedules.js';
|
|
30
|
+
import { registerInnerChatRoutes } from './routes/innerchat.js';
|
|
30
31
|
|
|
31
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
33
|
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
@@ -175,6 +176,7 @@ export function createApi(app, daemon) {
|
|
|
175
176
|
registerFileRoutes(app, daemon);
|
|
176
177
|
registerNetworkRoutes(app, daemon);
|
|
177
178
|
registerScheduleRoutes(app, daemon);
|
|
179
|
+
registerInnerChatRoutes(app, daemon);
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
// Token usage
|
|
@@ -46,6 +46,7 @@ import { MLXServerManager } from './mlx-server.js';
|
|
|
46
46
|
import { RepoImporter } from './repo-import.js';
|
|
47
47
|
import { ConversationManager } from './conversations.js';
|
|
48
48
|
import { Toys } from './toys.js';
|
|
49
|
+
import { InnerChat } from './innerchat.js';
|
|
49
50
|
import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';
|
|
50
51
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
51
52
|
import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
|
|
@@ -158,6 +159,7 @@ export class Daemon {
|
|
|
158
159
|
this.repoImporter = new RepoImporter(this);
|
|
159
160
|
this.modelLab = new ModelLab(this);
|
|
160
161
|
this.toys = new Toys(this);
|
|
162
|
+
this.innerchat = new InnerChat(this);
|
|
161
163
|
this.trajectoryCapture = null;
|
|
162
164
|
|
|
163
165
|
// Hook teams.delete to clean up agent-loop session files
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
export class InnerChat {
|
|
6
|
+
constructor(daemon) {
|
|
7
|
+
this.daemon = daemon;
|
|
8
|
+
this.messages = new Map();
|
|
9
|
+
this.pendingResponses = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async send(fromAgentId, toAgentId, message) {
|
|
13
|
+
const fromAgent = this.daemon.registry.get(fromAgentId);
|
|
14
|
+
const toAgent = this.daemon.registry.get(toAgentId);
|
|
15
|
+
if (!fromAgent) throw new Error(`Sender agent ${fromAgentId} not found`);
|
|
16
|
+
if (!toAgent) throw new Error(`Target agent ${toAgentId} not found`);
|
|
17
|
+
|
|
18
|
+
const id = randomUUID().slice(0, 12);
|
|
19
|
+
const msg = {
|
|
20
|
+
id,
|
|
21
|
+
from: { id: fromAgent.id, name: fromAgent.name, role: fromAgent.role },
|
|
22
|
+
to: { id: toAgent.id, name: toAgent.name, role: toAgent.role },
|
|
23
|
+
message,
|
|
24
|
+
response: null,
|
|
25
|
+
status: 'sent',
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
respondedAt: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.messages.set(id, msg);
|
|
31
|
+
this.pendingResponses.set(toAgentId, id);
|
|
32
|
+
|
|
33
|
+
const wrapped = `[InnerChat from ${fromAgent.name} (${fromAgent.role})]\n\n${message}\n\nReply normally — your response will be relayed back to ${fromAgent.name}.`;
|
|
34
|
+
|
|
35
|
+
let deliveryStatus = 'sent';
|
|
36
|
+
if (this.daemon.processes.hasAgentLoop(toAgentId)) {
|
|
37
|
+
const sent = await this.daemon.processes.sendMessage(toAgentId, wrapped, 'agent');
|
|
38
|
+
deliveryStatus = sent ? 'delivered' : 'queued';
|
|
39
|
+
} else if (this.daemon.processes.isRunning(toAgentId)) {
|
|
40
|
+
this.daemon.processes.queueMessage(toAgentId, wrapped);
|
|
41
|
+
deliveryStatus = 'queued';
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Target agent ${toAgent.name} is not running`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
msg.status = deliveryStatus;
|
|
47
|
+
this.daemon.broadcast({ type: 'innerchat:sent', data: msg });
|
|
48
|
+
this.daemon.audit.log('innerchat.send', { id, from: fromAgentId, to: toAgentId });
|
|
49
|
+
|
|
50
|
+
return msg;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onAgentOutput(agentId, output) {
|
|
54
|
+
const messageId = this.pendingResponses.get(agentId);
|
|
55
|
+
if (!messageId) return;
|
|
56
|
+
if (output.type !== 'result') return;
|
|
57
|
+
|
|
58
|
+
const msg = this.messages.get(messageId);
|
|
59
|
+
if (!msg) return;
|
|
60
|
+
|
|
61
|
+
let responseText = '';
|
|
62
|
+
if (typeof output.data === 'string') {
|
|
63
|
+
responseText = output.data;
|
|
64
|
+
} else if (Array.isArray(output.data)) {
|
|
65
|
+
responseText = output.data.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
66
|
+
}
|
|
67
|
+
if (!responseText.trim()) return;
|
|
68
|
+
|
|
69
|
+
msg.response = responseText.trim();
|
|
70
|
+
msg.status = 'responded';
|
|
71
|
+
msg.respondedAt = Date.now();
|
|
72
|
+
this.pendingResponses.delete(agentId);
|
|
73
|
+
|
|
74
|
+
const relay = `[InnerChat reply from ${msg.to.name} (${msg.to.role})]\n\n${msg.response}`;
|
|
75
|
+
const senderId = msg.from.id;
|
|
76
|
+
if (this.daemon.processes.hasAgentLoop(senderId)) {
|
|
77
|
+
this.daemon.processes.sendMessage(senderId, relay, 'agent').catch(() => {});
|
|
78
|
+
} else if (this.daemon.processes.isRunning(senderId)) {
|
|
79
|
+
this.daemon.processes.queueMessage(senderId, relay);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.daemon.broadcast({ type: 'innerchat:response', data: msg });
|
|
83
|
+
this.daemon.audit.log('innerchat.response', { id: messageId, from: msg.from.id, to: msg.to.id });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getMessages(agentId = null) {
|
|
87
|
+
const all = Array.from(this.messages.values());
|
|
88
|
+
if (!agentId) return all;
|
|
89
|
+
return all.filter(m => m.from.id === agentId || m.to.id === agentId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getMessage(id) {
|
|
93
|
+
return this.messages.get(id) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getPending(agentId) {
|
|
97
|
+
const messageId = this.pendingResponses.get(agentId);
|
|
98
|
+
return messageId ? this.messages.get(messageId) : null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1582,6 +1582,11 @@ For normal file edits within your scope, proceed without review.
|
|
|
1582
1582
|
|
|
1583
1583
|
registry.update(agentId, updates);
|
|
1584
1584
|
|
|
1585
|
+
// Notify innerchat for response capture
|
|
1586
|
+
if (this.daemon.innerchat) {
|
|
1587
|
+
this.daemon.innerchat.onAgentOutput(agentId, output);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1585
1590
|
// Throttle streaming broadcasts to 4/sec per agent
|
|
1586
1591
|
const isStreaming = output.type !== 'result';
|
|
1587
1592
|
if (isStreaming) {
|
|
@@ -39,6 +39,7 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
39
39
|
static authType = 'subscription';
|
|
40
40
|
static managesOwnContext = true; // Claude Code compacts context internally (~25-37% → 2-8%)
|
|
41
41
|
static models = [
|
|
42
|
+
{ id: 'claude-fable-5', name: 'Claude Fable 5', tier: 'heavy', contextWindow: 1_000_000 },
|
|
42
43
|
{ id: 'claude-opus-4-8', name: 'Claude Opus 4.8', tier: 'heavy', contextWindow: 1_000_000 },
|
|
43
44
|
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', tier: 'heavy', contextWindow: 1_000_000 },
|
|
44
45
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', tier: 'medium', contextWindow: 200_000 },
|
|
@@ -74,7 +75,7 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
74
75
|
normalizeConfig(config) {
|
|
75
76
|
if (typeof config.reasoningEffort === 'number') {
|
|
76
77
|
const e = config.reasoningEffort;
|
|
77
|
-
config.effort = e <=
|
|
78
|
+
config.effort = e <= 15 ? 'none' : e <= 30 ? 'low' : e <= 50 ? 'medium' : e <= 70 ? 'high' : e <= 85 ? 'xhigh' : 'ultra';
|
|
78
79
|
}
|
|
79
80
|
return config;
|
|
80
81
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
export function registerInnerChatRoutes(app, daemon) {
|
|
4
|
+
app.post('/api/innerchat/send', async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { from, to, message } = req.body;
|
|
7
|
+
if (!from || typeof from !== 'string') return res.status(400).json({ error: 'from (agent ID) is required' });
|
|
8
|
+
if (!to || typeof to !== 'string') return res.status(400).json({ error: 'to (agent ID) is required' });
|
|
9
|
+
if (!message || typeof message !== 'string' || !message.trim()) return res.status(400).json({ error: 'message is required' });
|
|
10
|
+
if (from === to) return res.status(400).json({ error: 'cannot send a message to yourself' });
|
|
11
|
+
|
|
12
|
+
const msg = await daemon.innerchat.send(from, to, message.trim());
|
|
13
|
+
res.json(msg);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
res.status(400).json({ error: err.message });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.get('/api/innerchat/messages', (req, res) => {
|
|
20
|
+
const { agentId } = req.query;
|
|
21
|
+
res.json({ messages: daemon.innerchat.getMessages(agentId || null) });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.get('/api/innerchat/messages/:id', (req, res) => {
|
|
25
|
+
const msg = daemon.innerchat.getMessage(req.params.id);
|
|
26
|
+
if (!msg) return res.status(404).json({ error: 'Message not found' });
|
|
27
|
+
res.json(msg);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get('/api/innerchat/pending/:agentId', (req, res) => {
|
|
31
|
+
const pending = daemon.innerchat.getPending(req.params.agentId);
|
|
32
|
+
res.json({ pending: pending || null });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -112,7 +112,7 @@ export function validateAgentConfig(config) {
|
|
|
112
112
|
if (!isNaN(v) && v >= 0 && v <= 100) verbosity = Math.round(v);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
const validEffort = ['min', 'low', 'default', 'high', 'max'];
|
|
115
|
+
const validEffort = ['min', 'low', 'default', 'high', 'max', 'ultra'];
|
|
116
116
|
const effort = validEffort.includes(config.effort) ? config.effort : undefined;
|
|
117
117
|
|
|
118
118
|
const validRouting = ['fixed', 'auto', 'auto-floor'];
|
|
@@ -227,7 +227,7 @@ export function validateGatewayConfig(config) {
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
const VALID_REASONING_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
230
|
+
const VALID_REASONING_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh', 'ultra'];
|
|
231
231
|
const VALID_VERBOSITIES = ['low', 'medium'];
|
|
232
232
|
|
|
233
233
|
export function validateReasoningEffort(value) {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// GROOVE — InnerChat Tests
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { InnerChat } from '../src/innerchat.js';
|
|
7
|
+
|
|
8
|
+
function makeDaemon() {
|
|
9
|
+
const broadcasts = [];
|
|
10
|
+
const audits = [];
|
|
11
|
+
const sentMessages = [];
|
|
12
|
+
const queuedMessages = [];
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
broadcasts,
|
|
16
|
+
audits,
|
|
17
|
+
sentMessages,
|
|
18
|
+
queuedMessages,
|
|
19
|
+
registry: {
|
|
20
|
+
_agents: new Map(),
|
|
21
|
+
get(id) { return this._agents.get(id) || null; },
|
|
22
|
+
add(agent) { this._agents.set(agent.id, agent); },
|
|
23
|
+
},
|
|
24
|
+
processes: {
|
|
25
|
+
_loops: new Set(),
|
|
26
|
+
_running: new Set(),
|
|
27
|
+
hasAgentLoop(id) { return this._loops.has(id); },
|
|
28
|
+
isRunning(id) { return this._running.has(id); },
|
|
29
|
+
async sendMessage(id, msg, source) {
|
|
30
|
+
sentMessages.push({ id, msg, source });
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
queueMessage(id, msg) {
|
|
34
|
+
queuedMessages.push({ id, msg });
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
broadcast(msg) { broadcasts.push(msg); },
|
|
38
|
+
audit: { log(type, data) { audits.push({ type, data }); } },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('InnerChat', () => {
|
|
43
|
+
let daemon;
|
|
44
|
+
let innerchat;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
daemon = makeDaemon();
|
|
48
|
+
innerchat = new InnerChat(daemon);
|
|
49
|
+
daemon.innerchat = innerchat;
|
|
50
|
+
|
|
51
|
+
daemon.registry.add({ id: 'a1', name: 'fullstack-1', role: 'fullstack' });
|
|
52
|
+
daemon.registry.add({ id: 'a2', name: 'fullstack-14', role: 'fullstack' });
|
|
53
|
+
daemon.processes._loops.add('a2');
|
|
54
|
+
daemon.processes._running.add('a2');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should send a message between agents', async () => {
|
|
58
|
+
const msg = await innerchat.send('a1', 'a2', 'What endpoint shape are you using?');
|
|
59
|
+
assert.ok(msg.id);
|
|
60
|
+
assert.equal(msg.from.id, 'a1');
|
|
61
|
+
assert.equal(msg.from.name, 'fullstack-1');
|
|
62
|
+
assert.equal(msg.to.id, 'a2');
|
|
63
|
+
assert.equal(msg.to.name, 'fullstack-14');
|
|
64
|
+
assert.equal(msg.message, 'What endpoint shape are you using?');
|
|
65
|
+
assert.equal(msg.response, null);
|
|
66
|
+
assert.equal(msg.status, 'delivered');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should deliver via sendMessage when agent has loop', async () => {
|
|
70
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
71
|
+
assert.equal(daemon.sentMessages.length, 1);
|
|
72
|
+
assert.equal(daemon.sentMessages[0].id, 'a2');
|
|
73
|
+
assert.ok(daemon.sentMessages[0].msg.includes('InnerChat from fullstack-1'));
|
|
74
|
+
assert.ok(daemon.sentMessages[0].msg.includes('Hello'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should queue when agent is running but no loop', async () => {
|
|
78
|
+
daemon.processes._loops.delete('a2');
|
|
79
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
80
|
+
assert.equal(daemon.queuedMessages.length, 1);
|
|
81
|
+
assert.equal(daemon.queuedMessages[0].id, 'a2');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw when target agent is not running', async () => {
|
|
85
|
+
daemon.processes._running.delete('a2');
|
|
86
|
+
daemon.processes._loops.delete('a2');
|
|
87
|
+
await assert.rejects(() => innerchat.send('a1', 'a2', 'Hello'), /not running/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw when sender does not exist', async () => {
|
|
91
|
+
await assert.rejects(() => innerchat.send('unknown', 'a2', 'Hello'), /not found/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw when sending to self', async () => {
|
|
95
|
+
// The route validates this, but let's verify it's caught
|
|
96
|
+
daemon.processes._loops.add('a1');
|
|
97
|
+
daemon.processes._running.add('a1');
|
|
98
|
+
const msg = await innerchat.send('a1', 'a1', 'Hello');
|
|
99
|
+
assert.ok(msg.id);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should broadcast innerchat:sent on send', async () => {
|
|
103
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
104
|
+
const sent = daemon.broadcasts.find(b => b.type === 'innerchat:sent');
|
|
105
|
+
assert.ok(sent);
|
|
106
|
+
assert.equal(sent.data.message, 'Hello');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should capture response on agent output', async () => {
|
|
110
|
+
const msg = await innerchat.send('a1', 'a2', 'What shape?');
|
|
111
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'GET /api/projects/:id returns { id, name }' });
|
|
112
|
+
assert.equal(msg.status, 'responded');
|
|
113
|
+
assert.equal(msg.response, 'GET /api/projects/:id returns { id, name }');
|
|
114
|
+
assert.ok(msg.respondedAt);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should relay response back to sender', async () => {
|
|
118
|
+
daemon.processes._loops.add('a1');
|
|
119
|
+
daemon.processes._running.add('a1');
|
|
120
|
+
await innerchat.send('a1', 'a2', 'What shape?');
|
|
121
|
+
daemon.sentMessages.length = 0;
|
|
122
|
+
|
|
123
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'The answer' });
|
|
124
|
+
assert.equal(daemon.sentMessages.length, 1);
|
|
125
|
+
assert.equal(daemon.sentMessages[0].id, 'a1');
|
|
126
|
+
assert.ok(daemon.sentMessages[0].msg.includes('InnerChat reply from fullstack-14'));
|
|
127
|
+
assert.ok(daemon.sentMessages[0].msg.includes('The answer'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should ignore non-result output', async () => {
|
|
131
|
+
await innerchat.send('a1', 'a2', 'What shape?');
|
|
132
|
+
innerchat.onAgentOutput('a2', { type: 'activity', subtype: 'stream', data: 'partial...' });
|
|
133
|
+
const msg = innerchat.getMessage(innerchat.getMessages()[0].id);
|
|
134
|
+
assert.equal(msg.status, 'delivered');
|
|
135
|
+
assert.equal(msg.response, null);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should ignore output from agents with no pending question', () => {
|
|
139
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'Some output' });
|
|
140
|
+
assert.equal(daemon.broadcasts.length, 0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should extract text from array-format output', async () => {
|
|
144
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
145
|
+
innerchat.onAgentOutput('a2', {
|
|
146
|
+
type: 'result',
|
|
147
|
+
data: [
|
|
148
|
+
{ type: 'text', text: 'Part 1' },
|
|
149
|
+
{ type: 'tool_use', name: 'Read', input: {} },
|
|
150
|
+
{ type: 'text', text: 'Part 2' },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
const msg = innerchat.getMessages()[0];
|
|
154
|
+
assert.equal(msg.response, 'Part 1\nPart 2');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should broadcast innerchat:response on response', async () => {
|
|
158
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
159
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'Response here' });
|
|
160
|
+
const resp = daemon.broadcasts.find(b => b.type === 'innerchat:response');
|
|
161
|
+
assert.ok(resp);
|
|
162
|
+
assert.equal(resp.data.response, 'Response here');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should list messages filtered by agent', async () => {
|
|
166
|
+
daemon.processes._loops.add('a1');
|
|
167
|
+
daemon.processes._running.add('a1');
|
|
168
|
+
daemon.registry.add({ id: 'a3', name: 'backend-1', role: 'backend' });
|
|
169
|
+
daemon.processes._loops.add('a3');
|
|
170
|
+
daemon.processes._running.add('a3');
|
|
171
|
+
|
|
172
|
+
await innerchat.send('a1', 'a2', 'Message 1');
|
|
173
|
+
await innerchat.send('a1', 'a3', 'Message 2');
|
|
174
|
+
|
|
175
|
+
assert.equal(innerchat.getMessages().length, 2);
|
|
176
|
+
assert.equal(innerchat.getMessages('a2').length, 1);
|
|
177
|
+
assert.equal(innerchat.getMessages('a3').length, 1);
|
|
178
|
+
assert.equal(innerchat.getMessages('a1').length, 2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return pending question for agent', async () => {
|
|
182
|
+
assert.equal(innerchat.getPending('a2'), null);
|
|
183
|
+
await innerchat.send('a1', 'a2', 'Question?');
|
|
184
|
+
const pending = innerchat.getPending('a2');
|
|
185
|
+
assert.ok(pending);
|
|
186
|
+
assert.equal(pending.message, 'Question?');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should clear pending after response', async () => {
|
|
190
|
+
await innerchat.send('a1', 'a2', 'Question?');
|
|
191
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'Answer' });
|
|
192
|
+
assert.equal(innerchat.getPending('a2'), null);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should audit send and response events', async () => {
|
|
196
|
+
await innerchat.send('a1', 'a2', 'Hello');
|
|
197
|
+
innerchat.onAgentOutput('a2', { type: 'result', data: 'Reply' });
|
|
198
|
+
const sendAudit = daemon.audits.find(a => a.type === 'innerchat.send');
|
|
199
|
+
const respAudit = daemon.audits.find(a => a.type === 'innerchat.response');
|
|
200
|
+
assert.ok(sendAudit);
|
|
201
|
+
assert.ok(respAudit);
|
|
202
|
+
});
|
|
203
|
+
});
|