groove-dev 0.27.182 → 0.27.184
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/CLAUDE.md +0 -7
- 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/deliver.js +130 -0
- package/node_modules/@groove-dev/daemon/src/index.js +7 -3
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +41 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +267 -60
- package/node_modules/@groove-dev/daemon/src/introducer.js +30 -1
- package/node_modules/@groove-dev/daemon/src/process.js +22 -1
- package/node_modules/@groove-dev/daemon/src/registry.js +5 -1
- package/node_modules/@groove-dev/daemon/src/rename.js +72 -0
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +22 -100
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +63 -16
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +11 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +35 -1
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +193 -134
- package/node_modules/@groove-dev/daemon/test/rename.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/teams.test.js +53 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-CTer01Vg.js → index-BpOyN6Zf.js} +227 -227
- package/node_modules/@groove-dev/gui/dist/assets/index-DiXB7yry.css +1 -0
- 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-panel.jsx +2 -95
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +56 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +35 -44
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +27 -4
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +22 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/deliver.js +130 -0
- package/packages/daemon/src/index.js +7 -3
- package/packages/daemon/src/innerchat-docs.js +41 -0
- package/packages/daemon/src/innerchat.js +267 -60
- package/packages/daemon/src/introducer.js +30 -1
- package/packages/daemon/src/process.js +22 -1
- package/packages/daemon/src/registry.js +5 -1
- package/packages/daemon/src/rename.js +72 -0
- package/packages/daemon/src/routes/agents.js +22 -100
- package/packages/daemon/src/routes/innerchat.js +63 -16
- package/packages/daemon/src/routes/teams.js +11 -0
- package/packages/daemon/src/teams.js +35 -1
- package/packages/gui/dist/assets/{index-CTer01Vg.js → index-BpOyN6Zf.js} +227 -227
- package/packages/gui/dist/assets/index-DiXB7yry.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-panel.jsx +2 -95
- package/packages/gui/src/components/chat/chat-messages.jsx +3 -1
- package/packages/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +56 -6
- package/packages/gui/src/stores/groove.js +35 -44
- package/packages/gui/src/stores/slices/agents-slice.js +27 -4
- package/packages/gui/src/stores/slices/teams-slice.js +22 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +0 -1
- package/packages/gui/dist/assets/index-DTFtRtkx.css +0 -1
|
@@ -5,6 +5,8 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSy
|
|
|
5
5
|
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from '../validate.js';
|
|
6
6
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from '../process.js';
|
|
7
7
|
import { getProvider } from '../providers/index.js';
|
|
8
|
+
import { deliverInstruction } from '../deliver.js';
|
|
9
|
+
import { renameAgent } from '../rename.js';
|
|
8
10
|
|
|
9
11
|
export function registerAgentRoutes(app, daemon) {
|
|
10
12
|
// List all agents
|
|
@@ -56,9 +58,21 @@ export function registerAgentRoutes(app, daemon) {
|
|
|
56
58
|
|
|
57
59
|
// Update agent
|
|
58
60
|
app.patch('/api/agents/:id', (req, res) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
try {
|
|
62
|
+
const { name, ...rest } = req.body || {};
|
|
63
|
+
|
|
64
|
+
// Renames migrate name-keyed files, so they can't go through the plain
|
|
65
|
+
// field update — registry.update ignores `name` without the opt-in.
|
|
66
|
+
let agent = daemon.registry.get(req.params.id);
|
|
67
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
68
|
+
|
|
69
|
+
if (Object.keys(rest).length) agent = daemon.registry.update(req.params.id, rest);
|
|
70
|
+
if (typeof name === 'string') agent = renameAgent(daemon, req.params.id, name);
|
|
71
|
+
|
|
72
|
+
res.json(agent);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
res.status(400).json({ error: err.message });
|
|
75
|
+
}
|
|
62
76
|
});
|
|
63
77
|
|
|
64
78
|
// Kill an agent (add ?purge=true to also remove from registry)
|
|
@@ -426,104 +440,12 @@ export function registerAgentRoutes(app, daemon) {
|
|
|
426
440
|
}
|
|
427
441
|
}
|
|
428
442
|
|
|
429
|
-
|
|
430
|
-
if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
|
|
431
|
-
if (daemon.rotator) daemon.rotator.recordUserMessage(req.params.id);
|
|
432
|
-
|
|
433
|
-
// Agent loop path — send message directly to the running loop
|
|
434
|
-
const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
|
|
435
|
-
if (daemon.processes.hasAgentLoop(req.params.id)) {
|
|
436
|
-
const sent = await daemon.processes.sendMessage(req.params.id, wrappedMessage);
|
|
437
|
-
if (sent) {
|
|
438
|
-
daemon.audit.log('agent.chat', { id: req.params.id });
|
|
439
|
-
return res.json({ id: agent.id, status: 'message_sent' });
|
|
440
|
-
}
|
|
441
|
-
// Loop exists but not running — fall through to resume/rotate
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// One-shot providers (groove-network): kill any running instance and
|
|
445
|
-
// respawn with the user's message as --prompt. No handoff brief, no
|
|
446
|
-
// session resume, no message queue — each chat message is a fresh spawn.
|
|
447
|
-
const provider = getProvider(agent.provider);
|
|
448
|
-
if (provider?.constructor?.isOneShot) {
|
|
449
|
-
const oldConfig = { ...agent };
|
|
450
|
-
if (daemon.processes.isRunning(req.params.id)) {
|
|
451
|
-
await daemon.processes.kill(req.params.id);
|
|
452
|
-
}
|
|
453
|
-
daemon.registry.remove(req.params.id, { silent: true });
|
|
454
|
-
daemon.locks.release(req.params.id);
|
|
455
|
-
|
|
456
|
-
let newAgent;
|
|
457
|
-
try {
|
|
458
|
-
newAgent = await daemon.processes.spawn({
|
|
459
|
-
role: oldConfig.role,
|
|
460
|
-
scope: oldConfig.scope,
|
|
461
|
-
provider: oldConfig.provider,
|
|
462
|
-
model: oldConfig.model,
|
|
463
|
-
prompt: finalMessage,
|
|
464
|
-
permission: oldConfig.permission || 'full',
|
|
465
|
-
workingDir: oldConfig.workingDir,
|
|
466
|
-
name: oldConfig.name,
|
|
467
|
-
teamId: oldConfig.teamId,
|
|
468
|
-
});
|
|
469
|
-
} catch (spawnErr) {
|
|
470
|
-
daemon.registry.flushPendingRemovals();
|
|
471
|
-
throw spawnErr;
|
|
472
|
-
}
|
|
473
|
-
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
|
|
474
|
-
return res.json(newAgent);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Non-interactive CLI providers (e.g. Gemini): respawn with the new
|
|
478
|
-
// message as the prompt, preserving original introContext. These providers
|
|
479
|
-
// run one prompt per spawn and cannot resume sessions.
|
|
480
|
-
if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
|
|
481
|
-
const oldConfig = { ...agent };
|
|
482
|
-
daemon.registry.remove(req.params.id, { silent: true });
|
|
483
|
-
daemon.locks.release(req.params.id);
|
|
484
|
-
|
|
485
|
-
let newAgent;
|
|
486
|
-
try {
|
|
487
|
-
newAgent = await daemon.processes.spawn({
|
|
488
|
-
role: oldConfig.role,
|
|
489
|
-
scope: oldConfig.scope,
|
|
490
|
-
provider: oldConfig.provider,
|
|
491
|
-
model: oldConfig.model,
|
|
492
|
-
prompt: finalMessage,
|
|
493
|
-
introContext: oldConfig.introContext,
|
|
494
|
-
permission: oldConfig.permission || 'full',
|
|
495
|
-
workingDir: oldConfig.workingDir,
|
|
496
|
-
name: oldConfig.name,
|
|
497
|
-
teamId: oldConfig.teamId,
|
|
498
|
-
});
|
|
499
|
-
} catch (spawnErr) {
|
|
500
|
-
daemon.registry.flushPendingRemovals();
|
|
501
|
-
throw spawnErr;
|
|
502
|
-
}
|
|
503
|
-
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
|
|
504
|
-
return res.json(newAgent);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Running CLI agent (no loop) — queue the message for delivery after
|
|
508
|
-
// the current task completes instead of killing and respawning.
|
|
509
|
-
if (daemon.processes.isRunning(req.params.id)) {
|
|
510
|
-
daemon.processes.queueMessage(req.params.id, wrappedMessage);
|
|
511
|
-
daemon.audit.log('agent.chat.queued', { id: req.params.id });
|
|
512
|
-
return res.json({ id: agent.id, status: 'message_queued' });
|
|
513
|
-
}
|
|
443
|
+
const result = await deliverInstruction(daemon, req.params.id, finalMessage);
|
|
514
444
|
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const SESSION_RESUME_CEILING = 5_000_000;
|
|
520
|
-
const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
|
|
521
|
-
const newAgent = resumed
|
|
522
|
-
? await daemon.processes.resume(req.params.id, wrappedMessage)
|
|
523
|
-
: await daemon.rotator.rotate(req.params.id, { additionalPrompt: wrappedMessage });
|
|
524
|
-
|
|
525
|
-
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed });
|
|
526
|
-
res.json(newAgent);
|
|
445
|
+
// Respawn paths return the fresh agent record; in-place delivery just
|
|
446
|
+
// acknowledges with a status the GUI uses to show queued vs sent.
|
|
447
|
+
if (result.agent && result.agentId !== req.params.id) return res.json(result.agent);
|
|
448
|
+
res.json({ id: agent.id, status: result.status });
|
|
527
449
|
} catch (err) {
|
|
528
450
|
res.status(400).json({ error: err.message });
|
|
529
451
|
}
|
|
@@ -1,30 +1,77 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { MAX_EXCHANGES } from '../innerchat.js';
|
|
4
|
+
|
|
5
|
+
// Agents know each other by name, not id — resolve either, preferring an
|
|
6
|
+
// exact name match so `fullstack-1` never resolves to `fullstack-14`.
|
|
7
|
+
function resolveAgent(daemon, ref) {
|
|
8
|
+
if (!ref || typeof ref !== 'string') return null;
|
|
9
|
+
const all = daemon.registry.getAll();
|
|
10
|
+
return all.find((a) => a.id === ref)
|
|
11
|
+
|| all.find((a) => a.name === ref)
|
|
12
|
+
|| all.find((a) => a.name.toLowerCase() === ref.toLowerCase())
|
|
13
|
+
|| null;
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
export function registerInnerChatRoutes(app, daemon) {
|
|
4
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Ask another agent a question and BLOCK until it answers.
|
|
19
|
+
*
|
|
20
|
+
* This request is held open deliberately — the calling agent is waiting on
|
|
21
|
+
* it, and the response body is the other agent's reply. That's what lets two
|
|
22
|
+
* agents iterate without a human relaying between them.
|
|
23
|
+
*/
|
|
24
|
+
app.post('/api/innerchat/ask', async (req, res) => {
|
|
5
25
|
try {
|
|
6
|
-
const { from, to, message } = req.body;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
const { from, to, message, timeoutMs } = req.body || {};
|
|
27
|
+
|
|
28
|
+
const fromAgent = resolveAgent(daemon, from);
|
|
29
|
+
if (!fromAgent) return res.status(404).json({ error: `Unknown calling agent: ${from}` });
|
|
30
|
+
|
|
31
|
+
const toAgent = resolveAgent(daemon, to);
|
|
32
|
+
if (!toAgent) {
|
|
33
|
+
const names = daemon.registry.getAll()
|
|
34
|
+
.filter((a) => a.id !== fromAgent.id)
|
|
35
|
+
.map((a) => a.name);
|
|
36
|
+
return res.status(404).json({
|
|
37
|
+
error: `No agent named "${to}".`,
|
|
38
|
+
availableAgents: names,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
43
|
+
return res.status(400).json({ error: 'message is required' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Held open until the target answers — see the class doc in innerchat.js.
|
|
47
|
+
req.setTimeout(0);
|
|
48
|
+
res.setTimeout(0);
|
|
49
|
+
|
|
50
|
+
const result = await daemon.innerchat.ask(fromAgent.id, toAgent.id, message.trim(), { timeoutMs });
|
|
51
|
+
|
|
52
|
+
res.json({
|
|
53
|
+
from: toAgent.name,
|
|
54
|
+
reply: result.reply,
|
|
55
|
+
threadId: result.threadId,
|
|
56
|
+
exchangesUsed: result.exchanges,
|
|
57
|
+
exchangesRemaining: result.remaining,
|
|
58
|
+
maxExchanges: MAX_EXCHANGES,
|
|
59
|
+
});
|
|
14
60
|
} catch (err) {
|
|
15
|
-
|
|
61
|
+
// The agent reads this body — keep it actionable, it's the whole signal.
|
|
62
|
+
res.status(409).json({ error: err.message });
|
|
16
63
|
}
|
|
17
64
|
});
|
|
18
65
|
|
|
19
|
-
app.get('/api/innerchat/
|
|
66
|
+
app.get('/api/innerchat/threads', (req, res) => {
|
|
20
67
|
const { agentId } = req.query;
|
|
21
|
-
res.json({
|
|
68
|
+
res.json({ threads: daemon.innerchat.getThreads(agentId || null) });
|
|
22
69
|
});
|
|
23
70
|
|
|
24
|
-
app.get('/api/innerchat/
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
res.json(
|
|
71
|
+
app.get('/api/innerchat/threads/:id', (req, res) => {
|
|
72
|
+
const thread = daemon.innerchat.getThread(req.params.id);
|
|
73
|
+
if (!thread) return res.status(404).json({ error: 'Thread not found' });
|
|
74
|
+
res.json(thread);
|
|
28
75
|
});
|
|
29
76
|
|
|
30
77
|
app.get('/api/innerchat/pending/:agentId', (req, res) => {
|
|
@@ -26,6 +26,17 @@ export function registerTeamRoutes(app, daemon) {
|
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
// Reorder the sidebar list. Registered before /api/teams/:id so Express
|
|
30
|
+
// doesn't capture "reorder" as a team id.
|
|
31
|
+
app.post('/api/teams/reorder', (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const teams = daemon.teams.reorder(req.body?.orderedIds);
|
|
34
|
+
res.json({ teams });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
res.status(400).json({ error: err.message });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
29
40
|
app.get('/api/teams/archived', (req, res) => {
|
|
30
41
|
res.json({ archived: daemon.teams.listArchived() });
|
|
31
42
|
});
|
|
@@ -113,6 +113,34 @@ export class Teams {
|
|
|
113
113
|
return team;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Reorder the team list. Teams persist as a JSON array, so Map insertion
|
|
118
|
+
* order is the display order — rebuilding the Map is the whole operation.
|
|
119
|
+
*
|
|
120
|
+
* Ids missing from `orderedIds` keep their relative order at the end, so a
|
|
121
|
+
* client working from a stale list can't drop teams.
|
|
122
|
+
*/
|
|
123
|
+
reorder(orderedIds) {
|
|
124
|
+
if (!Array.isArray(orderedIds)) throw new Error('orderedIds must be an array');
|
|
125
|
+
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const next = new Map();
|
|
128
|
+
for (const id of orderedIds) {
|
|
129
|
+
const team = this.teams.get(id);
|
|
130
|
+
if (!team || seen.has(id)) continue;
|
|
131
|
+
seen.add(id);
|
|
132
|
+
next.set(id, team);
|
|
133
|
+
}
|
|
134
|
+
for (const [id, team] of this.teams) {
|
|
135
|
+
if (!seen.has(id)) next.set(id, team);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.teams = next;
|
|
139
|
+
this._save();
|
|
140
|
+
this.daemon.broadcast({ type: 'teams:reordered', teams: this.list() });
|
|
141
|
+
return this.list();
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
get(id) {
|
|
117
145
|
return this.teams.get(id) || null;
|
|
118
146
|
}
|
|
@@ -148,11 +176,17 @@ export class Teams {
|
|
|
148
176
|
renameSync(oldWorkingDir, newWorkingDir);
|
|
149
177
|
team.workingDir = newWorkingDir;
|
|
150
178
|
|
|
151
|
-
//
|
|
179
|
+
// Repoint every agent under the old tree, not just those sitting at
|
|
180
|
+
// its root — an agent in a subdirectory would otherwise keep a path
|
|
181
|
+
// that no longer exists.
|
|
152
182
|
const agents = this.daemon.registry.getAll().filter((a) => a.teamId === id);
|
|
153
183
|
for (const agent of agents) {
|
|
184
|
+
if (!agent.workingDir) continue;
|
|
154
185
|
if (agent.workingDir === oldWorkingDir) {
|
|
155
186
|
this.daemon.registry.update(agent.id, { workingDir: newWorkingDir });
|
|
187
|
+
} else if (agent.workingDir.startsWith(`${oldWorkingDir}/`)) {
|
|
188
|
+
const suffix = agent.workingDir.slice(oldWorkingDir.length);
|
|
189
|
+
this.daemon.registry.update(agent.id, { workingDir: `${newWorkingDir}${suffix}` });
|
|
156
190
|
}
|
|
157
191
|
}
|
|
158
192
|
} catch (err) {
|