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.
@@ -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 || 30000;
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
  }