@jellyos/agent 0.1.3 → 0.1.5

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.
Files changed (90) hide show
  1. package/README.md +9 -9
  2. package/README.npm.md +212 -0
  3. package/bin/jellyos-mcp +26 -0
  4. package/dist/api/ExtensionAPI.d.ts +6 -0
  5. package/dist/api/Registry.js +3 -1
  6. package/dist/cli.js +117 -42
  7. package/dist/index.d.ts +24 -1
  8. package/dist/index.js +19 -2
  9. package/dist/mcp/entry.d.ts +2 -0
  10. package/dist/mcp/entry.js +71 -0
  11. package/dist/mcp/server.d.ts +31 -0
  12. package/dist/mcp/server.js +128 -0
  13. package/dist/models/CostTracker.d.ts +66 -0
  14. package/dist/models/CostTracker.js +148 -0
  15. package/dist/models/ModelRegistry.d.ts +157 -0
  16. package/dist/models/ModelRegistry.js +496 -0
  17. package/dist/models/index.d.ts +5 -0
  18. package/dist/models/index.js +3 -0
  19. package/dist/runner/AgentRunner.d.ts +23 -2
  20. package/dist/runner/AgentRunner.js +264 -24
  21. package/dist/runner/ModelClient.d.ts +26 -6
  22. package/dist/runner/ModelClient.js +147 -28
  23. package/dist/runner/SwarmRouter.d.ts +10 -7
  24. package/dist/runner/SwarmRouter.js +85 -28
  25. package/dist/runner/ToolDispatcher.d.ts +10 -0
  26. package/dist/runner/ToolDispatcher.js +106 -2
  27. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  28. package/dist/scheduler/AgentScheduler.js +253 -0
  29. package/dist/session/ContextStore.d.ts +96 -0
  30. package/dist/session/ContextStore.js +207 -0
  31. package/dist/session/GoalManager.d.ts +101 -0
  32. package/dist/session/GoalManager.js +167 -0
  33. package/dist/session/MemoryStore.d.ts +48 -0
  34. package/dist/session/MemoryStore.js +166 -0
  35. package/dist/session/SessionManager.d.ts +45 -4
  36. package/dist/session/SessionManager.js +151 -8
  37. package/dist/telemetry/Tracer.d.ts +48 -0
  38. package/dist/telemetry/Tracer.js +102 -0
  39. package/dist/tests/ContextStore.test.d.ts +2 -0
  40. package/dist/tests/ContextStore.test.js +74 -0
  41. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  42. package/dist/tests/ModelRegistry.test.js +69 -0
  43. package/dist/tests/SessionManager.test.d.ts +2 -0
  44. package/dist/tests/SessionManager.test.js +108 -0
  45. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  46. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  47. package/dist/tools/MarketSentiment.d.ts +166 -0
  48. package/dist/tools/MarketSentiment.js +209 -0
  49. package/dist/tools/NewsSentiment.d.ts +67 -0
  50. package/dist/tools/NewsSentiment.js +226 -0
  51. package/dist/tools/PriceFeed.d.ts +105 -0
  52. package/dist/tools/PriceFeed.js +282 -0
  53. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  54. package/dist/tools/TechnicalAnalysis.js +357 -0
  55. package/dist/tools/index.d.ts +7 -0
  56. package/dist/tools/index.js +4 -0
  57. package/dist/tui/App.d.ts +7 -5
  58. package/dist/tui/App.js +350 -65
  59. package/dist/tui/REPL.d.ts +2 -1
  60. package/dist/tui/REPL.js +11 -6
  61. package/dist/tui/StatusBar.js +1 -1
  62. package/package.json +9 -4
  63. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  64. package/dist/api/ExtensionAPI.js.map +0 -1
  65. package/dist/api/Registry.d.ts.map +0 -1
  66. package/dist/api/Registry.js.map +0 -1
  67. package/dist/cli.d.ts.map +0 -1
  68. package/dist/cli.js.map +0 -1
  69. package/dist/index.d.ts.map +0 -1
  70. package/dist/index.js.map +0 -1
  71. package/dist/loader.d.ts.map +0 -1
  72. package/dist/loader.js.map +0 -1
  73. package/dist/runner/AgentRunner.d.ts.map +0 -1
  74. package/dist/runner/AgentRunner.js.map +0 -1
  75. package/dist/runner/ModelClient.d.ts.map +0 -1
  76. package/dist/runner/ModelClient.js.map +0 -1
  77. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  78. package/dist/runner/SwarmRouter.js.map +0 -1
  79. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  80. package/dist/runner/ToolDispatcher.js.map +0 -1
  81. package/dist/session/SessionManager.d.ts.map +0 -1
  82. package/dist/session/SessionManager.js.map +0 -1
  83. package/dist/tui/App.d.ts.map +0 -1
  84. package/dist/tui/App.js.map +0 -1
  85. package/dist/tui/REPL.d.ts.map +0 -1
  86. package/dist/tui/REPL.js.map +0 -1
  87. package/dist/tui/StatusBar.d.ts.map +0 -1
  88. package/dist/tui/StatusBar.js.map +0 -1
  89. package/dist/tui/theme.d.ts.map +0 -1
  90. package/dist/tui/theme.js.map +0 -1
