@axiom-lattice/gateway 3.0.2 → 3.0.3
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/dist/index.js +31 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -6
- package/.eslintrc.json +0 -22
- package/.turbo/turbo-build.log +0 -46
- package/AGENTS.md +0 -112
- package/CHANGELOG.md +0 -1190
- package/LOGGER_CONFIG.md +0 -135
- package/RESUME_STREAM_CONTENT_BASED.md +0 -326
- package/RESUME_STREAM_README.md +0 -388
- package/jest.config.js +0 -72
- package/public/sdk/README.md +0 -695
- package/public/sdk/dashboard-engine-skill.md +0 -1122
- package/public/sdk/dashboard-single-file-spec.md +0 -357
- package/public/sdk/data-query-sdk-skill.md +0 -307
- package/public/sdk/data-query-sdk.d.ts +0 -252
- package/public/sdk/data-query-sdk.js +0 -970
- package/public/sdk/occupancy-dashboard.html +0 -363
- package/public/sdk/test-dashboard.html +0 -690
- package/scripts/seed-lark-installation.ts +0 -54
- package/src/__tests__/__mocks__/e2b.ts +0 -1
- package/src/__tests__/a2a.test.ts +0 -428
- package/src/__tests__/channel-installations.test.ts +0 -199
- package/src/__tests__/data-query.test.ts +0 -77
- package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
- package/src/__tests__/setup.ts +0 -5
- package/src/__tests__/workspace.test.ts +0 -400
- package/src/channels/__tests__/routes.test.ts +0 -50
- package/src/channels/lark/LarkChannelAdapter.ts +0 -217
- package/src/channels/lark/README.md +0 -187
- package/src/channels/lark/__tests__/controller.test.ts +0 -136
- package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
- package/src/channels/lark/__tests__/parser.test.ts +0 -72
- package/src/channels/lark/__tests__/sender.test.ts +0 -37
- package/src/channels/lark/__tests__/verification.test.ts +0 -157
- package/src/channels/lark/controller.ts +0 -48
- package/src/channels/lark/mapping-service.ts +0 -138
- package/src/channels/lark/parser.ts +0 -68
- package/src/channels/lark/routes.ts +0 -22
- package/src/channels/lark/sender.ts +0 -58
- package/src/channels/lark/types.ts +0 -20
- package/src/channels/lark/verification.ts +0 -32
- package/src/channels/registry.ts +0 -20
- package/src/channels/routes.ts +0 -30
- package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
- package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
- package/src/channels/wechat/context-store.ts +0 -54
- package/src/channels/wechat/controller.ts +0 -59
- package/src/channels/wechat/routes.ts +0 -7
- package/src/channels/wechat/types.ts +0 -62
- package/src/channels/wechat/wechat-client.ts +0 -162
- package/src/config/local-a2a-templates.ts +0 -59
- package/src/config.ts +0 -124
- package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
- package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
- package/src/controllers/__tests__/mime.test.ts +0 -74
- package/src/controllers/__tests__/resources.test.ts +0 -122
- package/src/controllers/__tests__/run.test.ts +0 -115
- package/src/controllers/__tests__/tasks.test.ts +0 -215
- package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
- package/src/controllers/agent_task.ts +0 -72
- package/src/controllers/assistant.ts +0 -490
- package/src/controllers/auth.ts +0 -524
- package/src/controllers/channel-bindings.ts +0 -135
- package/src/controllers/channel-installations.ts +0 -409
- package/src/controllers/collections.ts +0 -176
- package/src/controllers/config.ts +0 -126
- package/src/controllers/connections.ts +0 -181
- package/src/controllers/data-query.ts +0 -236
- package/src/controllers/database-configs.ts +0 -440
- package/src/controllers/embeddings.ts +0 -22
- package/src/controllers/entries.ts +0 -167
- package/src/controllers/eval.ts +0 -513
- package/src/controllers/export-import.ts +0 -166
- package/src/controllers/health.ts +0 -36
- package/src/controllers/local-a2a-templates.ts +0 -18
- package/src/controllers/local-a2a.ts +0 -149
- package/src/controllers/mcp-configs.ts +0 -673
- package/src/controllers/memory.ts +0 -257
- package/src/controllers/menu-items.ts +0 -114
- package/src/controllers/metrics-configs.ts +0 -1061
- package/src/controllers/middleware-types.ts +0 -15
- package/src/controllers/models.ts +0 -199
- package/src/controllers/personal-assistant.ts +0 -191
- package/src/controllers/resources.ts +0 -477
- package/src/controllers/run.ts +0 -338
- package/src/controllers/sandbox.ts +0 -255
- package/src/controllers/schedules.ts +0 -336
- package/src/controllers/skills.ts +0 -256
- package/src/controllers/tasks.ts +0 -213
- package/src/controllers/tenants.ts +0 -121
- package/src/controllers/thread_status.ts +0 -66
- package/src/controllers/threads.ts +0 -260
- package/src/controllers/tools.ts +0 -139
- package/src/controllers/users.ts +0 -135
- package/src/controllers/workflow-tracking.ts +0 -693
- package/src/controllers/workspace.ts +0 -879
- package/src/export_registrations/a2a-api-key.registration.ts +0 -89
- package/src/export_registrations/agent.registration.ts +0 -80
- package/src/export_registrations/binding.registration.ts +0 -106
- package/src/export_registrations/channel-installation.registration.ts +0 -88
- package/src/export_registrations/collection.registration.ts +0 -85
- package/src/export_registrations/connection.registration.ts +0 -97
- package/src/export_registrations/database-config.registration.ts +0 -94
- package/src/export_registrations/eval.registration.ts +0 -334
- package/src/export_registrations/index.ts +0 -31
- package/src/export_registrations/mcp-config.registration.ts +0 -97
- package/src/export_registrations/menu.registration.ts +0 -91
- package/src/export_registrations/metrics-config.registration.ts +0 -94
- package/src/export_registrations/skill.registration.ts +0 -88
- package/src/export_registrations/task.registration.ts +0 -99
- package/src/index.ts +0 -497
- package/src/logger/Logger.ts +0 -186
- package/src/router/MessageContext.ts +0 -14
- package/src/router/MessageRouter.ts +0 -490
- package/src/router/middlewares/auditLogger.ts +0 -34
- package/src/router/middlewares/deduplication.ts +0 -38
- package/src/router/middlewares/index.ts +0 -3
- package/src/router/middlewares/rateLimit.ts +0 -46
- package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
- package/src/routes/a2a-bridge.ts +0 -266
- package/src/routes/a2a-test-connection.ts +0 -126
- package/src/routes/a2a.ts +0 -851
- package/src/routes/channel-bindings.ts +0 -11
- package/src/routes/channel-installations.ts +0 -33
- package/src/routes/index.ts +0 -640
- package/src/routes/menu-items.ts +0 -10
- package/src/routes/resource-routes.ts +0 -35
- package/src/routes/stt.ts +0 -102
- package/src/schemas/data-query.ts +0 -69
- package/src/schemas/index.ts +0 -852
- package/src/services/ExportImportService.ts +0 -296
- package/src/services/__tests__/ExportImportService.test.ts +0 -130
- package/src/services/__tests__/eval-runner.test.ts +0 -131
- package/src/services/__tests__/local-a2a.test.ts +0 -124
- package/src/services/agent_task_consumer.ts +0 -348
- package/src/services/collection_service.ts +0 -258
- package/src/services/eval-runner.ts +0 -283
- package/src/services/local-a2a.ts +0 -354
- package/src/services/queue_service.ts +0 -104
- package/src/services/sandbox_service.ts +0 -223
- package/src/services/skill_service.ts +0 -97
- package/src/services/supabase.ts +0 -60
- package/src/swagger.ts +0 -77
- package/src/types/index.ts +0 -118
- package/src/types/koa.d.ts +0 -7
- package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
- package/src/utils/__tests__/path-containment.test.ts +0 -35
- package/src/utils/a2a-url.ts +0 -20
- package/src/utils/local-a2a-runtime.ts +0 -107
- package/src/utils/mime.ts +0 -44
- package/src/utils/path-containment.ts +0 -38
- package/tsconfig.json +0 -26
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
import {
|
|
3
|
-
getStoreLattice,
|
|
4
|
-
modelLatticeManager,
|
|
5
|
-
LatticeEvalProject,
|
|
6
|
-
} from "@axiom-lattice/core";
|
|
7
|
-
import type { EvalStore } from "@axiom-lattice/protocols";
|
|
8
|
-
import type {
|
|
9
|
-
CaseRunResult,
|
|
10
|
-
LatticeEvalBatchReport,
|
|
11
|
-
LatticeEvalProjectType,
|
|
12
|
-
LatticeEvalSuiteType,
|
|
13
|
-
LatticeEvalLogEvent,
|
|
14
|
-
} from "@axiom-lattice/core";
|
|
15
|
-
import { v4 as uuidv4 } from "uuid";
|
|
16
|
-
|
|
17
|
-
export interface EvalStreamEvent {
|
|
18
|
-
type: "progress" | "completed" | "error";
|
|
19
|
-
runId: string;
|
|
20
|
-
data: Record<string, unknown>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface RunContext {
|
|
24
|
-
runId: string;
|
|
25
|
-
projectId: string;
|
|
26
|
-
tenantId: string;
|
|
27
|
-
abortController: AbortController;
|
|
28
|
-
promise: Promise<{ report: LatticeEvalBatchReport }>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function mapLogs(logs: LatticeEvalLogEvent[]): Array<{ timestamp: string; level: string; message: string; data?: unknown }> {
|
|
32
|
-
return logs.map((l) => ({
|
|
33
|
-
timestamp: l.ts,
|
|
34
|
-
level: l.level,
|
|
35
|
-
message: l.message,
|
|
36
|
-
data: l.data,
|
|
37
|
-
}));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* EvalRunner — in-process evaluation run scheduler.
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* Single-instance assumption: run state lives in an in-memory Map plus a
|
|
45
|
-
* per-instance EventEmitter. Deploying multiple gateway instances requires
|
|
46
|
-
* a distributed lease (runner instance id + heartbeat) before the orphan
|
|
47
|
-
* cleanup below is safe — without it, instance B could mark instance A's
|
|
48
|
-
* live run failed. The createdAt guard limits cleanup to runs created
|
|
49
|
-
* BEFORE this process started, which are true orphans of a restart.
|
|
50
|
-
*/
|
|
51
|
-
class EvalRunner {
|
|
52
|
-
private runs = new Map<string, RunContext>();
|
|
53
|
-
private eventEmitter = new EventEmitter();
|
|
54
|
-
private readonly processStartedAt = new Date();
|
|
55
|
-
|
|
56
|
-
getEventEmitter(): EventEmitter {
|
|
57
|
-
return this.eventEmitter;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async startRun(tenantId: string, projectId: string, suiteIds?: string[]): Promise<string> {
|
|
61
|
-
const store = this.getEvalStore();
|
|
62
|
-
const project = await store.getProjectById(tenantId, projectId);
|
|
63
|
-
if (!project) throw new Error("Project not found");
|
|
64
|
-
|
|
65
|
-
for (const ctx of this.runs.values()) {
|
|
66
|
-
if (ctx.projectId === projectId) {
|
|
67
|
-
throw new Error("A run is already in progress for this project");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const existingRuns = await store.getRunsByTenant(tenantId, { projectId, status: "running" });
|
|
72
|
-
for (const run of existingRuns) {
|
|
73
|
-
// Only clean runs created before this process started: they are true
|
|
74
|
-
// orphans of a gateway restart. Runs created after startup but missing
|
|
75
|
-
// from memory belong to another live instance — leave them alone.
|
|
76
|
-
const orphaned = !this.runs.has(run.id) && run.createdAt < this.processStartedAt;
|
|
77
|
-
if (orphaned) {
|
|
78
|
-
await store.updateRunStatus(tenantId, run.id, {
|
|
79
|
-
status: "failed",
|
|
80
|
-
error: "Gateway restarted — run orphaned",
|
|
81
|
-
completedAt: new Date(),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const stillRunning = await store.getRunsByTenant(tenantId, { projectId, status: "running" });
|
|
86
|
-
if (stillRunning.length > 0) {
|
|
87
|
-
throw new Error("A run is already in progress for this project");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let suites = await store.getSuitesByProject(tenantId, projectId);
|
|
91
|
-
if (suiteIds && suiteIds.length > 0) {
|
|
92
|
-
suites = suites.filter((s) => suiteIds.includes(s.id));
|
|
93
|
-
if (suites.length === 0) {
|
|
94
|
-
throw new Error("No suites match the requested suiteIds filter");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const evalSuites: LatticeEvalSuiteType[] = [];
|
|
98
|
-
let totalCases = 0;
|
|
99
|
-
|
|
100
|
-
for (const suite of suites) {
|
|
101
|
-
const cases = await store.getCasesBySuite(tenantId, suite.id);
|
|
102
|
-
totalCases += cases.length;
|
|
103
|
-
evalSuites.push({
|
|
104
|
-
suiteName: suite.name,
|
|
105
|
-
cases: cases.map((c) => ({
|
|
106
|
-
caseId: c.id,
|
|
107
|
-
input: { message: c.inputMessage, files: c.inputFiles },
|
|
108
|
-
steps: c.steps,
|
|
109
|
-
output: { type: "message_content" },
|
|
110
|
-
eval: {
|
|
111
|
-
content_assertion: c.contentAssertion,
|
|
112
|
-
eval_rubrics: c.rubrics?.map((r) => ({
|
|
113
|
-
dimension: r.name,
|
|
114
|
-
weight: r.weight,
|
|
115
|
-
description: r.description,
|
|
116
|
-
})),
|
|
117
|
-
},
|
|
118
|
-
})),
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const judgeCfg = (project.judgeModelConfig ?? {}) as Record<string, unknown>;
|
|
123
|
-
const judgeModelKey = (judgeCfg.modelKey as string) || "";
|
|
124
|
-
let resolvedJudgeModelKey: string;
|
|
125
|
-
|
|
126
|
-
if (judgeModelKey) {
|
|
127
|
-
if (!modelLatticeManager.hasLattice(judgeModelKey)) {
|
|
128
|
-
throw new Error(`Judge model "${judgeModelKey}" is not registered`);
|
|
129
|
-
}
|
|
130
|
-
resolvedJudgeModelKey = judgeModelKey;
|
|
131
|
-
} else {
|
|
132
|
-
const firstModel = modelLatticeManager.getAllLattices()[0];
|
|
133
|
-
if (!firstModel) {
|
|
134
|
-
throw new Error("No model registered — cannot run eval without a judge model");
|
|
135
|
-
}
|
|
136
|
-
resolvedJudgeModelKey = firstModel.key;
|
|
137
|
-
console.warn(`[eval-runner] Project "${project.name}" has no judge modelKey — falling back to first registered model "${firstModel.key}"`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const runId = uuidv4();
|
|
141
|
-
const concurrency = Math.max(1, Math.floor((project as unknown as Record<string, unknown>).concurrency as number) || 3);
|
|
142
|
-
await store.createRun(tenantId, projectId, runId, { totalCases, concurrency });
|
|
143
|
-
|
|
144
|
-
const tsc = (project.targetServerConfig ?? {}) as Record<string, unknown>;
|
|
145
|
-
const workspaceId = (tsc.workspace_id as string) || "";
|
|
146
|
-
const projectIdFromConfig = (tsc.project_id as string) || "";
|
|
147
|
-
|
|
148
|
-
const projectConfig: LatticeEvalProjectType = {
|
|
149
|
-
projectName: project.name,
|
|
150
|
-
version: project.version,
|
|
151
|
-
description: project.description,
|
|
152
|
-
suites: evalSuites,
|
|
153
|
-
judge_agent_config: { modelKey: resolvedJudgeModelKey },
|
|
154
|
-
lattice_server_config: {
|
|
155
|
-
tenant_id: tenantId,
|
|
156
|
-
workspace_id: workspaceId,
|
|
157
|
-
project_id: projectIdFromConfig,
|
|
158
|
-
},
|
|
159
|
-
concurrency,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const abortController = new AbortController();
|
|
163
|
-
const stats = { passed: 0, failed: 0, totalScore: 0 };
|
|
164
|
-
|
|
165
|
-
const onCaseComplete = async (result: CaseRunResult, suiteName: string): Promise<void> => {
|
|
166
|
-
const passed = result.result?.pass ?? false;
|
|
167
|
-
const score = result.result?.final_score ?? 0;
|
|
168
|
-
|
|
169
|
-
if (passed) stats.passed++; else stats.failed++;
|
|
170
|
-
stats.totalScore += score;
|
|
171
|
-
|
|
172
|
-
const completedCount = stats.passed + stats.failed;
|
|
173
|
-
|
|
174
|
-
await store.createRunResult(tenantId, runId, uuidv4(), {
|
|
175
|
-
suiteName,
|
|
176
|
-
caseId: result.caseId,
|
|
177
|
-
pass: passed,
|
|
178
|
-
score,
|
|
179
|
-
summary: result.result?.summary,
|
|
180
|
-
dimensionResults: result.result?.dimension_results?.map((d) => ({
|
|
181
|
-
name: d.name,
|
|
182
|
-
score: d.score,
|
|
183
|
-
reason: d.reason,
|
|
184
|
-
})),
|
|
185
|
-
durationMs: result.duration_ms,
|
|
186
|
-
messages: result.messages,
|
|
187
|
-
logs: mapLogs(result.logs),
|
|
188
|
-
error: result.error,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const avgScore = completedCount > 0 ? stats.totalScore / completedCount : 0;
|
|
192
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
193
|
-
passedCases: stats.passed,
|
|
194
|
-
failedCases: stats.failed,
|
|
195
|
-
avgScore,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
199
|
-
type: "progress",
|
|
200
|
-
runId,
|
|
201
|
-
data: { suiteName, caseId: result.caseId, pass: passed, score, completed: completedCount, total: totalCases },
|
|
202
|
-
} satisfies EvalStreamEvent);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const runPromise = (async () => {
|
|
206
|
-
try {
|
|
207
|
-
const evalProject = new LatticeEvalProject(projectConfig, onCaseComplete);
|
|
208
|
-
const calibration = await evalProject.calibrateJudge();
|
|
209
|
-
if (!calibration.ok) {
|
|
210
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
211
|
-
status: "failed",
|
|
212
|
-
error: `Judge calibration failed: ${calibration.reason}`,
|
|
213
|
-
completedAt: new Date(),
|
|
214
|
-
});
|
|
215
|
-
throw new Error(`Judge calibration failed: ${calibration.reason}`);
|
|
216
|
-
}
|
|
217
|
-
const { report } = await evalProject.runAllSuitesBatch(concurrency, abortController.signal);
|
|
218
|
-
|
|
219
|
-
if (!abortController.signal.aborted) {
|
|
220
|
-
const completedCount = stats.passed + stats.failed;
|
|
221
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
222
|
-
status: "completed",
|
|
223
|
-
completedAt: new Date(),
|
|
224
|
-
passedCases: stats.passed,
|
|
225
|
-
failedCases: stats.failed,
|
|
226
|
-
avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
230
|
-
type: "completed",
|
|
231
|
-
runId,
|
|
232
|
-
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 },
|
|
233
|
-
} satisfies EvalStreamEvent);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return { report };
|
|
237
|
-
} catch (err) {
|
|
238
|
-
const errorMsg = (err as Error).message;
|
|
239
|
-
if (!abortController.signal.aborted) {
|
|
240
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
241
|
-
status: "failed",
|
|
242
|
-
error: errorMsg,
|
|
243
|
-
completedAt: new Date(),
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
247
|
-
type: "error",
|
|
248
|
-
runId,
|
|
249
|
-
data: { message: errorMsg },
|
|
250
|
-
} satisfies EvalStreamEvent);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
throw err;
|
|
254
|
-
} finally {
|
|
255
|
-
this.runs.delete(runId);
|
|
256
|
-
}
|
|
257
|
-
})();
|
|
258
|
-
|
|
259
|
-
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise });
|
|
260
|
-
runPromise.catch(() => {});
|
|
261
|
-
|
|
262
|
-
return runId;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async abortRun(runId: string): Promise<boolean> {
|
|
266
|
-
const ctx = this.runs.get(runId);
|
|
267
|
-
if (!ctx) return false;
|
|
268
|
-
ctx.abortController.abort();
|
|
269
|
-
const store = this.getEvalStore();
|
|
270
|
-
await store.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: new Date() });
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
isRunning(runId: string): boolean {
|
|
275
|
-
return this.runs.has(runId);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private getEvalStore(): EvalStore {
|
|
279
|
-
return getStoreLattice("default", "eval").store as EvalStore;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export const evalRunner = new EvalRunner();
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import {
|
|
6
|
-
type LocalA2AProviderState,
|
|
7
|
-
type LocalRuntimeConfig,
|
|
8
|
-
} from "@axiom-lattice/protocols";
|
|
9
|
-
|
|
10
|
-
interface LocalA2AProcessState extends LocalA2AProviderState {
|
|
11
|
-
enabled: boolean;
|
|
12
|
-
localRuntime?: LocalRuntimeConfig;
|
|
13
|
-
process?: ChildProcess;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const STDERR_TAIL_LIMIT = 4096;
|
|
17
|
-
|
|
18
|
-
function readBooleanEnv(
|
|
19
|
-
name: string,
|
|
20
|
-
defaultValue: boolean,
|
|
21
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
22
|
-
): boolean {
|
|
23
|
-
const value = env[name];
|
|
24
|
-
if (value === undefined) return defaultValue;
|
|
25
|
-
return value === "1" || value.toLowerCase() === "true";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function findWorkspaceRoot(startDir: string): string | null {
|
|
29
|
-
let current = resolve(startDir);
|
|
30
|
-
|
|
31
|
-
const { root } = parse(current);
|
|
32
|
-
|
|
33
|
-
while (current !== root) {
|
|
34
|
-
if (existsSync(join(current, "pnpm-workspace.yaml"))) {
|
|
35
|
-
return current;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const parent = dirname(current);
|
|
39
|
-
if (parent === current) return null;
|
|
40
|
-
current = parent;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function resolveLocalA2AWorkingDirectory(options: {
|
|
47
|
-
envWorkdir?: string;
|
|
48
|
-
commandCwd?: string;
|
|
49
|
-
currentCwd: string;
|
|
50
|
-
}): string {
|
|
51
|
-
if (options.envWorkdir) return options.envWorkdir;
|
|
52
|
-
if (options.commandCwd) return options.commandCwd;
|
|
53
|
-
return findWorkspaceRoot(options.currentCwd) || options.currentCwd;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function validateWorkingDirectory(cwd: string): string | null {
|
|
57
|
-
try {
|
|
58
|
-
const stat = statSync(cwd);
|
|
59
|
-
if (!stat.isDirectory()) {
|
|
60
|
-
return `Working directory is not a directory: ${cwd}`;
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
} catch (error: unknown) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
return `Working directory is not accessible: ${cwd} (${message})`;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function appendStderrTail(current: string, chunk: string): string {
|
|
70
|
-
const next = `${current}${chunk}`;
|
|
71
|
-
if (next.length <= STDERR_TAIL_LIMIT) return next;
|
|
72
|
-
return next.slice(-STDERR_TAIL_LIMIT);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatExitMessage(exitReason: string | number, stderrTail: string): string {
|
|
76
|
-
const stderr = stderrTail.trim();
|
|
77
|
-
return stderr
|
|
78
|
-
? `Exited before becoming ready (${exitReason}): ${stderr}`
|
|
79
|
-
: `Exited before becoming ready (${exitReason})`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function probeReady(
|
|
83
|
-
agentCardUrl: string,
|
|
84
|
-
healthUrl: string | undefined,
|
|
85
|
-
timeoutMs: number,
|
|
86
|
-
): Promise<boolean> {
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
-
try {
|
|
90
|
-
if (healthUrl) {
|
|
91
|
-
const resp = await fetch(healthUrl, { signal: controller.signal });
|
|
92
|
-
if (!resp.ok) return false;
|
|
93
|
-
}
|
|
94
|
-
const cardResp = await fetch(agentCardUrl, { signal: controller.signal });
|
|
95
|
-
return cardResp.ok;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
} finally {
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export class LocalA2AService {
|
|
104
|
-
private static instance: LocalA2AService | null = null;
|
|
105
|
-
private states: Map<string, LocalA2AProcessState> = new Map();
|
|
106
|
-
private autoStartEnabled: boolean;
|
|
107
|
-
|
|
108
|
-
constructor() {
|
|
109
|
-
this.autoStartEnabled = readBooleanEnv("LOCAL_A2A_AUTO_START", true);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
public static getInstance(): LocalA2AService {
|
|
113
|
-
if (!LocalA2AService.instance) {
|
|
114
|
-
LocalA2AService.instance = new LocalA2AService();
|
|
115
|
-
}
|
|
116
|
-
return LocalA2AService.instance;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
getOrCreateState(
|
|
120
|
-
runtimeId: string,
|
|
121
|
-
localRuntime: LocalRuntimeConfig,
|
|
122
|
-
): LocalA2AProcessState {
|
|
123
|
-
let state = this.states.get(runtimeId);
|
|
124
|
-
if (!state) {
|
|
125
|
-
state = {
|
|
126
|
-
runtimeId,
|
|
127
|
-
label: localRuntime.label || runtimeId,
|
|
128
|
-
status: this.autoStartEnabled ? "stopped" : "disabled",
|
|
129
|
-
enabled: this.autoStartEnabled,
|
|
130
|
-
assistantId: runtimeId,
|
|
131
|
-
agentCardUrl: localRuntime.agentCardUrl,
|
|
132
|
-
healthUrl: localRuntime.healthUrl,
|
|
133
|
-
localRuntime,
|
|
134
|
-
};
|
|
135
|
-
this.states.set(runtimeId, state);
|
|
136
|
-
} else {
|
|
137
|
-
state.localRuntime = localRuntime;
|
|
138
|
-
state.label = localRuntime.label || state.label;
|
|
139
|
-
state.agentCardUrl = localRuntime.agentCardUrl;
|
|
140
|
-
state.healthUrl = localRuntime.healthUrl;
|
|
141
|
-
}
|
|
142
|
-
return state;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async getRegisteredState(
|
|
146
|
-
runtimeId: string,
|
|
147
|
-
localRuntime: LocalRuntimeConfig,
|
|
148
|
-
): Promise<LocalA2AProviderState> {
|
|
149
|
-
const state = this.getOrCreateState(runtimeId, localRuntime);
|
|
150
|
-
|
|
151
|
-
if (state.process && state.status === "running") {
|
|
152
|
-
const alive = await probeReady(
|
|
153
|
-
localRuntime.agentCardUrl,
|
|
154
|
-
localRuntime.healthUrl,
|
|
155
|
-
2000,
|
|
156
|
-
);
|
|
157
|
-
if (!alive) {
|
|
158
|
-
state.process = undefined;
|
|
159
|
-
state.pid = undefined;
|
|
160
|
-
state.status = "stopped";
|
|
161
|
-
state.message = "Process exited unexpectedly";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return this.snapshotState(state);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async startProvider(
|
|
169
|
-
runtimeId: string,
|
|
170
|
-
localRuntime: LocalRuntimeConfig,
|
|
171
|
-
): Promise<LocalA2AProviderState> {
|
|
172
|
-
const state = this.getOrCreateState(runtimeId, localRuntime);
|
|
173
|
-
|
|
174
|
-
if (!state.enabled) {
|
|
175
|
-
state.status = "disabled";
|
|
176
|
-
state.message = "Disabled by configuration";
|
|
177
|
-
return this.snapshotState(state);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (state.process && state.status === "running") {
|
|
181
|
-
return this.snapshotState(state);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
state.enabled = true;
|
|
185
|
-
const { executable, args, cwd, env: cmdEnv } = localRuntime.command;
|
|
186
|
-
const resolvedCwd = resolveLocalA2AWorkingDirectory({
|
|
187
|
-
envWorkdir: process.env.LOCAL_A2A_WORKDIR || undefined,
|
|
188
|
-
commandCwd: cwd,
|
|
189
|
-
currentCwd: process.cwd(),
|
|
190
|
-
});
|
|
191
|
-
const cwdError = validateWorkingDirectory(resolvedCwd);
|
|
192
|
-
if (cwdError) {
|
|
193
|
-
state.process = undefined;
|
|
194
|
-
state.pid = undefined;
|
|
195
|
-
state.status = "failed";
|
|
196
|
-
state.message = cwdError;
|
|
197
|
-
return this.snapshotState(state);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
state.status = "starting";
|
|
201
|
-
state.message = "Starting";
|
|
202
|
-
let stderrTail = "";
|
|
203
|
-
|
|
204
|
-
const childEnv = cmdEnv ? { ...process.env, ...cmdEnv } : process.env;
|
|
205
|
-
|
|
206
|
-
const child = spawn(executable, args, {
|
|
207
|
-
cwd: resolvedCwd,
|
|
208
|
-
env: childEnv,
|
|
209
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
210
|
-
shell: false,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
state.process = child;
|
|
214
|
-
state.pid = child.pid ?? undefined;
|
|
215
|
-
|
|
216
|
-
child.once("error", (error: Error) => {
|
|
217
|
-
const current = this.states.get(runtimeId);
|
|
218
|
-
if (!current) return;
|
|
219
|
-
current.process = undefined;
|
|
220
|
-
current.pid = undefined;
|
|
221
|
-
current.status = "failed";
|
|
222
|
-
const stderr = stderrTail.trim();
|
|
223
|
-
current.message = stderr
|
|
224
|
-
? `Failed to start: ${error.message}: ${stderr}`
|
|
225
|
-
: `Failed to start: ${error.message}`;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
229
|
-
const text = chunk.toString("utf8").trim();
|
|
230
|
-
if (text) console.log(`[local-a2a:${runtimeId}] ${text}`);
|
|
231
|
-
});
|
|
232
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
233
|
-
const rawText = chunk.toString("utf8");
|
|
234
|
-
stderrTail = appendStderrTail(stderrTail, rawText);
|
|
235
|
-
const text = rawText.trim();
|
|
236
|
-
if (text) console.error(`[local-a2a:${runtimeId}] ${text}`);
|
|
237
|
-
});
|
|
238
|
-
child.on("close", (code, signal) => {
|
|
239
|
-
const current = this.states.get(runtimeId);
|
|
240
|
-
if (!current) return;
|
|
241
|
-
current.process = undefined;
|
|
242
|
-
current.pid = undefined;
|
|
243
|
-
const exitReason = signal ?? code ?? "unknown";
|
|
244
|
-
if (current.status === "running") {
|
|
245
|
-
current.status = "stopped";
|
|
246
|
-
current.message = `Stopped (${exitReason})`;
|
|
247
|
-
} else if (current.status === "starting") {
|
|
248
|
-
current.status = "failed";
|
|
249
|
-
current.message = formatExitMessage(exitReason, stderrTail);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const ready = await this.waitUntilReady(localRuntime, state, 15000);
|
|
254
|
-
if (!ready) {
|
|
255
|
-
if (state.process) {
|
|
256
|
-
state.process.kill("SIGTERM");
|
|
257
|
-
}
|
|
258
|
-
state.process = undefined;
|
|
259
|
-
state.pid = undefined;
|
|
260
|
-
if ((state.status as string) !== "failed") {
|
|
261
|
-
state.status = "failed";
|
|
262
|
-
state.message = "Server did not become ready";
|
|
263
|
-
}
|
|
264
|
-
return this.snapshotState(state);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
state.status = "running";
|
|
268
|
-
state.message = "Running";
|
|
269
|
-
state.lastStartedAt = new Date().toISOString();
|
|
270
|
-
return this.snapshotState(state);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async stopProvider(runtimeId: string): Promise<LocalA2AProviderState> {
|
|
274
|
-
const state = this.states.get(runtimeId);
|
|
275
|
-
if (!state) {
|
|
276
|
-
return this.emptyState(runtimeId);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const child = state.process;
|
|
280
|
-
if (child) {
|
|
281
|
-
child.kill("SIGTERM");
|
|
282
|
-
await delay(750);
|
|
283
|
-
if (child.exitCode === null) {
|
|
284
|
-
child.kill("SIGKILL");
|
|
285
|
-
}
|
|
286
|
-
state.process = undefined;
|
|
287
|
-
}
|
|
288
|
-
state.pid = undefined;
|
|
289
|
-
state.status = "stopped";
|
|
290
|
-
state.message = "Stopped";
|
|
291
|
-
state.lastStoppedAt = new Date().toISOString();
|
|
292
|
-
return this.snapshotState(state);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async restartProvider(
|
|
296
|
-
runtimeId: string,
|
|
297
|
-
localRuntime: LocalRuntimeConfig,
|
|
298
|
-
): Promise<LocalA2AProviderState> {
|
|
299
|
-
await this.stopProvider(runtimeId);
|
|
300
|
-
return this.startProvider(runtimeId, localRuntime);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
removeState(runtimeId: string): void {
|
|
304
|
-
const state = this.states.get(runtimeId);
|
|
305
|
-
if (state?.process) {
|
|
306
|
-
state.process.kill("SIGTERM");
|
|
307
|
-
state.process = undefined;
|
|
308
|
-
}
|
|
309
|
-
this.states.delete(runtimeId);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private async waitUntilReady(
|
|
313
|
-
localRuntime: LocalRuntimeConfig,
|
|
314
|
-
state: LocalA2AProcessState,
|
|
315
|
-
timeoutMs: number,
|
|
316
|
-
): Promise<boolean> {
|
|
317
|
-
const start = Date.now();
|
|
318
|
-
while (Date.now() - start < timeoutMs) {
|
|
319
|
-
if (state.status === "failed") return false;
|
|
320
|
-
if (await probeReady(localRuntime.agentCardUrl, localRuntime.healthUrl, 1500)) {
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
await delay(500);
|
|
324
|
-
}
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private emptyState(runtimeId: string): LocalA2AProviderState {
|
|
329
|
-
return {
|
|
330
|
-
runtimeId,
|
|
331
|
-
label: runtimeId,
|
|
332
|
-
status: "stopped",
|
|
333
|
-
enabled: false,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private snapshotState(state: LocalA2AProcessState): LocalA2AProviderState {
|
|
338
|
-
return {
|
|
339
|
-
runtimeId: state.runtimeId,
|
|
340
|
-
label: state.label,
|
|
341
|
-
status: state.status,
|
|
342
|
-
enabled: state.enabled,
|
|
343
|
-
assistantId: state.assistantId,
|
|
344
|
-
agentCardUrl: state.agentCardUrl,
|
|
345
|
-
healthUrl: state.healthUrl,
|
|
346
|
-
pid: state.pid,
|
|
347
|
-
message: state.message,
|
|
348
|
-
lastStartedAt: state.lastStartedAt,
|
|
349
|
-
lastStoppedAt: state.lastStoppedAt,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export const localA2AService = LocalA2AService.getInstance();
|