@axiom-lattice/gateway 2.1.63 → 2.1.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.63",
3
+ "version": "2.1.65",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -39,10 +39,10 @@
39
39
  "pg": "^8.11.0",
40
40
  "redis": "^5.0.1",
41
41
  "uuid": "^9.0.1",
42
- "@axiom-lattice/core": "2.1.57",
43
- "@axiom-lattice/pg-stores": "1.0.47",
44
- "@axiom-lattice/protocols": "2.1.30",
45
- "@axiom-lattice/queue-redis": "1.0.29"
42
+ "@axiom-lattice/core": "2.1.59",
43
+ "@axiom-lattice/pg-stores": "1.0.49",
44
+ "@axiom-lattice/protocols": "2.1.31",
45
+ "@axiom-lattice/queue-redis": "1.0.30"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^29.5.14",
@@ -0,0 +1,385 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { getStoreLattice, agentInstanceManager } from "@axiom-lattice/core";
3
+ import type { WorkflowTrackingStore, WorkflowRun, RunStep } from "@axiom-lattice/protocols";
4
+
5
+ function getTenantId(request: FastifyRequest): string {
6
+ const userTenantId = (request as any).user?.tenantId;
7
+ if (userTenantId) return userTenantId;
8
+ return (request.headers["x-tenant-id"] as string) || "default";
9
+ }
10
+
11
+ function getTrackingStore(): WorkflowTrackingStore | null {
12
+ try {
13
+ const storeLattice = getStoreLattice("default", "workflowTracking");
14
+ return storeLattice.store;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ interface ApiResponse<T = any> {
21
+ success: boolean;
22
+ message: string;
23
+ data?: T;
24
+ }
25
+
26
+ async function getDefinitionsFromAssistants(tenantId: string): Promise<Array<{
27
+ assistantId: string;
28
+ assistantName: string;
29
+ topologyEdges: { from: string; to: string; purpose: string }[];
30
+ totalEdges: number;
31
+ }>> {
32
+ try {
33
+ const storeLattice = getStoreLattice("default", "assistant");
34
+ const assistantStore = storeLattice.store;
35
+ const assistants = await assistantStore.getAllAssistants(tenantId);
36
+
37
+ const results: Array<{
38
+ assistantId: string;
39
+ assistantName: string;
40
+ topologyEdges: { from: string; to: string; purpose: string }[];
41
+ totalEdges: number;
42
+ }> = [];
43
+
44
+ for (const a of assistants) {
45
+ const def = a.graphDefinition;
46
+ if (!def || def.type !== "processing") continue;
47
+ if (!def.middleware) continue;
48
+ for (const mw of def.middleware) {
49
+ if (mw.type === "topology" && mw.enabled && mw.config?.edges?.length > 0) {
50
+ results.push({
51
+ assistantId: a.id,
52
+ assistantName: a.name,
53
+ topologyEdges: mw.config.edges,
54
+ totalEdges: mw.config.edges.length,
55
+ });
56
+ }
57
+ }
58
+ }
59
+ return results;
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ export async function getAllWorkflowDefinitions(
66
+ request: FastifyRequest,
67
+ reply: FastifyReply
68
+ ): Promise<ApiResponse<{ records: any[] }>> {
69
+ const tenantId = getTenantId(request);
70
+
71
+ try {
72
+ // Get definitions from assistant configs (always available, even before first run)
73
+ const configDefs = await getDefinitionsFromAssistants(tenantId);
74
+
75
+ if (configDefs.length === 0) {
76
+ return {
77
+ success: true,
78
+ message: "No workflow definitions found",
79
+ data: { records: [] },
80
+ };
81
+ }
82
+
83
+ // Merge in run-derived data (lastRunAt, runCount) if tracking store exists
84
+ const runMap = new Map<string, { lastRunAt?: string; runCount: number }>();
85
+ try {
86
+ const store = getTrackingStore();
87
+ if (store) {
88
+ const runs = await store.getWorkflowRunsByTenantId(tenantId);
89
+ for (const r of runs) {
90
+ const entry = runMap.get(r.assistantId) || { runCount: 0 };
91
+ entry.runCount++;
92
+ if (!entry.lastRunAt || new Date(r.startedAt) > new Date(entry.lastRunAt)) {
93
+ entry.lastRunAt = r.startedAt instanceof Date ? r.startedAt.toISOString() : String(r.startedAt);
94
+ }
95
+ runMap.set(r.assistantId, entry);
96
+ }
97
+ }
98
+ } catch {
99
+ // tracking store unavailable, definitions from config only
100
+ }
101
+
102
+ const definitions = configDefs.map((d) => {
103
+ const runInfo = runMap.get(d.assistantId);
104
+ return {
105
+ ...d,
106
+ lastRunAt: runInfo?.lastRunAt || null,
107
+ runCount: runInfo?.runCount || 0,
108
+ };
109
+ });
110
+
111
+ return {
112
+ success: true,
113
+ message: "Successfully retrieved workflow definitions",
114
+ data: { records: definitions },
115
+ };
116
+ } catch (error) {
117
+ request.log.error(error, "Failed to get workflow definitions");
118
+ return reply.status(500).send({ success: false, message: "Failed to retrieve workflow definitions" });
119
+ }
120
+ }
121
+
122
+ export async function getAllWorkflowRuns(
123
+ request: FastifyRequest<{ Querystring: { assistantId?: string; status?: string } }>,
124
+ reply: FastifyReply
125
+ ): Promise<ApiResponse<{ records: WorkflowRun[]; total: number }>> {
126
+ const tenantId = getTenantId(request);
127
+ const { assistantId, status } = request.query;
128
+
129
+ try {
130
+ const store = getTrackingStore();
131
+ if (!store) {
132
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
133
+ }
134
+
135
+ let runs: WorkflowRun[];
136
+ if (assistantId) {
137
+ runs = await store.getWorkflowRunsByAssistantId(tenantId, assistantId);
138
+ } else {
139
+ runs = await store.getWorkflowRunsByTenantId(tenantId);
140
+ }
141
+
142
+ if (status) {
143
+ runs = runs.filter(r => r.status === status);
144
+ }
145
+
146
+ return {
147
+ success: true,
148
+ message: "Successfully retrieved workflow runs",
149
+ data: { records: runs, total: runs.length },
150
+ };
151
+ } catch (error) {
152
+ request.log.error(error, "Failed to get workflow runs");
153
+ return reply.status(500).send({ success: false, message: "Failed to retrieve workflow runs" });
154
+ }
155
+ }
156
+
157
+ // GET /api/workflows/inbox
158
+ export async function getInboxItems(
159
+ request: FastifyRequest,
160
+ reply: FastifyReply
161
+ ): Promise<ApiResponse<{ records: any[] }>> {
162
+ const tenantId = getTenantId(request);
163
+
164
+ try {
165
+ const store = getTrackingStore();
166
+ if (!store) {
167
+ return { success: true, message: "No tracking store configured", data: { records: [] } };
168
+ }
169
+
170
+ // Resolve assistant names
171
+ const nameMap: Record<string, string> = {};
172
+ try {
173
+ const asStoreLattice = getStoreLattice("default", "assistant");
174
+ const assistantStore = asStoreLattice.store;
175
+ const assistants = await assistantStore.getAllAssistants(tenantId);
176
+ for (const a of assistants) {
177
+ nameMap[a.id] = a.name;
178
+ }
179
+ } catch {
180
+ // names unavailable, will fall back to ID
181
+ }
182
+
183
+ const runs = await store.getWorkflowRunsByTenantId(tenantId);
184
+ const runningRuns = runs.filter(r => r.status === "running");
185
+ if (runningRuns.length === 0) {
186
+ return { success: true, message: "No running workflows", data: { records: [] } };
187
+ }
188
+
189
+ const inboxItems: any[] = [];
190
+ for (const r of runningRuns) {
191
+ try {
192
+ const agent = agentInstanceManager.getAgent({
193
+ assistant_id: r.assistantId,
194
+ thread_id: r.threadId,
195
+ tenant_id: r.tenantId,
196
+ });
197
+ const runStatus = await agent.getRunStatus();
198
+
199
+ if (runStatus !== "interrupted") continue;
200
+
201
+ const state = await agent.getCurrentState();
202
+ const interrupts = state.tasks
203
+ ?.flatMap((t: any) => t.interrupts || []) || [];
204
+
205
+ for (const i of interrupts) {
206
+ inboxItems.push({
207
+ runId: r.id,
208
+ assistantId: r.assistantId,
209
+ assistantName: nameMap[r.assistantId] || r.assistantId,
210
+ threadId: r.threadId,
211
+ tenantId: r.tenantId,
212
+ interruptId: i.id,
213
+ interruptValue: i.value,
214
+ startedAt: r.startedAt,
215
+ totalEdges: r.totalEdges,
216
+ completedEdges: r.completedEdges,
217
+ });
218
+ }
219
+ } catch {
220
+ // agent unavailable, skip
221
+ }
222
+ }
223
+
224
+ inboxItems.sort((a, b) =>
225
+ new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
226
+ );
227
+
228
+ return {
229
+ success: true,
230
+ message: "Successfully retrieved inbox items",
231
+ data: { records: inboxItems },
232
+ };
233
+ } catch (error) {
234
+ request.log.error(error, "Failed to get inbox items");
235
+ return reply.status(500).send({ success: false, message: "Failed to retrieve inbox items" });
236
+ }
237
+ }
238
+
239
+ export async function getWorkflowDefinitions(
240
+ request: FastifyRequest<{ Params: { assistantId: string } }>,
241
+ reply: FastifyReply
242
+ ): Promise<ApiResponse<{ records: any[] }>> {
243
+ const { assistantId } = request.params;
244
+ const tenantId = getTenantId(request);
245
+
246
+ try {
247
+ const store = getTrackingStore();
248
+ if (!store) {
249
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
250
+ }
251
+
252
+ const runs = await store.getWorkflowRunsByAssistantId(tenantId, assistantId);
253
+ const seen = new Set<string>();
254
+ const definitions = runs
255
+ .filter(r => {
256
+ const key = JSON.stringify(r.topologyEdges);
257
+ if (seen.has(key)) return false;
258
+ seen.add(key);
259
+ return true;
260
+ })
261
+ .map(r => ({
262
+ assistantId: r.assistantId,
263
+ topologyEdges: r.topologyEdges,
264
+ totalEdges: r.totalEdges,
265
+ lastRunAt: r.startedAt,
266
+ }));
267
+
268
+ return {
269
+ success: true,
270
+ message: "Successfully retrieved workflow definitions",
271
+ data: { records: definitions },
272
+ };
273
+ } catch (error) {
274
+ request.log.error(error, "Failed to get workflow definitions");
275
+ return reply.status(500).send({ success: false, message: "Failed to retrieve workflow definitions" });
276
+ }
277
+ }
278
+
279
+ export async function getWorkflowRuns(
280
+ request: FastifyRequest<{ Params: { assistantId: string; threadId: string } }>,
281
+ reply: FastifyReply
282
+ ): Promise<ApiResponse<{ records: WorkflowRun[]; total: number }>> {
283
+ const { threadId } = request.params;
284
+ const tenantId = getTenantId(request);
285
+
286
+ try {
287
+ const store = getTrackingStore();
288
+ if (!store) {
289
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
290
+ }
291
+
292
+ const runs = await store.getWorkflowRunsByThreadId(tenantId, threadId);
293
+ return {
294
+ success: true,
295
+ message: "Successfully retrieved workflow runs",
296
+ data: { records: runs, total: runs.length },
297
+ };
298
+ } catch (error) {
299
+ request.log.error(error, "Failed to get workflow runs");
300
+ return reply.status(500).send({ success: false, message: "Failed to retrieve workflow runs" });
301
+ }
302
+ }
303
+
304
+ export async function getWorkflowRun(
305
+ request: FastifyRequest<{ Params: { runId: string } }>,
306
+ reply: FastifyReply
307
+ ): Promise<ApiResponse<WorkflowRun>> {
308
+ const { runId } = request.params;
309
+
310
+ try {
311
+ const store = getTrackingStore();
312
+ if (!store) {
313
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
314
+ }
315
+
316
+ const run = await store.getWorkflowRun(runId);
317
+ if (!run) {
318
+ return reply.status(404).send({ success: false, message: "Workflow run not found" });
319
+ }
320
+
321
+ return { success: true, message: "Successfully retrieved workflow run", data: run };
322
+ } catch (error) {
323
+ request.log.error(error, "Failed to get workflow run");
324
+ return reply.status(500).send({ success: false, message: "Failed to retrieve workflow run" });
325
+ }
326
+ }
327
+
328
+ export async function getRunSteps(
329
+ request: FastifyRequest<{ Params: { runId: string }; Querystring: { step_type?: string; status?: string } }>,
330
+ reply: FastifyReply
331
+ ): Promise<ApiResponse<{ records: RunStep[]; total: number }>> {
332
+ const { runId } = request.params;
333
+ const { step_type, status: stepStatus } = request.query;
334
+
335
+ try {
336
+ const store = getTrackingStore();
337
+ if (!store) {
338
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
339
+ }
340
+
341
+ let steps: RunStep[];
342
+ if (step_type) {
343
+ steps = await store.getRunStepsByType(runId, step_type as any);
344
+ } else {
345
+ steps = await store.getRunSteps(runId);
346
+ }
347
+
348
+ if (stepStatus) {
349
+ steps = steps.filter(s => s.status === stepStatus);
350
+ }
351
+
352
+ return {
353
+ success: true,
354
+ message: "Successfully retrieved run steps",
355
+ data: { records: steps, total: steps.length },
356
+ };
357
+ } catch (error) {
358
+ request.log.error(error, "Failed to get run steps");
359
+ return reply.status(500).send({ success: false, message: "Failed to retrieve run steps" });
360
+ }
361
+ }
362
+
363
+ export async function getRunTasks(
364
+ request: FastifyRequest<{ Params: { runId: string } }>,
365
+ reply: FastifyReply
366
+ ): Promise<ApiResponse<{ records: RunStep[]; total: number }>> {
367
+ const { runId } = request.params;
368
+
369
+ try {
370
+ const store = getTrackingStore();
371
+ if (!store) {
372
+ return reply.status(404).send({ success: false, message: "No workflow tracking store configured" });
373
+ }
374
+
375
+ const steps = await store.getInterruptedSteps(runId);
376
+ return {
377
+ success: true,
378
+ message: "Successfully retrieved user tasks",
379
+ data: { records: steps, total: steps.length },
380
+ };
381
+ } catch (error) {
382
+ request.log.error(error, "Failed to get run tasks");
383
+ return reply.status(500).send({ success: false, message: "Failed to retrieve user tasks" });
384
+ }
385
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  sandboxLatticeManager,
21
21
  sqlDatabaseManager,
22
22
  getStoreLattice,
23
+ storeLatticeManager,
23
24
  agentInstanceManager,
24
25
  createSandboxProvider,
25
26
  type CreateSandboxProviderConfig,
@@ -273,6 +274,23 @@ const start = async (config?: LatticeGatewayConfig) => {
273
274
  logger.info("Registered sandbox manager from env configuration");
274
275
  }
275
276
 
277
+ // Swap workflow tracking to PostgreSQL if DATABASE_URL is set
278
+ if (process.env.DATABASE_URL) {
279
+ try {
280
+ const { PostgreSQLWorkflowTrackingStore } = await import("@axiom-lattice/pg-stores");
281
+ const pgStore = new PostgreSQLWorkflowTrackingStore({
282
+ poolConfig: process.env.DATABASE_URL,
283
+ });
284
+ if (storeLatticeManager.hasLattice("default", "workflowTracking")) {
285
+ storeLatticeManager.removeLattice("default", "workflowTracking");
286
+ }
287
+ storeLatticeManager.registerLattice("default", "workflowTracking", pgStore);
288
+ logger.info("Workflow tracking store switched to PostgreSQL");
289
+ } catch (error) {
290
+ logger.warn("Failed to switch workflow tracking to PostgreSQL, keeping in-memory: " + (error instanceof Error ? error.message : String(error)));
291
+ }
292
+ }
293
+
276
294
  const target_port = config?.port || Number(process.env.PORT) || 4001;
277
295
 
278
296
  await app.listen({ port: target_port, host: "0.0.0.0" });
@@ -13,6 +13,7 @@ import * as healthController from "../controllers/health";
13
13
  import * as skillsController from "../controllers/skills";
14
14
  import * as toolsController from "../controllers/tools";
15
15
  import * as dataQueryController from "../controllers/data-query";
16
+ import * as workflowTrackingController from "../controllers/workflow-tracking";
16
17
  import {
17
18
  createRunSchema,
18
19
  getAllMemoryItemsSchema,
@@ -349,6 +350,51 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
349
350
 
350
351
  registerChannelInstallationRoutes(app);
351
352
 
353
+ // Workflow tracking routes
354
+ app.get(
355
+ "/api/workflows/definitions",
356
+ workflowTrackingController.getAllWorkflowDefinitions
357
+ );
358
+
359
+ app.get<{
360
+ Querystring: { assistantId?: string; status?: string };
361
+ }>(
362
+ "/api/workflows/runs",
363
+ workflowTrackingController.getAllWorkflowRuns
364
+ );
365
+
366
+ app.get(
367
+ "/api/workflows/inbox",
368
+ workflowTrackingController.getInboxItems
369
+ );
370
+
371
+ app.get<{
372
+ Params: { assistantId: string };
373
+ }>(
374
+ "/api/assistants/:assistantId/workflows/definitions",
375
+ workflowTrackingController.getWorkflowDefinitions
376
+ );
377
+
378
+ app.get<{
379
+ Params: { assistantId: string; threadId: string };
380
+ }>(
381
+ "/api/assistants/:assistantId/threads/:threadId/workflows/runs",
382
+ workflowTrackingController.getWorkflowRuns
383
+ );
384
+
385
+ app.get<{
386
+ Params: { runId: string };
387
+ }>("/api/workflows/runs/:runId", workflowTrackingController.getWorkflowRun);
388
+
389
+ app.get<{
390
+ Params: { runId: string };
391
+ Querystring: { step_type?: string; status?: string };
392
+ }>("/api/workflows/runs/:runId/steps", workflowTrackingController.getRunSteps);
393
+
394
+ app.get<{
395
+ Params: { runId: string };
396
+ }>("/api/workflows/runs/:runId/tasks", workflowTrackingController.getRunTasks);
397
+
352
398
  // // Thread 状态查询路由
353
399
  // app.get("/api/threads/:thread_id/status", getThreadStatusHandler);
354
400
  // app.get("/api/assistants/:assistant_id/threads/status", getAgentThreadsHandler);
@@ -8,8 +8,11 @@ export interface AgentTaskRequest {
8
8
  thread_id: string;
9
9
  "x-tenant-id": string;
10
10
  command?: any;
11
- callback_event?: string; // 可选的回调事件名称
12
- runConfig?: Record<string, any>; // RunConfig for subagent execution
11
+ callback_event?: string;
12
+ runConfig?: Record<string, any>;
13
+ main_thread_id?: string;
14
+ main_tenant_id?: string;
15
+ main_assistant_id?: string;
13
16
  }
14
17
 
15
18
  /**
@@ -29,6 +32,9 @@ const handleAgentTask = async (
29
32
  command,
30
33
  callback_event,
31
34
  runConfig,
35
+ main_thread_id,
36
+ main_tenant_id,
37
+ main_assistant_id,
32
38
  } = taskRequest;
33
39
 
34
40
 
@@ -50,6 +56,38 @@ const handleAgentTask = async (
50
56
  state: evt.state,
51
57
  config: { assistant_id, thread_id, tenant_id },
52
58
  });
59
+
60
+ if (main_thread_id && main_tenant_id) {
61
+ try {
62
+ const mainAgent = agentInstanceManager.getAgent({
63
+ assistant_id: main_assistant_id ?? assistant_id,
64
+ thread_id: main_thread_id,
65
+ tenant_id: main_tenant_id,
66
+ });
67
+ if (mainAgent) {
68
+ const messages = evt.state?.values?.messages;
69
+ const lastAIMessage = messages
70
+ ?.filter((m: any) => m.type === 'ai' || m.getType?.() === 'ai')
71
+ .pop();
72
+ const summary = lastAIMessage?.content
73
+ ?.substring(0, 500) ?? '(no output)';
74
+
75
+ mainAgent.addMessage(
76
+ {
77
+ input: {
78
+ message: `[Async task completed]\ntask_id: ${thread_id}\n${summary}`,
79
+ },
80
+ },
81
+ ).catch((err: Error) => {
82
+ console.error('Failed to notify main thread:', err);
83
+ });
84
+
85
+ mainAgent.updateAsyncTaskStatus(thread_id, 'completed');
86
+ }
87
+ } catch (err) {
88
+ console.error('Failed to notify main thread:', err);
89
+ }
90
+ }
53
91
  })
54
92
  agent.subscribeOnce("message:interrupted", (evt) => {
55
93
  eventBus.publish(callback_event, {