skyloom 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/.github/workflows/ci.yml +36 -0
- package/CONVERSION_PLAN.md +191 -0
- package/README.md +67 -0
- package/dist/agents/dew.d.ts +15 -0
- package/dist/agents/dew.d.ts.map +1 -0
- package/dist/agents/dew.js +74 -0
- package/dist/agents/dew.js.map +1 -0
- package/dist/agents/fair.d.ts +15 -0
- package/dist/agents/fair.d.ts.map +1 -0
- package/dist/agents/fair.js +106 -0
- package/dist/agents/fair.js.map +1 -0
- package/dist/agents/fog.d.ts +15 -0
- package/dist/agents/fog.d.ts.map +1 -0
- package/dist/agents/fog.js +52 -0
- package/dist/agents/fog.js.map +1 -0
- package/dist/agents/frost.d.ts +15 -0
- package/dist/agents/frost.d.ts.map +1 -0
- package/dist/agents/frost.js +54 -0
- package/dist/agents/frost.js.map +1 -0
- package/dist/agents/rain.d.ts +15 -0
- package/dist/agents/rain.d.ts.map +1 -0
- package/dist/agents/rain.js +54 -0
- package/dist/agents/rain.js.map +1 -0
- package/dist/agents/snow.d.ts +27 -0
- package/dist/agents/snow.d.ts.map +1 -0
- package/dist/agents/snow.js +226 -0
- package/dist/agents/snow.js.map +1 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +402 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mode.d.ts +17 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +56 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/core/agent.d.ts +174 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1332 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agent_helpers.d.ts +51 -0
- package/dist/core/agent_helpers.d.ts.map +1 -0
- package/dist/core/agent_helpers.js +477 -0
- package/dist/core/agent_helpers.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +191 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cache.d.ts +63 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checkpoint.d.ts +19 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +120 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/circuit_breaker.d.ts +46 -0
- package/dist/core/circuit_breaker.d.ts.map +1 -0
- package/dist/core/circuit_breaker.js +99 -0
- package/dist/core/circuit_breaker.js.map +1 -0
- package/dist/core/config.d.ts +97 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +281 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +78 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +84 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/factory.d.ts +63 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +537 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/icons.d.ts +28 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +86 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +54 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm.d.ts +121 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +532 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/logger.d.ts +57 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +122 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mcp.d.ts +190 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +822 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/mcp_server.d.ts +26 -0
- package/dist/core/mcp_server.d.ts.map +1 -0
- package/dist/core/mcp_server.js +211 -0
- package/dist/core/mcp_server.js.map +1 -0
- package/dist/core/memory.d.ts +190 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +988 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/middleware.d.ts +114 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +248 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/pipelines.d.ts +87 -0
- package/dist/core/pipelines.d.ts.map +1 -0
- package/dist/core/pipelines.js +301 -0
- package/dist/core/pipelines.js.map +1 -0
- package/dist/core/profile.d.ts +23 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +289 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/router.d.ts +24 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +111 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schemas.d.ts +82 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +200 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.d.ts.map +1 -0
- package/dist/core/semantic.js +175 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/skill.d.ts +68 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +350 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/tool.d.ts +99 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +341 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tool_router.d.ts +29 -0
- package/dist/core/tool_router.d.ts.map +1 -0
- package/dist/core/tool_router.js +172 -0
- package/dist/core/tool_router.js.map +1 -0
- package/dist/core/workspace.d.ts +48 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +179 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/plugins/loader.d.ts +17 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +96 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +78 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tools/builtin.d.ts +10 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +414 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/computer.d.ts +12 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +326 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/delegate.d.ts +10 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +647 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/tts.d.ts +33 -0
- package/dist/web/tts.d.ts.map +1 -0
- package/dist/web/tts.js +69 -0
- package/dist/web/tts.js.map +1 -0
- package/package.json +60 -0
- package/scripts/install.js +48 -0
- package/scripts/link.js +10 -0
- package/setup.bat +79 -0
- package/skill-test-ty2fOA/test.md +10 -0
- package/src/agents/dew.ts +70 -0
- package/src/agents/fair.ts +102 -0
- package/src/agents/fog.ts +48 -0
- package/src/agents/frost.ts +50 -0
- package/src/agents/rain.ts +50 -0
- package/src/agents/snow.ts +239 -0
- package/src/cli/main.ts +405 -0
- package/src/cli/mode.ts +58 -0
- package/src/core/agent.ts +1506 -0
- package/src/core/agent_helpers.ts +461 -0
- package/src/core/bus.ts +221 -0
- package/src/core/cache.ts +153 -0
- package/src/core/checkpoint.ts +94 -0
- package/src/core/circuit_breaker.ts +119 -0
- package/src/core/config.ts +341 -0
- package/src/core/constants.ts +95 -0
- package/src/core/factory.ts +627 -0
- package/src/core/icons.ts +53 -0
- package/src/core/index.ts +31 -0
- package/src/core/llm.ts +724 -0
- package/src/core/logger.ts +144 -0
- package/src/core/mcp.ts +953 -0
- package/src/core/mcp_server.ts +176 -0
- package/src/core/memory.ts +1169 -0
- package/src/core/middleware.ts +350 -0
- package/src/core/pipelines.ts +424 -0
- package/src/core/profile.ts +255 -0
- package/src/core/router.ts +124 -0
- package/src/core/schemas.ts +282 -0
- package/src/core/semantic.ts +211 -0
- package/src/core/skill.ts +342 -0
- package/src/core/tool.ts +427 -0
- package/src/core/tool_router.ts +193 -0
- package/src/core/workspace.ts +150 -0
- package/src/plugins/loader.ts +66 -0
- package/src/skills/loader.ts +46 -0
- package/src/sql.js.d.ts +29 -0
- package/src/tools/builtin.ts +382 -0
- package/src/tools/computer.ts +269 -0
- package/src/tools/delegate.ts +49 -0
- package/src/web/server.ts +634 -0
- package/src/web/tts.ts +93 -0
- package/tests/bus.test.ts +121 -0
- package/tests/icons.test.ts +45 -0
- package/tests/router.test.ts +86 -0
- package/tests/schemas.test.ts +51 -0
- package/tests/semantic.test.ts +83 -0
- package/tests/setup.ts +10 -0
- package/tests/skill.test.ts +172 -0
- package/tests/tool.test.ts +108 -0
- package/tests/tool_router.test.ts +71 -0
- package/tsconfig.json +37 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight LRU cache for LLM responses.
|
|
3
|
+
*
|
|
4
|
+
* Deduplicates identical requests within a configurable time window.
|
|
5
|
+
* Keyed by (model, messages_json) hash.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cache entry containing timestamp and response.
|
|
12
|
+
*/
|
|
13
|
+
interface CacheEntry {
|
|
14
|
+
timestamp: number;
|
|
15
|
+
response: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* LRU cache for LLM completions with TTL expiration.
|
|
20
|
+
*/
|
|
21
|
+
export class LLMCache {
|
|
22
|
+
private maxSize: number;
|
|
23
|
+
private ttlSeconds: number;
|
|
24
|
+
private cache: Map<string, CacheEntry> = new Map();
|
|
25
|
+
|
|
26
|
+
constructor(maxSize: number = 128, ttlSeconds: number = 60) {
|
|
27
|
+
this.maxSize = maxSize;
|
|
28
|
+
this.ttlSeconds = ttlSeconds;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a cache key from model, messages, and parameters.
|
|
33
|
+
*
|
|
34
|
+
* Uses SHA256 hash for deterministic, compact keys. Parameters are sorted
|
|
35
|
+
* to ensure consistent keys regardless of key order.
|
|
36
|
+
*/
|
|
37
|
+
private makeKey(
|
|
38
|
+
model: string,
|
|
39
|
+
messages: Record<string, any>[],
|
|
40
|
+
params?: Record<string, any>
|
|
41
|
+
): string {
|
|
42
|
+
const raw = JSON.stringify([model, messages, params || {}], this.jsonReplacer);
|
|
43
|
+
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* JSON replacer for consistent serialization.
|
|
48
|
+
*/
|
|
49
|
+
private jsonReplacer(_key: string, value: any): any {
|
|
50
|
+
if (typeof value === 'function') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get a cached response if it exists and hasn't expired.
|
|
58
|
+
*
|
|
59
|
+
* @param model - LLM model name
|
|
60
|
+
* @param messages - Conversation messages
|
|
61
|
+
* @param params - Optional parameters
|
|
62
|
+
* @returns Cached response or null if miss/expired
|
|
63
|
+
*/
|
|
64
|
+
get(
|
|
65
|
+
model: string,
|
|
66
|
+
messages: Record<string, any>[],
|
|
67
|
+
params?: Record<string, any>
|
|
68
|
+
): string | null {
|
|
69
|
+
const key = this.makeKey(model, messages, params);
|
|
70
|
+
const entry = this.cache.get(key);
|
|
71
|
+
|
|
72
|
+
if (!entry) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if expired
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const ageSeconds = (now - entry.timestamp) / 1000;
|
|
79
|
+
|
|
80
|
+
if (ageSeconds >= this.ttlSeconds) {
|
|
81
|
+
this.cache.delete(key);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Move to end (LRU)
|
|
86
|
+
this.cache.delete(key);
|
|
87
|
+
this.cache.set(key, entry);
|
|
88
|
+
|
|
89
|
+
return entry.response;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Cache a response.
|
|
94
|
+
*
|
|
95
|
+
* Refuses to cache empty or very short content to avoid bloat.
|
|
96
|
+
* Also performs LRU eviction if cache exceeds max_size.
|
|
97
|
+
*
|
|
98
|
+
* @param model - LLM model name
|
|
99
|
+
* @param messages - Conversation messages
|
|
100
|
+
* @param response - LLM response to cache
|
|
101
|
+
* @param params - Optional parameters
|
|
102
|
+
*/
|
|
103
|
+
set(
|
|
104
|
+
model: string,
|
|
105
|
+
messages: Record<string, any>[],
|
|
106
|
+
response: string,
|
|
107
|
+
params?: Record<string, any>
|
|
108
|
+
): void {
|
|
109
|
+
// Don't cache empty or very short responses
|
|
110
|
+
if (!response || response.length < 10) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const key = this.makeKey(model, messages, params);
|
|
115
|
+
const entry: CacheEntry = {
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
response,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.cache.delete(key);
|
|
121
|
+
this.cache.set(key, entry);
|
|
122
|
+
|
|
123
|
+
// Evict oldest entries if over capacity
|
|
124
|
+
while (this.cache.size > this.maxSize) {
|
|
125
|
+
const firstKey = this.cache.keys().next().value;
|
|
126
|
+
if (firstKey !== undefined) this.cache.delete(firstKey);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear all cache entries.
|
|
132
|
+
*/
|
|
133
|
+
clear(): void {
|
|
134
|
+
this.cache.clear();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get current cache size.
|
|
139
|
+
*/
|
|
140
|
+
get size(): number {
|
|
141
|
+
return this.cache.size;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get cache hit rate statistics.
|
|
146
|
+
*/
|
|
147
|
+
getStats(): { size: number; maxSize: number } {
|
|
148
|
+
return {
|
|
149
|
+
size: this.cache.size,
|
|
150
|
+
maxSize: this.maxSize,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration checkpoint — save/restore task state.
|
|
3
|
+
*
|
|
4
|
+
* Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
|
|
5
|
+
* interrupted by Ctrl-C can be resumed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { USER_CONFIG_DIR } from './config';
|
|
11
|
+
|
|
12
|
+
function checkpointPath(): string {
|
|
13
|
+
return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Save current orchestration state so it can be resumed later.
|
|
18
|
+
*/
|
|
19
|
+
export function save(
|
|
20
|
+
goal: string,
|
|
21
|
+
tasks: any[],
|
|
22
|
+
results: any[],
|
|
23
|
+
completedIds?: Set<string>
|
|
24
|
+
): void {
|
|
25
|
+
const cids = completedIds || new Set(results.map((r: any) => r.id));
|
|
26
|
+
const payload = {
|
|
27
|
+
goal,
|
|
28
|
+
tasks: tasks.map(serializeTask),
|
|
29
|
+
results: results.map(serializeResult),
|
|
30
|
+
completed_ids: Array.from(cids).sort(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const p = checkpointPath();
|
|
34
|
+
const dir = path.dirname(p);
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tmp = p + '.tmp';
|
|
40
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
41
|
+
fs.renameSync(tmp, p);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Return the last saved checkpoint dict, or null if none / unreadable.
|
|
46
|
+
*/
|
|
47
|
+
export function load(): Record<string, any> | null {
|
|
48
|
+
const p = checkpointPath();
|
|
49
|
+
if (!fs.existsSync(p)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
54
|
+
return typeof data === 'object' && data !== null ? data : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Delete the checkpoint file.
|
|
62
|
+
*/
|
|
63
|
+
export function clear(): void {
|
|
64
|
+
try {
|
|
65
|
+
const p = checkpointPath();
|
|
66
|
+
if (fs.existsSync(p)) {
|
|
67
|
+
fs.unlinkSync(p);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore cleanup errors
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Serialization helpers ──
|
|
75
|
+
|
|
76
|
+
function serializeTask(t: any): Record<string, any> {
|
|
77
|
+
return {
|
|
78
|
+
id: t.id,
|
|
79
|
+
description: t.description,
|
|
80
|
+
assigned_to: t.assignedTo ?? t.assigned_to,
|
|
81
|
+
all_deps: t.allDeps ?? t.all_deps ?? [],
|
|
82
|
+
status: t.status?.value ?? t.status ?? 'unknown',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function serializeResult(r: any): Record<string, any> {
|
|
87
|
+
return {
|
|
88
|
+
id: r.id,
|
|
89
|
+
agent: r.agent,
|
|
90
|
+
description: r.description,
|
|
91
|
+
success: r.success,
|
|
92
|
+
content: r.content,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker pattern for fault tolerance
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CircuitBreakerConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
failureThreshold?: number;
|
|
8
|
+
successThreshold?: number;
|
|
9
|
+
resetTimeout?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CircuitBreakerState = "closed" | "open" | "half_open";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Circuit breaker implementation
|
|
16
|
+
*/
|
|
17
|
+
export class CircuitBreaker {
|
|
18
|
+
private state: CircuitBreakerState = "closed";
|
|
19
|
+
private failureCount = 0;
|
|
20
|
+
private successCount = 0;
|
|
21
|
+
private lastFailureTime = 0;
|
|
22
|
+
private failureThreshold: number;
|
|
23
|
+
private successThreshold: number;
|
|
24
|
+
private resetTimeout: number;
|
|
25
|
+
|
|
26
|
+
constructor(config: CircuitBreakerConfig) {
|
|
27
|
+
this.failureThreshold = config.failureThreshold ?? 5;
|
|
28
|
+
this.successThreshold = config.successThreshold ?? 3;
|
|
29
|
+
this.resetTimeout = config.resetTimeout ?? 60000; // 60 seconds
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if execution is allowed
|
|
34
|
+
*/
|
|
35
|
+
canExecute(): boolean {
|
|
36
|
+
if (this.state === "closed") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.state === "open") {
|
|
41
|
+
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
42
|
+
if (timeSinceLastFailure > this.resetTimeout) {
|
|
43
|
+
this.state = "half_open";
|
|
44
|
+
this.successCount = 0;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// half_open state allows attempts
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Record a successful execution
|
|
56
|
+
*/
|
|
57
|
+
recordSuccess(): void {
|
|
58
|
+
this.failureCount = 0;
|
|
59
|
+
|
|
60
|
+
if (this.state === "half_open") {
|
|
61
|
+
this.successCount++;
|
|
62
|
+
if (this.successCount >= this.successThreshold) {
|
|
63
|
+
this.state = "closed";
|
|
64
|
+
this.successCount = 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record a failed execution
|
|
71
|
+
*/
|
|
72
|
+
recordFailure(): void {
|
|
73
|
+
this.lastFailureTime = Date.now();
|
|
74
|
+
this.failureCount++;
|
|
75
|
+
|
|
76
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
77
|
+
this.state = "open";
|
|
78
|
+
this.failureCount = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.state === "half_open") {
|
|
82
|
+
this.state = "open";
|
|
83
|
+
this.failureCount = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get current state
|
|
89
|
+
*/
|
|
90
|
+
getState(): CircuitBreakerState {
|
|
91
|
+
return this.state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reset the circuit breaker
|
|
96
|
+
*/
|
|
97
|
+
reset(): void {
|
|
98
|
+
this.state = "closed";
|
|
99
|
+
this.failureCount = 0;
|
|
100
|
+
this.successCount = 0;
|
|
101
|
+
this.lastFailureTime = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get or create a circuit breaker for a service
|
|
107
|
+
*/
|
|
108
|
+
const breakers = new Map<string, CircuitBreaker>();
|
|
109
|
+
|
|
110
|
+
export function getBreaker(name: string, config?: CircuitBreakerConfig): CircuitBreaker {
|
|
111
|
+
if (!breakers.has(name)) {
|
|
112
|
+
breakers.set(name, new CircuitBreaker(config ?? { name }));
|
|
113
|
+
}
|
|
114
|
+
return breakers.get(name)!;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function clearBreakers(): void {
|
|
118
|
+
breakers.clear();
|
|
119
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Skyloom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as yaml from "yaml";
|
|
9
|
+
import { getLogger } from "./logger";
|
|
10
|
+
|
|
11
|
+
const log = getLogger("config");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration directory paths
|
|
15
|
+
*/
|
|
16
|
+
const LEGACY_CONFIG_DIR = path.join(os.homedir(), ".weather-agents");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the user configuration directory
|
|
20
|
+
* Migrates from legacy ~/.weather-agents to ~/.skyloom if needed
|
|
21
|
+
*/
|
|
22
|
+
function resolveUserConfigDir(): string {
|
|
23
|
+
const newDir = path.join(os.homedir(), ".skyloom");
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(newDir) && fs.existsSync(LEGACY_CONFIG_DIR)) {
|
|
26
|
+
try {
|
|
27
|
+
fs.renameSync(LEGACY_CONFIG_DIR, newDir);
|
|
28
|
+
log.info("Migrated config directory", {
|
|
29
|
+
from: LEGACY_CONFIG_DIR,
|
|
30
|
+
to: newDir,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
log.warn("Failed to migrate config directory", {
|
|
34
|
+
error: (error as Error).message,
|
|
35
|
+
});
|
|
36
|
+
return LEGACY_CONFIG_DIR;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return newDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const USER_CONFIG_DIR = resolveUserConfigDir();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find the config directory (bundled or user-provided)
|
|
47
|
+
*/
|
|
48
|
+
function findConfigDir(): string {
|
|
49
|
+
// Try bundled config directory (relative to src or dist)
|
|
50
|
+
const possiblePaths = [
|
|
51
|
+
path.join(__dirname, "..", "..", "..", "config"),
|
|
52
|
+
path.join(__dirname, "..", "config"),
|
|
53
|
+
path.join(process.cwd(), "config"),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const configPath of possiblePaths) {
|
|
57
|
+
if (fs.existsSync(path.join(configPath, "default.yaml"))) {
|
|
58
|
+
return configPath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fall back to user config directory
|
|
63
|
+
return path.join(USER_CONFIG_DIR, "config");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const CONFIG_DIR = findConfigDir();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Memory configuration
|
|
70
|
+
*/
|
|
71
|
+
export interface MemoryConfig {
|
|
72
|
+
dbPath: string;
|
|
73
|
+
shortTermLimit: number;
|
|
74
|
+
maxPersistedMessages?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* All agent names — single source of truth
|
|
79
|
+
*/
|
|
80
|
+
export const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load a YAML file safely
|
|
84
|
+
*/
|
|
85
|
+
function loadYaml(filePath: string): Record<string, unknown> | null {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
92
|
+
const data = yaml.parse(content);
|
|
93
|
+
|
|
94
|
+
return typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
log.error("Failed to load YAML file", {
|
|
97
|
+
file: filePath,
|
|
98
|
+
error: (error as Error).message,
|
|
99
|
+
});
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Model catalog entry
|
|
106
|
+
*/
|
|
107
|
+
export interface ModelEntry {
|
|
108
|
+
name: string;
|
|
109
|
+
provider?: string;
|
|
110
|
+
context_window?: number;
|
|
111
|
+
max_output?: number;
|
|
112
|
+
input_cost_per_1k?: number;
|
|
113
|
+
output_cost_per_1k?: number;
|
|
114
|
+
fallback?: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load available models from models.yaml
|
|
119
|
+
*/
|
|
120
|
+
export function loadModelCatalog(): Record<string, ModelEntry[]> {
|
|
121
|
+
const modelPath = path.join(CONFIG_DIR, "models.yaml");
|
|
122
|
+
const data = loadYaml(modelPath);
|
|
123
|
+
|
|
124
|
+
if (!data) {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const catalog: Record<string, ModelEntry[]> = {};
|
|
129
|
+
|
|
130
|
+
for (const [provider, models] of Object.entries(data)) {
|
|
131
|
+
if (typeof models === "object" && models !== null) {
|
|
132
|
+
catalog[provider] = [];
|
|
133
|
+
|
|
134
|
+
for (const [name, info] of Object.entries(models as Record<string, unknown>)) {
|
|
135
|
+
const entry: ModelEntry = { name };
|
|
136
|
+
|
|
137
|
+
if (typeof info === "object" && info !== null) {
|
|
138
|
+
Object.assign(entry, info);
|
|
139
|
+
} else if (typeof info === "string") {
|
|
140
|
+
entry.provider = info;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
catalog[provider].push(entry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return catalog;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Provider catalog entry
|
|
153
|
+
*/
|
|
154
|
+
export interface ProviderEntry {
|
|
155
|
+
env_var?: string;
|
|
156
|
+
region?: string;
|
|
157
|
+
docs_url?: string;
|
|
158
|
+
base_url?: string;
|
|
159
|
+
aliases?: string[];
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let providerCatalogCache: Record<string, ProviderEntry> | null = null;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load provider catalog from providers.yaml
|
|
167
|
+
*/
|
|
168
|
+
export function loadProviderCatalog(): Record<string, ProviderEntry> {
|
|
169
|
+
if (providerCatalogCache) {
|
|
170
|
+
return providerCatalogCache;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const catalog: Record<string, ProviderEntry> = {};
|
|
174
|
+
|
|
175
|
+
// Load bundled providers
|
|
176
|
+
const bundledPath = path.join(CONFIG_DIR, "providers.yaml");
|
|
177
|
+
const bundledData = loadYaml(bundledPath);
|
|
178
|
+
|
|
179
|
+
if (bundledData) {
|
|
180
|
+
for (const [key, value] of Object.entries(bundledData)) {
|
|
181
|
+
if (typeof value === "object" && value !== null) {
|
|
182
|
+
catalog[key] = { ...value } as ProviderEntry;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Load user overrides
|
|
188
|
+
const userPath = path.join(USER_CONFIG_DIR, "providers.yaml");
|
|
189
|
+
const userData = loadYaml(userPath);
|
|
190
|
+
|
|
191
|
+
if (userData) {
|
|
192
|
+
for (const [key, value] of Object.entries(userData)) {
|
|
193
|
+
if (typeof value === "object" && value !== null) {
|
|
194
|
+
catalog[key] = {
|
|
195
|
+
...catalog[key],
|
|
196
|
+
...value,
|
|
197
|
+
} as ProviderEntry;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
providerCatalogCache = catalog;
|
|
203
|
+
return catalog;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Agent configuration
|
|
208
|
+
*/
|
|
209
|
+
export interface AgentConfig {
|
|
210
|
+
model: string;
|
|
211
|
+
provider: string;
|
|
212
|
+
temperature?: number;
|
|
213
|
+
top_p?: number;
|
|
214
|
+
max_tokens?: number;
|
|
215
|
+
system_prompt?: string;
|
|
216
|
+
tools?: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Skyloom configuration
|
|
221
|
+
*/
|
|
222
|
+
export interface SkyloomConfig {
|
|
223
|
+
agents: Record<string, AgentConfig>;
|
|
224
|
+
providers?: Record<string, ProviderEntry>;
|
|
225
|
+
models?: Record<string, ModelEntry[]>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Load default configuration
|
|
230
|
+
*/
|
|
231
|
+
export function loadDefaultConfig(): SkyloomConfig {
|
|
232
|
+
const defaultPath = path.join(CONFIG_DIR, "default.yaml");
|
|
233
|
+
const data = loadYaml(defaultPath);
|
|
234
|
+
|
|
235
|
+
if (!data) {
|
|
236
|
+
return { agents: {} };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data as unknown as SkyloomConfig;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Load user configuration (from ~/.skyloom/config.yaml)
|
|
244
|
+
*/
|
|
245
|
+
export function loadUserConfig(): SkyloomConfig | null {
|
|
246
|
+
const userPath = path.join(USER_CONFIG_DIR, "config.yaml");
|
|
247
|
+
const data = loadYaml(userPath);
|
|
248
|
+
|
|
249
|
+
if (!data) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return data as unknown as SkyloomConfig;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Merge user config on top of default config
|
|
258
|
+
*/
|
|
259
|
+
export function mergeConfigs(defaultCfg: SkyloomConfig, userCfg: SkyloomConfig | null): SkyloomConfig {
|
|
260
|
+
if (!userCfg) {
|
|
261
|
+
return defaultCfg;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
agents: {
|
|
266
|
+
...defaultCfg.agents,
|
|
267
|
+
...userCfg.agents,
|
|
268
|
+
},
|
|
269
|
+
providers: {
|
|
270
|
+
...defaultCfg.providers,
|
|
271
|
+
...userCfg.providers,
|
|
272
|
+
},
|
|
273
|
+
models: {
|
|
274
|
+
...defaultCfg.models,
|
|
275
|
+
...userCfg.models,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Load the complete configuration
|
|
282
|
+
*/
|
|
283
|
+
export function loadConfig(): SkyloomConfig {
|
|
284
|
+
const defaultCfg = loadDefaultConfig();
|
|
285
|
+
const userCfg = loadUserConfig();
|
|
286
|
+
return mergeConfigs(defaultCfg, userCfg);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Save user configuration
|
|
291
|
+
*/
|
|
292
|
+
export function saveUserConfig(config: SkyloomConfig): void {
|
|
293
|
+
// Ensure user config directory exists
|
|
294
|
+
if (!fs.existsSync(USER_CONFIG_DIR)) {
|
|
295
|
+
fs.mkdirSync(USER_CONFIG_DIR, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const userPath = path.join(USER_CONFIG_DIR, "config.yaml");
|
|
299
|
+
const content = yaml.stringify(config);
|
|
300
|
+
|
|
301
|
+
fs.writeFileSync(userPath, content, "utf-8");
|
|
302
|
+
log.info("Saved user configuration", { path: userPath });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get an agent configuration
|
|
307
|
+
*/
|
|
308
|
+
export function getAgentConfig(config: SkyloomConfig, agentName: string): AgentConfig | null {
|
|
309
|
+
return config.agents[agentName] || null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Format models for display
|
|
314
|
+
*/
|
|
315
|
+
export function formatModelsForDisplay(catalog: Record<string, ModelEntry[]>): string {
|
|
316
|
+
const lines: string[] = [];
|
|
317
|
+
|
|
318
|
+
for (const [provider, models] of Object.entries(catalog)) {
|
|
319
|
+
lines.push(` [${provider.toUpperCase()}]`);
|
|
320
|
+
|
|
321
|
+
for (const m of models) {
|
|
322
|
+
const costParts: string[] = [];
|
|
323
|
+
if (m.input_cost_per_1k) {
|
|
324
|
+
costParts.push(`$${m.input_cost_per_1k.toFixed(4)}/1k in`);
|
|
325
|
+
}
|
|
326
|
+
if (m.output_cost_per_1k) {
|
|
327
|
+
costParts.push(`$${m.output_cost_per_1k.toFixed(4)}/1k out`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const costStr = costParts.length > 0 ? ` cost=(${costParts.join(", ")})` : "";
|
|
331
|
+
const fallbackStr =
|
|
332
|
+
m.fallback && m.fallback.length > 0 ? ` fallback->${m.fallback.join(" > ")}` : "";
|
|
333
|
+
|
|
334
|
+
lines.push(
|
|
335
|
+
` ${m.name} (ctx=${m.context_window || "?"}, max=${m.max_output || "?"})${costStr}${fallbackStr}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|