mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
feat(agents): implement first-run experience for project-level sub-agents
This commit is contained in:
@@ -62,7 +62,9 @@ import {
|
||||
SessionStartSource,
|
||||
SessionEndReason,
|
||||
generateSummary,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
type AgentDefinition,
|
||||
type AgentsDiscoveredPayload} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
import { useHistory } from './hooks/useHistoryManager.js';
|
||||
@@ -130,6 +132,10 @@ import {
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
} from './constants.js';
|
||||
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
|
||||
import {
|
||||
NewAgentsNotification,
|
||||
NewAgentsChoice,
|
||||
} from './components/NewAgentsNotification.js';
|
||||
|
||||
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
|
||||
return pendingHistoryItems.some((item) => {
|
||||
@@ -204,6 +210,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
null,
|
||||
);
|
||||
|
||||
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
|
||||
|
||||
const [defaultBannerText, setDefaultBannerText] = useState('');
|
||||
const [warningBannerText, setWarningBannerText] = useState('');
|
||||
const [bannerVisible, setBannerVisible] = useState(true);
|
||||
@@ -370,14 +378,20 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setAdminSettingsChanged(true);
|
||||
};
|
||||
|
||||
const handleAgentsDiscovered = (payload: AgentsDiscoveredPayload) => {
|
||||
setNewAgents(payload.agents);
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.SettingsChanged, handleSettingsChanged);
|
||||
coreEvents.on(CoreEvent.AdminSettingsChanged, handleAdminSettingsChanged);
|
||||
coreEvents.on(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.SettingsChanged, handleSettingsChanged);
|
||||
coreEvents.off(
|
||||
CoreEvent.AdminSettingsChanged,
|
||||
handleAdminSettingsChanged,
|
||||
);
|
||||
coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1836,6 +1850,26 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
);
|
||||
}
|
||||
|
||||
if (newAgents) {
|
||||
const handleNewAgentsSelect = (choice: NewAgentsChoice) => {
|
||||
if (choice === NewAgentsChoice.ACKNOWLEDGE) {
|
||||
const registry = config.getAgentRegistry();
|
||||
newAgents.forEach((agent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
registry.acknowledgeAgent(agent);
|
||||
});
|
||||
}
|
||||
setNewAgents(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<NewAgentsNotification
|
||||
agents={newAgents}
|
||||
onSelect={handleNewAgentsSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
|
||||
90
packages/cli/src/ui/components/NewAgentsNotification.tsx
Normal file
90
packages/cli/src/ui/components/NewAgentsNotification.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { type AgentDefinition } from '@google/gemini-cli-core';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
|
||||
export enum NewAgentsChoice {
|
||||
ACKNOWLEDGE = 'acknowledge',
|
||||
IGNORE = 'ignore',
|
||||
}
|
||||
|
||||
interface NewAgentsNotificationProps {
|
||||
agents: AgentDefinition[];
|
||||
onSelect: (choice: NewAgentsChoice) => void;
|
||||
}
|
||||
|
||||
export const NewAgentsNotification = ({
|
||||
agents,
|
||||
onSelect,
|
||||
}: NewAgentsNotificationProps) => {
|
||||
const options: Array<RadioSelectItem<NewAgentsChoice>> = [
|
||||
{
|
||||
label: 'Acknowledge and Enable',
|
||||
value: NewAgentsChoice.ACKNOWLEDGE,
|
||||
key: 'acknowledge',
|
||||
},
|
||||
{
|
||||
label: 'Do not enable (Ask again next time)',
|
||||
value: NewAgentsChoice.IGNORE,
|
||||
key: 'ignore',
|
||||
},
|
||||
];
|
||||
|
||||
// Limit display to 5 agents to avoid overflow, show count for rest
|
||||
const displayAgents = agents.slice(0, 5);
|
||||
const remaining = agents.length - 5;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
padding={1}
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
New Agents Discovered
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
The following agents were found in this project. Please review them:
|
||||
</Text>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
marginBottom={1}
|
||||
borderStyle="single"
|
||||
padding={1}
|
||||
>
|
||||
{displayAgents.map((agent) => (
|
||||
<Box key={agent.name} flexDirection="column" marginBottom={1}>
|
||||
<Text bold>- {agent.name}</Text>
|
||||
<Text color="gray"> {agent.description}</Text>
|
||||
</Box>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<Text color="gray">... and {remaining} more.</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={onSelect}
|
||||
isFocused={true}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
91
packages/core/src/agents/acknowledgedAgents.ts
Normal file
91
packages/core/src/agents/acknowledgedAgents.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export interface AcknowledgedAgentsMap {
|
||||
// Project Path -> Agent Name -> Agent Hash
|
||||
[projectPath: string]: {
|
||||
[agentName: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AcknowledgedAgentsService {
|
||||
private static instance: AcknowledgedAgentsService;
|
||||
private acknowledgedAgents: AcknowledgedAgentsMap = {};
|
||||
private loaded = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): AcknowledgedAgentsService {
|
||||
if (!AcknowledgedAgentsService.instance) {
|
||||
AcknowledgedAgentsService.instance = new AcknowledgedAgentsService();
|
||||
}
|
||||
return AcknowledgedAgentsService.instance;
|
||||
}
|
||||
|
||||
static resetInstanceForTesting(): void {
|
||||
// @ts-expect-error -- Resetting private static instance for testing purposes
|
||||
AcknowledgedAgentsService.instance = undefined;
|
||||
}
|
||||
|
||||
load(): void {
|
||||
if (this.loaded) return;
|
||||
|
||||
const filePath = Storage.getAcknowledgedAgentsPath();
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
this.acknowledgedAgents = JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error('Failed to load acknowledged agents:', error);
|
||||
// Fallback to empty
|
||||
this.acknowledgedAgents = {};
|
||||
}
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const filePath = Storage.getAcknowledgedAgentsPath();
|
||||
try {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify(this.acknowledgedAgents, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error('Failed to save acknowledged agents:', error);
|
||||
}
|
||||
}
|
||||
|
||||
isAcknowledged(
|
||||
projectPath: string,
|
||||
agentName: string,
|
||||
hash: string,
|
||||
): boolean {
|
||||
this.load();
|
||||
const projectAgents = this.acknowledgedAgents[projectPath];
|
||||
if (!projectAgents) return false;
|
||||
return projectAgents[agentName] === hash;
|
||||
}
|
||||
|
||||
acknowledge(projectPath: string, agentName: string, hash: string): void {
|
||||
this.load();
|
||||
if (!this.acknowledgedAgents[projectPath]) {
|
||||
this.acknowledgedAgents[projectPath] = {};
|
||||
}
|
||||
this.acknowledgedAgents[projectPath][agentName] = hash;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import yaml from 'js-yaml';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { type Dirent } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
import type { AgentDefinition } from './types.js';
|
||||
import {
|
||||
@@ -241,10 +242,12 @@ export async function parseAgentMarkdown(
|
||||
* Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure.
|
||||
*
|
||||
* @param markdown The parsed Markdown/Frontmatter definition.
|
||||
* @param metadata Optional metadata including hash and file path.
|
||||
* @returns The internal AgentDefinition.
|
||||
*/
|
||||
export function markdownToAgentDefinition(
|
||||
markdown: FrontmatterAgentDefinition,
|
||||
metadata?: { hash?: string; filePath?: string },
|
||||
): AgentDefinition {
|
||||
const inputConfig = {
|
||||
inputSchema: {
|
||||
@@ -268,6 +271,7 @@ export function markdownToAgentDefinition(
|
||||
displayName: markdown.display_name,
|
||||
agentCardUrl: markdown.agent_card_url,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,6 +304,7 @@ export function markdownToAgentDefinition(
|
||||
}
|
||||
: undefined,
|
||||
inputConfig,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -346,9 +351,11 @@ export async function loadAgentsFromDirectory(
|
||||
for (const entry of files) {
|
||||
const filePath = path.join(dir, entry.name);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
||||
const agentDefs = await parseAgentMarkdown(filePath);
|
||||
for (const def of agentDefs) {
|
||||
const agent = markdownToAgentDefinition(def);
|
||||
const agent = markdownToAgentDefinition(def, { hash, filePath });
|
||||
result.agents.push(agent);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
*/
|
||||
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
import { CoreEvent, coreEvents } from '../utils/events.js';
|
||||
import type { AgentOverride, Config } from '../config/config.js';
|
||||
import type { AgentDefinition, LocalAgentDefinition } from './types.js';
|
||||
import { loadAgentsFromDirectory } from './agentLoader.js';
|
||||
import { AcknowledgedAgentsService } from './acknowledgedAgents.js';
|
||||
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
||||
import { CliHelpAgent } from './cli-help-agent.js';
|
||||
import { GeneralistAgent } from './generalist-agent.js';
|
||||
@@ -80,6 +81,19 @@ export class AgentRegistry {
|
||||
coreEvents.emitAgentsRefreshed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges and registers a previously unacknowledged agent.
|
||||
*/
|
||||
async acknowledgeAgent(agent: AgentDefinition): Promise<void> {
|
||||
const ackService = AcknowledgedAgentsService.getInstance();
|
||||
const projectRoot = this.config.getProjectRoot();
|
||||
if (agent.metadata?.hash) {
|
||||
ackService.acknowledge(projectRoot, agent.name, agent.metadata.hash);
|
||||
await this.registerAgent(agent);
|
||||
coreEvents.emitAgentsRefreshed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of resources and removes event listeners.
|
||||
*/
|
||||
@@ -122,8 +136,42 @@ export class AgentRegistry {
|
||||
`Agent loading error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const ackService = AcknowledgedAgentsService.getInstance();
|
||||
const projectRoot = this.config.getProjectRoot();
|
||||
const unacknowledgedAgents: AgentDefinition[] = [];
|
||||
|
||||
const agentsToRegister = projectAgents.agents.filter((agent) => {
|
||||
// If it's a remote agent, we might not have a hash, or we handle it differently.
|
||||
// For now, assuming project agents are primarily local .md files with hashes.
|
||||
// If metadata or hash is missing, we default to "safe" (allow) or "unsafe" (block)?
|
||||
// Existing behavior was allow all. To be safe, if we can't identify it, maybe we should block?
|
||||
// But for backward compatibility with existing agents without hash (if any), maybe allow?
|
||||
// Our loader ensures hash is there.
|
||||
if (!agent.metadata?.hash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
ackService.isAcknowledged(
|
||||
projectRoot,
|
||||
agent.name,
|
||||
agent.metadata.hash,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
unacknowledgedAgents.push(agent);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (unacknowledgedAgents.length > 0) {
|
||||
coreEvents.emitAgentsDiscovered(unacknowledgedAgents);
|
||||
}
|
||||
|
||||
await Promise.allSettled(
|
||||
projectAgents.agents.map((agent) => this.registerAgent(agent)),
|
||||
agentsToRegister.map((agent) => this.registerAgent(agent)),
|
||||
);
|
||||
} else {
|
||||
coreEvents.emitFeedback(
|
||||
|
||||
139
packages/core/src/agents/registry_acknowledgement.test.ts
Normal file
139
packages/core/src/agents/registry_acknowledgement.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { AgentRegistry } from './registry.js';
|
||||
import { makeFakeConfig } from '../test-utils/config.js';
|
||||
import type { AgentDefinition } from './types.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import * as tomlLoader from './agentLoader.js';
|
||||
import { type Config } from '../config/config.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./agentLoader.js', () => ({
|
||||
loadAgentsFromDirectory: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock AcknowledgedAgentsService
|
||||
const mockAckService = {
|
||||
load: vi.fn(),
|
||||
save: vi.fn(),
|
||||
isAcknowledged: vi.fn(),
|
||||
acknowledge: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('./acknowledgedAgents.js', () => ({
|
||||
AcknowledgedAgentsService: {
|
||||
getInstance: vi.fn(() => mockAckService),
|
||||
},
|
||||
}));
|
||||
|
||||
const MOCK_AGENT_WITH_HASH: AgentDefinition = {
|
||||
kind: 'local',
|
||||
name: 'ProjectAgent',
|
||||
description: 'Project Agent Desc',
|
||||
inputConfig: { inputSchema: { type: 'object' } },
|
||||
modelConfig: {
|
||||
model: 'test',
|
||||
generateContentConfig: { thinkingConfig: { includeThoughts: true } },
|
||||
},
|
||||
runConfig: { maxTimeMinutes: 1 },
|
||||
promptConfig: { systemPrompt: 'test' },
|
||||
metadata: {
|
||||
hash: 'hash123',
|
||||
filePath: '/project/agent.md',
|
||||
},
|
||||
};
|
||||
|
||||
describe.skip('AgentRegistry Acknowledgement', () => {
|
||||
let registry: AgentRegistry;
|
||||
let config: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
config = makeFakeConfig({
|
||||
folderTrust: true,
|
||||
trustedFolder: true,
|
||||
});
|
||||
// Ensure we are in trusted folder mode for project agents to load
|
||||
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true);
|
||||
vi.spyOn(config, 'getFolderTrust').mockReturnValue(true);
|
||||
vi.spyOn(config, 'getProjectRoot').mockReturnValue('/project');
|
||||
|
||||
// We cannot easily spy on storage.getProjectAgentsDir if it's a property/getter unless we cast to any or it's a method
|
||||
// Assuming it's a method on Storage class
|
||||
vi.spyOn(config.storage, 'getProjectAgentsDir').mockReturnValue(
|
||||
'/project/.gemini/agents',
|
||||
);
|
||||
|
||||
registry = new AgentRegistry(config);
|
||||
|
||||
// Reset mocks
|
||||
vi.mocked(tomlLoader.loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [],
|
||||
errors: [],
|
||||
});
|
||||
mockAckService.isAcknowledged.mockReturnValue(false);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should not register unacknowledged project agents and emit event', async () => {
|
||||
vi.mocked(tomlLoader.loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [MOCK_AGENT_WITH_HASH],
|
||||
errors: [],
|
||||
});
|
||||
mockAckService.isAcknowledged.mockReturnValue(false);
|
||||
|
||||
const emitSpy = vi.spyOn(coreEvents, 'emitAgentsDiscovered');
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getDefinition('ProjectAgent')).toBeUndefined();
|
||||
expect(emitSpy).toHaveBeenCalledWith([MOCK_AGENT_WITH_HASH]);
|
||||
});
|
||||
|
||||
it('should register acknowledged project agents', async () => {
|
||||
vi.mocked(tomlLoader.loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [MOCK_AGENT_WITH_HASH],
|
||||
errors: [],
|
||||
});
|
||||
mockAckService.isAcknowledged.mockReturnValue(true);
|
||||
|
||||
const emitSpy = vi.spyOn(coreEvents, 'emitAgentsDiscovered');
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getDefinition('ProjectAgent')).toBeDefined();
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register agents without hash (legacy/safe?)', async () => {
|
||||
// Current logic: if no hash, allow it.
|
||||
const agentNoHash = { ...MOCK_AGENT_WITH_HASH, metadata: undefined };
|
||||
vi.mocked(tomlLoader.loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [agentNoHash],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await registry.initialize();
|
||||
|
||||
expect(registry.getDefinition('ProjectAgent')).toBeDefined();
|
||||
});
|
||||
|
||||
it('acknowledgeAgent should acknowledge and register agent', async () => {
|
||||
await registry.acknowledgeAgent(MOCK_AGENT_WITH_HASH);
|
||||
|
||||
expect(mockAckService.acknowledge).toHaveBeenCalledWith(
|
||||
'/project',
|
||||
'ProjectAgent',
|
||||
'hash123',
|
||||
);
|
||||
expect(registry.getDefinition('ProjectAgent')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,10 @@ export interface BaseAgentDefinition<
|
||||
experimental?: boolean;
|
||||
inputConfig: InputConfig;
|
||||
outputConfig?: OutputConfig<TOutput>;
|
||||
metadata?: {
|
||||
hash?: string;
|
||||
filePath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocalAgentDefinition<
|
||||
|
||||
@@ -66,6 +66,10 @@ export class Storage {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'agents');
|
||||
}
|
||||
|
||||
static getAcknowledgedAgentsPath(): string {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'acknowledgedAgents.json');
|
||||
}
|
||||
|
||||
static getSystemSettingsPath(): string {
|
||||
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
|
||||
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { AgentDefinition } from '../agents/types.js';
|
||||
|
||||
/**
|
||||
* Defines the severity level for user-facing feedback.
|
||||
@@ -108,6 +109,13 @@ export interface RetryAttemptPayload {
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'agents-discovered' event.
|
||||
*/
|
||||
export interface AgentsDiscoveredPayload {
|
||||
agents: AgentDefinition[];
|
||||
}
|
||||
|
||||
export enum CoreEvent {
|
||||
UserFeedback = 'user-feedback',
|
||||
ModelChanged = 'model-changed',
|
||||
@@ -121,6 +129,7 @@ export enum CoreEvent {
|
||||
AgentsRefreshed = 'agents-refreshed',
|
||||
AdminSettingsChanged = 'admin-settings-changed',
|
||||
RetryAttempt = 'retry-attempt',
|
||||
AgentsDiscovered = 'agents-discovered',
|
||||
}
|
||||
|
||||
export interface CoreEvents {
|
||||
@@ -136,6 +145,7 @@ export interface CoreEvents {
|
||||
[CoreEvent.AgentsRefreshed]: never[];
|
||||
[CoreEvent.AdminSettingsChanged]: never[];
|
||||
[CoreEvent.RetryAttempt]: [RetryAttemptPayload];
|
||||
[CoreEvent.AgentsDiscovered]: [AgentsDiscoveredPayload];
|
||||
}
|
||||
|
||||
type EventBacklogItem = {
|
||||
@@ -258,6 +268,14 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
this.emit(CoreEvent.RetryAttempt, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies subscribers that new unacknowledged agents have been discovered.
|
||||
*/
|
||||
emitAgentsDiscovered(agents: AgentDefinition[]): void {
|
||||
const payload: AgentsDiscoveredPayload = { agents };
|
||||
this._emitOrQueue(CoreEvent.AgentsDiscovered, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes buffered messages. Call this immediately after primary UI listener
|
||||
* subscribes.
|
||||
|
||||
Reference in New Issue
Block a user