89 lines
2.9 KiB
TypeScript
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
|
|
});
|
|
}
|