115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import type { CliDeps } from "../../cli/deps.js";
|
|
import { loadConfig } from "../../config/config.js";
|
|
import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js";
|
|
import { runCronIsolatedAgentTurn } from "../../cron/isolated-agent.js";
|
|
import type { CronJob } from "../../cron/types.js";
|
|
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
|
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
|
import type { createSubsystemLogger } from "../../logging.js";
|
|
import type { HookMessageChannel, HooksConfigResolved } from "../hooks.js";
|
|
import { createHooksRequestHandler } from "../server-http.js";
|
|
|
|
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
|
|
|
export function createGatewayHooksRequestHandler(params: {
|
|
deps: CliDeps;
|
|
getHooksConfig: () => HooksConfigResolved | null;
|
|
bindHost: string;
|
|
port: number;
|
|
logHooks: SubsystemLogger;
|
|
}) {
|
|
const { deps, getHooksConfig, bindHost, port, logHooks } = params;
|
|
|
|
const dispatchWakeHook = (value: { text: string; mode: "now" | "next-heartbeat" }) => {
|
|
const sessionKey = resolveMainSessionKeyFromConfig();
|
|
enqueueSystemEvent(value.text, { sessionKey });
|
|
if (value.mode === "now") {
|
|
requestHeartbeatNow({ reason: "hook:wake" });
|
|
}
|
|
};
|
|
|
|
const dispatchAgentHook = (value: {
|
|
message: string;
|
|
name: string;
|
|
wakeMode: "now" | "next-heartbeat";
|
|
sessionKey: string;
|
|
deliver: boolean;
|
|
channel: HookMessageChannel;
|
|
to?: string;
|
|
model?: string;
|
|
thinking?: string;
|
|
timeoutSeconds?: number;
|
|
}) => {
|
|
const sessionKey = value.sessionKey.trim() ? value.sessionKey.trim() : `hook:${randomUUID()}`;
|
|
const mainSessionKey = resolveMainSessionKeyFromConfig();
|
|
const jobId = randomUUID();
|
|
const now = Date.now();
|
|
const job: CronJob = {
|
|
id: jobId,
|
|
name: value.name,
|
|
enabled: true,
|
|
createdAtMs: now,
|
|
updatedAtMs: now,
|
|
schedule: { kind: "at", atMs: now },
|
|
sessionTarget: "isolated",
|
|
wakeMode: value.wakeMode,
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message: value.message,
|
|
model: value.model,
|
|
thinking: value.thinking,
|
|
timeoutSeconds: value.timeoutSeconds,
|
|
deliver: value.deliver,
|
|
channel: value.channel,
|
|
to: value.to,
|
|
},
|
|
state: { nextRunAtMs: now },
|
|
};
|
|
|
|
const runId = randomUUID();
|
|
void (async () => {
|
|
try {
|
|
const cfg = loadConfig();
|
|
const result = await runCronIsolatedAgentTurn({
|
|
cfg,
|
|
deps,
|
|
job,
|
|
message: value.message,
|
|
sessionKey,
|
|
lane: "cron",
|
|
});
|
|
const summary = result.summary?.trim() || result.error?.trim() || result.status;
|
|
const prefix =
|
|
result.status === "ok" ? `Hook ${value.name}` : `Hook ${value.name} (${result.status})`;
|
|
enqueueSystemEvent(`${prefix}: ${summary}`.trim(), {
|
|
sessionKey: mainSessionKey,
|
|
});
|
|
if (value.wakeMode === "now") {
|
|
requestHeartbeatNow({ reason: `hook:${jobId}` });
|
|
}
|
|
} catch (err) {
|
|
logHooks.warn(`hook agent failed: ${String(err)}`);
|
|
enqueueSystemEvent(`Hook ${value.name} (error): ${String(err)}`, {
|
|
sessionKey: mainSessionKey,
|
|
});
|
|
if (value.wakeMode === "now") {
|
|
requestHeartbeatNow({ reason: `hook:${jobId}:error` });
|
|
}
|
|
}
|
|
})();
|
|
|
|
return runId;
|
|
};
|
|
|
|
return createHooksRequestHandler({
|
|
getHooksConfig,
|
|
bindHost,
|
|
port,
|
|
logHooks,
|
|
dispatchAgentHook,
|
|
dispatchWakeHook,
|
|
});
|
|
}
|