panopticon init
This commit is contained in:
153
src/session.ts
Normal file
153
src/session.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Shared session creation utilities for orchestrator, workers, and synthesizer.
|
||||
*/
|
||||
import {
|
||||
createAgentSession,
|
||||
DefaultResourceLoader,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
AuthStorage,
|
||||
ModelRegistry,
|
||||
readOnlyTools,
|
||||
type AgentSession,
|
||||
type AgentSessionEvent,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { Config, SessionMetrics } from "./types.js";
|
||||
|
||||
export interface SessionOptions {
|
||||
role: "orchestrator" | "worker" | "synthesizer";
|
||||
projectPath: string;
|
||||
config: Config;
|
||||
systemPrompt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model spec like "anthropic/claude-sonnet-4-5" into a Model object.
|
||||
*/
|
||||
function resolveModel(spec: string, modelRegistry: ModelRegistry) {
|
||||
const [provider, modelId] = spec.split("/");
|
||||
// Try ModelRegistry first, fall back to getModel
|
||||
try {
|
||||
return getModel(provider as any, modelId as any);
|
||||
} catch {
|
||||
throw new Error(`Cannot resolve model: ${spec}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pi AgentSession for a given role.
|
||||
*/
|
||||
export async function createSession(options: SessionOptions): Promise<{
|
||||
session: AgentSession;
|
||||
metrics: SessionMetrics;
|
||||
}> {
|
||||
const { role, projectPath, config, systemPrompt } = options;
|
||||
|
||||
const authStorage = AuthStorage.create();
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
const modelSpec = config.models[role];
|
||||
const model = resolveModel(modelSpec, modelRegistry);
|
||||
const thinkingLevel: ThinkingLevel = config.thinkingLevels[role];
|
||||
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: projectPath,
|
||||
systemPrompt,
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noExtensions: true,
|
||||
noThemes: true,
|
||||
});
|
||||
await loader.reload();
|
||||
|
||||
const settingsManager = SettingsManager.inMemory({
|
||||
retry: { enabled: true, maxRetries: 2 },
|
||||
});
|
||||
|
||||
// Workers get read-only tools; orchestrator and synthesizer get no tools
|
||||
const tools = role === "worker" ? readOnlyTools : [];
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
cwd: projectPath,
|
||||
model,
|
||||
thinkingLevel,
|
||||
tools,
|
||||
resourceLoader: loader,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager,
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
// Track metrics
|
||||
const metrics = trackSession(session);
|
||||
|
||||
return { session, metrics };
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to session events and collect metrics.
|
||||
*/
|
||||
function trackSession(session: AgentSession): SessionMetrics {
|
||||
const metrics: SessionMetrics = {
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
toolCalls: 0,
|
||||
errors: [],
|
||||
durationMs: 0,
|
||||
};
|
||||
|
||||
session.subscribe((event: AgentSessionEvent) => {
|
||||
if (event.type === "message_end") {
|
||||
const msg = event.message as any;
|
||||
if (msg.role === "assistant" && msg.usage) {
|
||||
metrics.tokensIn += msg.usage.input ?? 0;
|
||||
metrics.tokensOut += msg.usage.output ?? 0;
|
||||
}
|
||||
}
|
||||
if (event.type === "tool_execution_end") {
|
||||
metrics.toolCalls++;
|
||||
if (event.isError) {
|
||||
metrics.errors.push(`${event.toolName}: ${JSON.stringify(event.result).slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the final text response from a session after prompt() completes.
|
||||
*/
|
||||
export function extractFinalResponse(session: AgentSession): string {
|
||||
const messages = session.messages;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i] as any;
|
||||
if (msg.role === "assistant" && msg.content) {
|
||||
const textParts = msg.content.filter((c: any) => c.type === "text");
|
||||
return textParts.map((c: any) => c.text).join("\n");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a prompt with a timeout via AbortController.
|
||||
*/
|
||||
export async function promptWithTimeout(
|
||||
session: AgentSession,
|
||||
prompt: string,
|
||||
timeoutMs: number
|
||||
): Promise<string> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
await session.prompt(prompt);
|
||||
return extractFinalResponse(session);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user