feat: communication spine — gateway, TUI, Discord (#61)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #61.
This commit is contained in:
131
apps/gateway/src/agent/agent.service.ts
Normal file
131
apps/gateway/src/agent/agent.service.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Injectable, Logger, type OnModuleDestroy } from '@nestjs/common';
|
||||
import {
|
||||
createAgentSession,
|
||||
SessionManager,
|
||||
type AgentSession as PiAgentSession,
|
||||
type AgentSessionEvent,
|
||||
} from '@mariozechner/pi-coding-agent';
|
||||
|
||||
export interface AgentSession {
|
||||
id: string;
|
||||
piSession: PiAgentSession;
|
||||
listeners: Set<(event: AgentSessionEvent) => void>;
|
||||
unsubscribe: () => void;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AgentService implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(AgentService.name);
|
||||
private readonly sessions = new Map<string, AgentSession>();
|
||||
private readonly creating = new Map<string, Promise<AgentSession>>();
|
||||
|
||||
async createSession(sessionId: string): Promise<AgentSession> {
|
||||
const existing = this.sessions.get(sessionId);
|
||||
if (existing) return existing;
|
||||
|
||||
const inflight = this.creating.get(sessionId);
|
||||
if (inflight) return inflight;
|
||||
|
||||
const promise = this.doCreateSession(sessionId).finally(() => {
|
||||
this.creating.delete(sessionId);
|
||||
});
|
||||
this.creating.set(sessionId, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async doCreateSession(sessionId: string): Promise<AgentSession> {
|
||||
this.logger.log(`Creating agent session: ${sessionId}`);
|
||||
|
||||
let piSession: PiAgentSession;
|
||||
try {
|
||||
const result = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
tools: [],
|
||||
});
|
||||
piSession = result.session;
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to create Pi SDK session for ${sessionId}`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
throw new Error(`Agent session creation failed for ${sessionId}: ${String(err)}`);
|
||||
}
|
||||
|
||||
const listeners = new Set<(event: AgentSessionEvent) => void>();
|
||||
|
||||
const unsubscribe = piSession.subscribe((event) => {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (err) {
|
||||
this.logger.error(`Event listener error in session ${sessionId}`, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const session: AgentSession = {
|
||||
id: sessionId,
|
||||
piSession,
|
||||
listeners,
|
||||
unsubscribe,
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
this.logger.log(`Agent session ${sessionId} ready`);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
getSession(sessionId: string): AgentSession | undefined {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
async prompt(sessionId: string, message: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`No agent session found: ${sessionId}`);
|
||||
}
|
||||
try {
|
||||
await session.piSession.prompt(message);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Pi SDK prompt failed for session=${sessionId}, messageLength=${message.length}`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
onEvent(sessionId: string, listener: (event: AgentSessionEvent) => void): () => void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`No agent session found: ${sessionId}`);
|
||||
}
|
||||
session.listeners.add(listener);
|
||||
return () => session.listeners.delete(listener);
|
||||
}
|
||||
|
||||
async destroySession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
this.logger.log(`Destroying agent session ${sessionId}`);
|
||||
try {
|
||||
session.unsubscribe();
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to unsubscribe Pi session ${sessionId}`, String(err));
|
||||
}
|
||||
session.listeners.clear();
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
this.logger.log('Shutting down all agent sessions');
|
||||
const stops = Array.from(this.sessions.keys()).map((id) => this.destroySession(id));
|
||||
const results = await Promise.allSettled(stops);
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.error('Session shutdown failure', String(result.reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user