feat(core): add command, intent, and routing primitives
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { GatewayEvent, GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { LaneQueue } from '../lane-queue.js';
|
||||
import { createAgentHandlers } from './agent.js';
|
||||
import { CommandRegistry, registerBuiltinCommands } from '../../commands/index.js';
|
||||
|
||||
describe('createAgentHandlers command fast-path', () => {
|
||||
const mockAgent = {
|
||||
process: vi.fn(async () => 'agent response'),
|
||||
getUsage: vi.fn(() => ({
|
||||
primary: { inputTokens: 10, outputTokens: 5, calls: 1 },
|
||||
delegation: {},
|
||||
total: { inputTokens: 10, outputTokens: 5, calls: 1, estimatedCost: 0 },
|
||||
})),
|
||||
getModelTier: vi.fn(() => 'default'),
|
||||
setModelTier: vi.fn(),
|
||||
compact: vi.fn(async () => null),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
const sessionBridge = {
|
||||
getAgent: vi.fn(() => mockAgent),
|
||||
getSessionId: vi.fn(() => 'ws:conn-1'),
|
||||
setBusy: vi.fn(),
|
||||
setOnToolUse: vi.fn(),
|
||||
isBusy: vi.fn(() => false),
|
||||
};
|
||||
|
||||
const sessionManager = {
|
||||
setSessionConfig: vi.fn(),
|
||||
deleteSessionConfig: vi.fn(),
|
||||
};
|
||||
|
||||
const commandRegistry = new CommandRegistry();
|
||||
registerBuiltinCommands(commandRegistry);
|
||||
|
||||
const handlers = createAgentHandlers({
|
||||
sessionBridge: sessionBridge as any,
|
||||
laneQueue: new LaneQueue(),
|
||||
sessionManager: sessionManager as any,
|
||||
commandRegistry,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockAgent.process.mockResolvedValue('agent response');
|
||||
mockAgent.compact.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it('handles known commands without calling agent.process', async () => {
|
||||
const sent: OutboundMessage[] = [];
|
||||
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
||||
const req: GatewayRequest = {
|
||||
id: 1,
|
||||
method: 'agent.send',
|
||||
params: { message: '/usage', connectionId: 'conn-1' },
|
||||
};
|
||||
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
expect(mockAgent.process).not.toHaveBeenCalled();
|
||||
expect(sent).toHaveLength(1);
|
||||
const event = sent[0] as GatewayEvent;
|
||||
expect(event.event).toBe('done');
|
||||
expect((event.data as { content: string }).content).toContain('Token Usage');
|
||||
});
|
||||
|
||||
it('handles metadata commands via fast-path', async () => {
|
||||
const sent: OutboundMessage[] = [];
|
||||
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
||||
const req: GatewayRequest = {
|
||||
id: 2,
|
||||
method: 'agent.send',
|
||||
params: {
|
||||
message: '/reset',
|
||||
connectionId: 'conn-1',
|
||||
metadata: { isCommand: true, command: 'reset' },
|
||||
},
|
||||
};
|
||||
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
expect(mockAgent.reset).toHaveBeenCalledOnce();
|
||||
expect(sessionManager.deleteSessionConfig).toHaveBeenCalledWith('ws', 'ws:conn-1', 'modelTier');
|
||||
expect(mockAgent.process).not.toHaveBeenCalled();
|
||||
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toContain('Session reset.');
|
||||
});
|
||||
|
||||
it('falls through to agent.process for unknown commands', async () => {
|
||||
const sent: OutboundMessage[] = [];
|
||||
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
||||
const req: GatewayRequest = {
|
||||
id: 3,
|
||||
method: 'agent.send',
|
||||
params: { message: '/not-a-real-command', connectionId: 'conn-1' },
|
||||
};
|
||||
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
expect(mockAgent.process).toHaveBeenCalledWith('/not-a-real-command', undefined);
|
||||
expect((sent[0] as GatewayEvent).event).toBe('done');
|
||||
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toBe('agent response');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeError, makeResponse, ErrorCode } from '../protocol.js';
|
||||
import type { SessionManager } from '../../session/manager.js';
|
||||
|
||||
export interface HistoryHandlerDeps {
|
||||
sessionManager: SessionManager;
|
||||
}
|
||||
|
||||
export function createHistoryHandlers(deps: HistoryHandlerDeps) {
|
||||
return {
|
||||
'history.search': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { query?: string; sessionId?: string; limit?: number } | undefined;
|
||||
if (!params?.query) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'query is required');
|
||||
}
|
||||
|
||||
const results = deps.sessionManager.searchHistory(params.query, {
|
||||
sessionId: params.sessionId,
|
||||
limit: params.limit,
|
||||
});
|
||||
|
||||
return makeResponse(request.id, { results });
|
||||
},
|
||||
|
||||
'history.reindex': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const reindexed = deps.sessionManager.reindexHistory();
|
||||
return makeResponse(request.id, { reindexed });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeError, makeResponse, ErrorCode } from '../protocol.js';
|
||||
import type { ComponentRegistry } from '../../intents/index.js';
|
||||
|
||||
export interface IntentHandlerDeps {
|
||||
intentRegistry?: ComponentRegistry;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function createIntentHandlers(deps: IntentHandlerDeps) {
|
||||
return {
|
||||
'intents.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const rules = deps.intentRegistry?.list() ?? [];
|
||||
return makeResponse(request.id, {
|
||||
enabled: deps.enabled,
|
||||
rules,
|
||||
});
|
||||
},
|
||||
|
||||
'intents.match': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { input?: string } | undefined;
|
||||
if (!params?.input) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'input is required');
|
||||
}
|
||||
|
||||
const match = deps.intentRegistry?.match(params.input) ?? null;
|
||||
return makeResponse(request.id, {
|
||||
enabled: deps.enabled,
|
||||
match,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeError, makeResponse, ErrorCode } from '../protocol.js';
|
||||
import type { ComponentRegistry } from '../../intents/index.js';
|
||||
import type { RoutingPolicy } from '../../routing/index.js';
|
||||
|
||||
export interface RoutingHandlerDeps {
|
||||
intentRegistry?: ComponentRegistry;
|
||||
routingPolicy?: RoutingPolicy;
|
||||
}
|
||||
|
||||
export function createRoutingHandlers(deps: RoutingHandlerDeps) {
|
||||
return {
|
||||
'routing.decide': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { input?: string } | undefined;
|
||||
if (!params?.input) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'input is required');
|
||||
}
|
||||
|
||||
const match = deps.intentRegistry?.match(params.input) ?? null;
|
||||
const decision = deps.routingPolicy?.decide({ confidence: match?.score ?? null }) ?? {
|
||||
path: 'llm',
|
||||
reason: 'disabled',
|
||||
};
|
||||
|
||||
return makeResponse(request.id, {
|
||||
match,
|
||||
decision,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user