@@ -0,0 +1,496 @@
1
+ /**
2
+ * ModelRegistry — dynamic model discovery, tiering, and routing.
3
+ *
4
+ * Queries OpenRouter's /models endpoint at startup to build a local registry
5
+ * of all available models. Classifies them into tiers (orchestrator / analyst /
6
+ * worker / free), tracks performance, handles deprecation, and exposes
7
+ * model selection as both a tool and a REPL command.
8
+ *
9
+ * Also supports direct provider routing (Anthropic/OpenAI/Google keys) for
10
+ * models that can be reached cheaper outside OpenRouter.
11
+ */
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { Type } from "@sinclair/typebox";
16
+ // ── Tier classification ───────────────────────────────────────────────────────
17
+ /**
18
+ * Tier classification rules (priority-ordered).
19
+ * Models match the first rule whose pattern and context-length requirements
20
+ * are met. Fallback is "worker".
21
+ */
22
+ const TIER_RULES = [
23
+ // ── Orchestrator: top-tier reasoning models (2024-2026) ──────────────────
24
+ // Claude Opus 4.x, GPT-5.x flagship/pro, Gemini 3.x Pro, DeepSeek V4 Pro,
25
+ // Grok 4.x, o3-pro/o4, Qwen3 Max variants, Kimi K2
26
+ {
27
+ tier: "orchestrator",
28
+ pattern: /claude.*opus[-.]?4|gpt-5\.[3-9].*pro|gpt-5\.5(?!-nano|-mini)|o3-pro|o4[-.]|gemini-3\.[0-9]-pro|deepseek-v4-pro|grok[-.]?4\.[0-9]|qwen3.*max(?!-thinking)|qwen3\.6-max|kimi-k2(?!-thinking)/i,
29
+ notFree: true,
30
+ },
31
+ // ── Analyst: strong reasoning, moderate cost (2024-2026) ─────────────────
32
+ // Claude Sonnet 4.x, GPT-5.x mid-tier, Gemini 3.x Flash,
33
+ // DeepSeek V4 (non-pro), Grok 3.x, Qwen3 235B, Mistral Medium 3
34
+ {
35
+ tier: "analyst",
36
+ pattern: /claude.*sonnet[-.]?4|gpt-5\.[0-4](?!.*-pro)|gpt-5\.5-mini|gemini-3\.[0-9]-flash|deepseek-v4(?!-pro)|grok[-.]?3|qwen3-235b|qwen3\.6-(?!max)|mistral-medium-3|claude.*haiku[-.]?4/i,
37
+ notFree: true,
38
+ },
39
+ // ── Free tier: zero-cost models ──────────────────────────────────────────
40
+ { tier: "free", pattern: /:free$|openrouter\/free/i },
41
+ // ── Worker: everything else (default) ────────────────────────────────────
42
+ { tier: "worker", pattern: /.*/ },
43
+ ];
44
+ export function classifyModel(model) {
45
+ for (const rule of TIER_RULES) {
46
+ if (!rule.pattern.test(model.id))
47
+ continue;
48
+ if (rule.notFree && parseFloat(model.pricing.prompt || "0") <= 0)
49
+ continue;
50
+ if (rule.minContext && (model.context_length || 0) < rule.minContext)
51
+ continue;
52
+ if (rule.minTokens && (model.top_provider.max_completion_tokens || 0) < rule.minTokens)
53
+ continue;
54
+ return rule.tier;
55
+ }
56
+ return "worker";
57
+ }
58
+ // ── ModelRegistry class ───────────────────────────────────────────────────────
59
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
60
+ const CACHE_FILE = join(JELLY_HOME, "models.json");
61
+ const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours
62
+ export class ModelRegistry {
63
+ allModels = [];
64
+ tieredPool = { orchestrator: [], analyst: [], worker: [], free: [] };
65
+ modelMap = new Map();
66
+ loaded = false;
67
+ lastFetch = 0;
68
+ // ── Initialisation ────────────────────────────────────────────────────────
69
+ /** Query OpenRouter /models and build the tiered pool. Called once at startup. */
70
+ async initialise(apiKey) {
71
+ const key = apiKey ?? process.env.OPENROUTER_API_KEY;
72
+ if (!key) {
73
+ this.loadFromCache();
74
+ return;
75
+ }
76
+ // Try live fetch
77
+ try {
78
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
79
+ headers: { Authorization: `Bearer ${key}`, "User-Agent": "JellyOS/1.0" },
80
+ signal: AbortSignal.timeout(15_000),
81
+ });
82
+ if (res.ok) {
83
+ const body = await res.json();
84
+ this.allModels = body.data ?? [];
85
+ this.lastFetch = Date.now();
86
+ this.saveCache();
87
+ }
88
+ else {
89
+ this.loadFromCache();
90
+ }
91
+ }
92
+ catch {
93
+ this.loadFromCache();
94
+ }
95
+ this.rebuildTiers();
96
+ this.loaded = true;
97
+ }
98
+ /** Rebuild tier classifications (call after cache load or fetch). */
99
+ rebuildTiers() {
100
+ this.tieredPool = { orchestrator: [], analyst: [], worker: [], free: [] };
101
+ this.modelMap.clear();
102
+ for (const m of this.allModels) {
103
+ const tier = classifyModel(m);
104
+ const cost = parseFloat(m.pricing.prompt || "0") * 1_000_000_000; // nano-dollars per 1K
105
+ const tm = {
106
+ tier, model: m, costPer1K: cost,
107
+ available: true, failures: 0, lastFailure: 0,
108
+ avgLatency: 0, uses: 0,
109
+ };
110
+ this.tieredPool[tier].push(tm);
111
+ this.modelMap.set(m.id, tm);
112
+ }
113
+ // Sort each tier by cost (cheapest first within each tier)
114
+ for (const tier of ["orchestrator", "analyst", "worker", "free"]) {
115
+ this.tieredPool[tier].sort((a, b) => a.costPer1K - b.costPer1K);
116
+ }
117
+ }
118
+ // ── Cache ─────────────────────────────────────────────────────────────────
119
+ saveCache() {
120
+ try {
121
+ mkdirSync(JELLY_HOME, { recursive: true });
122
+ writeFileSync(CACHE_FILE, JSON.stringify({
123
+ models: this.allModels,
124
+ fetchedAt: this.lastFetch,
125
+ version: 1,
126
+ }));
127
+ }
128
+ catch { /* cache write is best-effort */ }
129
+ }
130
+ loadFromCache() {
131
+ try {
132
+ if (!existsSync(CACHE_FILE))
133
+ return;
134
+ const raw = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
135
+ if (Array.isArray(raw.models)) {
136
+ this.allModels = raw.models;
137
+ this.lastFetch = raw.fetchedAt ?? 0;
138
+ }
139
+ }
140
+ catch { /* cache read is best-effort */ }
141
+ }
142
+ /** True if cache is stale and we should re-fetch on next opportunity. */
143
+ get cacheStale() {
144
+ return Date.now() - this.lastFetch > CACHE_TTL;
145
+ }
146
+ // ── Model selection ────────────────────────────────────────────────────────
147
+ /**
148
+ * Pick the best available model for a given tier.
149
+ * Favours lower cost among available models, excluding those with >= 3
150
+ * consecutive failures (which get a 5-minute cooldown).
151
+ */
152
+ pick(tier) {
153
+ const pool = this.tieredPool[tier] ?? this.tieredPool["worker"];
154
+ const now = Date.now();
155
+ for (const tm of pool) {
156
+ // Cooldown: if 3+ consecutive failures, wait 5 minutes
157
+ if (!tm.available || (tm.failures >= 3 && now - tm.lastFailure < 300_000))
158
+ continue;
159
+ return tm.model;
160
+ }
161
+ // Fallback: return any available model from any tier
162
+ for (const t of ["worker", "analyst", "orchestrator", "free"]) {
163
+ for (const tm of this.tieredPool[t]) {
164
+ if (tm.available && tm.failures < 3)
165
+ return tm.model;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ /**
171
+ * Per-model and per-tier temperature profiles.
172
+ * Reasoning/thinking models REQUIRE temperature=1.0 (API enforces).
173
+ * Code/structured tasks want low temp; creative analysis wants higher.
174
+ */
175
+ getTemperature(modelId, tier, envTemp) {
176
+ // Exact model overrides (reasoning models MUST be 1.0)
177
+ const MODEL_TEMPS = {
178
+ // OpenAI o-series — no temperature param supported at all
179
+ "openai/o3": 1.0,
180
+ "openai/o3-pro": 1.0,
181
+ "openai/o3-mini": 1.0,
182
+ "openai/o4": 1.0,
183
+ "openai/o4-mini": 1.0,
184
+ // Thinking variants require 1.0
185
+ "qwen/qwen3-max-thinking": 1.0,
186
+ "qwen/qwen3.6-max-preview": 1.0,
187
+ "qwen/qwen3-235b-a22b-thinking-2507": 1.0,
188
+ "moonshotai/kimi-k2-thinking": 1.0,
189
+ "arcee-ai/trinity-large-thinking": 1.0,
190
+ };
191
+ if (MODEL_TEMPS[modelId] !== undefined)
192
+ return MODEL_TEMPS[modelId];
193
+ // Any model with "thinking" in the ID needs 1.0
194
+ if (/thinking/i.test(modelId))
195
+ return 1.0;
196
+ // Tier defaults
197
+ const TIER_TEMPS = {
198
+ orchestrator: 0.7, // balanced reasoning
199
+ analyst: 0.5, // more deterministic for analysis
200
+ worker: 0.3, // deterministic for structured tasks
201
+ free: 0.5,
202
+ };
203
+ // User env var overrides tier defaults (but not model-specific overrides)
204
+ if (process.env.TEMPERATURE)
205
+ return envTemp;
206
+ return TIER_TEMPS[tier] ?? 0.7;
207
+ }
208
+ /**
209
+ * Per-tier max token budgets.
210
+ * Orchestrators get generous budgets; free workers get minimal.
211
+ */
212
+ getTokenBudget(modelId, tier, envMax) {
213
+ const TIER_BUDGETS = {
214
+ orchestrator: 32_768,
215
+ analyst: 16_384,
216
+ worker: 4_096,
217
+ free: 2_048,
218
+ };
219
+ // Thinking models need at least 16K for the thinking budget
220
+ const isThinking = /thinking|o3|o4/i.test(modelId);
221
+ const base = isThinking
222
+ ? Math.max(16_384, TIER_BUDGETS[tier])
223
+ : TIER_BUDGETS[tier];
224
+ // User env var is a hard cap
225
+ return Math.min(base, envMax);
226
+ }
227
+ /**
228
+ * Build a full ModelConfig chain from the tiered pool.
229
+ * Uses user-configured models from env first, then fills with tiered picks.
230
+ */
231
+ buildModelChain(userModels) {
232
+ const env = process.env;
233
+ const tokens = parseInt(env.MAX_TOKENS ?? "99999"); // now used as cap, not target
234
+ const temp = parseFloat(env.TEMPERATURE ?? "0.7");
235
+ const results = [];
236
+ // User-specified models always come first
237
+ for (const mid of userModels) {
238
+ const cfg = this.buildConfig(mid, tokens, temp);
239
+ if (cfg)
240
+ results.push(cfg);
241
+ }
242
+ if (!this.loaded)
243
+ return results;
244
+ // Fill remaining slots with tiered picks (up to 5 total, max 2 free)
245
+ const tiers = ["orchestrator", "analyst", "analyst", "worker", "free"];
246
+ let freeUsed = 0;
247
+ for (const tier of tiers) {
248
+ if (results.length >= 5)
249
+ break;
250
+ if (tier === "free" && freeUsed >= 2)
251
+ continue;
252
+ const picked = this.pick(tier);
253
+ if (!picked)
254
+ continue;
255
+ // Skip if already in the list
256
+ if (results.some(r => r.model === picked.id))
257
+ continue;
258
+ const cfg = this.buildConfig(picked.id, tokens, temp);
259
+ if (cfg) {
260
+ results.push(cfg);
261
+ if (tier === "free")
262
+ freeUsed++;
263
+ }
264
+ }
265
+ return results.slice(0, 5);
266
+ }
267
+ /** Build a single ModelConfig, preferring direct provider when possible. */
268
+ buildConfig(modelId, maxTokens, temperature, tier) {
269
+ // Apply per-model/per-tier temperature and token budget
270
+ const resolvedTier = tier ?? this.getTier(modelId);
271
+ const env = process.env;
272
+ const envMax = parseInt(env.MAX_TOKENS ?? "99999");
273
+ const envTemp = parseFloat(env.TEMPERATURE ?? "0.7");
274
+ temperature = this.getTemperature(modelId, resolvedTier, envTemp);
275
+ maxTokens = this.getTokenBudget(modelId, resolvedTier, Math.min(maxTokens, envMax));
276
+ // Direct Anthropic routing (cheaper — no OR markup)
277
+ if (modelId.startsWith("anthropic/") && env.ANTHROPIC_API_KEY) {
278
+ const stripped = modelId.replace("anthropic/", "");
279
+ // OpenRouter model IDs differ from Anthropic API IDs — map them correctly
280
+ const ANTHROPIC_API_ALIASES = {
281
+ // Opus 4.x
282
+ "claude-opus-4.7": "claude-opus-4-20260101",
283
+ "claude-opus-4.7-fast": "claude-opus-4-20260101",
284
+ "claude-opus-4.6": "claude-opus-4-20251120",
285
+ "claude-opus-4.6-fast": "claude-opus-4-20251120",
286
+ "claude-opus-4.5": "claude-opus-4-20251015",
287
+ "claude-opus-4": "claude-opus-4-20250514",
288
+ // Sonnet 4.x
289
+ "claude-sonnet-4.6": "claude-sonnet-4-20251120",
290
+ "claude-sonnet-4.5": "claude-sonnet-4-20251015",
291
+ "claude-sonnet-4": "claude-sonnet-4-20250514",
292
+ // Haiku 4.x
293
+ "claude-haiku-4.5": "claude-haiku-4-20251015",
294
+ "claude-haiku-4": "claude-haiku-4-20250514",
295
+ // Legacy aliases (safe to keep)
296
+ "claude-3-5-sonnet-20241022": "claude-3-5-sonnet-20241022",
297
+ "claude-3-haiku-20240307": "claude-3-haiku-20240307",
298
+ };
299
+ const model = ANTHROPIC_API_ALIASES[stripped] ?? stripped;
300
+ return {
301
+ baseUrl: "https://api.anthropic.com/v1",
302
+ apiKey: env.ANTHROPIC_API_KEY,
303
+ model,
304
+ maxTokens,
305
+ temperature,
306
+ };
307
+ }
308
+ // Direct OpenAI routing
309
+ if (modelId.startsWith("openai/") && env.OPENAI_API_KEY) {
310
+ const model = modelId.replace("openai/", "");
311
+ return {
312
+ baseUrl: env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
313
+ apiKey: env.OPENAI_API_KEY,
314
+ model,
315
+ maxTokens,
316
+ temperature,
317
+ };
318
+ }
319
+ // OpenRouter fallback
320
+ if (env.OPENROUTER_API_KEY) {
321
+ return {
322
+ baseUrl: "https://openrouter.ai/api/v1",
323
+ apiKey: env.OPENROUTER_API_KEY,
324
+ model: modelId,
325
+ maxTokens,
326
+ temperature,
327
+ siteUrl: env.OPENROUTER_SITE_URL ?? "https://jellychain.fun",
328
+ siteName: env.OPENROUTER_SITE_NAME ?? "JellyOS",
329
+ };
330
+ }
331
+ // Local model
332
+ if (env.OPENAI_BASE_URL) {
333
+ return {
334
+ baseUrl: env.OPENAI_BASE_URL,
335
+ apiKey: env.OPENAI_API_KEY ?? "local",
336
+ model: modelId,
337
+ maxTokens,
338
+ temperature,
339
+ };
340
+ }
341
+ return null;
342
+ }
343
+ // ── Failure tracking ─────────────────────────────────────────────────────
344
+ recordFailure(modelId) {
345
+ const tm = this.modelMap.get(modelId);
346
+ if (!tm)
347
+ return;
348
+ tm.failures++;
349
+ tm.lastFailure = Date.now();
350
+ if (tm.failures >= 3)
351
+ tm.available = false;
352
+ }
353
+ recordSuccess(modelId, latencyMs) {
354
+ const tm = this.modelMap.get(modelId);
355
+ if (!tm)
356
+ return;
357
+ tm.failures = 0;
358
+ tm.available = true;
359
+ tm.avgLatency = tm.uses === 0 ? latencyMs : (tm.avgLatency * tm.uses + latencyMs) / (tm.uses + 1);
360
+ tm.uses++;
361
+ }
362
+ /** Mark a model as permanently deprecated (404, model removed). */
363
+ markDeprecated(modelId) {
364
+ const tm = this.modelMap.get(modelId);
365
+ if (!tm)
366
+ return;
367
+ tm.available = false;
368
+ tm.failures = 999; // never recover
369
+ }
370
+ getTier(modelId) {
371
+ return this.modelMap.get(modelId)?.tier ?? "worker";
372
+ }
373
+ // ── Queries ───────────────────────────────────────────────────────────────
374
+ get modelCount() {
375
+ return this.allModels.length;
376
+ }
377
+ /** Search / filter models by keyword (case-insensitive). */
378
+ search(query, maxResults = 30) {
379
+ const q = query.toLowerCase();
380
+ const results = [];
381
+ for (const tm of this.modelMap.values()) {
382
+ if (tm.model.id.toLowerCase().includes(q) || tm.model.name.toLowerCase().includes(q)) {
383
+ results.push(tm);
384
+ }
385
+ }
386
+ return results.slice(0, maxResults);
387
+ }
388
+ /** Get all models for a tier. */
389
+ getPool(tier) {
390
+ return [...this.tieredPool[tier]];
391
+ }
392
+ /** Human-readable summary of the current pool. */
393
+ summary() {
394
+ const lines = [];
395
+ for (const tier of ["orchestrator", "analyst", "worker", "free"]) {
396
+ const pool = this.tieredPool[tier];
397
+ const avail = pool.filter(tm => tm.available && tm.failures < 3).length;
398
+ lines.push(`${tier}: ${avail}/${pool.length} available`);
399
+ }
400
+ return `Model pool (${this.allModels.length} total):\n` + lines.join("\n");
401
+ }
402
+ /**
403
+ * Find the cheapest model meeting minimum requirements.
404
+ * @param tier - Required tier (or "any")
405
+ * @param minContext - Minimum context length
406
+ * @param maxCost - Maximum cost in nano-dollars per 1K prompt
407
+ */
408
+ findCheapest(tier, minContext = 0, maxCost = Infinity) {
409
+ const allTiers = ["orchestrator", "analyst", "worker", "free"];
410
+ const pool = tier === "any"
411
+ ? allTiers.flatMap(t => this.tieredPool[t])
412
+ : this.tieredPool[tier] ?? [];
413
+ return pool
414
+ .filter(tm => tm.available && tm.failures < 3)
415
+ .filter(tm => (tm.model.context_length || 0) >= minContext)
416
+ .filter(tm => tm.costPer1K <= maxCost)
417
+ .sort((a, b) => a.costPer1K - b.costPer1K)[0] ?? null;
418
+ }
419
+ // ── Tool: list_models ─────────────────────────────────────────────────────
420
+ listModelsParams = Type.Object({
421
+ query: Type.Optional(Type.String({ description: "Search query (model name or provider)" })),
422
+ tier: Type.Optional(Type.String({ description: "Filter by tier: orchestrator, analyst, worker, free" })),
423
+ limit: Type.Optional(Type.Number({ description: "Max results", default: 20 })),
424
+ available_only: Type.Optional(Type.Boolean({ description: "Only show available models", default: true })),
425
+ });
426
+ async listModelsTool(_id, params) {
427
+ let results;
428
+ if (params.query) {
429
+ results = this.search(params.query, params.limit ?? 20);
430
+ }
431
+ else if (params.tier && this.tieredPool[params.tier]) {
432
+ results = this.tieredPool[params.tier];
433
+ }
434
+ else {
435
+ results = [...this.modelMap.values()];
436
+ }
437
+ if (params.available_only !== false) {
438
+ results = results.filter(tm => tm.available && tm.failures < 3);
439
+ }
440
+ results = results.slice(0, params.limit ?? 20);
441
+ const text = results.map(tm => {
442
+ const costStr = tm.costPer1K <= 0 ? "FREE" : `$${(tm.costPer1K / 1_000_000_000).toFixed(6)}/1K`;
443
+ const ctx = tm.model.context_length
444
+ ? tm.model.context_length >= 1_000_000
445
+ ? `${(tm.model.context_length / 1_000_000).toFixed(1)}M ctx`
446
+ : `${(tm.model.context_length / 1000).toFixed(0)}K ctx`
447
+ : "?K ctx";
448
+ const status = tm.available ? "" : " [UNAVAILABLE]";
449
+ return `[${tm.tier}] ${tm.model.id.padEnd(46)} ${costStr.padEnd(18)} ${ctx}${status}`;
450
+ }).join("\n");
451
+ return {
452
+ content: [{ type: "text", text: text || "(no models match)" }],
453
+ details: { count: results.length },
454
+ };
455
+ }
456
+ // ── Tool: pick_model ──────────────────────────────────────────────────────
457
+ pickModelParams = Type.Object({
458
+ tier: Type.Optional(Type.String({ description: "Tier: orchestrator, analyst, worker, free, any" })),
459
+ min_context: Type.Optional(Type.Number({ description: "Minimum context length in tokens" })),
460
+ max_cost: Type.Optional(Type.Number({ description: "Maximum cost per 1K prompt in nano-dollars" })),
461
+ });
462
+ async pickModelTool(_id, params) {
463
+ const tier = (params.tier ?? "any");
464
+ const cheapest = this.findCheapest(tier, params.min_context ?? 0, params.max_cost ?? Infinity);
465
+ if (!cheapest) {
466
+ return { content: [{ type: "text", text: "No matching model found." }], details: {} };
467
+ }
468
+ const costDollars = cheapest.costPer1K / 1_000_000_000;
469
+ return {
470
+ content: [{
471
+ type: "text",
472
+ text: `Recommended: ${cheapest.model.id}\n Tier: ${cheapest.tier}\n Context: ${(cheapest.model.context_length ?? 0).toLocaleString()} tokens\n Cost: $${costDollars.toFixed(6)}/1K prompt tokens\n Provider: ${cheapest.model.id.split("/")[0]}`,
473
+ }],
474
+ details: { model_id: cheapest.model.id, tier: cheapest.tier, cost_per_1k: costDollars },
475
+ };
476
+ }
477
+ // ── Tool: model_summary ───────────────────────────────────────────────────
478
+ summaryParams = Type.Object({});
479
+ async summaryTool() {
480
+ return {
481
+ content: [{ type: "text", text: this.summary() }],
482
+ details: {
483
+ total: this.allModels.length,
484
+ tiers: {
485
+ orchestrator: this.tieredPool.orchestrator.filter(tm => tm.available).length,
486
+ analyst: this.tieredPool.analyst.filter(tm => tm.available).length,
487
+ worker: this.tieredPool.worker.filter(tm => tm.available).length,
488
+ free: this.tieredPool.free.filter(tm => tm.available).length,
489
+ },
490
+ },
491
+ };
492
+ }
493
+ }
494
+ /** Singleton — initialised once at startup, used everywhere */
495
+ export const modelRegistry = new ModelRegistry();
496
+ //# sourceMappingURL=ModelRegistry.js.map
@@ -0,0 +1,5 @@
1
+ export { ModelRegistry, modelRegistry, classifyModel } from "./ModelRegistry.js";
2
+ export type { OpenRouterModel, TieredModel, TieredPool, ModelTier } from "./ModelRegistry.js";
3
+ export { CostTracker } from "./CostTracker.js";
4
+ export type { UsageEntry, SessionUsage, LifetimeUsage } from "./CostTracker.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { ModelRegistry, modelRegistry, classifyModel } from "./ModelRegistry.js";
2
+ export { CostTracker } from "./CostTracker.js";
3
+ //# sourceMappingURL=index.js.map
@@ -9,6 +9,11 @@
9
9
  import type { Registry } from "../api/Registry.js";
10
10
  import type { SessionManager } from "../session/SessionManager.js";
11
11
  import type { SessionContext } from "../api/ExtensionAPI.js";
12
+ import type { ModelRegistry } from "../models/ModelRegistry.js";
13
+ import type { CostTracker } from "../models/CostTracker.js";
14
+ import type { GoalManager } from "../session/GoalManager.js";
15
+ import type { ContextStore } from "../session/ContextStore.js";
16
+ import { Tracer } from "../telemetry/Tracer.js";
12
17
  export type RunnerEvent = {
13
18
  type: "text_delta";
14
19
  text: string;
@@ -40,6 +45,13 @@ export type RunnerEvent = {
40
45
  } | {
41
46
  type: "error";
42
47
  message: string;
48
+ }
49
+ /** #10: Approval gate — TUI pauses and waits for user y/n */
50
+ | {
51
+ type: "approval_request";
52
+ toolName: string;
53
+ args: string;
54
+ approve: (yes: boolean) => void;
43
55
  };
44
56
  export type RunnerEventHandler = (event: RunnerEvent) => void;
45
57
  export declare class AgentRunner {
@@ -48,10 +60,17 @@ export declare class AgentRunner {
48
60
  private onEvent;
49
61
  private sessionCtx;
50
62
  private effectLevel;
63
+ private goalManager?;
64
+ private contextStore?;
51
65
  private modelChain;
52
66
  private dispatcher;
53
67
  private swarmRouter;
54
- constructor(registry: Registry, session: SessionManager, onEvent: RunnerEventHandler, sessionCtx: SessionContext, effectLevel?: string);
68
+ private modelRegistry?;
69
+ private costTracker?;
70
+ private abortController;
71
+ /** #25: Cancel the current in-flight stream immediately */
72
+ abort(): void;
73
+ constructor(registry: Registry, session: SessionManager, onEvent: RunnerEventHandler, sessionCtx: SessionContext, effectLevel?: string, modelReg?: ModelRegistry, costTracker?: CostTracker, goalManager?: GoalManager | undefined, contextStore?: ContextStore | undefined);
55
74
  /**
56
75
  * Live reconfigure effect level without recreating the runner.
57
76
  * Called by the /effect REPL command immediately on each invocation so that
@@ -60,7 +79,9 @@ export declare class AgentRunner {
60
79
  setEffectLevel(level: string): void;
61
80
  /** Run one user turn — may invoke multiple tool rounds and model fallbacks internally */
62
81
  run(userMessage: string): Promise<void>;
82
+ private buildLiveContext;
83
+ private buildDynamicSystemSuffix;
63
84
  private runSwarm;
64
- runSingleAgent(): Promise<void>;
85
+ runSingleAgent(userMessage?: string, tracer?: Tracer): Promise<void>;
65
86
  }
66
87
  //# sourceMappingURL=AgentRunner.d.ts.map