feat(FN-1263): add agent budget tracking and reset APIs
- Add AgentBudgetConfig and AgentBudgetStatus types to runtime config and export them from @fusion/core - Implement AgentStore.getBudgetStatus to compute usage, thresholds, over-budget flags, and next reset timestamps for daily/weekly/monthly/lifetime periods - Implement AgentStore.resetBudgetUsage to zero token counters, record budgetResetAt, and emit agent updates while preserving other runtime settings - Add comprehensive AgentStore budget management tests covering no-budget behavior, threshold calculations, reset behavior, and reset-day edge cases
This commit is contained in:
@@ -175,6 +175,332 @@ describe("AgentStore", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Budget Management ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Budget Management", () => {
|
||||||
|
describe("getBudgetStatus", () => {
|
||||||
|
it("throws if agent not found", async () => {
|
||||||
|
await expect(store.getBudgetStatus("nonexistent")).rejects.toThrow("not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no-limit status when agent has no budgetConfig", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "No Budget Config",
|
||||||
|
role: "executor",
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
|
||||||
|
expect(status.currentUsage).toBe(0);
|
||||||
|
expect(status.budgetLimit).toBeNull();
|
||||||
|
expect(status.usagePercent).toBeNull();
|
||||||
|
expect(status.thresholdPercent).toBeNull();
|
||||||
|
expect(status.isOverBudget).toBe(false);
|
||||||
|
expect(status.isOverThreshold).toBe(false);
|
||||||
|
expect(status.lastResetAt).toBeNull();
|
||||||
|
expect(status.nextResetAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no-limit status when budgetConfig has no tokenBudget", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Threshold Only",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
usageThreshold: 0.9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
|
||||||
|
expect(status.budgetLimit).toBeNull();
|
||||||
|
expect(status.usagePercent).toBeNull();
|
||||||
|
expect(status.thresholdPercent).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes usage from totalInputTokens + totalOutputTokens", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Usage Counter",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 20000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 5000,
|
||||||
|
totalOutputTokens: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.currentUsage).toBe(8000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects over-budget when usage >= tokenBudget", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Over Budget",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 800,
|
||||||
|
totalOutputTokens: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.isOverBudget).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects over-threshold when usagePercent >= thresholdPercent", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Threshold Hit",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 10000,
|
||||||
|
usageThreshold: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 3000,
|
||||||
|
totalOutputTokens: 2500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.usagePercent).toBeCloseTo(55, 10);
|
||||||
|
expect(status.thresholdPercent).toBe(50);
|
||||||
|
expect(status.isOverThreshold).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is not over-threshold when below threshold", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Threshold Safe",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 10000,
|
||||||
|
usageThreshold: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 2500,
|
||||||
|
totalOutputTokens: 2500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.usagePercent).toBe(50);
|
||||||
|
expect(status.isOverThreshold).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps usagePercent to 100 when over budget", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Clamp Usage",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 400,
|
||||||
|
totalOutputTokens: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.usagePercent).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns lastResetAt from runtimeConfig.budgetResetAt", async () => {
|
||||||
|
const budgetResetAt = "2026-01-01T00:00:00.000Z";
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Has Reset Timestamp",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetResetAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.lastResetAt).toBe(budgetResetAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null nextResetAt for lifetime budget period", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Lifetime Budget",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
budgetPeriod: "lifetime",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
expect(status.nextResetAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes nextResetAt for daily period as next midnight", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Daily Budget",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
budgetPeriod: "daily",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
|
||||||
|
expect(status.nextResetAt).not.toBeNull();
|
||||||
|
const nextResetAt = new Date(status.nextResetAt!);
|
||||||
|
expect(nextResetAt.getTime()).toBeGreaterThan(now);
|
||||||
|
expect(nextResetAt.getHours()).toBe(0);
|
||||||
|
expect(nextResetAt.getMinutes()).toBe(0);
|
||||||
|
expect(nextResetAt.getSeconds()).toBe(0);
|
||||||
|
expect(nextResetAt.getMilliseconds()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes nextResetAt for weekly period using resetDay", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Weekly Budget",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
budgetPeriod: "weekly",
|
||||||
|
resetDay: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
|
||||||
|
expect(status.nextResetAt).not.toBeNull();
|
||||||
|
const nextResetAt = new Date(status.nextResetAt!);
|
||||||
|
expect(nextResetAt.getTime()).toBeGreaterThan(now);
|
||||||
|
expect(nextResetAt.getDay()).toBe(1);
|
||||||
|
expect(nextResetAt.getHours()).toBe(0);
|
||||||
|
expect(nextResetAt.getMinutes()).toBe(0);
|
||||||
|
expect(nextResetAt.getSeconds()).toBe(0);
|
||||||
|
expect(nextResetAt.getMilliseconds()).toBe(0);
|
||||||
|
expect(nextResetAt.getTime() - now).toBeLessThanOrEqual(8 * 24 * 60 * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps resetDay to month length for monthly period", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Monthly Budget",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
budgetPeriod: "monthly",
|
||||||
|
resetDay: 31,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const status = await store.getBudgetStatus(agent.id);
|
||||||
|
|
||||||
|
expect(status.nextResetAt).not.toBeNull();
|
||||||
|
const nextResetAt = new Date(status.nextResetAt!);
|
||||||
|
const lastDayOfMonth = new Date(nextResetAt.getFullYear(), nextResetAt.getMonth() + 1, 0).getDate();
|
||||||
|
|
||||||
|
expect(nextResetAt.getTime()).toBeGreaterThan(now);
|
||||||
|
expect(nextResetAt.getDate()).toBe(Math.min(31, lastDayOfMonth));
|
||||||
|
expect(nextResetAt.getHours()).toBe(0);
|
||||||
|
expect(nextResetAt.getMinutes()).toBe(0);
|
||||||
|
expect(nextResetAt.getSeconds()).toBe(0);
|
||||||
|
expect(nextResetAt.getMilliseconds()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resetBudgetUsage", () => {
|
||||||
|
it("throws if agent not found", async () => {
|
||||||
|
await expect(store.resetBudgetUsage("nonexistent")).rejects.toThrow("not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets totalInputTokens and totalOutputTokens to 0", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Reset Usage",
|
||||||
|
role: "executor",
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.updateAgent(agent.id, {
|
||||||
|
totalInputTokens: 1200,
|
||||||
|
totalOutputTokens: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.resetBudgetUsage(agent.id);
|
||||||
|
|
||||||
|
const updated = await store.getAgent(agent.id);
|
||||||
|
expect(updated).not.toBeNull();
|
||||||
|
expect(updated?.totalInputTokens).toBe(0);
|
||||||
|
expect(updated?.totalOutputTokens).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets budgetResetAt to current timestamp", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Reset Timestamp",
|
||||||
|
role: "executor",
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeReset = Date.now();
|
||||||
|
await store.resetBudgetUsage(agent.id);
|
||||||
|
|
||||||
|
const updated = await store.getAgent(agent.id);
|
||||||
|
const rawBudgetResetAt = (updated?.runtimeConfig as Record<string, unknown> | undefined)?.budgetResetAt;
|
||||||
|
|
||||||
|
expect(typeof rawBudgetResetAt).toBe("string");
|
||||||
|
|
||||||
|
const parsedResetAt = new Date(rawBudgetResetAt as string).getTime();
|
||||||
|
expect(parsedResetAt).toBeGreaterThanOrEqual(beforeReset - 5000);
|
||||||
|
expect(parsedResetAt).toBeGreaterThan(Date.now() - 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves other runtimeConfig values", async () => {
|
||||||
|
const agent = await store.createAgent({
|
||||||
|
name: "Preserve Config",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeatIntervalMs: 30000,
|
||||||
|
budgetConfig: {
|
||||||
|
tokenBudget: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.resetBudgetUsage(agent.id);
|
||||||
|
|
||||||
|
const updated = await store.getAgent(agent.id);
|
||||||
|
const runtimeConfig = updated?.runtimeConfig as Record<string, unknown>;
|
||||||
|
|
||||||
|
expect(runtimeConfig.heartbeatIntervalMs).toBe(30000);
|
||||||
|
expect(runtimeConfig.budgetConfig).toEqual({ tokenBudget: 1000 });
|
||||||
|
expect(typeof runtimeConfig.budgetResetAt).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── updateAgent ───────────────────────────────────────────────────
|
// ── updateAgent ───────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("updateAgent", () => {
|
describe("updateAgent", () => {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import type {
|
|||||||
AgentHeartbeatEvent,
|
AgentHeartbeatEvent,
|
||||||
AgentHeartbeatRun,
|
AgentHeartbeatRun,
|
||||||
AgentDetail,
|
AgentDetail,
|
||||||
|
AgentBudgetConfig,
|
||||||
|
AgentBudgetStatus,
|
||||||
AgentTaskSession,
|
AgentTaskSession,
|
||||||
AgentConfigRevision,
|
AgentConfigRevision,
|
||||||
AgentConfigSnapshot,
|
AgentConfigSnapshot,
|
||||||
@@ -211,6 +213,59 @@ export class AgentStore extends EventEmitter {
|
|||||||
return computeAccessState(agent);
|
return computeAccessState(agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get computed budget usage status for an agent.
|
||||||
|
* @param agentId - The agent ID
|
||||||
|
* @returns Computed budget usage status
|
||||||
|
* @throws Error if agent not found
|
||||||
|
*/
|
||||||
|
async getBudgetStatus(agentId: string): Promise<AgentBudgetStatus> {
|
||||||
|
const agent = await this.getAgent(agentId);
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error(`Agent ${agentId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalInputTokens = agent.totalInputTokens ?? 0;
|
||||||
|
const totalOutputTokens = agent.totalOutputTokens ?? 0;
|
||||||
|
const currentUsage = totalInputTokens + totalOutputTokens;
|
||||||
|
|
||||||
|
const runtimeConfig = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
|
||||||
|
const budgetConfig = runtimeConfig.budgetConfig as AgentBudgetConfig | undefined;
|
||||||
|
const rawLastResetAt = runtimeConfig.budgetResetAt;
|
||||||
|
const lastResetAt = typeof rawLastResetAt === "string" ? rawLastResetAt : null;
|
||||||
|
|
||||||
|
if (!budgetConfig || budgetConfig.tokenBudget === undefined) {
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
currentUsage,
|
||||||
|
budgetLimit: null,
|
||||||
|
usagePercent: null,
|
||||||
|
thresholdPercent: null,
|
||||||
|
isOverBudget: false,
|
||||||
|
isOverThreshold: false,
|
||||||
|
lastResetAt,
|
||||||
|
nextResetAt: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenBudget = budgetConfig.tokenBudget;
|
||||||
|
const usagePercent = Math.min((currentUsage / tokenBudget) * 100, 100);
|
||||||
|
const usageThreshold = budgetConfig.usageThreshold ?? 0.8;
|
||||||
|
const thresholdPercent = usageThreshold * 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
currentUsage,
|
||||||
|
budgetLimit: tokenBudget,
|
||||||
|
usagePercent,
|
||||||
|
thresholdPercent,
|
||||||
|
isOverBudget: currentUsage >= tokenBudget,
|
||||||
|
isOverThreshold: usagePercent >= thresholdPercent,
|
||||||
|
lastResetAt,
|
||||||
|
nextResetAt: this.computeNextResetAt(budgetConfig.budgetPeriod, budgetConfig.resetDay),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get detailed agent info including heartbeat history.
|
* Get detailed agent info including heartbeat history.
|
||||||
* @param agentId - The agent ID
|
* @param agentId - The agent ID
|
||||||
@@ -752,6 +807,35 @@ export class AgentStore extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset budget token usage counters for an agent.
|
||||||
|
* @param agentId - The agent ID
|
||||||
|
* @throws Error if agent not found
|
||||||
|
*/
|
||||||
|
async resetBudgetUsage(agentId: string): Promise<void> {
|
||||||
|
await this.withLock(agentId, async () => {
|
||||||
|
const agent = await this.getAgent(agentId);
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error(`Agent ${agentId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const budgetResetAt = new Date().toISOString();
|
||||||
|
const updated: Agent = {
|
||||||
|
...agent,
|
||||||
|
totalInputTokens: 0,
|
||||||
|
totalOutputTokens: 0,
|
||||||
|
runtimeConfig: {
|
||||||
|
...(agent.runtimeConfig ?? {}),
|
||||||
|
budgetResetAt,
|
||||||
|
},
|
||||||
|
updatedAt: budgetResetAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.writeAgent(updated);
|
||||||
|
this.emit("agent:updated", updated);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset an agent from any state back to "idle".
|
* Reset an agent from any state back to "idle".
|
||||||
* Clears transient execution state (taskId, lastError, pauseReason)
|
* Clears transient execution state (taskId, lastError, pauseReason)
|
||||||
@@ -1499,6 +1583,61 @@ export class AgentStore extends EventEmitter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeNextResetAt(period: AgentBudgetConfig["budgetPeriod"], resetDay?: number): string | null {
|
||||||
|
if (!period || period === "lifetime") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (period === "daily") {
|
||||||
|
const nextMidnight = new Date(now);
|
||||||
|
nextMidnight.setHours(0, 0, 0, 0);
|
||||||
|
nextMidnight.setDate(nextMidnight.getDate() + 1);
|
||||||
|
return nextMidnight.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === "weekly") {
|
||||||
|
const normalizedResetDay =
|
||||||
|
typeof resetDay === "number" && Number.isFinite(resetDay)
|
||||||
|
? Math.max(0, Math.min(6, Math.floor(resetDay)))
|
||||||
|
: 0;
|
||||||
|
const nextWeeklyReset = new Date(now);
|
||||||
|
nextWeeklyReset.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const currentDay = nextWeeklyReset.getDay();
|
||||||
|
let daysUntilReset = (normalizedResetDay - currentDay + 7) % 7;
|
||||||
|
if (daysUntilReset === 0) {
|
||||||
|
daysUntilReset = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextWeeklyReset.setDate(nextWeeklyReset.getDate() + daysUntilReset);
|
||||||
|
return nextWeeklyReset.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === "monthly") {
|
||||||
|
const normalizedResetDay =
|
||||||
|
typeof resetDay === "number" && Number.isFinite(resetDay)
|
||||||
|
? Math.max(1, Math.min(31, Math.floor(resetDay)))
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const createMonthlyReset = (year: number, month: number): Date => {
|
||||||
|
const lastDayOfMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
const clampedResetDay = Math.min(normalizedResetDay, lastDayOfMonth);
|
||||||
|
return new Date(year, month, clampedResetDay, 0, 0, 0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextMonthlyReset = createMonthlyReset(now.getFullYear(), now.getMonth());
|
||||||
|
if (nextMonthlyReset <= now) {
|
||||||
|
nextMonthlyReset = createMonthlyReset(now.getFullYear(), now.getMonth() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextMonthlyReset.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private getBundleDir(agentId: string): string {
|
private getBundleDir(agentId: string): string {
|
||||||
return join(this.agentsDir, `${agentId}-instructions`);
|
return join(this.agentsDir, `${agentId}-instructions`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js";
|
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js";
|
||||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox } from "./types.js";
|
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox } from "./types.js";
|
||||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||||
export {
|
export {
|
||||||
BUILTIN_AGENT_PROMPTS,
|
BUILTIN_AGENT_PROMPTS,
|
||||||
|
|||||||
@@ -1785,6 +1785,42 @@ export interface AgentHeartbeatConfig {
|
|||||||
* "on-heartbeat" defers message handling to the next scheduled heartbeat (default).
|
* "on-heartbeat" defers message handling to the next scheduled heartbeat (default).
|
||||||
*/
|
*/
|
||||||
messageResponseMode?: MessageResponseMode;
|
messageResponseMode?: MessageResponseMode;
|
||||||
|
/** Per-agent budget governance configuration. When set, enables budget tracking and enforcement. */
|
||||||
|
budgetConfig?: AgentBudgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-agent budget configuration, stored in agent.runtimeConfig.budgetConfig */
|
||||||
|
export interface AgentBudgetConfig {
|
||||||
|
/** Total token cap (input + output). When undefined, no budget limit is enforced. */
|
||||||
|
tokenBudget?: number;
|
||||||
|
/** Warning threshold as a fraction (0–1). Default: 0.8. Triggers isOverThreshold when usagePercent >= this value * 100. */
|
||||||
|
usageThreshold?: number;
|
||||||
|
/** Budget accumulation period. Default: "lifetime". */
|
||||||
|
budgetPeriod?: "daily" | "weekly" | "monthly" | "lifetime";
|
||||||
|
/** Day of month/week for period reset (1–31 for monthly, 0–6 for weekly where 0=Sunday). Only used when budgetPeriod is "monthly" or "weekly". */
|
||||||
|
resetDay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Computed budget status for an agent at a point in time. */
|
||||||
|
export interface AgentBudgetStatus {
|
||||||
|
/** The agent this status belongs to */
|
||||||
|
agentId: string;
|
||||||
|
/** Total tokens consumed (input + output) */
|
||||||
|
currentUsage: number;
|
||||||
|
/** Token cap from config, or null when no budget is configured */
|
||||||
|
budgetLimit: number | null;
|
||||||
|
/** Usage as a percentage of budget (0–100), or null when no budget */
|
||||||
|
usagePercent: number | null;
|
||||||
|
/** The configured threshold fraction (e.g., 0.8), or null when no budget */
|
||||||
|
thresholdPercent: number | null;
|
||||||
|
/** Whether currentUsage >= budgetLimit */
|
||||||
|
isOverBudget: boolean;
|
||||||
|
/** Whether usagePercent >= thresholdPercent * 100 */
|
||||||
|
isOverThreshold: boolean;
|
||||||
|
/** ISO-8601 timestamp of the last budget reset, or null */
|
||||||
|
lastResetAt: string | null;
|
||||||
|
/** ISO-8601 timestamp of the next scheduled reset, or null for lifetime/no budget */
|
||||||
|
nextResetAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration for an agent's instruction bundle — a collection of markdown files
|
/** Configuration for an agent's instruction bundle — a collection of markdown files
|
||||||
|
|||||||
Reference in New Issue
Block a user