tiger-agent 0.2.4 → 0.3.1
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/.env.example +10 -0
- package/README.md +264 -4
- package/package.json +1 -1
- package/src/agent/contextFileMirrors.js +39 -0
- package/src/agent/contextFiles.js +4 -1
- package/src/agent/mainAgent.js +4 -3
- package/src/agent/reflectionAgent.js +3 -2
- package/src/agent/skills.js +1 -1
- package/src/apiProviders.js +8 -9
- package/src/apiProviders.js.bak +222 -0
- package/src/cli.js +4 -0
- package/src/config.js +11 -0
- package/src/llmClient.js +11 -1
- package/src/swarm/agentRuntime.js +699 -0
- package/src/swarm/agentRuntime.js.bak +456 -0
- package/src/swarm/configStore.js +360 -0
- package/src/swarm/index.js +25 -0
- package/src/swarm/taskBus.js +246 -0
- package/src/telegram/bot.js +329 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// ─── Zhipu AI (BigModel) JWT auth ──────────────────────────────────────────
|
|
6
|
+
// Their v4 API requires HS256 JWT derived from the api-key (format: "id.secret")
|
|
7
|
+
function b64url(buf) {
|
|
8
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function zhipuJwt(apiKey) {
|
|
12
|
+
const dot = apiKey.indexOf('.');
|
|
13
|
+
if (dot === -1) return apiKey; // fallback: treat as plain token
|
|
14
|
+
const id = apiKey.slice(0, dot);
|
|
15
|
+
const secret = apiKey.slice(dot + 1);
|
|
16
|
+
const now = Math.floor(Date.now() / 1000);
|
|
17
|
+
const hdr = b64url(Buffer.from(JSON.stringify({ alg: 'HS256', sign_type: 'SIGN' })));
|
|
18
|
+
const pay = b64url(Buffer.from(JSON.stringify({ api_key: id, exp: now + 3600, timestamp: now })));
|
|
19
|
+
const sig = b64url(crypto.createHmac('sha256', secret).update(`${hdr}.${pay}`).digest());
|
|
20
|
+
return `${hdr}.${pay}.${sig}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── OpenAI-compatible adapters ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function standardFormat(messages, options) {
|
|
26
|
+
const payload = {
|
|
27
|
+
model: options.model,
|
|
28
|
+
messages,
|
|
29
|
+
temperature: options.temperature ?? 0.3
|
|
30
|
+
};
|
|
31
|
+
if (options.tools && options.tools.length) payload.tools = options.tools;
|
|
32
|
+
if (options.tool_choice) payload.tool_choice = options.tool_choice;
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function standardParse(data) {
|
|
37
|
+
const message = data.choices?.[0]?.message || {};
|
|
38
|
+
const u = data.usage || {};
|
|
39
|
+
const tokens = (u.prompt_tokens || 0) + (u.completion_tokens || 0);
|
|
40
|
+
return { message, tokens };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Claude (Anthropic) adapters ───────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function claudeFormat(messages, options) {
|
|
46
|
+
const systemMsg = messages.find((m) => m.role === 'system');
|
|
47
|
+
const rest = messages.filter((m) => m.role !== 'system');
|
|
48
|
+
|
|
49
|
+
// Convert tool definitions: OpenAI → Claude
|
|
50
|
+
let tools;
|
|
51
|
+
if (options.tools && options.tools.length) {
|
|
52
|
+
tools = options.tools.map((t) => ({
|
|
53
|
+
name: t.function.name,
|
|
54
|
+
description: t.function.description || '',
|
|
55
|
+
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert message content: tool_calls & tool results
|
|
60
|
+
const converted = rest.map((m) => {
|
|
61
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
62
|
+
const content = [];
|
|
63
|
+
if (m.content) content.push({ type: 'text', text: m.content });
|
|
64
|
+
for (const tc of m.tool_calls) {
|
|
65
|
+
let input = {};
|
|
66
|
+
try { input = JSON.parse(tc.function.arguments || '{}'); } catch (_) {}
|
|
67
|
+
content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input });
|
|
68
|
+
}
|
|
69
|
+
return { role: 'assistant', content };
|
|
70
|
+
}
|
|
71
|
+
if (m.role === 'tool') {
|
|
72
|
+
return {
|
|
73
|
+
role: 'user',
|
|
74
|
+
content: [{ type: 'tool_result', tool_use_id: m.tool_call_id, content: String(m.content || '') }]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return m;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const payload = {
|
|
81
|
+
model: options.model,
|
|
82
|
+
max_tokens: options.max_tokens || 8192,
|
|
83
|
+
messages: converted
|
|
84
|
+
};
|
|
85
|
+
if (systemMsg) payload.system = systemMsg.content;
|
|
86
|
+
if (tools && tools.length) {
|
|
87
|
+
payload.tools = tools;
|
|
88
|
+
// Convert OpenAI tool_choice format → Claude format
|
|
89
|
+
const tc = options.tool_choice;
|
|
90
|
+
if (tc && tc !== 'none') {
|
|
91
|
+
if (tc === 'auto' || tc === 'required') {
|
|
92
|
+
payload.tool_choice = { type: tc === 'required' ? 'any' : 'auto' };
|
|
93
|
+
} else if (tc && typeof tc === 'object' && tc.type === 'function') {
|
|
94
|
+
payload.tool_choice = { type: 'tool', name: tc.function.name };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return payload;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function claudeParse(data) {
|
|
102
|
+
const content = Array.isArray(data.content) ? data.content : [];
|
|
103
|
+
const textBlock = content.find((b) => b.type === 'text');
|
|
104
|
+
const toolUseBlocks = content.filter((b) => b.type === 'tool_use');
|
|
105
|
+
|
|
106
|
+
const message = { role: 'assistant', content: textBlock ? textBlock.text : '' };
|
|
107
|
+
if (toolUseBlocks.length) {
|
|
108
|
+
message.tool_calls = toolUseBlocks.map((tb) => ({
|
|
109
|
+
id: tb.id,
|
|
110
|
+
type: 'function',
|
|
111
|
+
function: { name: tb.name, arguments: JSON.stringify(tb.input || {}) }
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const u = data.usage || {};
|
|
116
|
+
const tokens = (u.input_tokens || 0) + (u.output_tokens || 0);
|
|
117
|
+
return { message, tokens };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Provider registry ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function buildProviders(env) {
|
|
123
|
+
return {
|
|
124
|
+
kimi: {
|
|
125
|
+
id: 'kimi',
|
|
126
|
+
name: 'Kimi Code',
|
|
127
|
+
baseUrl: (env.KIMI_BASE_URL || 'https://api.kimi.com/coding/v1').replace(/\/$/, ''),
|
|
128
|
+
chatModel: env.KIMI_CHAT_MODEL ? env.KIMI_CHAT_MODEL.replace(/^kimi-coding\//, '') : 'k2p5',
|
|
129
|
+
embedModel: env.KIMI_EMBED_MODEL || '',
|
|
130
|
+
apiKey: env.KIMI_CODE_API_KEY || env.KIMI_API_KEY || '',
|
|
131
|
+
userAgent: env.KIMI_USER_AGENT || 'KimiCLI/0.77',
|
|
132
|
+
authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
133
|
+
chatPath: '/chat/completions',
|
|
134
|
+
embedPath: '/embeddings',
|
|
135
|
+
formatRequest: standardFormat,
|
|
136
|
+
parseResponse: standardParse,
|
|
137
|
+
timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
moonshot: {
|
|
141
|
+
id: 'moonshot',
|
|
142
|
+
name: 'Kimi Moonshot',
|
|
143
|
+
baseUrl: (env.MOONSHOT_BASE_URL || 'https://api.moonshot.cn/v1').replace(/\/$/, ''),
|
|
144
|
+
chatModel: env.MOONSHOT_MODEL || 'kimi-k1',
|
|
145
|
+
embedModel: env.MOONSHOT_EMBED_MODEL || 'kimi-embedding-v1',
|
|
146
|
+
apiKey: env.MOONSHOT_API_KEY || env.KIMI_API_KEY || '',
|
|
147
|
+
userAgent: '',
|
|
148
|
+
authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
149
|
+
chatPath: '/chat/completions',
|
|
150
|
+
embedPath: '/embeddings',
|
|
151
|
+
formatRequest: standardFormat,
|
|
152
|
+
parseResponse: standardParse,
|
|
153
|
+
timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
zai: {
|
|
157
|
+
id: 'zai',
|
|
158
|
+
name: 'Z.ai',
|
|
159
|
+
baseUrl: (env.ZAI_BASE_URL || 'https://api.z.ai/api/coding/paas/v4').replace(/\/$/, ''),
|
|
160
|
+
chatModel: env.ZAI_MODEL || 'glm-4.7',
|
|
161
|
+
embedModel: env.ZAI_EMBED_MODEL || '',
|
|
162
|
+
apiKey: env.ZAI_API_KEY || '',
|
|
163
|
+
userAgent: '',
|
|
164
|
+
// api.z.ai uses plain Bearer; old bigmodel.cn used Zhipu JWT
|
|
165
|
+
authHeaders: (key) => {
|
|
166
|
+
const baseUrl = (env.ZAI_BASE_URL || '').toLowerCase();
|
|
167
|
+
if (baseUrl.includes('bigmodel.cn')) return { Authorization: `Bearer ${zhipuJwt(key)}` };
|
|
168
|
+
return { Authorization: `Bearer ${key}` };
|
|
169
|
+
},
|
|
170
|
+
chatPath: '/chat/completions',
|
|
171
|
+
embedPath: '/embeddings',
|
|
172
|
+
formatRequest: standardFormat,
|
|
173
|
+
parseResponse: standardParse,
|
|
174
|
+
timeout: Number(env.ZAI_TIMEOUT_MS || 30000)
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
minimax: {
|
|
178
|
+
id: 'minimax',
|
|
179
|
+
name: 'MiniMax',
|
|
180
|
+
baseUrl: (env.MINIMAX_BASE_URL || 'https://api.minimax.chat/v1').replace(/\/$/, ''),
|
|
181
|
+
chatModel: env.MINIMAX_MODEL || 'abab6.5s-chat',
|
|
182
|
+
embedModel: env.MINIMAX_EMBED_MODEL || '',
|
|
183
|
+
apiKey: env.MINIMAX_API_KEY || '',
|
|
184
|
+
userAgent: '',
|
|
185
|
+
authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
186
|
+
chatPath: '/chat/completions',
|
|
187
|
+
embedPath: '/embeddings',
|
|
188
|
+
formatRequest: standardFormat,
|
|
189
|
+
parseResponse: standardParse,
|
|
190
|
+
timeout: Number(env.MINIMAX_TIMEOUT_MS || 30000)
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
claude: {
|
|
194
|
+
id: 'claude',
|
|
195
|
+
name: 'Claude (Anthropic)',
|
|
196
|
+
baseUrl: (env.CLAUDE_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, ''),
|
|
197
|
+
chatModel: env.CLAUDE_MODEL || 'claude-sonnet-4-6',
|
|
198
|
+
embedModel: '',
|
|
199
|
+
apiKey: env.CLAUDE_API_KEY || env.ANTHROPIC_API_KEY || '',
|
|
200
|
+
userAgent: '',
|
|
201
|
+
authHeaders: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
|
|
202
|
+
chatPath: '/v1/messages',
|
|
203
|
+
embedPath: null, // Claude does not expose an embeddings endpoint
|
|
204
|
+
formatRequest: claudeFormat,
|
|
205
|
+
parseResponse: claudeParse,
|
|
206
|
+
timeout: Number(env.CLAUDE_TIMEOUT_MS || 60000)
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Singleton — providers are built once from process.env on first access
|
|
212
|
+
let _providers = null;
|
|
213
|
+
function getProviders() {
|
|
214
|
+
if (!_providers) _providers = buildProviders(process.env);
|
|
215
|
+
return _providers;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getProvider(id) {
|
|
219
|
+
return getProviders()[id] || null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { getProviders, getProvider };
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const { startReflectionScheduler } = require('./agent/reflectionScheduler');
|
|
|
9
9
|
const { initVectorMemory } = require('./agent/db');
|
|
10
10
|
const { startTelegramBot } = require('./telegram/bot');
|
|
11
11
|
const { handleMessage } = require('./agent/mainAgent');
|
|
12
|
+
const { ensureSwarmLayout } = require('./swarm');
|
|
12
13
|
|
|
13
14
|
// Source root — always inside the npm package
|
|
14
15
|
const srcRoot = path.resolve(__dirname, '..');
|
|
@@ -51,6 +52,7 @@ function getExistingSupervisorPid() {
|
|
|
51
52
|
|
|
52
53
|
function startTelegramInBackground() {
|
|
53
54
|
ensureContextFiles();
|
|
55
|
+
ensureSwarmLayout();
|
|
54
56
|
const existingPid = getExistingSupervisorPid();
|
|
55
57
|
if (existingPid && isPidRunning(existingPid)) {
|
|
56
58
|
process.stdout.write(`Telegram background bot is already running (PID ${existingPid}).\n`);
|
|
@@ -113,6 +115,7 @@ function printVectorMemoryStatus(vectorStatus) {
|
|
|
113
115
|
|
|
114
116
|
async function runCli() {
|
|
115
117
|
ensureContextFiles();
|
|
118
|
+
ensureSwarmLayout();
|
|
116
119
|
startReflectionScheduler();
|
|
117
120
|
const vectorStatus = initVectorMemory();
|
|
118
121
|
printVectorMemoryStatus(vectorStatus);
|
|
@@ -171,6 +174,7 @@ async function main() {
|
|
|
171
174
|
|
|
172
175
|
if (isTelegramMode(argv)) {
|
|
173
176
|
ensureContextFiles();
|
|
177
|
+
ensureSwarmLayout();
|
|
174
178
|
startReflectionScheduler();
|
|
175
179
|
const vectorStatus = initVectorMemory();
|
|
176
180
|
printVectorMemoryStatus(vectorStatus);
|
package/src/config.js
CHANGED
|
@@ -101,6 +101,12 @@ const vectorDbPath = path.resolve(process.env.VECTOR_DB_PATH || './db/memory.sql
|
|
|
101
101
|
const sqliteVecExtension = cleanEnvValue(process.env.SQLITE_VEC_EXTENSION || '');
|
|
102
102
|
const memoryIngestEveryTurns = Math.max(1, Number(process.env.MEMORY_INGEST_EVERY_TURNS || 2));
|
|
103
103
|
const memoryIngestMinChars = Math.max(20, Number(process.env.MEMORY_INGEST_MIN_CHARS || 140));
|
|
104
|
+
const swarmAgentTimeoutMs = Math.max(0, Number(process.env.SWARM_AGENT_TIMEOUT_MS || 0));
|
|
105
|
+
const swarmRouteOnProviderError =
|
|
106
|
+
['1', 'true', 'yes', 'on'].includes(cleanEnvValue(process.env.SWARM_ROUTE_ON_PROVIDER_ERROR || '').toLowerCase());
|
|
107
|
+
const swarmDefaultFlow = cleanEnvValue(process.env.SWARM_DEFAULT_FLOW || 'auto').toLowerCase() || 'auto';
|
|
108
|
+
const swarmFirstAgentPolicy = cleanEnvValue(process.env.SWARM_FIRST_AGENT_POLICY || 'auto').toLowerCase() || 'auto';
|
|
109
|
+
const swarmFirstAgent = cleanEnvValue(process.env.SWARM_FIRST_AGENT || '').toLowerCase();
|
|
104
110
|
|
|
105
111
|
module.exports = {
|
|
106
112
|
kimiProvider,
|
|
@@ -127,6 +133,11 @@ module.exports = {
|
|
|
127
133
|
sqliteVecExtension,
|
|
128
134
|
memoryIngestEveryTurns,
|
|
129
135
|
memoryIngestMinChars,
|
|
136
|
+
swarmAgentTimeoutMs,
|
|
137
|
+
swarmRouteOnProviderError,
|
|
138
|
+
swarmDefaultFlow,
|
|
139
|
+
swarmFirstAgentPolicy,
|
|
140
|
+
swarmFirstAgent,
|
|
130
141
|
dbPath: path.resolve(process.env.DB_PATH || './db/agent.json'),
|
|
131
142
|
maxMessages: Number(process.env.MAX_MESSAGES || 200),
|
|
132
143
|
recentMessages: Number(process.env.RECENT_MESSAGES || 40)
|
package/src/llmClient.js
CHANGED
|
@@ -38,7 +38,7 @@ async function fetchProvider(provider, endpoint, body, maxRetries = 3) {
|
|
|
38
38
|
await sleep(delay);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const timeout = provider.timeout ||
|
|
41
|
+
const timeout = provider.timeout || Number(process.env.SWARM_AGENT_TIMEOUT_MS || 180000);
|
|
42
42
|
const ctrl = new AbortController();
|
|
43
43
|
const timer = setTimeout(() => ctrl.abort(), timeout);
|
|
44
44
|
|
|
@@ -79,6 +79,7 @@ async function chatCompletion(messages, options = {}) {
|
|
|
79
79
|
// Build candidate list: active provider first, then fallbacks
|
|
80
80
|
const activeId = tokenManager.getCurrentProvider();
|
|
81
81
|
const candidates = [activeId, ...tokenManager.getNextCandidates(activeId)];
|
|
82
|
+
const fallbackOnAnyProviderError = Boolean(options.fallbackOnAnyProviderError);
|
|
82
83
|
|
|
83
84
|
let firstError = null;
|
|
84
85
|
|
|
@@ -110,6 +111,15 @@ async function chatCompletion(messages, options = {}) {
|
|
|
110
111
|
continue;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// Optional broader failover (used by swarm): timeout/network/API errors can route to next provider.
|
|
115
|
+
if (fallbackOnAnyProviderError) {
|
|
116
|
+
const switched = tokenManager.autoSwitch('provider_error');
|
|
117
|
+
if (switched.switched) {
|
|
118
|
+
process.stderr.write(`[llm] provider_error on ${providerId} → switched to ${switched.to}\n`);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
// Any other error (auth, network, server error) — surface immediately
|
|
114
124
|
throw err;
|
|
115
125
|
}
|