fix(api): require api server callbacks

This commit is contained in:
ZacharyZhang-NY committed 2026-04-22 14:27:49 -04:00
1 parent 529bb109db
commit 5155136937
9 files changed
+103 -59

No files matched your search

+2 -4
View File
@@ -21,12 +21,10 @@ export { AgentApiServer };
*/
export function createApiServer(
db: AppDatabase,
onDomainChanged: () => void,
onWakeAgent: (agentId: string, companyId: string, trigger: string) => string | null,
callbacks?: Partial<ApiServerEventCallbacks>,
callbacks: ApiServerEventCallbacks,
allowedOrigins?: Iterable<string>,
): AgentApiServer {
const server = new AgentApiServer(db, onDomainChanged, onWakeAgent, callbacks, allowedOrigins);
const server = new AgentApiServer(db, callbacks, allowedOrigins);
registerAgentRoutes(server);
registerAgentControlRoutes(server);
registerAgentStandupRoute(server);
+35 -22
View File
@@ -83,6 +83,35 @@ export interface ApiServerEventCallbacks {
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
}
const REQUIRED_API_SERVER_CALLBACKS: readonly (keyof ApiServerEventCallbacks)[] = [
"onDomainChanged",
"onWakeAgent",
"onTaskCreated",
"onTaskStatusChanged",
"onCommentPosted",
"onApprovalCreated",
"onApprovalResolved",
"createDocument",
"onAutoAssignTask",
"onHumanMessage",
"injectRuntimeMessage",
"emitEvent",
"notify",
];
function assertCompleteApiServerCallbacks(
callbacks: Partial<ApiServerEventCallbacks> | null | undefined,
): asserts callbacks is ApiServerEventCallbacks {
if (!callbacks) {
throw new Error("Missing AgentApiServer callbacks.");
}
for (const name of REQUIRED_API_SERVER_CALLBACKS) {
if (typeof callbacks[name] !== "function") {
throw new Error(`Missing AgentApiServer callback: ${name}`);
}
}
}
/**
* Token-bucket rate limiter scoped per (companyId, agentId).
*
@@ -139,32 +168,16 @@ export class AgentApiServer {
constructor(
db: AppDatabase,
onDomainChanged: () => void,
onWakeAgent: (agentId: string, companyId: string, trigger: string) => string | null,
callbacks?: Partial<ApiServerEventCallbacks>,
callbacks: ApiServerEventCallbacks,
allowedOrigins: Iterable<string> = ["app://-"],
) {
assertCompleteApiServerCallbacks(callbacks);
this._db = db;
this._onDomainChangedCb = onDomainChanged;
this._onWakeAgentCb = onWakeAgent;
this._emitEventCb = callbacks?.emitEvent ?? (() => {});
this._onDomainChangedCb = callbacks.onDomainChanged;
this._onWakeAgentCb = callbacks.onWakeAgent;
this._emitEventCb = callbacks.emitEvent;
this._allowedOrigins = new Set(Array.from(allowedOrigins).filter(Boolean));
this._callbacks = {
onDomainChanged,
onWakeAgent,
onTaskCreated: callbacks?.onTaskCreated ?? (() => {}),
onTaskStatusChanged: callbacks?.onTaskStatusChanged ?? (() => {}),
onCommentPosted: callbacks?.onCommentPosted ?? (() => {}),
onApprovalCreated: callbacks?.onApprovalCreated ?? (() => {}),
onApprovalResolved: callbacks?.onApprovalResolved ?? (() => {}),
createDocument: callbacks?.createDocument
?? (() => { throw new Error("createDocument callback is not wired."); }),
onAutoAssignTask: callbacks?.onAutoAssignTask ?? (() => null),
onHumanMessage: callbacks?.onHumanMessage ?? (() => {}),
injectRuntimeMessage: callbacks?.injectRuntimeMessage ?? (() => false),
emitEvent: callbacks?.emitEvent ?? (() => {}),
notify: callbacks?.notify ?? (() => {}),
};
this._callbacks = { ...callbacks };
}
setBrowserActionHandler(handler: (actionId: string, companyId: string) => void) {
+4 -22
View File
@@ -1,17 +1,13 @@
import log from "electron-log/main.js";
import type { AgentApiServer } from "../api-server";
import type { AgentApiServer, ApiServerEventCallbacks } from "../api-server";
import type { AppDatabase } from "../database";
import type { AgentMessageRecord, DesktopEvent, SectionId } from "@shared/types";
import type { DesktopEvent } from "@shared/types";
import type { NotificationOptions } from "../notification-service";
import type { createServiceGraph } from "../service-graph";
import type { createThreadIntegration } from "../thread-integration";
import type { createCheckpointEngine } from "../checkpoint-engine";
import type { AutomationEventContext } from "../automation/types";
import {
createDocumentWithLifecycle,
type DocumentLifecycleInput,
type DocumentLifecycleOutcome,
} from "../document-lifecycle";
import { createDocumentWithLifecycle } from "../document-lifecycle";
import type { DocumentLifecycleProcessor } from "../document-lifecycle-processor";
type ServiceGraph = ReturnType<typeof createServiceGraph>;
@@ -19,21 +15,7 @@ type ThreadIntegration = ReturnType<typeof createThreadIntegration>;
type CheckpointEngine = ReturnType<typeof createCheckpointEngine>;
export interface CreateApiServerFn {
(db: AppDatabase, callbacks: {
onDomainChanged: () => void;
onWakeAgent: (agentId: string, companyId: string, trigger: string) => string | null;
onTaskCreated: (taskId: string, companyId: string) => void;
onTaskStatusChanged: (taskId: string, companyId: string, prev: string, next: string) => void;
onCommentPosted: (taskId: string, companyId: string, authorAgentId: string | null) => void;
onApprovalCreated: (approvalId: string, companyId: string, taskId: string | null, approvalType: string, summary: string) => void;
onApprovalResolved: (approvalId: string, companyId: string, decision: string) => void;
createDocument: (input: DocumentLifecycleInput) => DocumentLifecycleOutcome;
onAutoAssignTask: (taskId: string, companyId: string) => string | null;
onHumanMessage: (companyId: string, toAgentId?: string | null, threadId?: string | null, waitStateId?: string | null) => void;
injectRuntimeMessage: (recipientAgentId: string, companyId: string, message: AgentMessageRecord) => boolean;
emitEvent: (event: DesktopEvent) => void;
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
}, allowedOrigins: string[]): AgentApiServer;
(db: AppDatabase, callbacks: ApiServerEventCallbacks, allowedOrigins: string[]): AgentApiServer;
}
export interface WireApiServerDeps {
+1 -7
View File
@@ -69,13 +69,7 @@ export function createMainRuntime({
devServerUrl,
workerPath,
createApiServer: (db, callbacks, allowedOrigins) =>
createApiServer(
db,
support.publishDomainChanged,
(agentId, companyId, trigger) => callbacks.onWakeAgent(agentId, companyId, trigger),
callbacks,
allowedOrigins,
),
createApiServer(db, callbacks, allowedOrigins),
createRunToken: createAgentToken,
seedDefaultAutomationRules: support.seedDefaultAutomationRules,
isQuittingRef: {
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
import { createApiServer } from "../../src/main/api-server";
import { AppDatabase } from "../../src/main/database";
import { createTestApiServerCallbacks } from "../support/api-server-callbacks";
const tempDirs: string[] = [];
@@ -83,7 +84,7 @@ async function createHarness() {
});
const run = db.runs.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "openai-codex" });
const token = createAgentToken(ctoId, companyId, run.id);
const server = createApiServer(db, () => {}, () => null);
const server = createApiServer(db, createTestApiServerCallbacks());
const port = await server.start();
return {
+2 -1
View File
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
import { createApiServer } from "../../src/main/api-server";
import { AppDatabase } from "../../src/main/database";
import { createTestApiServerCallbacks } from "../support/api-server-callbacks";
const tempDirs: string[] = [];
@@ -57,7 +58,7 @@ async function createHarness() {
});
const run = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" });
const token = createAgentToken(agentId, companyId, run.id);
const server = createApiServer(db, () => {}, () => null);
const server = createApiServer(db, createTestApiServerCallbacks());
const port = await server.start();
return {
@@ -7,6 +7,7 @@ import { createApiServer } from "../../src/main/api-server";
import { AppDatabase } from "../../src/main/database";
import { processRunCompletion } from "../../src/main/automation/automation-run-completion";
import { createDocumentWithLifecycle } from "../../src/main/document-lifecycle";
import { createTestApiServerCallbacks } from "../support/api-server-callbacks";
const tempDirs: string[] = [];
@@ -92,7 +93,7 @@ async function createHarness() {
const token = createAgentToken(agentId, companyId, run.id);
const silentLogger = { warn: () => {}, error: () => {} };
const noopLifecycleProcessor = { drainForDocument: () => {} };
const server = createApiServer(db, () => {}, () => null, {
const server = createApiServer(db, createTestApiServerCallbacks({
createDocument: (input) =>
createDocumentWithLifecycle(
{
@@ -107,7 +108,7 @@ async function createHarness() {
},
input,
),
});
}));
const port = await server.start();
return {
+24
View File
@@ -0,0 +1,24 @@
import type { ApiServerEventCallbacks } from "../../src/main/api-server";
export function createTestApiServerCallbacks(
overrides: Partial<ApiServerEventCallbacks> = {},
): ApiServerEventCallbacks {
return {
onDomainChanged: () => {},
onWakeAgent: () => null,
onTaskCreated: () => {},
onTaskStatusChanged: () => {},
onCommentPosted: () => {},
onApprovalCreated: () => {},
onApprovalResolved: () => {},
createDocument: () => {
throw new Error("createDocument test callback was not provided.");
},
onAutoAssignTask: () => null,
onHumanMessage: () => {},
injectRuntimeMessage: () => false,
emitEvent: () => {},
notify: () => {},
...overrides,
};
}
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { createApiServer, type ApiServerEventCallbacks } from "../../src/main/api-server";
import type { AppDatabase } from "../../src/main/database";
import { createTestApiServerCallbacks } from "../support/api-server-callbacks";
describe("api server callback contract", () => {
it("fails during construction when the callback object is missing", () => {
expect(() => {
createApiServer({} as AppDatabase, undefined as unknown as ApiServerEventCallbacks);
}).toThrow("Missing AgentApiServer callbacks.");
});
it("fails during construction when a required callback is missing", () => {
const callbacks: Partial<ApiServerEventCallbacks> = createTestApiServerCallbacks();
delete callbacks.onTaskCreated;
expect(() => {
createApiServer({} as AppDatabase, callbacks as ApiServerEventCallbacks);
}).toThrow("Missing AgentApiServer callback: onTaskCreated");
});
it("constructs the server when every callback is explicit", () => {
const callbacks = createTestApiServerCallbacks();
const server = createApiServer({} as AppDatabase, callbacks);
expect(server.callbacks.onTaskCreated).toBe(callbacks.onTaskCreated);
expect(server.onDomainChanged).toBe(callbacks.onDomainChanged);
expect(server.onWakeAgent).toBe(callbacks.onWakeAgent);
});
});