feat(M5-004,M5-005,M5-006,M5-007): session-conversation binding, session:info broadcast, agent creation from TUI, and session metrics (#321)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #321.
This commit is contained in:
2026-03-23 00:58:07 +00:00
committed by jason.woltje
parent b18976a7aa
commit 1035d13fc0
10 changed files with 3159 additions and 12 deletions

View File

@@ -23,7 +23,7 @@ import { createFileTools } from './tools/file-tools.js';
import { createGitTools } from './tools/git-tools.js';
import { createShellTools } from './tools/shell-tools.js';
import { createWebTools } from './tools/web-tools.js';
import type { SessionInfoDto } from './session.dto.js';
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
import { SystemOverrideService } from '../preferences/system-override.service.js';
import { PreferencesService } from '../preferences/preferences.service.js';
import { SessionGCService } from '../gc/session-gc.service.js';
@@ -93,6 +93,12 @@ export interface AgentSession {
allowedTools: string[] | null;
/** User ID that owns this session, used for preference lookups. */
userId?: string;
/** Agent config ID applied to this session, if any (M5-001). */
agentConfigId?: string;
/** Human-readable agent name applied to this session, if any (M5-001). */
agentName?: string;
/** M5-007: per-session metrics. */
metrics: SessionMetrics;
}
@Injectable()
@@ -184,11 +190,13 @@ export class AgentService implements OnModuleDestroy {
sessionId: string,
options?: AgentSessionOptions,
): Promise<AgentSession> {
// Merge DB agent config when agentConfigId is provided
// Merge DB agent config when agentConfigId is provided (M5-001)
let mergedOptions = options;
let resolvedAgentName: string | undefined;
if (options?.agentConfigId) {
const agentConfig = await this.brain.agents.findById(options.agentConfigId);
if (agentConfig) {
resolvedAgentName = agentConfig.name;
mergedOptions = {
provider: options.provider ?? agentConfig.provider,
modelId: options.modelId ?? agentConfig.model,
@@ -197,6 +205,8 @@ export class AgentService implements OnModuleDestroy {
sandboxDir: options.sandboxDir,
isAdmin: options.isAdmin,
agentConfigId: options.agentConfigId,
userId: options.userId,
conversationHistory: options.conversationHistory,
};
this.logger.log(
`Merged agent config "${agentConfig.name}" (${agentConfig.id}) into session ${sessionId}`,
@@ -330,6 +340,14 @@ export class AgentService implements OnModuleDestroy {
sandboxDir,
allowedTools,
userId: mergedOptions?.userId,
agentConfigId: mergedOptions?.agentConfigId,
agentName: resolvedAgentName,
metrics: {
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
modelSwitches: 0,
messageCount: 0,
lastActivityAt: new Date().toISOString(),
},
};
this.sessions.set(sessionId, session);
@@ -458,10 +476,12 @@ export class AgentService implements OnModuleDestroy {
id: s.id,
provider: s.provider,
modelId: s.modelId,
...(s.agentName ? { agentName: s.agentName } : {}),
createdAt: new Date(s.createdAt).toISOString(),
promptCount: s.promptCount,
channels: Array.from(s.channels),
durationMs: now - s.createdAt,
metrics: { ...s.metrics },
}));
}
@@ -472,13 +492,93 @@ export class AgentService implements OnModuleDestroy {
id: s.id,
provider: s.provider,
modelId: s.modelId,
...(s.agentName ? { agentName: s.agentName } : {}),
createdAt: new Date(s.createdAt).toISOString(),
promptCount: s.promptCount,
channels: Array.from(s.channels),
durationMs: Date.now() - s.createdAt,
metrics: { ...s.metrics },
};
}
/**
* Record token usage for a session turn (M5-007).
* Accumulates tokens across the session lifetime.
*/
recordTokenUsage(
sessionId: string,
tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number },
): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.metrics.tokens.input += tokens.input;
session.metrics.tokens.output += tokens.output;
session.metrics.tokens.cacheRead += tokens.cacheRead;
session.metrics.tokens.cacheWrite += tokens.cacheWrite;
session.metrics.tokens.total += tokens.total;
session.metrics.lastActivityAt = new Date().toISOString();
}
/**
* Record a model switch event for a session (M5-007).
*/
recordModelSwitch(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.metrics.modelSwitches += 1;
session.metrics.lastActivityAt = new Date().toISOString();
}
/**
* Increment message count for a session (M5-007).
*/
recordMessage(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.metrics.messageCount += 1;
session.metrics.lastActivityAt = new Date().toISOString();
}
/**
* Update the model tracked on a live session (M5-002).
* This records the model change in the session metadata so subsequent
* session:info emissions reflect the new model. The Pi session itself is
* not reconstructed — the model is used on the next createSession call for
* the same conversationId when the session is torn down or a new one is created.
*/
updateSessionModel(sessionId: string, modelId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
const prev = session.modelId;
session.modelId = modelId;
this.recordModelSwitch(sessionId);
this.logger.log(`Session ${sessionId}: model updated ${prev}${modelId} (M5-002)`);
}
/**
* Apply a new agent config to a live session mid-conversation (M5-003).
* Updates agentName, agentConfigId, and modelId on the session object.
* System prompt and tools take effect when the next session is created for
* this conversationId (they are baked in at session creation time).
*/
applyAgentConfig(
sessionId: string,
agentConfigId: string,
agentName: string,
modelId?: string,
): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.agentConfigId = agentConfigId;
session.agentName = agentName;
if (modelId) {
this.updateSessionModel(sessionId, modelId);
}
this.logger.log(
`Session ${sessionId}: agent switched to "${agentName}" (${agentConfigId}) (M5-003)`,
);
}
addChannel(sessionId: string, channel: string): void {
const session = this.sessions.get(sessionId);
if (session) {