@minasoft/mina-ai-router 0.1.4 → 0.2.0
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/README.md +67 -16
- package/dist/apps/cli/src/index.js +1247 -43
- package/dist/apps/http-server/src/index.js +598 -46
- package/dist/apps/http-server/src/public/assets/index-Bl059Jd0.js +9 -0
- package/dist/apps/http-server/src/public/assets/index-CaPxN_Ez.css +1 -0
- package/dist/apps/http-server/src/public/index.html +16 -0
- package/dist/apps/mcp-server/src/index.js +54 -7
- package/dist/packages/core/src/capability-profile.js +145 -0
- package/dist/packages/core/src/index.js +3 -0
- package/dist/packages/core/src/mcp-preflight.js +80 -0
- package/dist/packages/core/src/registry.js +128 -3
- package/dist/packages/core/src/request-store.js +158 -0
- package/dist/packages/core/src/response-parser.js +76 -8
- package/dist/packages/core/src/router.js +408 -13
- package/dist/packages/core/src/version.js +57 -0
- package/dist/packages/mcp/src/provider.js +57 -6
- package/dist/packages/transports/src/headless/headless-transport.js +13 -8
- package/dist/packages/transports/src/tmux/tmux-client.js +334 -0
- package/dist/packages/transports/src/tmux/tmux-transport.js +10 -0
- package/docs/DEVELOPER-START-GUIDE.md +9 -1
- package/docs/GETTING-STARTED.md +10 -5
- package/docs/HTTP-UI-MCP.md +39 -13
- package/docs/MCP-CLIENT-SETUP.md +56 -3
- package/docs/SKILL-INSTALL-GUIDE.md +21 -3
- package/docs/TROUBLESHOOTING.md +47 -0
- package/docs/USER-START-GUIDE.md +155 -26
- package/docs/assets/mina-ai-router-overview.svg +109 -0
- package/package.json +19 -5
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AgentRouter = void 0;
|
|
3
|
+
exports.AgentRouter = exports.AgentNotRouteReadyError = void 0;
|
|
4
4
|
const ids_1 = require("./ids");
|
|
5
5
|
const prompt_envelope_1 = require("./prompt-envelope");
|
|
6
6
|
const response_parser_1 = require("./response-parser");
|
|
7
|
+
const rawEvidenceExcerptLimit = 4000;
|
|
8
|
+
const defaultAgentStaleAfterMs = 15 * 60 * 1000;
|
|
9
|
+
class RequestNoLongerOpenError extends Error {
|
|
10
|
+
constructor(request) {
|
|
11
|
+
super(`Request "${request.id}" is ${request.status} and can no longer be updated by the active router call.`);
|
|
12
|
+
this.request = request;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
class AgentNotRouteReadyError extends Error {
|
|
16
|
+
constructor(agentId, reason) {
|
|
17
|
+
super(`Agent "${agentId}" is not ready to receive routed work: ${reason}`);
|
|
18
|
+
this.agentId = agentId;
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.AgentNotRouteReadyError = AgentNotRouteReadyError;
|
|
7
23
|
class AgentRouter {
|
|
8
24
|
constructor(options) {
|
|
9
25
|
this.busyAgents = new Set();
|
|
@@ -12,6 +28,7 @@ class AgentRouter {
|
|
|
12
28
|
this.transports = options.transports;
|
|
13
29
|
this.defaultSourceAgent = options.defaultSourceAgent ?? "main";
|
|
14
30
|
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 300000;
|
|
31
|
+
this.agentStaleAfterMs = options.agentStaleAfterMs ?? defaultAgentStaleAfterMs;
|
|
15
32
|
this.onStateChanged = options.onStateChanged;
|
|
16
33
|
}
|
|
17
34
|
listAgents() {
|
|
@@ -20,8 +37,10 @@ class AgentRouter {
|
|
|
20
37
|
listRequests() {
|
|
21
38
|
return this.requestStore.list();
|
|
22
39
|
}
|
|
23
|
-
async listAgentStatuses() {
|
|
40
|
+
async listAgentStatuses(options = {}) {
|
|
24
41
|
const requests = this.requestStore.list();
|
|
42
|
+
const checkedAt = new Date().toISOString();
|
|
43
|
+
let healthChanged = false;
|
|
25
44
|
return Promise.all(this.registry.list().map(async (agent) => {
|
|
26
45
|
const agentRequests = requests.filter((request) => request.targetAgent === agent.id);
|
|
27
46
|
const lastRequest = agentRequests[agentRequests.length - 1];
|
|
@@ -29,6 +48,39 @@ class AgentRouter {
|
|
|
29
48
|
const status = transport.status
|
|
30
49
|
? await transport.status(agent)
|
|
31
50
|
: { status: "unknown" };
|
|
51
|
+
const bootstrapStatus = status.bootstrapStatus ?? agent.bootstrapStatus;
|
|
52
|
+
if (status.bootstrapStatus && status.bootstrapStatus !== agent.bootstrapStatus) {
|
|
53
|
+
this.registry.register({
|
|
54
|
+
...agent,
|
|
55
|
+
bootstrapStatus: status.bootstrapStatus,
|
|
56
|
+
});
|
|
57
|
+
healthChanged = true;
|
|
58
|
+
}
|
|
59
|
+
const lastActivityAt = lastRequest?.updatedAt ?? agent.lastActivityAt;
|
|
60
|
+
const lastSeenAt = status.status === "available" ? checkedAt : agent.lastSeenAt;
|
|
61
|
+
const healthStatus = classifyAgentHealth({
|
|
62
|
+
transportStatus: status.status,
|
|
63
|
+
bootstrapStatus,
|
|
64
|
+
registrationStatus: agent.registrationStatus,
|
|
65
|
+
mcpPreflightStatus: agent.mcpPreflightStatus,
|
|
66
|
+
busy: this.busyAgents.has(agent.id),
|
|
67
|
+
lastSeenAt,
|
|
68
|
+
lastActivityAt,
|
|
69
|
+
lastRequest,
|
|
70
|
+
checkedAt,
|
|
71
|
+
staleAfterMs: this.agentStaleAfterMs,
|
|
72
|
+
});
|
|
73
|
+
const routeReadiness = classifyRouteReadiness({
|
|
74
|
+
healthStatus: healthStatus.status,
|
|
75
|
+
healthDetail: healthStatus.detail ?? status.detail,
|
|
76
|
+
bootstrapStatus,
|
|
77
|
+
registrationStatus: agent.registrationStatus,
|
|
78
|
+
mcpPreflightStatus: agent.mcpPreflightStatus,
|
|
79
|
+
});
|
|
80
|
+
if (lastSeenAt !== agent.lastSeenAt || lastActivityAt !== agent.lastActivityAt) {
|
|
81
|
+
this.registry.updateHealth(agent.id, { lastSeenAt, lastActivityAt });
|
|
82
|
+
healthChanged = true;
|
|
83
|
+
}
|
|
32
84
|
return {
|
|
33
85
|
id: agent.id,
|
|
34
86
|
name: agent.name,
|
|
@@ -38,22 +90,127 @@ class AgentRouter {
|
|
|
38
90
|
projectRoot: agent.projectRoot,
|
|
39
91
|
capabilitySummary: agent.capabilitySummary,
|
|
40
92
|
capabilitySources: agent.capabilitySources,
|
|
41
|
-
|
|
42
|
-
|
|
93
|
+
capabilitySource: agent.capabilitySource,
|
|
94
|
+
capabilityUpdatedAt: agent.capabilityUpdatedAt,
|
|
95
|
+
lastCapabilityRefreshAt: agent.lastCapabilityRefreshAt,
|
|
96
|
+
capabilityProfile: agent.capabilityProfile,
|
|
97
|
+
bootstrapStatus,
|
|
98
|
+
registrationSource: agent.registrationSource,
|
|
99
|
+
registrationStatus: agent.registrationStatus,
|
|
100
|
+
lastRegistrationAttemptAt: agent.lastRegistrationAttemptAt,
|
|
101
|
+
confirmedByAgentAt: agent.confirmedByAgentAt,
|
|
102
|
+
sessionFingerprint: agent.sessionFingerprint,
|
|
103
|
+
registrationHistory: agent.registrationHistory,
|
|
104
|
+
registrationWarnings: agent.registrationWarnings,
|
|
105
|
+
permissionProfile: agent.permissionProfile,
|
|
106
|
+
permissionProfileStatus: agent.permissionProfileStatus,
|
|
107
|
+
permissionProfileDetail: agent.permissionProfileDetail,
|
|
108
|
+
permissionPrompt: status.permissionPrompt,
|
|
109
|
+
mcpPreflightStatus: agent.mcpPreflightStatus,
|
|
110
|
+
mcpPreflightDetail: agent.mcpPreflightDetail,
|
|
111
|
+
mcpSetupCommand: agent.mcpSetupCommand,
|
|
112
|
+
mcpVerifyCommand: agent.mcpVerifyCommand,
|
|
113
|
+
mcpRemoveCommand: agent.mcpRemoveCommand,
|
|
114
|
+
mcpUrl: agent.mcpUrl,
|
|
115
|
+
lastSeenAt,
|
|
116
|
+
lastActivityAt,
|
|
117
|
+
activeRequestId: agent.activeRequestId,
|
|
118
|
+
leaseStatus: agent.leaseStatus,
|
|
119
|
+
leaseStartedAt: agent.leaseStartedAt,
|
|
120
|
+
leaseExpiresAt: agent.leaseExpiresAt,
|
|
121
|
+
leaseReleasedAt: agent.leaseReleasedAt,
|
|
122
|
+
healthCheckedAt: checkedAt,
|
|
123
|
+
staleAfterMs: this.agentStaleAfterMs,
|
|
124
|
+
status: healthStatus.status,
|
|
125
|
+
detail: healthStatus.detail ?? status.detail,
|
|
126
|
+
routeReady: routeReadiness.routeReady,
|
|
127
|
+
routeBlockedReason: routeReadiness.routeBlockedReason,
|
|
43
128
|
lastRequestStatus: lastRequest?.status,
|
|
129
|
+
isSelf: options.callerAgentId === agent.id || undefined,
|
|
44
130
|
};
|
|
45
|
-
}))
|
|
131
|
+
})).finally(() => {
|
|
132
|
+
if (healthChanged) {
|
|
133
|
+
this.persistState();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
46
136
|
}
|
|
47
137
|
getRequest(requestId) {
|
|
48
138
|
return this.requestStore.require(requestId);
|
|
49
139
|
}
|
|
140
|
+
recoverRequestLease(requestId, source, message = "Marked recovered by operator.") {
|
|
141
|
+
const updated = this.requestStore.markRecovered(requestId, source, message);
|
|
142
|
+
const agentId = updated.leaseOwnerAgentId ?? updated.targetAgent;
|
|
143
|
+
this.releaseAgentLease(agentId, updated.id);
|
|
144
|
+
this.persistState();
|
|
145
|
+
return this.requestStore.require(updated.id);
|
|
146
|
+
}
|
|
147
|
+
archiveRequest(requestId, source, reason = "Archived by operator.") {
|
|
148
|
+
const updated = this.requestStore.archive(requestId, reason, source);
|
|
149
|
+
if (updated.leaseStatus === "released") {
|
|
150
|
+
const agentId = updated.leaseOwnerAgentId ?? updated.targetAgent;
|
|
151
|
+
this.releaseAgentLease(agentId, updated.id);
|
|
152
|
+
}
|
|
153
|
+
this.persistState();
|
|
154
|
+
return this.requestStore.require(updated.id);
|
|
155
|
+
}
|
|
50
156
|
async callAgent(input) {
|
|
51
157
|
const target = this.registry.require(input.target);
|
|
158
|
+
if (input.sourceAgent && input.sourceAgent === target.id && !input.allowSelfCall) {
|
|
159
|
+
throw new Error(`Refusing self-call from agent "${target.id}" to itself. Choose another target from list_agents or pass allowSelfCall: true for diagnostics.`);
|
|
160
|
+
}
|
|
52
161
|
if (this.busyAgents.has(target.id)) {
|
|
53
162
|
throw new Error(`Agent "${target.id}" is busy with another request.`);
|
|
54
163
|
}
|
|
164
|
+
const transport = this.transports.get(target.transport);
|
|
165
|
+
const checkedAt = new Date().toISOString();
|
|
166
|
+
const transportStatus = transport.status
|
|
167
|
+
? await transport.status(target)
|
|
168
|
+
: { status: "unknown" };
|
|
169
|
+
const bootstrapStatus = transportStatus.bootstrapStatus ?? target.bootstrapStatus;
|
|
170
|
+
if (transportStatus.bootstrapStatus && transportStatus.bootstrapStatus !== target.bootstrapStatus) {
|
|
171
|
+
this.registry.register({
|
|
172
|
+
...target,
|
|
173
|
+
bootstrapStatus: transportStatus.bootstrapStatus,
|
|
174
|
+
});
|
|
175
|
+
this.persistState();
|
|
176
|
+
}
|
|
177
|
+
const agentRequests = this.requestStore.list().filter((request) => request.targetAgent === target.id);
|
|
178
|
+
const lastRequest = agentRequests[agentRequests.length - 1];
|
|
179
|
+
const healthStatus = classifyAgentHealth({
|
|
180
|
+
transportStatus: transportStatus.status,
|
|
181
|
+
bootstrapStatus,
|
|
182
|
+
registrationStatus: target.registrationStatus,
|
|
183
|
+
mcpPreflightStatus: target.mcpPreflightStatus,
|
|
184
|
+
busy: false,
|
|
185
|
+
lastSeenAt: transportStatus.status === "available" ? checkedAt : target.lastSeenAt,
|
|
186
|
+
lastActivityAt: lastRequest?.updatedAt ?? target.lastActivityAt,
|
|
187
|
+
lastRequest,
|
|
188
|
+
checkedAt,
|
|
189
|
+
staleAfterMs: this.agentStaleAfterMs,
|
|
190
|
+
});
|
|
191
|
+
const routeReadiness = classifyRouteReadiness({
|
|
192
|
+
healthStatus: healthStatus.status,
|
|
193
|
+
healthDetail: healthStatus.detail ?? transportStatus.detail,
|
|
194
|
+
bootstrapStatus,
|
|
195
|
+
registrationStatus: target.registrationStatus,
|
|
196
|
+
mcpPreflightStatus: target.mcpPreflightStatus,
|
|
197
|
+
});
|
|
198
|
+
if (!routeReadiness.routeReady) {
|
|
199
|
+
throw new AgentNotRouteReadyError(target.id, routeReadiness.routeBlockedReason ?? "Agent is not route-ready.");
|
|
200
|
+
}
|
|
55
201
|
this.busyAgents.add(target.id);
|
|
56
202
|
const now = new Date().toISOString();
|
|
203
|
+
const timeoutMs = input.timeoutMs ?? this.defaultTimeoutMs;
|
|
204
|
+
const leaseExpiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
205
|
+
this.registry.register({
|
|
206
|
+
...target,
|
|
207
|
+
activeRequestId: undefined,
|
|
208
|
+
leaseStatus: undefined,
|
|
209
|
+
leaseStartedAt: undefined,
|
|
210
|
+
leaseExpiresAt: undefined,
|
|
211
|
+
leaseReleasedAt: undefined,
|
|
212
|
+
lastActivityAt: now,
|
|
213
|
+
});
|
|
57
214
|
const request = this.requestStore.create({
|
|
58
215
|
id: (0, ids_1.createRequestId)(),
|
|
59
216
|
sourceAgent: input.sourceAgent ?? this.defaultSourceAgent,
|
|
@@ -62,19 +219,49 @@ class AgentRouter {
|
|
|
62
219
|
status: "created",
|
|
63
220
|
createdAt: now,
|
|
64
221
|
updatedAt: now,
|
|
222
|
+
retryOfRequestId: input.retryOfRequestId,
|
|
223
|
+
leaseStatus: "active",
|
|
224
|
+
leaseStartedAt: now,
|
|
225
|
+
leaseExpiresAt,
|
|
226
|
+
leaseOwnerAgentId: target.id,
|
|
227
|
+
leaseTargetSessionId: target.sessionId,
|
|
228
|
+
leaseTargetSessionFingerprint: target.sessionFingerprint,
|
|
229
|
+
});
|
|
230
|
+
this.registry.register({
|
|
231
|
+
...target,
|
|
232
|
+
activeRequestId: request.id,
|
|
233
|
+
leaseStatus: "active",
|
|
234
|
+
leaseStartedAt: now,
|
|
235
|
+
leaseExpiresAt,
|
|
236
|
+
leaseReleasedAt: undefined,
|
|
237
|
+
lastActivityAt: now,
|
|
65
238
|
});
|
|
66
239
|
this.persistState();
|
|
67
|
-
const transport = this.transports.get(target.transport);
|
|
68
240
|
const prompt = (0, prompt_envelope_1.buildPromptEnvelope)(request, target);
|
|
241
|
+
this.requestStore.patch(request.id, {
|
|
242
|
+
promptEvidence: rawEvidenceFromOutput(prompt, "prompt_envelope"),
|
|
243
|
+
});
|
|
244
|
+
let output = "";
|
|
69
245
|
try {
|
|
70
246
|
await transport.send(target, prompt, request.id);
|
|
71
|
-
this.
|
|
247
|
+
this.updateOpenRequestStatus(request.id, "sent");
|
|
72
248
|
this.persistState();
|
|
73
|
-
this.
|
|
249
|
+
this.updateOpenRequestStatus(request.id, "waiting");
|
|
74
250
|
this.persistState();
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
251
|
+
output = await transport.waitForResponse(target, request.id, input.timeoutMs ?? this.defaultTimeoutMs);
|
|
252
|
+
const parsed = (0, response_parser_1.inspectMarkedResponse)(output, request.id);
|
|
253
|
+
if (!parsed.ok) {
|
|
254
|
+
throw new response_parser_1.ResponseParseError(parsed.diagnostics);
|
|
255
|
+
}
|
|
256
|
+
const answer = parsed.answer;
|
|
257
|
+
this.updateOpenRequestStatus(request.id, "answered", {
|
|
258
|
+
answer,
|
|
259
|
+
diagnosticStatus: "answered",
|
|
260
|
+
parserDiagnostics: parsed.diagnostics,
|
|
261
|
+
rawEvidence: rawEvidenceFromOutput(output),
|
|
262
|
+
...releasedLeasePatch(),
|
|
263
|
+
});
|
|
264
|
+
this.releaseAgentLease(target.id, request.id);
|
|
78
265
|
this.persistState();
|
|
79
266
|
return {
|
|
80
267
|
requestId: request.id,
|
|
@@ -83,18 +270,226 @@ class AgentRouter {
|
|
|
83
270
|
};
|
|
84
271
|
}
|
|
85
272
|
catch (error) {
|
|
273
|
+
if (error instanceof RequestNoLongerOpenError) {
|
|
274
|
+
this.releaseAgentLease(target.id, request.id);
|
|
275
|
+
this.persistState();
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
86
278
|
const message = error instanceof Error ? error.message : String(error);
|
|
87
279
|
const status = /time(?:d)?\s*out|timeout/i.test(message) ? "timeout" : "failed";
|
|
88
|
-
|
|
280
|
+
const capturedOutput = output || extractLastCapture(message);
|
|
281
|
+
const updated = this.requestStore.updateOpenStatus(request.id, status, {
|
|
282
|
+
error: message,
|
|
283
|
+
diagnosticStatus: diagnosticStatusForError(error, status),
|
|
284
|
+
parserDiagnostics: error instanceof response_parser_1.ResponseParseError ? error.diagnostics : undefined,
|
|
285
|
+
rawEvidence: capturedOutput ? rawEvidenceFromOutput(capturedOutput) : undefined,
|
|
286
|
+
...(status === "timeout" ? orphanedLeasePatch() : releasedLeasePatch()),
|
|
287
|
+
});
|
|
288
|
+
if (!updated) {
|
|
289
|
+
this.releaseAgentLease(target.id, request.id);
|
|
290
|
+
throw new RequestNoLongerOpenError(this.requestStore.require(request.id));
|
|
291
|
+
}
|
|
292
|
+
if (status === "timeout") {
|
|
293
|
+
this.orphanAgentLease(target.id, request.id);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.releaseAgentLease(target.id, request.id);
|
|
297
|
+
}
|
|
89
298
|
this.persistState();
|
|
90
299
|
throw error;
|
|
91
300
|
}
|
|
92
301
|
finally {
|
|
93
|
-
this.
|
|
302
|
+
const current = this.registry.get(target.id);
|
|
303
|
+
if (current?.activeRequestId !== request.id) {
|
|
304
|
+
this.busyAgents.delete(target.id);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
updateOpenRequestStatus(requestId, status, patch = {}) {
|
|
309
|
+
const updated = this.requestStore.updateOpenStatus(requestId, status, patch);
|
|
310
|
+
if (!updated) {
|
|
311
|
+
throw new RequestNoLongerOpenError(this.requestStore.require(requestId));
|
|
94
312
|
}
|
|
313
|
+
return updated;
|
|
95
314
|
}
|
|
96
315
|
persistState() {
|
|
97
316
|
this.onStateChanged?.();
|
|
98
317
|
}
|
|
318
|
+
releaseAgentLease(agentId, requestId) {
|
|
319
|
+
const current = this.registry.get(agentId);
|
|
320
|
+
if (!current || current.activeRequestId !== requestId) {
|
|
321
|
+
this.busyAgents.delete(agentId);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.registry.register({
|
|
325
|
+
...current,
|
|
326
|
+
activeRequestId: undefined,
|
|
327
|
+
leaseStatus: "released",
|
|
328
|
+
leaseReleasedAt: new Date().toISOString(),
|
|
329
|
+
});
|
|
330
|
+
this.busyAgents.delete(agentId);
|
|
331
|
+
}
|
|
332
|
+
orphanAgentLease(agentId, requestId) {
|
|
333
|
+
const current = this.registry.get(agentId);
|
|
334
|
+
if (!current || current.activeRequestId !== requestId) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.registry.register({
|
|
338
|
+
...current,
|
|
339
|
+
activeRequestId: requestId,
|
|
340
|
+
leaseStatus: "orphaned",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
99
343
|
}
|
|
100
344
|
exports.AgentRouter = AgentRouter;
|
|
345
|
+
function releasedLeasePatch() {
|
|
346
|
+
return {
|
|
347
|
+
leaseStatus: "released",
|
|
348
|
+
leaseReleasedAt: new Date().toISOString(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function orphanedLeasePatch() {
|
|
352
|
+
return {
|
|
353
|
+
leaseStatus: "orphaned",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function diagnosticStatusForError(error, status) {
|
|
357
|
+
if (status === "timeout") {
|
|
358
|
+
return "timeout";
|
|
359
|
+
}
|
|
360
|
+
if (error instanceof response_parser_1.ResponseParseError) {
|
|
361
|
+
return "parse_failure";
|
|
362
|
+
}
|
|
363
|
+
return "transport_failure";
|
|
364
|
+
}
|
|
365
|
+
function classifyAgentHealth(input) {
|
|
366
|
+
if (input.lastRequest?.leaseStatus === "orphaned") {
|
|
367
|
+
return { status: "busy", detail: `Request "${input.lastRequest.id}" timed out in the router, but the target session lease is still attached for recovery.` };
|
|
368
|
+
}
|
|
369
|
+
if (input.lastRequest?.leaseStatus === "active" && ["created", "sent", "waiting"].includes(input.lastRequest.status)) {
|
|
370
|
+
return { status: "busy", detail: `Request "${input.lastRequest.id}" still has an active lease.` };
|
|
371
|
+
}
|
|
372
|
+
if (input.busy) {
|
|
373
|
+
return { status: "busy", detail: "Agent is currently handling a routed request." };
|
|
374
|
+
}
|
|
375
|
+
if (input.transportStatus === "missing") {
|
|
376
|
+
return { status: "missing" };
|
|
377
|
+
}
|
|
378
|
+
if (input.transportStatus === "needs-attention") {
|
|
379
|
+
return { status: "needs-attention" };
|
|
380
|
+
}
|
|
381
|
+
if (input.bootstrapStatus === "permission-required") {
|
|
382
|
+
return {
|
|
383
|
+
status: "needs-attention",
|
|
384
|
+
detail: "Agent is waiting for operator permission before it can receive routed work.",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (input.bootstrapStatus === "client-update-required") {
|
|
388
|
+
return {
|
|
389
|
+
status: "needs-attention",
|
|
390
|
+
detail: "Agent CLI is waiting at an update prompt before Mina can continue registration.",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (input.bootstrapStatus === "mcp-configuring") {
|
|
394
|
+
return {
|
|
395
|
+
status: "needs-attention",
|
|
396
|
+
detail: "Agent is waiting for Mina MCP setup before it can self-register or receive routed work.",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (input.bootstrapStatus === "registration-pending" || input.registrationStatus === "pending") {
|
|
400
|
+
return {
|
|
401
|
+
status: "needs-attention",
|
|
402
|
+
detail: "Agent self-registration has not been confirmed yet.",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (input.mcpPreflightStatus === "missing" || input.mcpPreflightStatus === "stale") {
|
|
406
|
+
return {
|
|
407
|
+
status: "needs-attention",
|
|
408
|
+
detail: "Agent MCP preflight is not ready; fix MCP setup before routing work.",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const attentionStatuses = new Set(["failed", "timeout"]);
|
|
412
|
+
if (input.lastRequest && attentionStatuses.has(input.lastRequest.status) && input.lastRequest.recoveryStatus !== "recovered") {
|
|
413
|
+
return {
|
|
414
|
+
status: "needs-attention",
|
|
415
|
+
detail: input.lastRequest.error
|
|
416
|
+
? `Last request ${input.lastRequest.status}: ${input.lastRequest.error}`
|
|
417
|
+
: `Last request is ${input.lastRequest.status}.`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const staleReference = input.lastSeenAt ?? input.lastActivityAt;
|
|
421
|
+
if (staleReference && isOlderThan(staleReference, input.checkedAt, input.staleAfterMs)) {
|
|
422
|
+
return {
|
|
423
|
+
status: "stale",
|
|
424
|
+
detail: `No confirmed agent reachability within ${Math.round(input.staleAfterMs / 1000)} seconds.`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (input.transportStatus === "available") {
|
|
428
|
+
return { status: "available" };
|
|
429
|
+
}
|
|
430
|
+
if (input.transportStatus === "stale") {
|
|
431
|
+
return { status: input.transportStatus };
|
|
432
|
+
}
|
|
433
|
+
return { status: "unknown" };
|
|
434
|
+
}
|
|
435
|
+
function classifyRouteReadiness(input) {
|
|
436
|
+
const blocker = routeBlocker(input);
|
|
437
|
+
return blocker
|
|
438
|
+
? { routeReady: false, routeBlockedReason: blocker }
|
|
439
|
+
: { routeReady: true };
|
|
440
|
+
}
|
|
441
|
+
function routeBlocker(input) {
|
|
442
|
+
if (input.bootstrapStatus === "permission-required") {
|
|
443
|
+
return "Agent is waiting for operator permission before it can receive routed work.";
|
|
444
|
+
}
|
|
445
|
+
if (input.bootstrapStatus === "client-update-required") {
|
|
446
|
+
return "Agent CLI is waiting at an update prompt before Mina can continue registration.";
|
|
447
|
+
}
|
|
448
|
+
if (input.bootstrapStatus === "mcp-configuring") {
|
|
449
|
+
return "Agent is waiting for Mina MCP setup before it can self-register or receive routed work.";
|
|
450
|
+
}
|
|
451
|
+
if (input.bootstrapStatus === "registration-pending" || input.registrationStatus === "pending") {
|
|
452
|
+
return "Agent self-registration has not been confirmed yet.";
|
|
453
|
+
}
|
|
454
|
+
if (input.mcpPreflightStatus === "missing" || input.mcpPreflightStatus === "stale") {
|
|
455
|
+
return "Agent MCP preflight is not ready; fix MCP setup before routing work.";
|
|
456
|
+
}
|
|
457
|
+
if (input.healthStatus === "busy") {
|
|
458
|
+
return input.healthDetail ?? "Agent is currently handling a routed request.";
|
|
459
|
+
}
|
|
460
|
+
if (input.healthStatus === "missing") {
|
|
461
|
+
return input.healthDetail ?? "Agent transport is missing.";
|
|
462
|
+
}
|
|
463
|
+
if (input.healthStatus === "stale") {
|
|
464
|
+
return input.healthDetail ?? "Agent health is stale.";
|
|
465
|
+
}
|
|
466
|
+
if (input.healthStatus === "needs-attention") {
|
|
467
|
+
return input.healthDetail ?? "Agent needs operator attention before it can receive routed work.";
|
|
468
|
+
}
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
function isOlderThan(timestamp, now, staleAfterMs) {
|
|
472
|
+
const timestampMs = Date.parse(timestamp);
|
|
473
|
+
const nowMs = Date.parse(now);
|
|
474
|
+
return Number.isFinite(timestampMs)
|
|
475
|
+
&& Number.isFinite(nowMs)
|
|
476
|
+
&& nowMs - timestampMs > staleAfterMs;
|
|
477
|
+
}
|
|
478
|
+
function rawEvidenceFromOutput(output, kind = "transport_capture") {
|
|
479
|
+
const excerpt = output.slice(-rawEvidenceExcerptLimit);
|
|
480
|
+
return {
|
|
481
|
+
kind,
|
|
482
|
+
capturedAt: new Date().toISOString(),
|
|
483
|
+
characterCount: output.length,
|
|
484
|
+
excerpt,
|
|
485
|
+
truncated: output.length > excerpt.length,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function extractLastCapture(message) {
|
|
489
|
+
const marker = "Last capture:\n";
|
|
490
|
+
const index = message.indexOf(marker);
|
|
491
|
+
if (index === -1) {
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
return message.slice(index + marker.length);
|
|
495
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.packageVersion = packageVersion;
|
|
4
|
+
exports.packageRoot = packageRoot;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const fallbackVersion = "0.0.0";
|
|
8
|
+
const packageName = "@minasoft/mina-ai-router";
|
|
9
|
+
function packageVersion() {
|
|
10
|
+
const packagePath = findPackageJson();
|
|
11
|
+
if (!packagePath) {
|
|
12
|
+
return fallbackVersion;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
|
|
16
|
+
return typeof parsed.version === "string" && parsed.version.trim()
|
|
17
|
+
? parsed.version
|
|
18
|
+
: fallbackVersion;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fallbackVersion;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function packageRoot() {
|
|
25
|
+
const packagePath = findPackageJson();
|
|
26
|
+
return packagePath ? (0, node_path_1.dirname)(packagePath) : undefined;
|
|
27
|
+
}
|
|
28
|
+
function findPackageJson() {
|
|
29
|
+
return findUpPackageJson(__dirname);
|
|
30
|
+
}
|
|
31
|
+
function findUpPackageJson(start) {
|
|
32
|
+
let current = (0, node_path_1.resolve)(start);
|
|
33
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
34
|
+
const candidate = (0, node_path_1.join)(current, "package.json");
|
|
35
|
+
if (isMinaPackageJson(candidate)) {
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
const parent = (0, node_path_1.dirname)(current);
|
|
39
|
+
if (parent === current) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function isMinaPackageJson(candidate) {
|
|
47
|
+
if (!(0, node_fs_1.existsSync)(candidate)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(candidate, "utf8"));
|
|
52
|
+
return parsed.name === packageName;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|