feat: agent session management — metrics, channels, dispose (P2-006) #78

Merged
jason.woltje merged 1 commits from feat/p2-006-session-management into main 2026-03-13 03:35:59 +00:00
6 changed files with 116 additions and 3 deletions

View File

@@ -3,13 +3,14 @@ import { AgentService } from './agent.service.js';
import { ProviderService } from './provider.service.js'; import { ProviderService } from './provider.service.js';
import { RoutingService } from './routing.service.js'; import { RoutingService } from './routing.service.js';
import { ProvidersController } from './providers.controller.js'; import { ProvidersController } from './providers.controller.js';
import { SessionsController } from './sessions.controller.js';
import { CoordModule } from '../coord/coord.module.js'; import { CoordModule } from '../coord/coord.module.js';
@Global() @Global()
@Module({ @Module({
imports: [CoordModule], imports: [CoordModule],
providers: [ProviderService, RoutingService, AgentService], providers: [ProviderService, RoutingService, AgentService],
controllers: [ProvidersController], controllers: [ProvidersController, SessionsController],
exports: [AgentService, ProviderService, RoutingService], exports: [AgentService, ProviderService, RoutingService],
}) })
export class AgentModule {} export class AgentModule {}

View File

@@ -12,6 +12,7 @@ import { CoordService } from '../coord/coord.service.js';
import { ProviderService } from './provider.service.js'; import { ProviderService } from './provider.service.js';
import { createBrainTools } from './tools/brain-tools.js'; import { createBrainTools } from './tools/brain-tools.js';
import { createCoordTools } from './tools/coord-tools.js'; import { createCoordTools } from './tools/coord-tools.js';
import type { SessionInfoDto } from './session.dto.js';
export interface AgentSessionOptions { export interface AgentSessionOptions {
provider?: string; provider?: string;
@@ -25,6 +26,9 @@ export interface AgentSession {
piSession: PiAgentSession; piSession: PiAgentSession;
listeners: Set<(event: AgentSessionEvent) => void>; listeners: Set<(event: AgentSessionEvent) => void>;
unsubscribe: () => void; unsubscribe: () => void;
createdAt: number;
promptCount: number;
channels: Set<string>;
} }
@Injectable() @Injectable()
@@ -107,6 +111,9 @@ export class AgentService implements OnModuleDestroy {
piSession, piSession,
listeners, listeners,
unsubscribe, unsubscribe,
createdAt: Date.now(),
promptCount: 0,
channels: new Set(),
}; };
this.sessions.set(sessionId, session); this.sessions.set(sessionId, session);
@@ -143,11 +150,53 @@ export class AgentService implements OnModuleDestroy {
return this.sessions.get(sessionId); return this.sessions.get(sessionId);
} }
listSessions(): SessionInfoDto[] {
const now = Date.now();
return Array.from(this.sessions.values()).map((s) => ({
id: s.id,
provider: s.provider,
modelId: s.modelId,
createdAt: new Date(s.createdAt).toISOString(),
promptCount: s.promptCount,
channels: Array.from(s.channels),
durationMs: now - s.createdAt,
}));
}
getSessionInfo(sessionId: string): SessionInfoDto | undefined {
const s = this.sessions.get(sessionId);
if (!s) return undefined;
return {
id: s.id,
provider: s.provider,
modelId: s.modelId,
createdAt: new Date(s.createdAt).toISOString(),
promptCount: s.promptCount,
channels: Array.from(s.channels),
durationMs: Date.now() - s.createdAt,
};
}
addChannel(sessionId: string, channel: string): void {
const session = this.sessions.get(sessionId);
if (session) {
session.channels.add(channel);
}
}
removeChannel(sessionId: string, channel: string): void {
const session = this.sessions.get(sessionId);
if (session) {
session.channels.delete(channel);
}
}
async prompt(sessionId: string, message: string): Promise<void> { async prompt(sessionId: string, message: string): Promise<void> {
const session = this.sessions.get(sessionId); const session = this.sessions.get(sessionId);
if (!session) { if (!session) {
throw new Error(`No agent session found: ${sessionId}`); throw new Error(`No agent session found: ${sessionId}`);
} }
session.promptCount += 1;
try { try {
await session.piSession.prompt(message); await session.piSession.prompt(message);
} catch (err) { } catch (err) {
@@ -177,7 +226,13 @@ export class AgentService implements OnModuleDestroy {
} catch (err) { } catch (err) {
this.logger.error(`Failed to unsubscribe session ${sessionId}`, String(err)); this.logger.error(`Failed to unsubscribe session ${sessionId}`, String(err));
} }
try {
session.piSession.dispose();
} catch (err) {
this.logger.error(`Failed to dispose piSession for ${sessionId}`, String(err));
}
session.listeners.clear(); session.listeners.clear();
session.channels.clear();
this.sessions.delete(sessionId); this.sessions.delete(sessionId);
} }

View File

@@ -0,0 +1,14 @@
export interface SessionInfoDto {
id: string;
provider: string;
modelId: string;
createdAt: string;
promptCount: number;
channels: string[];
durationMs: number;
}
export interface SessionListDto {
sessions: SessionInfoDto[];
total: number;
}

View File

@@ -0,0 +1,39 @@
import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard.js';
import { AgentService } from './agent.service.js';
@Controller('api/sessions')
@UseGuards(AuthGuard)
export class SessionsController {
constructor(private readonly agentService: AgentService) {}
@Get()
list() {
const sessions = this.agentService.listSessions();
return { sessions, total: sessions.length };
}
@Get(':id')
findOne(@Param('id') id: string) {
const info = this.agentService.getSessionInfo(id);
if (!info) throw new NotFoundException('Session not found');
return info;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async destroy(@Param('id') id: string) {
const info = this.agentService.getSessionInfo(id);
if (!info) throw new NotFoundException('Session not found');
await this.agentService.destroySession(id);
}
}

View File

@@ -50,6 +50,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
const session = this.clientSessions.get(client.id); const session = this.clientSessions.get(client.id);
if (session) { if (session) {
session.cleanup(); session.cleanup();
this.agentService.removeChannel(session.conversationId, `websocket:${client.id}`);
this.clientSessions.delete(client.id); this.clientSessions.delete(client.id);
} }
} }
@@ -97,6 +98,9 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
this.clientSessions.set(client.id, { conversationId, cleanup }); this.clientSessions.set(client.id, { conversationId, cleanup });
// Track channel connection
this.agentService.addChannel(conversationId, `websocket:${client.id}`);
// Send acknowledgment // Send acknowledgment
client.emit('message:ack', { conversationId, messageId: uuid() }); client.emit('message:ack', { conversationId, messageId: uuid() });

View File

@@ -26,8 +26,8 @@
| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 | | P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 |
| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 | | P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 |
| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 | | P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 |
| P2-005 | in-progress | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | | #23 | | P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
| P2-006 | not-started | Phase 2 | Agent session management — tmux + monitoring | — | #24 | | P2-006 | in-progress | Phase 2 | Agent session management — tmux + monitoring | — | #24 |
| P2-007 | not-started | Phase 2 | Verify Phase 2 — multi-provider routing works | — | #25 | | P2-007 | not-started | Phase 2 | Verify Phase 2 — multi-provider routing works | — | #25 |
| P3-001 | not-started | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | — | #26 | | P3-001 | not-started | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | — | #26 |
| P3-002 | not-started | Phase 3 | Auth pages — login, registration, SSO redirect | — | #27 | | P3-002 | not-started | Phase 3 | Auth pages — login, registration, SSO redirect | — | #27 |