@starlink-awaken/agentmesh 1.3.0 → 1.4.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/dist/src/cli.js CHANGED
@@ -7,7 +7,7 @@ import { existsSync, readFileSync } from 'node:fs';
7
7
  import { resolve, dirname, join } from 'node:path';
8
8
  import { initLogger } from './core/logger.js';
9
9
  const PROJECT_ROOT = resolve(dirname(import.meta.dir), '..');
10
- const VERSION = '1.3.0';
10
+ const VERSION = '1.4.0';
11
11
  const BANNER = `
12
12
  █████╗ ██████╗ ███████╗███╗ ██╗████████╗
13
13
  ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
@@ -21,9 +21,9 @@ export function recordRequest(log) {
21
21
  m.lastError = log.error;
22
22
  m.lastErrorTime = log.timestamp;
23
23
  }
24
- recentRequests.unshift(log);
24
+ recentRequests.push(log);
25
25
  if (recentRequests.length > MAX_RECENT)
26
- recentRequests.pop();
26
+ recentRequests.shift();
27
27
  }
28
28
  export function getMetrics() {
29
29
  const providers = {};
@@ -46,7 +46,7 @@ export function getMetrics() {
46
46
  total_requests: totalRequests,
47
47
  total_failures: totalFailures,
48
48
  providers,
49
- recent: recentRequests.slice(0, 20).map(r => ({
49
+ recent: recentRequests.slice(-20).reverse().map(r => ({
50
50
  time: new Date(r.timestamp).toISOString(),
51
51
  model: r.model,
52
52
  provider: r.provider,
@@ -1,4 +1,3 @@
1
1
  import type { ChatCompletionRequest, ResolvedProvider } from './types.js';
2
2
  export declare function callChatCompletions(provider: ResolvedProvider, request: ChatCompletionRequest): Promise<Response>;
3
3
  export declare function callResponsesApi(provider: ResolvedProvider, body: Record<string, any>): Promise<Response>;
4
- export declare function buildStreamingResponse(upstreamResp: Response): Response;
@@ -1,6 +1,5 @@
1
1
  import { circuitBreakerRegistry } from './circuit-breaker.js';
2
2
  import { withRetry, isRetryable } from './retry.js';
3
- // 所有目标 Provider 都兼容 OpenAI API 格式,统一客户端即可
4
3
  export async function callChatCompletions(provider, request) {
5
4
  const { base_url, api_key, name: providerName } = provider;
6
5
  const { model, messages, stream, temperature, max_tokens, tools, tool_choice } = request;
@@ -19,22 +18,18 @@ export async function callChatCompletions(provider, request) {
19
18
  'Content-Type': 'application/json',
20
19
  Authorization: `Bearer ${api_key}`,
21
20
  };
22
- // OpenRouter 需要额外的头部
23
21
  if (providerName === 'openrouter') {
24
22
  headers['HTTP-Referer'] = 'http://127.0.0.1:3000';
25
23
  headers['X-Title'] = 'Agent Mesh Gateway';
26
24
  }
27
25
  const url = `${base_url.replace(/\/$/, '')}/chat/completions`;
28
- // 熔断器检查
29
26
  if (!circuitBreakerRegistry.canRequest(providerName)) {
30
27
  throw new Error(`Circuit breaker open for ${providerName}`);
31
28
  }
32
29
  try {
33
30
  const resp = await withRetry(providerName, async () => {
34
31
  const r = await fetch(url, {
35
- method: 'POST',
36
- headers,
37
- body: JSON.stringify(body),
32
+ method: 'POST', headers, body: JSON.stringify(body),
38
33
  signal: AbortSignal.timeout(120_000),
39
34
  });
40
35
  return r;
@@ -42,7 +37,6 @@ export async function callChatCompletions(provider, request) {
42
37
  console.warn(`[Retry] ${providerName} attempt ${attempt} after ${status} — retrying in ${delayMs}ms`);
43
38
  });
44
39
  if (!resp.ok && isRetryable(resp.status)) {
45
- // Retry logic already handled in withRetry, but if we get here after max retries:
46
40
  circuitBreakerRegistry.recordFailure(providerName);
47
41
  }
48
42
  else if (resp.ok) {
@@ -59,37 +53,236 @@ export async function callChatCompletions(provider, request) {
59
53
  }
60
54
  }
61
55
  export async function callResponsesApi(provider, body) {
62
- // Responses API Chat Completions 转换
63
- const messages = convertResponsesInputToMessages(body.input || []);
56
+ const messages = convertInputToMessages(body.input || []);
64
57
  if (body.instructions) {
65
58
  messages.unshift({ role: 'system', content: body.instructions });
66
59
  }
67
- return callChatCompletions(provider, {
60
+ // 转换 tools 定义(Codex 的 tool schema → OpenAI format)
61
+ const tools = convertToolSchemas(body.tools);
62
+ const chatResp = await callChatCompletions(provider, {
68
63
  model: body.model,
69
64
  messages,
70
65
  stream: body.stream,
71
- tools: body.tools,
66
+ tools,
67
+ tool_choice: body.tool_choice,
68
+ });
69
+ // 非流式:直接转换响应
70
+ if (!body.stream) {
71
+ const ccData = (await chatResp.json());
72
+ return new Response(JSON.stringify(convertChatToResponses(ccData)), {
73
+ status: 200,
74
+ headers: { 'Content-Type': 'application/json' },
75
+ });
76
+ }
77
+ // 流式:解析 SSE → 转换 → 重新打包 SSE
78
+ const transformed = transformSSEStream(chatResp.body);
79
+ return new Response(transformed, {
80
+ status: 200,
81
+ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
72
82
  });
73
83
  }
74
- function convertResponsesInputToMessages(input) {
84
+ function convertInputToMessages(input) {
75
85
  const messages = [];
76
86
  for (const item of input) {
77
- if (item.role === 'system') {
78
- messages.push({ role: 'system', content: extractTextContent(item.content) });
79
- }
80
- else if (item.role === 'user') {
81
- messages.push({ role: 'user', content: extractTextContent(item.content) });
82
- }
83
- else if (item.role === 'assistant') {
84
- messages.push({ role: 'assistant', content: extractTextContent(item.content) });
85
- }
86
- else if (item.type === 'message') {
87
- const role = item.role || 'user';
88
- messages.push({ role, content: extractTextContent(item.content) });
87
+ switch (item.type || item.role) {
88
+ // 标准消息类型
89
+ case 'message':
90
+ messages.push({ role: item.role || 'user', content: extractTextContent(item.content) });
91
+ break;
92
+ case 'function_call': {
93
+ messages.push({
94
+ role: 'assistant',
95
+ content: null,
96
+ tool_calls: [{
97
+ id: item.call_id,
98
+ type: 'function',
99
+ function: {
100
+ name: item.name,
101
+ arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
102
+ },
103
+ }],
104
+ });
105
+ break;
106
+ }
107
+ case 'function_call_output':
108
+ messages.push({
109
+ role: 'tool',
110
+ tool_call_id: item.call_id,
111
+ content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output),
112
+ });
113
+ break;
114
+ case 'system':
115
+ messages.push({ role: 'system', content: extractTextContent(item.content) });
116
+ break;
117
+ case 'user':
118
+ messages.push({ role: 'user', content: extractTextContent(item.content) });
119
+ break;
120
+ case 'assistant':
121
+ messages.push({ role: 'assistant', content: extractTextContent(item.content) });
122
+ break;
123
+ default:
124
+ if (item.role) {
125
+ messages.push({ role: item.role, content: extractTextContent(item.content) });
126
+ }
89
127
  }
90
128
  }
91
129
  return messages;
92
130
  }
131
+ function convertChatToResponses(ccData) {
132
+ const choice = ccData.choices?.[0];
133
+ if (!choice) {
134
+ return { id: ccData.id, object: 'response', model: ccData.model, output: [], usage: ccData.usage };
135
+ }
136
+ const output = [];
137
+ if (choice.message?.content) {
138
+ output.push({
139
+ type: 'message',
140
+ role: 'assistant',
141
+ content: [{ type: 'output_text', text: choice.message.content }],
142
+ });
143
+ }
144
+ if (choice.message?.tool_calls) {
145
+ for (const tc of choice.message.tool_calls) {
146
+ output.push({
147
+ type: 'function_call',
148
+ call_id: tc.id,
149
+ name: tc.function?.name,
150
+ arguments: tc.function?.arguments,
151
+ });
152
+ }
153
+ }
154
+ // finish_reason = 'tool_calls' 表示等待 tool 结果(Codex 需要此信息)
155
+ const status = choice.finish_reason === 'tool_calls' ? 'requires_action' : 'completed';
156
+ return {
157
+ id: ccData.id,
158
+ object: 'response',
159
+ model: ccData.model,
160
+ status,
161
+ output,
162
+ usage: ccData.usage,
163
+ };
164
+ }
165
+ const EVENT_DELTA = 'response.output_text.delta';
166
+ const EVENT_TOOL_DELTA = 'response.function_call_arguments.delta';
167
+ const EVENT_COMPLETED = 'response.completed';
168
+ const sseEncoder = new TextEncoder();
169
+ function transformSSEStream(upstreamBody) {
170
+ let buffer = '';
171
+ let responseId = '';
172
+ let modelName = '';
173
+ let contentBuffer = '';
174
+ const toolCallAccum = {};
175
+ return new ReadableStream({
176
+ async start(controller) {
177
+ const reader = upstreamBody.getReader();
178
+ const decoder = new TextDecoder();
179
+ try {
180
+ while (true) {
181
+ const { done, value } = await reader.read();
182
+ if (done)
183
+ break;
184
+ buffer += decoder.decode(value, { stream: true });
185
+ let newlineIdx;
186
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
187
+ const line = buffer.slice(0, newlineIdx);
188
+ buffer = buffer.slice(newlineIdx + 1);
189
+ if (!line.startsWith('data: '))
190
+ continue;
191
+ const data = line.slice(6).trim();
192
+ if (data === '[DONE]') {
193
+ const finalEvt = buildResponseEvent(responseId, modelName, contentBuffer, toolCallAccum, true);
194
+ controller.enqueue(sseEncoder.encode(finalEvt));
195
+ controller.enqueue(sseEncoder.encode('data: [DONE]\n\n'));
196
+ continue;
197
+ }
198
+ try {
199
+ const chunk = JSON.parse(data);
200
+ responseId = chunk.id || responseId;
201
+ modelName = chunk.model || modelName;
202
+ const delta = chunk.choices?.[0]?.delta;
203
+ if (!delta)
204
+ continue;
205
+ if (delta.content) {
206
+ contentBuffer += delta.content;
207
+ const evt = `data: ${JSON.stringify({
208
+ type: EVENT_DELTA,
209
+ delta: delta.content,
210
+ })}\n\n`;
211
+ controller.enqueue(sseEncoder.encode(evt));
212
+ }
213
+ if (delta.tool_calls) {
214
+ for (const tc of delta.tool_calls) {
215
+ if (!toolCallAccum[tc.index]) {
216
+ toolCallAccum[tc.index] = {
217
+ id: tc.id || '',
218
+ type: 'function',
219
+ function: { name: tc.function?.name || '', arguments: '' },
220
+ };
221
+ }
222
+ if (tc.function?.arguments) {
223
+ toolCallAccum[tc.index].function.arguments += tc.function.arguments;
224
+ }
225
+ if (tc.id)
226
+ toolCallAccum[tc.index].id = tc.id;
227
+ if (tc.function?.name)
228
+ toolCallAccum[tc.index].function.name = tc.function.name;
229
+ }
230
+ // 发送 tool_call delta 事件
231
+ const tcEvt = `data: ${JSON.stringify({
232
+ type: EVENT_TOOL_DELTA,
233
+ tool_calls: Object.values(toolCallAccum),
234
+ })}\n\n`;
235
+ controller.enqueue(sseEncoder.encode(tcEvt));
236
+ }
237
+ }
238
+ catch {
239
+ // 非 JSON 行直接透传
240
+ controller.enqueue(sseEncoder.encode(line + '\n'));
241
+ }
242
+ }
243
+ }
244
+ }
245
+ catch (err) {
246
+ controller.error(err);
247
+ }
248
+ finally {
249
+ reader.releaseLock();
250
+ controller.close();
251
+ }
252
+ },
253
+ });
254
+ }
255
+ function buildResponseEvent(id, model, content, toolCalls, isFinal) {
256
+ const output = [];
257
+ if (content) {
258
+ output.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: content }] });
259
+ }
260
+ for (const tc of Object.values(toolCalls)) {
261
+ output.push({
262
+ type: 'function_call',
263
+ call_id: tc.id,
264
+ name: tc.function.name,
265
+ arguments: tc.function.arguments,
266
+ });
267
+ }
268
+ return `data: ${JSON.stringify({
269
+ type: isFinal ? EVENT_COMPLETED : EVENT_DELTA,
270
+ response: isFinal ? { id, model, object: 'response', status: 'completed', output } : undefined,
271
+ delta: isFinal ? undefined : content,
272
+ })}\n\n`;
273
+ }
274
+ function convertToolSchemas(tools) {
275
+ if (!tools || !Array.isArray(tools))
276
+ return undefined;
277
+ return tools.map(t => ({
278
+ type: 'function',
279
+ function: {
280
+ name: t.name,
281
+ description: t.description,
282
+ parameters: t.parameters || t.input_schema,
283
+ },
284
+ }));
285
+ }
93
286
  function extractTextContent(content) {
94
287
  if (typeof content === 'string')
95
288
  return content;
@@ -101,13 +294,3 @@ function extractTextContent(content) {
101
294
  }
102
295
  return String(content || '');
103
296
  }
104
- export function buildStreamingResponse(upstreamResp) {
105
- return new Response(upstreamResp.body, {
106
- status: upstreamResp.status,
107
- headers: {
108
- 'Content-Type': 'text/event-stream',
109
- 'Cache-Control': 'no-cache',
110
- Connection: 'keep-alive',
111
- },
112
- });
113
- }
@@ -1,7 +1,7 @@
1
1
  import { isProviderAvailable } from './quota.js';
2
2
  import { circuitBreakerRegistry } from './circuit-breaker.js';
3
- // 模型名重映射:对外模型名 → 实际 Provider 的模型名
4
- const MODEL_ALIASES = {
3
+ // 模型名重映射:对外模型名 → 实际 Provider 的模型名(可从 config 覆盖)
4
+ let modelAliases = {
5
5
  deepseek: {
6
6
  'gpt-5.3-codex': 'deepseek-v4-pro',
7
7
  'gpt-5.4': 'deepseek-v4-pro',
@@ -13,6 +13,26 @@ const MODEL_ALIASES = {
13
13
  let config;
14
14
  export function initModelRouter(cfg) {
15
15
  config = cfg;
16
+ // 从 config 加载模型别名(覆盖默认)
17
+ if (cfg.model_aliases) {
18
+ // cfg.model_aliases: { "gpt-x": "deepseek-v4-pro", "claude-y": "claude-opus" }
19
+ // 按目标模型名反查对应的 provider → 写入对应 provider 的别名表
20
+ for (const [aliasKey, realModel] of Object.entries(cfg.model_aliases)) {
21
+ for (const [providerName, providerCfg] of Object.entries(config.providers)) {
22
+ const providerModels = providerCfg.models || [];
23
+ if (providerModels.includes(realModel)) {
24
+ modelAliases[providerName] = modelAliases[providerName] || {};
25
+ modelAliases[providerName][aliasKey] = realModel;
26
+ break;
27
+ }
28
+ }
29
+ // 兜底:如果没匹配到任何 provider,放入 deepseek
30
+ modelAliases.deepseek = modelAliases.deepseek || {};
31
+ if (!modelAliases.deepseek[aliasKey]) {
32
+ modelAliases.deepseek[aliasKey] = realModel;
33
+ }
34
+ }
35
+ }
16
36
  }
17
37
  export function getConfig() {
18
38
  return config;
@@ -24,7 +44,7 @@ export function resolveProvider(model) {
24
44
  // 1. 按 model_routing 配置查找
25
45
  const routingEntries = Object.entries(config.model_routing);
26
46
  for (const [pattern, providers] of routingEntries) {
27
- if (model.includes(pattern)) {
47
+ if (model.startsWith(pattern)) {
28
48
  for (const providerName of providers) {
29
49
  const providerCfg = config.providers[providerName];
30
50
  if (!providerCfg)
@@ -79,7 +99,7 @@ export function resolveProvider(model) {
79
99
  return null;
80
100
  }
81
101
  export function remapModel(model, providerName) {
82
- return MODEL_ALIASES[providerName]?.[model] || model;
102
+ return modelAliases[providerName]?.[model] || model;
83
103
  }
84
104
  function resolveApiKey(_name, providerCfg) {
85
105
  if (providerCfg.api_key && providerCfg.api_key !== '') {
@@ -4,6 +4,14 @@ import { getQuotaSummary, probeQuota } from './quota.js';
4
4
  import { circuitBreakerRegistry } from './circuit-breaker.js';
5
5
  import { checkAllProviders } from './health.js';
6
6
  import { getMetrics, recordRequest } from './metrics.js';
7
+ import { checkRateLimit } from './rate-limit.js';
8
+ function logReq(originalModel, providerName, actualModel, reqStart, status, streaming, error) {
9
+ recordRequest({
10
+ timestamp: Date.now(), model: originalModel,
11
+ provider: providerName, actualModel, latencyMs: Date.now() - reqStart,
12
+ status, streaming, error,
13
+ });
14
+ }
7
15
  export async function modelGatewayRoutes(fastify) {
8
16
  // 健康检查 + 配额总览
9
17
  fastify.get('/model-gateway/health', async (_req, _reply) => {
@@ -41,6 +49,11 @@ export async function modelGatewayRoutes(fastify) {
41
49
  });
42
50
  // POST /v1/chat/completions — 标准 OpenAI 兼容端点
43
51
  fastify.post('/v1/chat/completions', async (request, reply) => {
52
+ const ip = request.ip || '127.0.0.1';
53
+ const rl = checkRateLimit('/v1/chat/completions', ip);
54
+ if (!rl.allowed) {
55
+ return reply.code(429).header('Retry-After', String(rl.resetSeconds)).header('X-RateLimit-Limit', rl.limit).header('X-RateLimit-Remaining', '0').send({ error: { message: 'Rate limit exceeded' } });
56
+ }
44
57
  const body = request.body;
45
58
  if (!body || !body.messages) {
46
59
  return reply.code(400).send({ error: { message: 'messages is required' } });
@@ -65,16 +78,16 @@ export async function modelGatewayRoutes(fastify) {
65
78
  tools: body.tools,
66
79
  tool_choice: body.tool_choice,
67
80
  });
68
- if (!upstreamResp.ok && upstreamResp.status !== 200) {
81
+ if (!upstreamResp.ok) {
69
82
  const errText = await upstreamResp.text();
70
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: upstreamResp.status, error: errText.slice(0, 200), streaming: !!body.stream });
83
+ logReq(originalModel, provider.name, model, reqStart, upstreamResp.status, !!body.stream, errText.slice(0, 200));
71
84
  console.error(`[ModelGW] ${provider.name} error ${upstreamResp.status}: ${errText.slice(0, 200)}`);
72
85
  return reply.code(upstreamResp.status).send({
73
86
  error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
74
87
  });
75
88
  }
76
89
  if (body.stream) {
77
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 200, streaming: true });
90
+ logReq(originalModel, provider.name, model, reqStart, 200, true);
78
91
  return reply.headers({
79
92
  'Content-Type': 'text/event-stream',
80
93
  'Cache-Control': 'no-cache',
@@ -82,11 +95,11 @@ export async function modelGatewayRoutes(fastify) {
82
95
  }).send(upstreamResp.body);
83
96
  }
84
97
  const data = await upstreamResp.json();
85
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 200, streaming: false });
98
+ logReq(originalModel, provider.name, model, reqStart, 200, false);
86
99
  reply.send(data);
87
100
  }
88
101
  catch (err) {
89
- recordRequest({ timestamp: Date.now(), model: originalModel, provider: provider.name, actualModel: model, latencyMs: Date.now() - reqStart, status: 502, error: err.message, streaming: !!body.stream });
102
+ logReq(originalModel, provider.name, model, reqStart, 502, !!body.stream, err.message);
90
103
  console.error(`[ModelGW] Error calling ${provider.name}:`, err.message);
91
104
  reply.code(502).send({
92
105
  error: { message: `Provider error: ${err.message}` },
@@ -95,6 +108,11 @@ export async function modelGatewayRoutes(fastify) {
95
108
  });
96
109
  // POST /v1/responses — Codex Desktop Responses API 适配
97
110
  fastify.post('/v1/responses', async (request, reply) => {
111
+ const ip = request.ip || '127.0.0.1';
112
+ const rl = checkRateLimit('/v1/responses', ip);
113
+ if (!rl.allowed) {
114
+ return reply.code(429).header('Retry-After', String(rl.resetSeconds)).send({ error: { message: 'Rate limit exceeded' } });
115
+ }
98
116
  const body = request.body;
99
117
  if (!body || !body.input) {
100
118
  return reply.code(400).send({ error: { message: 'input is required' } });
@@ -109,44 +127,28 @@ export async function modelGatewayRoutes(fastify) {
109
127
  const model = remapModel(originalModel, provider.name);
110
128
  body.model = model;
111
129
  console.log(`[ModelGW:Responses] ${originalModel} → ${provider.name}/${model}`);
130
+ const reqStart = Date.now();
112
131
  try {
113
132
  const upstreamResp = await callResponsesApi(provider, body);
114
133
  if (!upstreamResp.ok) {
115
134
  const errText = await upstreamResp.text();
135
+ logReq(originalModel, provider.name, model, reqStart, upstreamResp.status, !!body.stream, errText.slice(0, 200));
116
136
  return reply.code(upstreamResp.status).send({
117
137
  error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
118
138
  });
119
139
  }
140
+ logReq(originalModel, provider.name, model, reqStart, 200, !!body.stream);
120
141
  if (body.stream) {
121
142
  return reply.headers({
122
143
  'Content-Type': 'text/event-stream',
123
144
  'Cache-Control': 'no-cache',
124
145
  }).send(upstreamResp.body);
125
146
  }
126
- // Chat Completions 响应转回 Responses API 格式
127
- const ccData = (await upstreamResp.json());
128
- const choice = ccData.choices?.[0];
129
- const responsesData = {
130
- id: ccData.id,
131
- object: 'response',
132
- model: ccData.model,
133
- output: [
134
- {
135
- type: 'message',
136
- role: 'assistant',
137
- content: [
138
- {
139
- type: 'output_text',
140
- text: choice?.message?.content || '',
141
- },
142
- ],
143
- },
144
- ],
145
- usage: ccData.usage,
146
- };
147
- reply.send(responsesData);
147
+ const data = await upstreamResp.json();
148
+ reply.send(data);
148
149
  }
149
150
  catch (err) {
151
+ logReq(originalModel, provider.name, model, reqStart, 502, !!body.stream, err.message);
150
152
  console.error(`[ModelGW:Responses] Error:`, err.message);
151
153
  reply.code(502).send({
152
154
  error: { message: `Provider error: ${err.message}` },
@@ -9,6 +9,7 @@ export interface ModelGatewayConfig {
9
9
  providers: Record<string, ModelProviderConfig>;
10
10
  fallback_chain: string[];
11
11
  model_routing: Record<string, string[]>;
12
+ model_aliases?: Record<string, string>;
12
13
  }
13
14
  export interface ResolvedProvider {
14
15
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlink-awaken/agentmesh",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Unified Agent Gateway - Multi-Agent Scheduler and Router",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",