@axiom-lattice/gateway 2.1.111 → 2.1.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -13
- package/CHANGELOG.md +20 -0
- package/dist/index.js +2118 -319
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1948 -145
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -6
- package/src/controllers/__tests__/workflow-tracking.test.ts +113 -0
- package/src/controllers/connections.ts +7 -0
- package/src/controllers/eval.ts +44 -4
- package/src/controllers/export-import.ts +166 -0
- package/src/controllers/run.ts +30 -4
- package/src/controllers/workflow-tracking.ts +105 -11
- package/src/export_registrations/a2a-api-key.registration.ts +89 -0
- package/src/export_registrations/agent.registration.ts +80 -0
- package/src/export_registrations/binding.registration.ts +106 -0
- package/src/export_registrations/channel-installation.registration.ts +88 -0
- package/src/export_registrations/collection.registration.ts +85 -0
- package/src/export_registrations/connection.registration.ts +97 -0
- package/src/export_registrations/database-config.registration.ts +94 -0
- package/src/export_registrations/eval.registration.ts +334 -0
- package/src/export_registrations/index.ts +31 -0
- package/src/export_registrations/mcp-config.registration.ts +97 -0
- package/src/export_registrations/menu.registration.ts +91 -0
- package/src/export_registrations/metrics-config.registration.ts +94 -0
- package/src/export_registrations/skill.registration.ts +88 -0
- package/src/export_registrations/task.registration.ts +99 -0
- package/src/index.ts +18 -8
- package/src/routes/index.ts +42 -1
- package/src/services/ExportImportService.ts +296 -0
- package/src/services/__tests__/ExportImportService.test.ts +130 -0
- package/src/services/eval-runner.ts +63 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.113",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -41,11 +41,10 @@
|
|
|
41
41
|
"redis": "^5.0.1",
|
|
42
42
|
"uuid": "^9.0.1",
|
|
43
43
|
"zod": "3.25.76",
|
|
44
|
-
"@axiom-lattice/
|
|
45
|
-
"@axiom-lattice/
|
|
46
|
-
"@axiom-lattice/
|
|
47
|
-
"@axiom-lattice/
|
|
48
|
-
"@axiom-lattice/queue-redis": "1.0.49"
|
|
44
|
+
"@axiom-lattice/core": "2.1.100",
|
|
45
|
+
"@axiom-lattice/pg-stores": "1.0.91",
|
|
46
|
+
"@axiom-lattice/protocols": "2.1.51",
|
|
47
|
+
"@axiom-lattice/queue-redis": "1.0.50"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"@types/bcrypt": "^6.0.0",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
const mockQueryWorkflowRuns = jest.fn();
|
|
4
|
+
const mockGetRunSteps = jest.fn();
|
|
5
|
+
const mockGetAllAssistants = jest.fn();
|
|
6
|
+
const mockGetAgent = jest.fn();
|
|
7
|
+
|
|
8
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
9
|
+
getStoreLattice: (_key: string, type: string) =>
|
|
10
|
+
type === "workflowTracking"
|
|
11
|
+
? { store: { queryWorkflowRuns: mockQueryWorkflowRuns, getRunSteps: mockGetRunSteps } }
|
|
12
|
+
: { store: { getAllAssistants: mockGetAllAssistants } },
|
|
13
|
+
agentInstanceManager: { getAgent: mockGetAgent },
|
|
14
|
+
ThreadStatus: { IDLE: "IDLE", BUSY: "BUSY", INTERRUPTED: "INTERRUPTED" },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function makeRun(id: string, startedAt: string) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
tenantId: "t1",
|
|
21
|
+
assistantId: "a1",
|
|
22
|
+
threadId: `th_${id}`,
|
|
23
|
+
status: "interrupted" as const,
|
|
24
|
+
topologyEdges: [],
|
|
25
|
+
totalEdges: 2,
|
|
26
|
+
completedEdges: 1,
|
|
27
|
+
startedAt: new Date(startedAt),
|
|
28
|
+
createdAt: new Date(startedAt),
|
|
29
|
+
updatedAt: new Date(startedAt),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeRequest(query: Record<string, string> = {}) {
|
|
34
|
+
return {
|
|
35
|
+
headers: { "x-tenant-id": "t1" },
|
|
36
|
+
query,
|
|
37
|
+
log: { error: jest.fn(), warn: jest.fn() },
|
|
38
|
+
} as never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mockReply = { status: jest.fn().mockReturnThis(), send: jest.fn() } as never;
|
|
42
|
+
|
|
43
|
+
describe("getInboxItems", () => {
|
|
44
|
+
let getInboxItems: (req: never, reply: never) => Promise<{ data?: Record<string, unknown> }>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
mockGetAllAssistants.mockResolvedValue([{ id: "a1", name: "Assistant One" }] as never);
|
|
49
|
+
mockGetRunSteps.mockResolvedValue([] as never);
|
|
50
|
+
mockGetAgent.mockImplementation(() => { throw new Error("no agent"); }); // fallback path
|
|
51
|
+
const mod = await import("../../controllers/workflow-tracking");
|
|
52
|
+
getInboxItems = mod.getInboxItems as never;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("legacy mode: no query params returns all items without pagination fields", async () => {
|
|
56
|
+
mockQueryWorkflowRuns.mockResolvedValue({
|
|
57
|
+
records: [makeRun("r1", "2026-01-01"), makeRun("r2", "2026-01-02")],
|
|
58
|
+
total: 2,
|
|
59
|
+
} as never);
|
|
60
|
+
|
|
61
|
+
const res = await getInboxItems(makeRequest(), mockReply);
|
|
62
|
+
|
|
63
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted" });
|
|
64
|
+
expect(res.data).toBeDefined();
|
|
65
|
+
const records = res.data!.records as unknown[];
|
|
66
|
+
expect(records).toHaveLength(2);
|
|
67
|
+
expect(res.data!.total).toBeUndefined();
|
|
68
|
+
expect(res.data!.page).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("paged mode: passes limit/offset to store and returns total/page/pageSize", async () => {
|
|
72
|
+
mockQueryWorkflowRuns.mockResolvedValue({
|
|
73
|
+
records: [makeRun("r3", "2026-01-03")],
|
|
74
|
+
total: 42,
|
|
75
|
+
} as never);
|
|
76
|
+
|
|
77
|
+
const res = await getInboxItems(makeRequest({ page: "2", pageSize: "20" }), mockReply);
|
|
78
|
+
|
|
79
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 20, offset: 20 });
|
|
80
|
+
expect(res.data!.total).toBe(42);
|
|
81
|
+
expect(res.data!.page).toBe(2);
|
|
82
|
+
expect(res.data!.pageSize).toBe(20);
|
|
83
|
+
expect(res.data!.records).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("paged mode triggers when only pageSize is passed", async () => {
|
|
87
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
88
|
+
|
|
89
|
+
const res = await getInboxItems(makeRequest({ pageSize: "10" }), mockReply);
|
|
90
|
+
|
|
91
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 10, offset: 0 });
|
|
92
|
+
expect(res.data!.page).toBe(1);
|
|
93
|
+
expect(res.data!.pageSize).toBe(10);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("sanitizes invalid page and caps pageSize at 100", async () => {
|
|
97
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
98
|
+
|
|
99
|
+
const res = await getInboxItems(makeRequest({ page: "abc", pageSize: "500" }), mockReply);
|
|
100
|
+
|
|
101
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 100, offset: 0 });
|
|
102
|
+
expect(res.data!.page).toBe(1);
|
|
103
|
+
expect(res.data!.pageSize).toBe(100);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns empty records with pagination fields in paged mode", async () => {
|
|
107
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
108
|
+
|
|
109
|
+
const res = await getInboxItems(makeRequest({ page: "3", pageSize: "20" }), mockReply);
|
|
110
|
+
|
|
111
|
+
expect(res.data).toEqual({ records: [], total: 0, page: 3, pageSize: 20 });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -16,6 +16,13 @@ const testOrDiscoverBody = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
function getTenant(request: FastifyRequest): string {
|
|
19
|
+
// First try to get from authenticated user context
|
|
20
|
+
const userTenantId = (request as any).user?.tenantId;
|
|
21
|
+
if (userTenantId) {
|
|
22
|
+
return userTenantId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fallback to request headers for backward compatibility
|
|
19
26
|
return (request.headers["x-tenant-id"] as string) || "default";
|
|
20
27
|
}
|
|
21
28
|
|
package/src/controllers/eval.ts
CHANGED
|
@@ -25,12 +25,27 @@ export async function createProject(
|
|
|
25
25
|
const store = getEvalStore();
|
|
26
26
|
const id = uuidv4();
|
|
27
27
|
const data = request.body as any;
|
|
28
|
+
const serverCfg = (data.targetServerConfig || {}) as Record<string, unknown>;
|
|
29
|
+
const judgeCfg = (data.judgeModelConfig || {}) as Record<string, unknown>;
|
|
30
|
+
const modelKey = (judgeCfg.modelKey as string) || "";
|
|
31
|
+
if (modelKey) {
|
|
32
|
+
const { modelLatticeManager } = await import("@axiom-lattice/core");
|
|
33
|
+
if (!modelLatticeManager.hasLattice(modelKey)) {
|
|
34
|
+
return reply.status(400).send({ success: false, message: `Judge model "${modelKey}" is not registered` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
const project = await store.createProject(tenantId, id, {
|
|
29
38
|
name: data.name,
|
|
30
39
|
description: data.description,
|
|
31
40
|
version: data.version,
|
|
32
|
-
judgeModelConfig:
|
|
33
|
-
|
|
41
|
+
judgeModelConfig: {
|
|
42
|
+
modelKey,
|
|
43
|
+
displayName: (judgeCfg.displayName as string) || "",
|
|
44
|
+
},
|
|
45
|
+
targetServerConfig: {
|
|
46
|
+
base_url: (serverCfg.base_url as string) || "",
|
|
47
|
+
api_key: (serverCfg.api_key as string) || "",
|
|
48
|
+
},
|
|
34
49
|
concurrency: data.concurrency ?? 1,
|
|
35
50
|
reportConfig: data.reportConfig,
|
|
36
51
|
});
|
|
@@ -62,7 +77,7 @@ export async function listProjects(
|
|
|
62
77
|
const first = models[0];
|
|
63
78
|
const judgeModel = first
|
|
64
79
|
? { modelKey: first.key }
|
|
65
|
-
: {
|
|
80
|
+
: {};
|
|
66
81
|
|
|
67
82
|
const host = request.hostname || "localhost";
|
|
68
83
|
const baseUrl = `http://${host}:${process.env.PORT || 4001}`;
|
|
@@ -125,7 +140,32 @@ export async function updateProject(
|
|
|
125
140
|
if (!existing) {
|
|
126
141
|
return reply.status(404).send({ success: false, message: "Project not found" });
|
|
127
142
|
}
|
|
128
|
-
const
|
|
143
|
+
const body = request.body as Record<string, unknown>;
|
|
144
|
+
const updateData: Record<string, unknown> = { ...body };
|
|
145
|
+
// Strip in_process from user input — it is server-controlled
|
|
146
|
+
const serverCfg = (body.targetServerConfig || {}) as Record<string, unknown>;
|
|
147
|
+
if (body.targetServerConfig !== undefined) {
|
|
148
|
+
updateData.targetServerConfig = {
|
|
149
|
+
base_url: (serverCfg.base_url as string) || "",
|
|
150
|
+
api_key: (serverCfg.api_key as string) || "",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Only allow modelKey/displayName — never raw provider/apiKey/baseURL
|
|
154
|
+
if (body.judgeModelConfig !== undefined) {
|
|
155
|
+
const judgeCfg = (body.judgeModelConfig || {}) as Record<string, unknown>;
|
|
156
|
+
const modelKey = (judgeCfg.modelKey as string) || "";
|
|
157
|
+
if (modelKey) {
|
|
158
|
+
const { modelLatticeManager } = await import("@axiom-lattice/core");
|
|
159
|
+
if (!modelLatticeManager.hasLattice(modelKey)) {
|
|
160
|
+
return reply.status(400).send({ success: false, message: `Judge model "${modelKey}" is not registered` });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
updateData.judgeModelConfig = {
|
|
164
|
+
modelKey,
|
|
165
|
+
displayName: (judgeCfg.displayName as string) || "",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const updated = await store.updateProject(tenantId, pid, updateData);
|
|
129
169
|
return {
|
|
130
170
|
success: true,
|
|
131
171
|
message: "Successfully updated project",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import { ExportImportService } from '../services/ExportImportService';
|
|
3
|
+
import { ExportableEntityRegistry } from '@axiom-lattice/core';
|
|
4
|
+
|
|
5
|
+
const service = new ExportImportService();
|
|
6
|
+
|
|
7
|
+
function getTenantId(request: FastifyRequest): string {
|
|
8
|
+
return (request as any).user?.tenantId || (request.headers['x-tenant-id'] as string) || 'default';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** GET /api/tenants/exportable-types */
|
|
12
|
+
export async function getExportableTypes(
|
|
13
|
+
_request: FastifyRequest,
|
|
14
|
+
reply: FastifyReply,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const types = service.listExportableTypes();
|
|
17
|
+
return reply.send({ success: true, data: types });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** POST /api/tenants/:tenantId/export */
|
|
21
|
+
export async function exportConfig(
|
|
22
|
+
request: FastifyRequest<{ Params: { tenantId: string }; Body: { entityTypes: string[]; entityIds?: Record<string, string[]> } }>,
|
|
23
|
+
reply: FastifyReply,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
26
|
+
const { entityTypes, entityIds } = request.body;
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(entityTypes) || entityTypes.length === 0) {
|
|
29
|
+
return reply.status(400).send({ success: false, message: 'entityTypes must be a non-empty array' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const preview = await service.exportPreview(tenantId, entityTypes);
|
|
33
|
+
|
|
34
|
+
if (preview.missingDependencies.length > 0 || preview.cascadeAdditions.length > 0) {
|
|
35
|
+
return reply.send({
|
|
36
|
+
success: true,
|
|
37
|
+
data: {
|
|
38
|
+
needsConfirmation: true,
|
|
39
|
+
missingDependencies: preview.missingDependencies,
|
|
40
|
+
cascadeAdditions: preview.cascadeAdditions,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { jobId, bundle } = await service.export(tenantId, entityTypes, entityIds);
|
|
46
|
+
return reply.send({
|
|
47
|
+
success: true,
|
|
48
|
+
data: {
|
|
49
|
+
jobId,
|
|
50
|
+
summary: Object.fromEntries(
|
|
51
|
+
Object.entries(bundle.entities).map(([type, entities]) => [type, entities.length]),
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** POST /api/tenants/:tenantId/export/confirm */
|
|
58
|
+
export async function exportConfigConfirm(
|
|
59
|
+
request: FastifyRequest<{ Params: { tenantId: string }; Body: { entityTypes: string[]; entityIds?: Record<string, string[]> } }>,
|
|
60
|
+
reply: FastifyReply,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
63
|
+
const { entityTypes, entityIds } = request.body;
|
|
64
|
+
|
|
65
|
+
const { jobId, bundle } = await service.export(tenantId, entityTypes, entityIds);
|
|
66
|
+
return reply.send({
|
|
67
|
+
success: true,
|
|
68
|
+
data: {
|
|
69
|
+
jobId,
|
|
70
|
+
summary: Object.fromEntries(
|
|
71
|
+
Object.entries(bundle.entities).map(([type, entities]) => [type, entities.length]),
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** POST /api/tenants/:tenantId/export/entities */
|
|
78
|
+
export async function previewEntities(
|
|
79
|
+
request: FastifyRequest<{ Params: { tenantId: string }; Body: { entityTypes: string[] } }>,
|
|
80
|
+
reply: FastifyReply,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
83
|
+
const { entityTypes } = request.body;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const registry = ExportableEntityRegistry.getInstance();
|
|
87
|
+
const result: Record<string, Array<{ id: string; name: string; description?: string }>> = {};
|
|
88
|
+
|
|
89
|
+
for (const type of entityTypes) {
|
|
90
|
+
const def = registry.get(type);
|
|
91
|
+
const entities = await def.listForExport(tenantId);
|
|
92
|
+
result[type] = entities.map((e) => ({
|
|
93
|
+
id: e.data.id as string,
|
|
94
|
+
name: (e.data.name as string) || (e.data.id as string),
|
|
95
|
+
description: e.data.description as string | undefined,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return reply.send({ success: true, data: result });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return reply.status(400).send({ success: false, message: (err as Error).message });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** GET /api/tenants/:tenantId/export/:jobId/download */
|
|
106
|
+
export async function downloadExport(
|
|
107
|
+
request: FastifyRequest<{ Params: { tenantId: string; jobId: string } }>,
|
|
108
|
+
reply: FastifyReply,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
111
|
+
const bundle = service.getExportJob(request.params.jobId, tenantId);
|
|
112
|
+
|
|
113
|
+
if (!bundle) {
|
|
114
|
+
return reply.status(404).send({ success: false, message: 'Export job not found or expired' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return reply
|
|
118
|
+
.header('Content-Type', 'application/json')
|
|
119
|
+
.header('Content-Disposition', `attachment; filename="tenant-export-${bundle.sourceTenantId}.json"`)
|
|
120
|
+
.send({ success: true, data: bundle });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** POST /api/tenants/:tenantId/import/preview */
|
|
124
|
+
export async function importPreview(
|
|
125
|
+
request: FastifyRequest<{ Params: { tenantId: string }; Body: { bundle: any } }>,
|
|
126
|
+
reply: FastifyReply,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
129
|
+
const { bundle } = request.body;
|
|
130
|
+
|
|
131
|
+
if (!bundle) {
|
|
132
|
+
return reply.status(400).send({ success: false, message: 'No bundle provided in request body' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const preview = await service.previewImport(tenantId, bundle);
|
|
137
|
+
return reply.send({ success: true, data: preview });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return reply.status(400).send({
|
|
140
|
+
success: false,
|
|
141
|
+
message: `Invalid export file: ${(err as Error).message}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** POST /api/tenants/:tenantId/import/apply */
|
|
147
|
+
export async function importApply(
|
|
148
|
+
request: FastifyRequest<{
|
|
149
|
+
Params: { tenantId: string };
|
|
150
|
+
Body: { bundle: any; resolutions: Record<string, any> };
|
|
151
|
+
}>,
|
|
152
|
+
reply: FastifyReply,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const tenantId = getTenantId(request) || request.params.tenantId;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const { bundle, resolutions } = request.body;
|
|
158
|
+
const result = await service.applyImport(tenantId, bundle, resolutions);
|
|
159
|
+
return reply.send({ success: true, data: result });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return reply.status(400).send({
|
|
162
|
+
success: false,
|
|
163
|
+
message: `Import failed: ${(err as Error).message}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/controllers/run.ts
CHANGED
|
@@ -73,6 +73,27 @@ export const createRun = async (
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
// ADR-026: background mode — enqueue only, returns 202.
|
|
77
|
+
// Chunk delivery is handled by the thread-level resume_stream SSE.
|
|
78
|
+
if (background) {
|
|
79
|
+
const messageInput = message_id
|
|
80
|
+
? { ...input, id: message_id }
|
|
81
|
+
: input;
|
|
82
|
+
|
|
83
|
+
const result = await agent.addMessage({
|
|
84
|
+
input: messageInput,
|
|
85
|
+
command,
|
|
86
|
+
custom_run_config: mergedConfig,
|
|
87
|
+
}, mode as QueueMode);
|
|
88
|
+
|
|
89
|
+
reply.status(202).send({
|
|
90
|
+
success: true,
|
|
91
|
+
messageId: result.messageId,
|
|
92
|
+
queued: true,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
// Handle streaming requests
|
|
77
98
|
if (streaming) {
|
|
78
99
|
// Setup SSE
|
|
@@ -198,12 +219,17 @@ export const resumeStream = async (
|
|
|
198
219
|
project_id,
|
|
199
220
|
});
|
|
200
221
|
|
|
201
|
-
//
|
|
202
|
-
|
|
222
|
+
// ADR-026: Thread-level SSE, lives until client disconnect.
|
|
223
|
+
// Empty stopTypes means the iterator never stops on its own —
|
|
224
|
+
// MESSAGE_COMPLETED, THREAD_IDLE etc. pass through as data.
|
|
225
|
+
// Client disconnect (close event) or client abort (reply.raw.destroyed)
|
|
226
|
+
// terminate the loop.
|
|
227
|
+
const stream = agent.chunkStream(message_id, []);
|
|
228
|
+
let closed = false;
|
|
229
|
+
request.raw.on('close', () => { closed = true; });
|
|
203
230
|
|
|
204
|
-
// Stream the chunks to the client
|
|
205
231
|
for await (const chunk of stream) {
|
|
206
|
-
|
|
232
|
+
if (closed || reply.raw.destroyed) break;
|
|
207
233
|
reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
208
234
|
}
|
|
209
235
|
} catch (error: any) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
-
import { getStoreLattice, agentInstanceManager, ThreadStatus } from "@axiom-lattice/core";
|
|
2
|
+
import { getStoreLattice, agentInstanceManager, ThreadStatus, abortWorkflowRun as signalAbort } from "@axiom-lattice/core";
|
|
3
3
|
import type { WorkflowTrackingStore, WorkflowRun, RunStep } from "@axiom-lattice/protocols";
|
|
4
4
|
import { MessageChunkTypes } from "@axiom-lattice/protocols";
|
|
5
5
|
|
|
@@ -189,10 +189,18 @@ export async function getAllWorkflowRuns(
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// GET /api/workflows/inbox
|
|
192
|
+
// Pagination semantics:
|
|
193
|
+
// - No page/pageSize → legacy mode: all interrupted items, data = { records } (unchanged shape).
|
|
194
|
+
// - page and/or pageSize → paged mode: store-level pagination over interrupted runs,
|
|
195
|
+
// data = { records, total, page, pageSize }. total = number of interrupted RUNS
|
|
196
|
+
// (each run expands to 0..N inbox items after the agent-state check).
|
|
197
|
+
const INBOX_DEFAULT_PAGE_SIZE = 20;
|
|
198
|
+
const INBOX_MAX_PAGE_SIZE = 100;
|
|
199
|
+
|
|
192
200
|
export async function getInboxItems(
|
|
193
|
-
request: FastifyRequest
|
|
201
|
+
request: FastifyRequest<{ Querystring: { page?: string; pageSize?: string } }>,
|
|
194
202
|
reply: FastifyReply
|
|
195
|
-
): Promise<ApiResponse<{ records: any[] }>> {
|
|
203
|
+
): Promise<ApiResponse<{ records: any[]; total?: number; page?: number; pageSize?: number }>> {
|
|
196
204
|
const tenantId = getTenantId(request);
|
|
197
205
|
|
|
198
206
|
try {
|
|
@@ -214,10 +222,32 @@ export async function getInboxItems(
|
|
|
214
222
|
// names unavailable, will fall back to ID
|
|
215
223
|
}
|
|
216
224
|
|
|
217
|
-
const
|
|
218
|
-
const
|
|
225
|
+
const { page: pageParam, pageSize: pageSizeParam } = request.query;
|
|
226
|
+
const paged = pageParam !== undefined || pageSizeParam !== undefined;
|
|
227
|
+
const page = Math.max(1, parseInt(pageParam ?? "", 10) || 1);
|
|
228
|
+
const pageSize = Math.min(INBOX_MAX_PAGE_SIZE, Math.max(1, parseInt(pageSizeParam ?? "", 10) || INBOX_DEFAULT_PAGE_SIZE));
|
|
229
|
+
|
|
230
|
+
let pendingRuns: WorkflowRun[];
|
|
231
|
+
let total: number | undefined;
|
|
232
|
+
if (paged) {
|
|
233
|
+
const result = await store.queryWorkflowRuns(tenantId, {
|
|
234
|
+
status: "interrupted",
|
|
235
|
+
limit: pageSize,
|
|
236
|
+
offset: (page - 1) * pageSize,
|
|
237
|
+
});
|
|
238
|
+
pendingRuns = result.records;
|
|
239
|
+
total = result.total;
|
|
240
|
+
} else {
|
|
241
|
+
const result = await store.queryWorkflowRuns(tenantId, { status: "interrupted" });
|
|
242
|
+
pendingRuns = result.records;
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
if (pendingRuns.length === 0) {
|
|
220
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
message: "No pending workflows",
|
|
249
|
+
data: paged ? { records: [], total, page, pageSize } : { records: [] },
|
|
250
|
+
};
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
// Parallelize agent checks across interrupted workflows
|
|
@@ -283,15 +313,13 @@ export async function getInboxItems(
|
|
|
283
313
|
|
|
284
314
|
const results = await Promise.all(checkPromises);
|
|
285
315
|
const inboxItems = results.flat();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
289
|
-
);
|
|
316
|
+
// Runs are already sorted by updatedAt DESC, id DESC from queryWorkflowRuns.
|
|
317
|
+
// flatMap preserves run order within each run.
|
|
290
318
|
|
|
291
319
|
return {
|
|
292
320
|
success: true,
|
|
293
321
|
message: "Successfully retrieved inbox items",
|
|
294
|
-
data: { records: inboxItems },
|
|
322
|
+
data: paged ? { records: inboxItems, total, page, pageSize } : { records: inboxItems },
|
|
295
323
|
};
|
|
296
324
|
} catch (error) {
|
|
297
325
|
request.log.error(error, "Failed to get inbox items");
|
|
@@ -597,3 +625,69 @@ export async function replyInboxTask(
|
|
|
597
625
|
});
|
|
598
626
|
}
|
|
599
627
|
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Abort a running workflow.
|
|
631
|
+
*
|
|
632
|
+
* Stops the main graph execution AND signals all in-progress
|
|
633
|
+
* sub-agent invokes to cancel.
|
|
634
|
+
*/
|
|
635
|
+
export async function abortWorkflowRun(
|
|
636
|
+
request: FastifyRequest,
|
|
637
|
+
reply: FastifyReply
|
|
638
|
+
): Promise<void> {
|
|
639
|
+
try {
|
|
640
|
+
const { runId } = request.params as { runId: string };
|
|
641
|
+
const tenantId = getTenantId(request);
|
|
642
|
+
|
|
643
|
+
const store = getTrackingStore();
|
|
644
|
+
if (!store) {
|
|
645
|
+
return reply.status(404).send({
|
|
646
|
+
success: false,
|
|
647
|
+
message: "No workflow tracking store configured",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const run = await store.getWorkflowRun(runId);
|
|
652
|
+
if (!run) {
|
|
653
|
+
return reply.status(404).send({
|
|
654
|
+
success: false,
|
|
655
|
+
message: "Workflow run not found",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Persist abort intent FIRST — prevents restart after server crash
|
|
660
|
+
await store.updateWorkflowRun(runId, {
|
|
661
|
+
status: "cancelled",
|
|
662
|
+
errorMessage: "Aborted by user",
|
|
663
|
+
completedAt: new Date(),
|
|
664
|
+
}).catch(() => {});
|
|
665
|
+
|
|
666
|
+
// Stop the main graph execution
|
|
667
|
+
const workspace_id = request.headers["x-workspace-id"] as string;
|
|
668
|
+
const project_id = request.headers["x-project-id"] as string;
|
|
669
|
+
const agent = agentInstanceManager.getAgent({
|
|
670
|
+
assistant_id: run.assistantId,
|
|
671
|
+
thread_id: run.threadId,
|
|
672
|
+
tenant_id: tenantId,
|
|
673
|
+
workspace_id,
|
|
674
|
+
project_id,
|
|
675
|
+
});
|
|
676
|
+
await agent.abort();
|
|
677
|
+
|
|
678
|
+
// Signal sub-agents via AbortController registry
|
|
679
|
+
signalAbort(runId);
|
|
680
|
+
|
|
681
|
+
return reply.status(200).send({
|
|
682
|
+
success: true,
|
|
683
|
+
message: "Workflow aborted",
|
|
684
|
+
data: { runId, assistantId: run.assistantId, threadId: run.threadId },
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
request.log.error(error, "Failed to abort workflow run");
|
|
688
|
+
return reply.status(500).send({
|
|
689
|
+
success: false,
|
|
690
|
+
message: `Abort failed: ${(error as Error).message}`,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ExportableEntityDefinition, ExportableEntity, ImportPreviewResult } from '@axiom-lattice/core';
|
|
2
|
+
import { getStoreLattice } from '@axiom-lattice/core';
|
|
3
|
+
import type { A2AApiKeyStore } from '@axiom-lattice/protocols';
|
|
4
|
+
|
|
5
|
+
function getStore(): A2AApiKeyStore {
|
|
6
|
+
return getStoreLattice('default', 'a2aApiKey').store as unknown as A2AApiKeyStore;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const a2aApiKeyRegistration: ExportableEntityDefinition = {
|
|
10
|
+
entityType: 'a2a_api_key',
|
|
11
|
+
label: 'A2A API Key(API密钥)',
|
|
12
|
+
category: 'core',
|
|
13
|
+
dependsOn: [],
|
|
14
|
+
cascadeParents: [],
|
|
15
|
+
|
|
16
|
+
async listForExport(tenantId: string): Promise<ExportableEntity[]> {
|
|
17
|
+
const store = getStore();
|
|
18
|
+
const keys = await store.list({ tenantId, limit: 10000, offset: 0 });
|
|
19
|
+
return keys.map((k) => ({
|
|
20
|
+
_exportId: '',
|
|
21
|
+
data: {
|
|
22
|
+
id: k.id,
|
|
23
|
+
tenantId: k.tenantId,
|
|
24
|
+
projectId: k.projectId,
|
|
25
|
+
workspaceId: k.workspaceId,
|
|
26
|
+
label: k.label,
|
|
27
|
+
enabled: k.enabled,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async previewImport(
|
|
33
|
+
tenantId: string,
|
|
34
|
+
entities: ExportableEntity[],
|
|
35
|
+
): Promise<ImportPreviewResult> {
|
|
36
|
+
const store = getStore();
|
|
37
|
+
const existing = await store.list({ tenantId, limit: 10000, offset: 0 });
|
|
38
|
+
const existingIds = new Set(existing.map((k) => k.id));
|
|
39
|
+
|
|
40
|
+
const conflicts: ImportPreviewResult['conflicts'] = [];
|
|
41
|
+
const insertions: ImportPreviewResult['insertions'] = [];
|
|
42
|
+
|
|
43
|
+
for (const entity of entities) {
|
|
44
|
+
const d = entity.data as Record<string, unknown>;
|
|
45
|
+
const id = d.id as string;
|
|
46
|
+
|
|
47
|
+
if (existingIds.has(id)) {
|
|
48
|
+
conflicts.push({
|
|
49
|
+
_exportId: entity._exportId,
|
|
50
|
+
entityType: 'a2a_api_key',
|
|
51
|
+
conflictType: 'id_exists',
|
|
52
|
+
existingId: id,
|
|
53
|
+
existingName: (d.label as string) || id,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
insertions.push({
|
|
57
|
+
_exportId: entity._exportId,
|
|
58
|
+
entityType: 'a2a_api_key',
|
|
59
|
+
name: (d.label as string) || id,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { conflicts, insertions, errors: [] };
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async applyImport(tenantId, entity, resolution) {
|
|
68
|
+
if (resolution.action === 'skip') return {};
|
|
69
|
+
|
|
70
|
+
const store = getStore();
|
|
71
|
+
const d = entity.data as Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
if (resolution.action === 'overwrite') {
|
|
74
|
+
try {
|
|
75
|
+
await store.delete(d.id as string);
|
|
76
|
+
} catch { /* may not exist */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always generate a new key_value — key_value is globally unique
|
|
80
|
+
await store.create({
|
|
81
|
+
tenantId,
|
|
82
|
+
projectId: d.projectId as string | undefined,
|
|
83
|
+
workspaceId: d.workspaceId as string | undefined,
|
|
84
|
+
label: d.label as string | undefined,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return { newId: undefined };
|
|
88
|
+
},
|
|
89
|
+
};
|