Status: split heartbeat summary helpers
This commit is contained in:
parent
0a6f22a694
commit
ebfd32efc3
@ -14,7 +14,7 @@ import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
type HeartbeatSummary,
|
||||
resolveHeartbeatSummaryForAgent,
|
||||
} from "../infra/heartbeat-runner.js";
|
||||
} from "../infra/heartbeat-summary.js";
|
||||
import { buildChannelAccountBindings, resolvePreferredAccountId } from "../routing/bindings.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
@ -48,7 +48,7 @@ vi.mock("../infra/channel-summary.js", () => ({
|
||||
buildChannelSummary: vi.fn(async () => ["ok"]),
|
||||
}));
|
||||
|
||||
vi.mock("../infra/heartbeat-runner.js", () => ({
|
||||
vi.mock("../infra/heartbeat-summary.js", () => ({
|
||||
resolveHeartbeatSummaryForAgent: vi.fn(() => ({
|
||||
enabled: true,
|
||||
every: "5m",
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
listAgentsForGateway,
|
||||
resolveSessionModelRef,
|
||||
} from "../gateway/session-utils.js";
|
||||
import { resolveHeartbeatSummaryForAgent } from "../infra/heartbeat-runner.js";
|
||||
import { resolveHeartbeatSummaryForAgent } from "../infra/heartbeat-summary.js";
|
||||
import { peekSystemEvents } from "../infra/system-events.js";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
|
||||
@ -11,7 +11,6 @@ import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
|
||||
import { resolveHeartbeatReplyPayload } from "../auto-reply/heartbeat-reply-payload.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
DEFAULT_HEARTBEAT_EVERY,
|
||||
isHeartbeatContentEffectivelyEmpty,
|
||||
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
|
||||
stripHeartbeatToken,
|
||||
@ -21,7 +20,6 @@ import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import type { ChannelHeartbeatDeps } from "../channels/plugins/types.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
@ -56,6 +54,12 @@ import {
|
||||
} from "./heartbeat-events-filter.js";
|
||||
import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
|
||||
import { resolveHeartbeatReasonKind } from "./heartbeat-reason.js";
|
||||
import {
|
||||
isHeartbeatEnabledForAgent,
|
||||
resolveHeartbeatIntervalMs,
|
||||
resolveHeartbeatSummaryForAgent,
|
||||
type HeartbeatSummary,
|
||||
} from "./heartbeat-summary.js";
|
||||
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
|
||||
import {
|
||||
areHeartbeatsEnabled,
|
||||
@ -84,6 +88,12 @@ export type HeartbeatDeps = OutboundSendDeps &
|
||||
const log = createSubsystemLogger("gateway/heartbeat");
|
||||
|
||||
export { areHeartbeatsEnabled, setHeartbeatsEnabled };
|
||||
export {
|
||||
isHeartbeatEnabledForAgent,
|
||||
resolveHeartbeatIntervalMs,
|
||||
resolveHeartbeatSummaryForAgent,
|
||||
type HeartbeatSummary,
|
||||
} from "./heartbeat-summary.js";
|
||||
|
||||
type HeartbeatConfig = AgentDefaultsConfig["heartbeat"];
|
||||
type HeartbeatAgent = {
|
||||
@ -91,17 +101,6 @@ type HeartbeatAgent = {
|
||||
heartbeat?: HeartbeatConfig;
|
||||
};
|
||||
|
||||
export type HeartbeatSummary = {
|
||||
enabled: boolean;
|
||||
every: string;
|
||||
everyMs: number | null;
|
||||
prompt: string;
|
||||
target: string;
|
||||
model?: string;
|
||||
ackMaxChars: number;
|
||||
};
|
||||
|
||||
const DEFAULT_HEARTBEAT_TARGET = "none";
|
||||
export { isCronSystemEvent };
|
||||
|
||||
type HeartbeatAgentState = {
|
||||
@ -122,18 +121,6 @@ function hasExplicitHeartbeatAgents(cfg: OpenClawConfig) {
|
||||
return list.some((entry) => Boolean(entry?.heartbeat));
|
||||
}
|
||||
|
||||
export function isHeartbeatEnabledForAgent(cfg: OpenClawConfig, agentId?: string): boolean {
|
||||
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
|
||||
const list = cfg.agents?.list ?? [];
|
||||
const hasExplicit = hasExplicitHeartbeatAgents(cfg);
|
||||
if (hasExplicit) {
|
||||
return list.some(
|
||||
(entry) => Boolean(entry?.heartbeat) && normalizeAgentId(entry?.id) === resolvedAgentId,
|
||||
);
|
||||
}
|
||||
return resolvedAgentId === resolveDefaultAgentId(cfg);
|
||||
}
|
||||
|
||||
function resolveHeartbeatConfig(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
@ -149,54 +136,6 @@ function resolveHeartbeatConfig(
|
||||
return { ...defaults, ...overrides };
|
||||
}
|
||||
|
||||
export function resolveHeartbeatSummaryForAgent(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): HeartbeatSummary {
|
||||
const defaults = cfg.agents?.defaults?.heartbeat;
|
||||
const overrides = agentId ? resolveAgentConfig(cfg, agentId)?.heartbeat : undefined;
|
||||
const enabled = isHeartbeatEnabledForAgent(cfg, agentId);
|
||||
|
||||
if (!enabled) {
|
||||
return {
|
||||
enabled: false,
|
||||
every: "disabled",
|
||||
everyMs: null,
|
||||
prompt: resolveHeartbeatPromptText(defaults?.prompt),
|
||||
target: defaults?.target ?? DEFAULT_HEARTBEAT_TARGET,
|
||||
model: defaults?.model,
|
||||
ackMaxChars: Math.max(0, defaults?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS),
|
||||
};
|
||||
}
|
||||
|
||||
const merged = defaults || overrides ? { ...defaults, ...overrides } : undefined;
|
||||
const every = merged?.every ?? defaults?.every ?? overrides?.every ?? DEFAULT_HEARTBEAT_EVERY;
|
||||
const everyMs = resolveHeartbeatIntervalMs(cfg, undefined, merged);
|
||||
const prompt = resolveHeartbeatPromptText(
|
||||
merged?.prompt ?? defaults?.prompt ?? overrides?.prompt,
|
||||
);
|
||||
const target =
|
||||
merged?.target ?? defaults?.target ?? overrides?.target ?? DEFAULT_HEARTBEAT_TARGET;
|
||||
const model = merged?.model ?? defaults?.model ?? overrides?.model;
|
||||
const ackMaxChars = Math.max(
|
||||
0,
|
||||
merged?.ackMaxChars ??
|
||||
defaults?.ackMaxChars ??
|
||||
overrides?.ackMaxChars ??
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
);
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
every,
|
||||
everyMs,
|
||||
prompt,
|
||||
target,
|
||||
model,
|
||||
ackMaxChars,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHeartbeatAgents(cfg: OpenClawConfig): HeartbeatAgent[] {
|
||||
const list = cfg.agents?.list ?? [];
|
||||
if (hasExplicitHeartbeatAgents(cfg)) {
|
||||
@ -212,35 +151,6 @@ function resolveHeartbeatAgents(cfg: OpenClawConfig): HeartbeatAgent[] {
|
||||
return [{ agentId: fallbackId, heartbeat: resolveHeartbeatConfig(cfg, fallbackId) }];
|
||||
}
|
||||
|
||||
export function resolveHeartbeatIntervalMs(
|
||||
cfg: OpenClawConfig,
|
||||
overrideEvery?: string,
|
||||
heartbeat?: HeartbeatConfig,
|
||||
) {
|
||||
const raw =
|
||||
overrideEvery ??
|
||||
heartbeat?.every ??
|
||||
cfg.agents?.defaults?.heartbeat?.every ??
|
||||
DEFAULT_HEARTBEAT_EVERY;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = String(raw).trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
let ms: number;
|
||||
try {
|
||||
ms = parseDurationMs(trimmed, { defaultUnit: "m" });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (ms <= 0) {
|
||||
return null;
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
export function resolveHeartbeatPrompt(cfg: OpenClawConfig, heartbeat?: HeartbeatConfig) {
|
||||
return resolveHeartbeatPromptText(heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt);
|
||||
}
|
||||
|
||||
118
src/infra/heartbeat-summary.ts
Normal file
118
src/infra/heartbeat-summary.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { resolveAgentConfig, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
DEFAULT_HEARTBEAT_EVERY,
|
||||
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
|
||||
} from "../auto-reply/heartbeat.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
|
||||
type HeartbeatConfig = AgentDefaultsConfig["heartbeat"];
|
||||
|
||||
export type HeartbeatSummary = {
|
||||
enabled: boolean;
|
||||
every: string;
|
||||
everyMs: number | null;
|
||||
prompt: string;
|
||||
target: string;
|
||||
model?: string;
|
||||
ackMaxChars: number;
|
||||
};
|
||||
|
||||
const DEFAULT_HEARTBEAT_TARGET = "none";
|
||||
|
||||
function hasExplicitHeartbeatAgents(cfg: OpenClawConfig) {
|
||||
const list = cfg.agents?.list ?? [];
|
||||
return list.some((entry) => Boolean(entry?.heartbeat));
|
||||
}
|
||||
|
||||
export function isHeartbeatEnabledForAgent(cfg: OpenClawConfig, agentId?: string): boolean {
|
||||
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
|
||||
const list = cfg.agents?.list ?? [];
|
||||
const hasExplicit = hasExplicitHeartbeatAgents(cfg);
|
||||
if (hasExplicit) {
|
||||
return list.some(
|
||||
(entry) => Boolean(entry?.heartbeat) && normalizeAgentId(entry?.id) === resolvedAgentId,
|
||||
);
|
||||
}
|
||||
return resolvedAgentId === resolveDefaultAgentId(cfg);
|
||||
}
|
||||
|
||||
export function resolveHeartbeatIntervalMs(
|
||||
cfg: OpenClawConfig,
|
||||
overrideEvery?: string,
|
||||
heartbeat?: HeartbeatConfig,
|
||||
) {
|
||||
const raw =
|
||||
overrideEvery ??
|
||||
heartbeat?.every ??
|
||||
cfg.agents?.defaults?.heartbeat?.every ??
|
||||
DEFAULT_HEARTBEAT_EVERY;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = String(raw).trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
let ms: number;
|
||||
try {
|
||||
ms = parseDurationMs(trimmed, { defaultUnit: "m" });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (ms <= 0) {
|
||||
return null;
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
export function resolveHeartbeatSummaryForAgent(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): HeartbeatSummary {
|
||||
const defaults = cfg.agents?.defaults?.heartbeat;
|
||||
const overrides = agentId ? resolveAgentConfig(cfg, agentId)?.heartbeat : undefined;
|
||||
const enabled = isHeartbeatEnabledForAgent(cfg, agentId);
|
||||
|
||||
if (!enabled) {
|
||||
return {
|
||||
enabled: false,
|
||||
every: "disabled",
|
||||
everyMs: null,
|
||||
prompt: resolveHeartbeatPromptText(defaults?.prompt),
|
||||
target: defaults?.target ?? DEFAULT_HEARTBEAT_TARGET,
|
||||
model: defaults?.model,
|
||||
ackMaxChars: Math.max(0, defaults?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS),
|
||||
};
|
||||
}
|
||||
|
||||
const merged = defaults || overrides ? { ...defaults, ...overrides } : undefined;
|
||||
const every = merged?.every ?? defaults?.every ?? overrides?.every ?? DEFAULT_HEARTBEAT_EVERY;
|
||||
const everyMs = resolveHeartbeatIntervalMs(cfg, undefined, merged);
|
||||
const prompt = resolveHeartbeatPromptText(
|
||||
merged?.prompt ?? defaults?.prompt ?? overrides?.prompt,
|
||||
);
|
||||
const target =
|
||||
merged?.target ?? defaults?.target ?? overrides?.target ?? DEFAULT_HEARTBEAT_TARGET;
|
||||
const model = merged?.model ?? defaults?.model ?? overrides?.model;
|
||||
const ackMaxChars = Math.max(
|
||||
0,
|
||||
merged?.ackMaxChars ??
|
||||
defaults?.ackMaxChars ??
|
||||
overrides?.ackMaxChars ??
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
);
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
every,
|
||||
everyMs,
|
||||
prompt,
|
||||
target,
|
||||
model,
|
||||
ackMaxChars,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user