Files
Veskslop-t/poc/src/agent-client.ts
T

89 lines
2.9 KiB
TypeScript

/*
* Veskslop agent client — the daemon side of the local LLM harness.
*
* Respond-only for now: the daemon posts a task and the harness returns the
* reply text. The daemon sends that text through the normal paced path, so the
* harness never talks to Discord and no tools are exposed to the model.
*/
export interface AgentTask {
id: string;
authorId: string;
authorName: string;
channelId: string;
guildId?: string;
content: string;
context: Array<{ author: string; content: string; timestamp: string }>;
}
export interface AgentClientOptions {
url: string;
token?: string;
timeoutMs?: number;
}
export class AgentClient {
private readonly base: string;
private readonly timeoutMs: number;
constructor(private readonly options: AgentClientOptions) {
this.base = options.url.replace(/\/+$/, "");
this.timeoutMs = options.timeoutMs ?? 120_000;
}
get url(): string {
return this.base;
}
/**
* Ask the harness for a reply. Returns the reply text, or undefined when
* the harness answers with an empty reply.
*/
async respond(task: AgentTask): Promise<string | undefined> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(`${this.base}/task`, {
method: "POST",
headers: {
"content-type": "application/json",
...(this.options.token ? { authorization: `Bearer ${this.options.token}` } : {})
},
// mode: respond tells the harness to answer without tool calls.
body: JSON.stringify({ ...task, mode: "respond" }),
signal: controller.signal
});
if (!response.ok) {
const detail = await response.text().catch(() => "");
throw new Error(`agent returned ${response.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`);
}
const body = (await response.json().catch(() => ({}))) as { reply?: unknown };
const reply = typeof body.reply === "string" ? body.reply.trim() : "";
return reply || undefined;
} finally {
clearTimeout(timer);
}
}
}
/**
* Build the client from the environment. The harness stays off unless
* VESKSLOP_AGENT_ENABLED=1 is set.
*/
export function agentFromEnv(): AgentClient | undefined {
if (process.env.VESKSLOP_AGENT_ENABLED !== "1") return undefined;
const url = process.env.VESKSLOP_AGENT_URL ?? "http://127.0.0.1:8790";
const token = process.env.VESKSLOP_AGENT_TOKEN;
const timeoutMs = Number(process.env.VESKSLOP_AGENT_TIMEOUT_MS ?? 120_000);
return new AgentClient({
url,
...(token ? { token } : {}),
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 120_000
});
}