Add wakeword command routing and the reply-only agent client
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user