groove-dev 0.27.14 → 0.27.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +587 -68
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +1 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +373 -58
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +32 -132
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
|
@@ -258,6 +258,9 @@ MANDATORY RULES:
|
|
|
258
258
|
|
|
259
259
|
IMPORTANT: Do not use markdown formatting like ** or ### in your output. Write in plain text with clean formatting. Use line breaks, dashes, and indentation for structure.
|
|
260
260
|
|
|
261
|
+
`,
|
|
262
|
+
ambassador: `You are an Ambassador agent — the sole bridge between this Groove daemon and a federated peer. You communicate with the remote Ambassador using diplomatic pouch messages. You can read the local codebase for context. Your ONLY outbound channel is the federation pouch system. When you receive work from your local team, package it as a task-request and send it to your peer. When you receive results from your peer, deliver them to your local team. Report results to your local team. Priority order: deliver to the planner agent first. If no planner exists, deliver to the fullstack agent. If neither exists, broadcast to all running agents. Use the GROOVE coordination API at http://localhost:31415 to discover running agents (GET /api/agents) and instruct them (POST /api/agents/:id/instruct). You do NOT write code or modify files. You translate, negotiate, and coordinate.
|
|
263
|
+
|
|
261
264
|
`,
|
|
262
265
|
};
|
|
263
266
|
|
|
@@ -282,6 +285,24 @@ export class ProcessManager {
|
|
|
282
285
|
async spawn(config) {
|
|
283
286
|
const { registry, locks, introducer } = this.daemon;
|
|
284
287
|
|
|
288
|
+
// Validate workingDir is within the project directory
|
|
289
|
+
if (config.workingDir) {
|
|
290
|
+
const resolved = resolve(config.workingDir);
|
|
291
|
+
const projResolved = resolve(this.daemon?.projectDir || process.cwd());
|
|
292
|
+
if (!resolved.startsWith(projResolved)) {
|
|
293
|
+
throw new Error('workingDir must be within project directory');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Ambassador spawn guard: one ambassador per federation peer
|
|
298
|
+
if (config.role === 'ambassador') {
|
|
299
|
+
const peerId = config.peerId || config.metadata?.peerId;
|
|
300
|
+
if (!peerId) throw new Error('Ambassador agents require a peerId');
|
|
301
|
+
if (this.daemon.federation?.ambassadors?.hasAmbassadorForPeer(peerId)) {
|
|
302
|
+
throw new Error(`Ambassador already exists for peer ${peerId}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
285
306
|
// Clean stale recommended-team.json when spawning a new planner
|
|
286
307
|
if (config.role === 'planner') {
|
|
287
308
|
const dirs = [this.daemon.grooveDir];
|
|
@@ -350,6 +371,15 @@ export class ProcessManager {
|
|
|
350
371
|
locks.register(agent.id, agent.scope);
|
|
351
372
|
}
|
|
352
373
|
|
|
374
|
+
// Register ambassador with federation system
|
|
375
|
+
if (config.role === 'ambassador') {
|
|
376
|
+
const peerId = config.peerId || config.metadata?.peerId;
|
|
377
|
+
if (peerId && this.daemon.federation?.ambassadors) {
|
|
378
|
+
this.daemon.federation.ambassadors.registerAmbassador(peerId, agent.id);
|
|
379
|
+
registry.update(agent.id, { metadata: { ...agent.metadata, peerId } });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
353
383
|
// For slides-role agents, write the baked-in layout engine into the working
|
|
354
384
|
// directory. The agent imports it as `./groove-slides.cjs`. Core layout rules
|
|
355
385
|
// (Y-cursor, fitFontSize, collision/bounds/wrap gates) ship with Groove so
|
|
@@ -393,7 +423,7 @@ export class ProcessManager {
|
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
// Generate introduction context (team awareness + negotiation)
|
|
396
|
-
const introContext = introducer.generateContext(agent, { taskNegotiation });
|
|
426
|
+
const introContext = introducer.generateContext(agent, { taskNegotiation, hasTask: !!config.prompt });
|
|
397
427
|
|
|
398
428
|
// Track cold-start savings — agent gets context from planner/journalist/team
|
|
399
429
|
// instead of exploring the codebase from scratch
|
|
@@ -557,6 +587,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
557
587
|
const files = this.daemon.journalist?.getAgentFiles(agent) || [];
|
|
558
588
|
if (files.length > 0) this._triggerIdleQC(agent);
|
|
559
589
|
this._processHandoffs(agent);
|
|
590
|
+
this._writeCompletionHandoff(agent);
|
|
560
591
|
}
|
|
561
592
|
});
|
|
562
593
|
|
|
@@ -595,13 +626,14 @@ For normal file edits within your scope, proceed without review.
|
|
|
595
626
|
const spawnLine = `[${new Date().toISOString()}] GROOVE spawning: ${command} [${safeArgs.length} args]\n`;
|
|
596
627
|
logStream.write(spawnLine);
|
|
597
628
|
|
|
598
|
-
// Inject
|
|
599
|
-
// (
|
|
629
|
+
// Inject only the API key for the agent's own provider — least-privilege principle
|
|
630
|
+
// (avoids leaking unrelated credentials into agent environments)
|
|
600
631
|
if (this.daemon.credentials) {
|
|
601
|
-
|
|
602
|
-
|
|
632
|
+
const agentProvider = agent.provider || config.provider;
|
|
633
|
+
if (agentProvider) {
|
|
634
|
+
const meta = getProvider(agentProvider);
|
|
603
635
|
if (meta?.constructor?.envKey) {
|
|
604
|
-
const storedKey = this.daemon.credentials.getKey(
|
|
636
|
+
const storedKey = this.daemon.credentials.getKey(agentProvider);
|
|
605
637
|
if (storedKey) {
|
|
606
638
|
env[meta.constructor.envKey] = storedKey;
|
|
607
639
|
}
|
|
@@ -700,6 +732,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
700
732
|
this.daemon.integrations.refreshMcpJson();
|
|
701
733
|
}
|
|
702
734
|
|
|
735
|
+
// Extract recommended-team.json from planner text output if it wasn't written to disk.
|
|
736
|
+
// Non-Claude providers (Codex, Gemini) may embed the JSON in text rather than using Write.
|
|
737
|
+
if (finalStatus === 'completed' && agent.role === 'planner') {
|
|
738
|
+
this._extractRecommendedTeam(agent, logPath);
|
|
739
|
+
}
|
|
740
|
+
|
|
703
741
|
// Trigger journalist synthesis immediately on completion so the project
|
|
704
742
|
// map is fresh for the next agent that spawns (don't wait for 120s cycle)
|
|
705
743
|
if (finalStatus === 'completed' && this.daemon.journalist) {
|
|
@@ -714,8 +752,8 @@ For normal file edits within your scope, proceed without review.
|
|
|
714
752
|
if (finalStatus === 'completed') {
|
|
715
753
|
const files = this.daemon.journalist?.getAgentFiles(agent) || [];
|
|
716
754
|
if (files.length > 0) this._triggerIdleQC(agent);
|
|
717
|
-
// Process cross-scope handoff requests from this agent
|
|
718
755
|
this._processHandoffs(agent);
|
|
756
|
+
this._writeCompletionHandoff(agent);
|
|
719
757
|
}
|
|
720
758
|
|
|
721
759
|
// Update Layer 7 specialization profile for this agent's session
|
|
@@ -783,8 +821,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
783
821
|
|
|
784
822
|
for (const line of complete.split('\n')) {
|
|
785
823
|
if (!line) continue;
|
|
786
|
-
|
|
787
|
-
|
|
824
|
+
try {
|
|
825
|
+
const output = provider.parseOutput(line);
|
|
826
|
+
if (output) this._handleAgentOutput(agentId, output);
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error(`[Groove] parseOutput error for ${agentId}: ${err.message}`);
|
|
829
|
+
}
|
|
788
830
|
}
|
|
789
831
|
}
|
|
790
832
|
|
|
@@ -849,6 +891,27 @@ For normal file edits within your scope, proceed without review.
|
|
|
849
891
|
this.daemon.broadcast({ type: 'agent:output', agentId, data: output });
|
|
850
892
|
}
|
|
851
893
|
|
|
894
|
+
_extractRecommendedTeam(agent, logPath) {
|
|
895
|
+
try {
|
|
896
|
+
const workDir = agent.workingDir || this.daemon.projectDir;
|
|
897
|
+
const grooveDir = resolve(workDir, '.groove');
|
|
898
|
+
const targetPath = resolve(grooveDir, 'recommended-team.json');
|
|
899
|
+
|
|
900
|
+
if (existsSync(targetPath)) return;
|
|
901
|
+
|
|
902
|
+
const log = readFileSync(logPath, 'utf8');
|
|
903
|
+
const match = log.match(/\{[\s\S]*?"agents"\s*:\s*\[[\s\S]*?\]\s*\}/);
|
|
904
|
+
if (!match) return;
|
|
905
|
+
|
|
906
|
+
let parsed;
|
|
907
|
+
try { parsed = JSON.parse(match[0]); } catch { return; }
|
|
908
|
+
if (!parsed || !Array.isArray(parsed.agents) || parsed.agents.length === 0) return;
|
|
909
|
+
|
|
910
|
+
if (!existsSync(grooveDir)) mkdirSync(grooveDir, { recursive: true });
|
|
911
|
+
writeFileSync(targetPath, JSON.stringify(parsed, null, 2));
|
|
912
|
+
} catch { /* best effort */ }
|
|
913
|
+
}
|
|
914
|
+
|
|
852
915
|
/**
|
|
853
916
|
* Check if a completed/crashed agent was the last phase 1 agent in a team.
|
|
854
917
|
* If so, auto-spawn the phase 2 (QC/finisher) agents.
|
|
@@ -993,6 +1056,46 @@ For normal file edits within your scope, proceed without review.
|
|
|
993
1056
|
});
|
|
994
1057
|
}
|
|
995
1058
|
|
|
1059
|
+
_writeCompletionHandoff(agent) {
|
|
1060
|
+
if (!this.daemon.memory || !this.daemon.journalist) return;
|
|
1061
|
+
try {
|
|
1062
|
+
const agentData = this.daemon.registry.get(agent.id);
|
|
1063
|
+
const filteredLogs = this.daemon.journalist.collectFilteredLogs([agent]);
|
|
1064
|
+
const agentLog = filteredLogs[agent.id];
|
|
1065
|
+
const entries = agentLog?.entries || [];
|
|
1066
|
+
const files = this.daemon.journalist.getAgentFiles(agent) || [];
|
|
1067
|
+
|
|
1068
|
+
const toolSummary = entries
|
|
1069
|
+
.filter(e => e.type === 'tool')
|
|
1070
|
+
.map(e => `- ${e.tool}: ${e.input}`)
|
|
1071
|
+
.slice(-15)
|
|
1072
|
+
.join('\n');
|
|
1073
|
+
|
|
1074
|
+
const errorSummary = entries
|
|
1075
|
+
.filter(e => e.type === 'error')
|
|
1076
|
+
.map(e => `- ${e.text}`)
|
|
1077
|
+
.slice(-5)
|
|
1078
|
+
.join('\n');
|
|
1079
|
+
|
|
1080
|
+
const brief = [
|
|
1081
|
+
`Agent ${agent.name} (${agent.role}) completed.`,
|
|
1082
|
+
agent.prompt ? `Task: ${agent.prompt.slice(0, 300)}` : '',
|
|
1083
|
+
files.length > 0 ? `\nFiles modified:\n${files.slice(0, 15).map(f => '- ' + f).join('\n')}` : '',
|
|
1084
|
+
toolSummary ? `\nRecent actions:\n${toolSummary}` : '',
|
|
1085
|
+
errorSummary ? `\nErrors encountered:\n${errorSummary}` : '',
|
|
1086
|
+
].filter(Boolean).join('\n');
|
|
1087
|
+
|
|
1088
|
+
this.daemon.memory.appendHandoffBrief(agent.role, {
|
|
1089
|
+
timestamp: new Date().toISOString(),
|
|
1090
|
+
agentId: agent.id,
|
|
1091
|
+
reason: 'completed',
|
|
1092
|
+
oldTokens: agentData?.tokensUsed || 0,
|
|
1093
|
+
contextUsage: agentData?.contextUsage || 0,
|
|
1094
|
+
brief: brief.slice(0, 4000),
|
|
1095
|
+
}, agent.workingDir);
|
|
1096
|
+
} catch { /* best-effort */ }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
996
1099
|
/**
|
|
997
1100
|
* Process handoff files in .groove/handoffs/.
|
|
998
1101
|
* Agents write handoff requests when they need cross-scope work from a teammate.
|
|
@@ -1002,6 +1105,9 @@ For normal file edits within your scope, proceed without review.
|
|
|
1002
1105
|
const handoffsDir = resolve(this.daemon.grooveDir, 'handoffs');
|
|
1003
1106
|
if (!existsSync(handoffsDir)) return;
|
|
1004
1107
|
|
|
1108
|
+
const MAX_HANDOFFS_PER_ROLE = 3;
|
|
1109
|
+
if (!this.daemon._handoffCounts) this.daemon._handoffCounts = new Map();
|
|
1110
|
+
|
|
1005
1111
|
const registry = this.daemon.registry;
|
|
1006
1112
|
let files;
|
|
1007
1113
|
try { files = readdirSync(handoffsDir); } catch { return; }
|
|
@@ -1011,6 +1117,14 @@ For normal file edits within your scope, proceed without review.
|
|
|
1011
1117
|
const targetRole = file.replace(/\.md$/, '');
|
|
1012
1118
|
const filePath = resolve(handoffsDir, file);
|
|
1013
1119
|
|
|
1120
|
+
const roleKey = `${sourceAgent.teamId}:${targetRole}`;
|
|
1121
|
+
const count = this.daemon._handoffCounts.get(roleKey) || 0;
|
|
1122
|
+
if (count >= MAX_HANDOFFS_PER_ROLE) {
|
|
1123
|
+
console.warn(`[Groove] Handoff to ${targetRole} skipped — cycle cap reached (${count}/${MAX_HANDOFFS_PER_ROLE})`);
|
|
1124
|
+
try { unlinkSync(filePath); } catch {}
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1014
1128
|
let content;
|
|
1015
1129
|
try { content = readFileSync(filePath, 'utf8').trim(); } catch { continue; }
|
|
1016
1130
|
if (!content) { try { unlinkSync(filePath); } catch {} continue; }
|
|
@@ -1029,6 +1143,8 @@ For normal file edits within your scope, proceed without review.
|
|
|
1029
1143
|
continue;
|
|
1030
1144
|
}
|
|
1031
1145
|
|
|
1146
|
+
this.daemon._handoffCounts.set(roleKey, count + 1);
|
|
1147
|
+
|
|
1032
1148
|
// Wake the target agent with the handoff request
|
|
1033
1149
|
const message = `Cross-scope handoff from ${sourceAgent.name} (${sourceAgent.role}):\n\n${content}`;
|
|
1034
1150
|
this.daemon.processes.resume(target.id, message).then((newAgent) => {
|
|
@@ -1072,13 +1188,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1072
1188
|
const config = { ...agent };
|
|
1073
1189
|
const sessionId = agent.sessionId;
|
|
1074
1190
|
|
|
1075
|
-
//
|
|
1191
|
+
// Stop if running, then remove old entry so we can re-register with same name
|
|
1076
1192
|
if (this.handles.has(agentId)) {
|
|
1077
1193
|
await this.kill(agentId);
|
|
1078
|
-
} else {
|
|
1079
|
-
locks.release(agentId);
|
|
1080
|
-
registry.remove(agentId);
|
|
1081
1194
|
}
|
|
1195
|
+
registry.remove(agentId);
|
|
1196
|
+
locks.release(agentId);
|
|
1082
1197
|
|
|
1083
1198
|
// Build resume command
|
|
1084
1199
|
const { command, args, env } = provider.buildResumeCommand(sessionId, message, config.model);
|
|
@@ -1199,11 +1314,19 @@ For normal file edits within your scope, proceed without review.
|
|
|
1199
1314
|
|
|
1200
1315
|
async kill(agentId) {
|
|
1201
1316
|
this.peakContextUsage.delete(agentId);
|
|
1317
|
+
|
|
1318
|
+
// Unregister ambassador if this agent was one
|
|
1319
|
+
const agent = this.daemon.registry.get(agentId);
|
|
1320
|
+
if (agent?.role === 'ambassador' && agent?.metadata?.peerId) {
|
|
1321
|
+
this.daemon.federation?.ambassadors?.unregisterAmbassador(agent.metadata.peerId);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1202
1324
|
const handle = this.handles.get(agentId);
|
|
1203
1325
|
|
|
1204
1326
|
if (!handle) {
|
|
1205
|
-
// Not running —
|
|
1206
|
-
|
|
1327
|
+
// Not running — release locks but keep agent in registry.
|
|
1328
|
+
// Spawn's exit handler already set the terminal status.
|
|
1329
|
+
// Callers that want removal must call registry.remove() explicitly.
|
|
1207
1330
|
this.daemon.locks.release(agentId);
|
|
1208
1331
|
return;
|
|
1209
1332
|
}
|
|
@@ -1213,24 +1336,19 @@ For normal file edits within your scope, proceed without review.
|
|
|
1213
1336
|
// Agent loop path — clean async stop
|
|
1214
1337
|
if (loop) {
|
|
1215
1338
|
await loop.stop();
|
|
1216
|
-
//
|
|
1217
|
-
this.handles.delete(agentId);
|
|
1218
|
-
this.daemon.registry.remove(agentId);
|
|
1339
|
+
// Loop exit handler already updated status and cleaned handles
|
|
1219
1340
|
this.daemon.locks.release(agentId);
|
|
1220
1341
|
return;
|
|
1221
1342
|
}
|
|
1222
1343
|
|
|
1223
|
-
// CLI process path
|
|
1344
|
+
// CLI process path — spawn's exit handler sets status='killed' for SIGTERM
|
|
1224
1345
|
return new Promise((resolveKill) => {
|
|
1225
|
-
// Give the process 5s to exit gracefully
|
|
1226
1346
|
const forceTimer = setTimeout(() => {
|
|
1227
1347
|
try { proc.kill('SIGKILL'); } catch {}
|
|
1228
1348
|
}, 5000);
|
|
1229
1349
|
|
|
1230
1350
|
proc.on('exit', () => {
|
|
1231
1351
|
clearTimeout(forceTimer);
|
|
1232
|
-
this.handles.delete(agentId);
|
|
1233
|
-
this.daemon.registry.remove(agentId);
|
|
1234
1352
|
this.daemon.locks.release(agentId);
|
|
1235
1353
|
resolveKill();
|
|
1236
1354
|
});
|
|
@@ -1241,7 +1359,6 @@ For normal file edits within your scope, proceed without review.
|
|
|
1241
1359
|
// Already dead
|
|
1242
1360
|
clearTimeout(forceTimer);
|
|
1243
1361
|
this.handles.delete(agentId);
|
|
1244
|
-
this.daemon.registry.remove(agentId);
|
|
1245
1362
|
this.daemon.locks.release(agentId);
|
|
1246
1363
|
resolveKill();
|
|
1247
1364
|
}
|
|
@@ -6,6 +6,7 @@ export class Provider {
|
|
|
6
6
|
static displayName = 'Base Provider';
|
|
7
7
|
static command = '';
|
|
8
8
|
static authType = 'none'; // 'subscription' | 'api-key' | 'local' | 'none'
|
|
9
|
+
static managesOwnContext = false; // true if provider compacts context internally (e.g. Claude Code)
|
|
9
10
|
static models = [];
|
|
10
11
|
|
|
11
12
|
static isInstalled() {
|
|
@@ -11,6 +11,7 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
11
11
|
static displayName = 'Claude Code';
|
|
12
12
|
static command = 'claude';
|
|
13
13
|
static authType = 'subscription';
|
|
14
|
+
static managesOwnContext = true; // Claude Code compacts context internally (~25-37% → 2-8%)
|
|
14
15
|
static models = [
|
|
15
16
|
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', tier: 'heavy', contextWindow: 1_000_000 },
|
|
16
17
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', tier: 'medium', contextWindow: 200_000 },
|
|
@@ -16,12 +16,12 @@ export class CodexProvider extends Provider {
|
|
|
16
16
|
// Auth hint — Codex uses its own auth system, not just env vars
|
|
17
17
|
static authHint = 'Codex requires `codex login` — run: echo "YOUR_KEY" | codex login --with-api-key';
|
|
18
18
|
static models = [
|
|
19
|
-
{ id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', tier: 'heavy', pricing: { input: 0.015, output: 0.06 } },
|
|
20
|
-
{ id: 'gpt-5.4', name: 'GPT-5.4', tier: 'heavy', pricing: { input: 0.005, output: 0.02 } },
|
|
21
|
-
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini', tier: 'medium', pricing: { input: 0.001, output: 0.004 } },
|
|
22
|
-
{ id: 'gpt-5.4-nano', name: 'GPT-5.4 Nano', tier: 'light', pricing: { input: 0.0004, output: 0.0016 } },
|
|
23
|
-
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', tier: 'medium', pricing: { input: 0.0005, output: 0.002 } },
|
|
24
|
-
{ id: 'gpt-5-nano', name: 'GPT-5 Nano', tier: 'light', pricing: { input: 0.0001, output: 0.0004 } },
|
|
19
|
+
{ id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', tier: 'heavy', maxContext: 200000, pricing: { input: 0.015, output: 0.06 } },
|
|
20
|
+
{ id: 'gpt-5.4', name: 'GPT-5.4', tier: 'heavy', maxContext: 200000, pricing: { input: 0.005, output: 0.02 } },
|
|
21
|
+
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini', tier: 'medium', maxContext: 200000, pricing: { input: 0.001, output: 0.004 } },
|
|
22
|
+
{ id: 'gpt-5.4-nano', name: 'GPT-5.4 Nano', tier: 'light', maxContext: 200000, pricing: { input: 0.0004, output: 0.0016 } },
|
|
23
|
+
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', tier: 'medium', maxContext: 200000, pricing: { input: 0.0005, output: 0.002 } },
|
|
24
|
+
{ id: 'gpt-5-nano', name: 'GPT-5 Nano', tier: 'light', maxContext: 200000, pricing: { input: 0.0001, output: 0.0004 } },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
static isInstalled() {
|
|
@@ -83,16 +83,19 @@ export class CodexProvider extends Provider {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
buildSpawnCommand(agent) {
|
|
86
|
-
// Use 'codex exec' for non-interactive (headless) operation
|
|
87
86
|
const args = ['exec'];
|
|
88
87
|
|
|
89
88
|
if (agent.model) args.push('--model', agent.model);
|
|
90
89
|
|
|
91
|
-
|
|
90
|
+
args.push('--json');
|
|
92
91
|
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
93
92
|
|
|
93
|
+
if (agent.workingDir) args.push('-C', agent.workingDir);
|
|
94
|
+
|
|
94
95
|
if (agent.prompt) args.push(agent.prompt);
|
|
95
96
|
|
|
97
|
+
this._currentModel = agent.model;
|
|
98
|
+
|
|
96
99
|
return {
|
|
97
100
|
command: 'codex',
|
|
98
101
|
args,
|
|
@@ -101,7 +104,7 @@ export class CodexProvider extends Provider {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
buildHeadlessCommand(prompt, model) {
|
|
104
|
-
const args = ['exec', prompt];
|
|
107
|
+
const args = ['exec', '--json', prompt];
|
|
105
108
|
if (model) args.push('--model', model);
|
|
106
109
|
return { command: 'codex', args, env: {} };
|
|
107
110
|
}
|
|
@@ -120,33 +123,126 @@ export class CodexProvider extends Provider {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
parseOutput(line) {
|
|
123
|
-
// Codex outputs plain text and stderr logging
|
|
124
126
|
const trimmed = line.trim();
|
|
125
127
|
if (!trimmed) return null;
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
let event;
|
|
128
130
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
event = JSON.parse(trimmed);
|
|
132
|
+
} catch {
|
|
133
|
+
return { type: 'activity', data: trimmed };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
switch (event.type) {
|
|
137
|
+
case 'thread.started':
|
|
138
|
+
return { type: 'activity', subtype: 'assistant', sessionId: event.thread_id, data: [{ type: 'text', text: '' }] };
|
|
139
|
+
|
|
140
|
+
case 'turn.started':
|
|
141
|
+
return null;
|
|
142
|
+
|
|
143
|
+
case 'item.started': {
|
|
144
|
+
const item = event.item || {};
|
|
145
|
+
if (item.type === 'command_execution') {
|
|
146
|
+
return {
|
|
147
|
+
type: 'activity', subtype: 'assistant',
|
|
148
|
+
data: [{ type: 'tool_use', id: item.id || 'exec', name: 'Bash', input: { command: item.command } }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (item.type === 'todo_list') {
|
|
152
|
+
const steps = (item.items || []).map((s) => s.text).join(', ');
|
|
153
|
+
return {
|
|
154
|
+
type: 'activity', subtype: 'assistant',
|
|
155
|
+
data: [{ type: 'tool_use', id: item.id || 'plan', name: 'Plan', input: { steps } }],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (item.type === 'file_edit' || item.type === 'file_write') {
|
|
159
|
+
return {
|
|
160
|
+
type: 'activity', subtype: 'assistant',
|
|
161
|
+
data: [{ type: 'tool_use', id: item.id || 'edit', name: item.type === 'file_write' ? 'Write' : 'Edit', input: { path: item.path || item.file || '' } }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (item.type === 'file_read') {
|
|
165
|
+
return {
|
|
166
|
+
type: 'activity', subtype: 'assistant',
|
|
167
|
+
data: [{ type: 'tool_use', id: item.id || 'read', name: 'Read', input: { path: item.path || item.file || '' } }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
132
170
|
return {
|
|
133
|
-
type: 'activity',
|
|
134
|
-
|
|
135
|
-
costSource: 'estimated',
|
|
171
|
+
type: 'activity', subtype: 'assistant',
|
|
172
|
+
data: [{ type: 'tool_use', id: item.id || 'tool', name: item.type || 'Tool', input: {} }],
|
|
136
173
|
};
|
|
137
174
|
}
|
|
138
|
-
} catch { /* plain text */ }
|
|
139
175
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
176
|
+
case 'item.completed': {
|
|
177
|
+
const item = event.item || {};
|
|
178
|
+
if (item.type === 'agent_message') {
|
|
179
|
+
return {
|
|
180
|
+
type: 'activity', subtype: 'assistant',
|
|
181
|
+
data: [{ type: 'text', text: item.text || '' }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (item.type === 'command_execution') {
|
|
185
|
+
const output = (item.aggregated_output || '').slice(0, 2000);
|
|
186
|
+
return {
|
|
187
|
+
type: 'activity', subtype: 'assistant',
|
|
188
|
+
data: [
|
|
189
|
+
{ type: 'tool_use', id: item.id || 'exec', name: 'Bash', input: { command: item.command } },
|
|
190
|
+
...(output ? [{ type: 'text', text: output }] : []),
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (item.type === 'todo_list') {
|
|
195
|
+
const steps = (item.items || []).map((s) => `${s.completed ? '✓' : '○'} ${s.text}`).join('\n');
|
|
196
|
+
return {
|
|
197
|
+
type: 'activity', subtype: 'assistant',
|
|
198
|
+
data: [{ type: 'text', text: steps }],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
|
|
202
|
+
return {
|
|
203
|
+
type: 'activity', subtype: 'assistant',
|
|
204
|
+
data: [{ type: 'tool_use', id: item.id || 'file', name: item.type === 'file_read' ? 'Read' : item.type === 'file_write' ? 'Write' : 'Edit', input: { path: item.path || item.file || '' } }],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
143
209
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
case 'turn.completed': {
|
|
211
|
+
const usage = event.usage;
|
|
212
|
+
if (!usage) return null;
|
|
213
|
+
|
|
214
|
+
const inputTokens = usage.input_tokens || 0;
|
|
215
|
+
const outputTokens = usage.output_tokens || 0;
|
|
216
|
+
const cachedTokens = usage.cached_input_tokens || 0;
|
|
217
|
+
const totalTokens = inputTokens + outputTokens;
|
|
218
|
+
|
|
219
|
+
const model = CodexProvider.models.find((m) => m.id === this._currentModel);
|
|
220
|
+
const pricing = model?.pricing;
|
|
221
|
+
const maxContext = model?.maxContext || 200000;
|
|
222
|
+
|
|
223
|
+
let estimatedCostUsd = 0;
|
|
224
|
+
if (pricing) {
|
|
225
|
+
const newInput = inputTokens - cachedTokens;
|
|
226
|
+
estimatedCostUsd = (newInput / 1000) * pricing.input
|
|
227
|
+
+ (cachedTokens / 1000) * pricing.input * 0.5
|
|
228
|
+
+ (outputTokens / 1000) * pricing.output;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
type: 'activity', subtype: 'assistant',
|
|
233
|
+
data: [{ type: 'text', text: '' }],
|
|
234
|
+
tokensUsed: totalTokens,
|
|
235
|
+
inputTokens,
|
|
236
|
+
outputTokens,
|
|
237
|
+
cacheReadTokens: cachedTokens,
|
|
238
|
+
contextUsage: inputTokens / maxContext,
|
|
239
|
+
estimatedCostUsd,
|
|
240
|
+
costSource: pricing ? 'calculated' : 'estimated',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
default:
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
151
247
|
}
|
|
152
248
|
}
|
|
@@ -11,11 +11,11 @@ export class GeminiProvider extends Provider {
|
|
|
11
11
|
static authType = 'api-key';
|
|
12
12
|
static envKey = 'GEMINI_API_KEY';
|
|
13
13
|
static models = [
|
|
14
|
-
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', tier: 'heavy', pricing: { input: 0.00125, output: 0.01 } },
|
|
15
|
-
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', tier: 'medium', pricing: { input: 0.00015, output: 0.0006 } },
|
|
16
|
-
{ id: 'gemini-3.1-flash-lite-preview', name: 'Gemini 3.1 Flash Lite', tier: 'light', pricing: { input: 0.000075, output: 0.0003 } },
|
|
17
|
-
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', tier: 'heavy', pricing: { input: 0.00125, output: 0.01 } },
|
|
18
|
-
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', tier: 'medium', pricing: { input: 0.00015, output: 0.0006 } },
|
|
14
|
+
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', tier: 'heavy', maxContext: 1000000, pricing: { input: 0.00125, output: 0.01 } },
|
|
15
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', tier: 'medium', maxContext: 1000000, pricing: { input: 0.00015, output: 0.0006 } },
|
|
16
|
+
{ id: 'gemini-3.1-flash-lite-preview', name: 'Gemini 3.1 Flash Lite', tier: 'light', maxContext: 1000000, pricing: { input: 0.000075, output: 0.0003 } },
|
|
17
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', tier: 'heavy', maxContext: 1000000, pricing: { input: 0.00125, output: 0.01 } },
|
|
18
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', tier: 'medium', maxContext: 1000000, pricing: { input: 0.00015, output: 0.0006 } },
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
static isInstalled() {
|
|
@@ -36,12 +36,12 @@ export class GeminiProvider extends Provider {
|
|
|
36
36
|
|
|
37
37
|
if (agent.model) args.push('--model', agent.model);
|
|
38
38
|
|
|
39
|
-
// YOLO mode — auto-approve all tool calls (file writes, shell commands)
|
|
40
|
-
// Without this, Gemini in headless mode can only output text
|
|
41
39
|
args.push('--yolo');
|
|
40
|
+
args.push('--output-format', 'stream-json');
|
|
41
|
+
args.push('-p', '');
|
|
42
|
+
|
|
43
|
+
this._currentModel = agent.model;
|
|
42
44
|
|
|
43
|
-
// Pass prompt via stdin to avoid OS arg length limits
|
|
44
|
-
// (intro context + role prompt + skill content can be very long)
|
|
45
45
|
return {
|
|
46
46
|
command: 'gemini',
|
|
47
47
|
args,
|
|
@@ -51,7 +51,7 @@ export class GeminiProvider extends Provider {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
buildHeadlessCommand(prompt, model) {
|
|
54
|
-
const args = ['-p', prompt];
|
|
54
|
+
const args = ['--output-format', 'stream-json', '-p', prompt];
|
|
55
55
|
if (model) args.push('--model', model);
|
|
56
56
|
return { command: 'gemini', args, env: {} };
|
|
57
57
|
}
|
|
@@ -71,12 +71,99 @@ export class GeminiProvider extends Provider {
|
|
|
71
71
|
parseOutput(line) {
|
|
72
72
|
const trimmed = line.trim();
|
|
73
73
|
if (!trimmed) return null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
74
|
+
|
|
75
|
+
let event;
|
|
76
|
+
try {
|
|
77
|
+
event = JSON.parse(trimmed);
|
|
78
|
+
} catch {
|
|
79
|
+
return { type: 'activity', data: trimmed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (event.type) {
|
|
83
|
+
case 'agent_start':
|
|
84
|
+
return { type: 'activity', subtype: 'assistant', sessionId: event.streamId, data: [{ type: 'text', text: '' }] };
|
|
85
|
+
|
|
86
|
+
case 'session_update':
|
|
87
|
+
return null;
|
|
88
|
+
|
|
89
|
+
case 'message': {
|
|
90
|
+
if (event.role === 'user') return null;
|
|
91
|
+
const raw = event.content;
|
|
92
|
+
const parts = Array.isArray(raw) ? raw : (typeof raw === 'string' ? [{ text: raw }] : raw ? [raw] : []);
|
|
93
|
+
const blocks = parts.map((p) => {
|
|
94
|
+
if (p.type === 'thought') return { type: 'text', text: p.thought || '' };
|
|
95
|
+
return { type: 'text', text: p.text || '' };
|
|
96
|
+
}).filter((b) => b.text);
|
|
97
|
+
if (!blocks.length) return null;
|
|
98
|
+
return { type: 'activity', subtype: 'assistant', data: blocks };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'tool_request': {
|
|
102
|
+
const toolName = (event.name || '').includes('shell') || (event.name || '').includes('exec')
|
|
103
|
+
? 'Bash' : event.name || 'Tool';
|
|
104
|
+
const input = event.name === 'Bash' || (event.name || '').includes('shell')
|
|
105
|
+
? { command: typeof event.args === 'string' ? event.args : JSON.stringify(event.args || {}) }
|
|
106
|
+
: (event.args || {});
|
|
107
|
+
return {
|
|
108
|
+
type: 'activity', subtype: 'assistant',
|
|
109
|
+
data: [{ type: 'tool_use', id: event.requestId || 'tool', name: toolName, input }],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'tool_response': {
|
|
114
|
+
const rawContent = event.content;
|
|
115
|
+
const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
|
|
116
|
+
const content = contentParts.map((p) => p.text || '').join('').slice(0, 2000);
|
|
117
|
+
const toolName = (event.name || '').includes('shell') || (event.name || '').includes('exec')
|
|
118
|
+
? 'Bash' : event.name || 'Tool';
|
|
119
|
+
return {
|
|
120
|
+
type: 'activity', subtype: 'assistant',
|
|
121
|
+
data: [
|
|
122
|
+
{ type: 'tool_use', id: event.requestId || 'tool', name: toolName, input: {} },
|
|
123
|
+
...(content ? [{ type: 'text', text: content }] : []),
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'usage': {
|
|
129
|
+
const inputTokens = event.inputTokens || 0;
|
|
130
|
+
const outputTokens = event.outputTokens || 0;
|
|
131
|
+
const cachedTokens = event.cachedTokens || 0;
|
|
132
|
+
const totalTokens = inputTokens + outputTokens;
|
|
133
|
+
|
|
134
|
+
const model = GeminiProvider.models.find((m) => m.id === this._currentModel);
|
|
135
|
+
const pricing = model?.pricing;
|
|
136
|
+
const maxContext = model?.maxContext || 1000000;
|
|
137
|
+
|
|
138
|
+
let estimatedCostUsd = 0;
|
|
139
|
+
if (pricing) {
|
|
140
|
+
const newInput = inputTokens - cachedTokens;
|
|
141
|
+
estimatedCostUsd = (newInput / 1000) * pricing.input
|
|
142
|
+
+ (cachedTokens / 1000) * pricing.input * 0.5
|
|
143
|
+
+ (outputTokens / 1000) * pricing.output;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: 'activity', subtype: 'assistant',
|
|
148
|
+
data: [{ type: 'text', text: '' }],
|
|
149
|
+
tokensUsed: totalTokens,
|
|
150
|
+
inputTokens,
|
|
151
|
+
outputTokens,
|
|
152
|
+
cacheReadTokens: cachedTokens,
|
|
153
|
+
contextUsage: inputTokens / maxContext,
|
|
154
|
+
estimatedCostUsd,
|
|
155
|
+
costSource: pricing ? 'calculated' : 'estimated',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'agent_end':
|
|
160
|
+
return { type: 'activity', subtype: 'assistant', data: [{ type: 'text', text: '' }] };
|
|
161
|
+
|
|
162
|
+
case 'error':
|
|
163
|
+
return { type: 'activity', subtype: 'assistant', data: [{ type: 'text', text: `Error: ${event.message || 'unknown'}` }] };
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
81
168
|
}
|
|
82
169
|
}
|