@soleri/core 9.11.0 → 9.13.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/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/persona/defaults.d.ts +8 -0
- package/dist/persona/defaults.d.ts.map +1 -1
- package/dist/persona/defaults.js +49 -0
- package/dist/persona/defaults.js.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +140 -9
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +12 -0
- package/src/persona/defaults.test.ts +39 -1
- package/src/persona/defaults.ts +65 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/planner.test.ts +2 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +131 -0
- package/src/runtime/orchestrate-ops.ts +158 -10
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- package/dist/vault/vault-interfaces.js.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const CRON_TAG = '# soleri:dream';
|
|
7
|
+
const SOLERI_DIR = join(homedir(), '.soleri');
|
|
8
|
+
const LOG_PATH = join(SOLERI_DIR, 'dream-cron.log');
|
|
9
|
+
|
|
10
|
+
export interface CronSchedule {
|
|
11
|
+
isScheduled: boolean;
|
|
12
|
+
time: string | null;
|
|
13
|
+
logPath: string | null;
|
|
14
|
+
projectDir: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureSoleriDir(): void {
|
|
18
|
+
if (!existsSync(SOLERI_DIR)) {
|
|
19
|
+
mkdirSync(SOLERI_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCurrentCrontab(): string {
|
|
24
|
+
try {
|
|
25
|
+
return execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
26
|
+
} catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeCrontab(content: string): void {
|
|
32
|
+
execSync(`echo ${JSON.stringify(content)} | crontab -`, { encoding: 'utf-8' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveClaudePath(): string {
|
|
36
|
+
try {
|
|
37
|
+
const result = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
38
|
+
if (result) return result;
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through to default
|
|
41
|
+
}
|
|
42
|
+
return join(homedir(), '.claude', 'local', 'claude');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDreamLine(line: string): { minute: string; hour: string; projectDir: string } | null {
|
|
46
|
+
if (!line.includes(CRON_TAG)) return null;
|
|
47
|
+
const parts = line.trim().split(/\s+/);
|
|
48
|
+
if (parts.length < 6) return null;
|
|
49
|
+
const minute = parts[0];
|
|
50
|
+
const hour = parts[1];
|
|
51
|
+
// Extract --project-dir value from the line
|
|
52
|
+
const projDirMatch = line.match(/--project-dir\s+(\S+)/);
|
|
53
|
+
const projectDir = projDirMatch ? projDirMatch[1] : '';
|
|
54
|
+
return { minute, hour, projectDir };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSchedule(): CronSchedule {
|
|
58
|
+
try {
|
|
59
|
+
const crontab = getCurrentCrontab();
|
|
60
|
+
const dreamLine = crontab.split('\n').find((l) => l.includes(CRON_TAG));
|
|
61
|
+
if (!dreamLine) {
|
|
62
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
63
|
+
}
|
|
64
|
+
const parsed = parseDreamLine(dreamLine);
|
|
65
|
+
if (!parsed) {
|
|
66
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
67
|
+
}
|
|
68
|
+
const time = `${parsed.hour.padStart(2, '0')}:${parsed.minute.padStart(2, '0')}`;
|
|
69
|
+
return {
|
|
70
|
+
isScheduled: true,
|
|
71
|
+
time,
|
|
72
|
+
logPath: LOG_PATH,
|
|
73
|
+
projectDir: parsed.projectDir || null,
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function schedule(time: string, projectDir: string): CronSchedule {
|
|
81
|
+
ensureSoleriDir();
|
|
82
|
+
|
|
83
|
+
const match = time.match(/^(\d{1,2}):(\d{2})$/);
|
|
84
|
+
if (!match) {
|
|
85
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hour = parseInt(match[1], 10);
|
|
89
|
+
let minute = parseInt(match[2], 10);
|
|
90
|
+
|
|
91
|
+
// Add a few minutes offset to avoid running exactly on the hour
|
|
92
|
+
if (minute === 0) {
|
|
93
|
+
minute = 3;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const claudePath = resolveClaudePath();
|
|
97
|
+
const cronLine = `${minute} ${hour} * * * ${claudePath} --dangerously-skip-permissions -p "Run /ernesto-dream" --project-dir ${projectDir} >> ${LOG_PATH} 2>&1 ${CRON_TAG}`;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const crontab = getCurrentCrontab();
|
|
101
|
+
// Remove any existing dream lines (idempotent)
|
|
102
|
+
const filtered = crontab
|
|
103
|
+
.split('\n')
|
|
104
|
+
.filter((l) => !l.includes(CRON_TAG))
|
|
105
|
+
.join('\n');
|
|
106
|
+
|
|
107
|
+
const newCrontab = filtered.endsWith('\n')
|
|
108
|
+
? `${filtered}${cronLine}\n`
|
|
109
|
+
: `${filtered}\n${cronLine}\n`;
|
|
110
|
+
writeCrontab(newCrontab);
|
|
111
|
+
|
|
112
|
+
const formattedTime = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
113
|
+
return {
|
|
114
|
+
isScheduled: true,
|
|
115
|
+
time: formattedTime,
|
|
116
|
+
logPath: LOG_PATH,
|
|
117
|
+
projectDir,
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function unschedule(): CronSchedule {
|
|
125
|
+
try {
|
|
126
|
+
const crontab = getCurrentCrontab();
|
|
127
|
+
const filtered = crontab
|
|
128
|
+
.split('\n')
|
|
129
|
+
.filter((l) => !l.includes(CRON_TAG))
|
|
130
|
+
.join('\n');
|
|
131
|
+
|
|
132
|
+
writeCrontab(filtered);
|
|
133
|
+
} catch {
|
|
134
|
+
// Graceful degradation — if crontab fails, just return unscheduled
|
|
135
|
+
}
|
|
136
|
+
return { isScheduled: false, time: null, logPath: null, projectDir: null };
|
|
137
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { PersistenceProvider } from '../persistence/types.js';
|
|
2
|
+
import type { Vault } from '../vault/vault.js';
|
|
3
|
+
import type { Curator } from '../curator/curator.js';
|
|
4
|
+
|
|
5
|
+
export interface DreamReport {
|
|
6
|
+
durationMs: number;
|
|
7
|
+
duplicatesFound: number;
|
|
8
|
+
staleArchived: number;
|
|
9
|
+
contradictionsFound: number;
|
|
10
|
+
totalDreams: number;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DreamStatus {
|
|
15
|
+
sessionsSinceLastDream: number;
|
|
16
|
+
lastDreamAt: string | null;
|
|
17
|
+
lastDreamDurationMs: number | null;
|
|
18
|
+
totalDreams: number;
|
|
19
|
+
gateEligible: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DreamEngine {
|
|
23
|
+
private provider: PersistenceProvider;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
vault: Vault,
|
|
27
|
+
private curator: Curator,
|
|
28
|
+
private sessionThreshold: number = 5,
|
|
29
|
+
private hourThreshold: number = 24,
|
|
30
|
+
) {
|
|
31
|
+
this.provider = vault.getProvider();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
run(): DreamReport {
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
const result = this.curator.consolidate({
|
|
37
|
+
dryRun: false,
|
|
38
|
+
staleDaysThreshold: 90,
|
|
39
|
+
duplicateThreshold: 0.45,
|
|
40
|
+
contradictionThreshold: 0.4,
|
|
41
|
+
});
|
|
42
|
+
const durationMs = Date.now() - start;
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
this.provider.run(
|
|
46
|
+
`UPDATE dream_meta SET
|
|
47
|
+
sessions_since_last_dream = 0,
|
|
48
|
+
last_dream_at = ?,
|
|
49
|
+
last_dream_duration_ms = ?,
|
|
50
|
+
last_dream_report = ?,
|
|
51
|
+
total_dreams = total_dreams + 1,
|
|
52
|
+
updated_at = ?
|
|
53
|
+
WHERE id = 1`,
|
|
54
|
+
[now, durationMs, JSON.stringify(result), now],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const meta = this.getMeta();
|
|
58
|
+
return {
|
|
59
|
+
durationMs,
|
|
60
|
+
duplicatesFound: result.duplicates?.length ?? 0,
|
|
61
|
+
staleArchived: result.staleEntries?.length ?? 0,
|
|
62
|
+
contradictionsFound: result.contradictions?.length ?? 0,
|
|
63
|
+
totalDreams: meta.total_dreams as number,
|
|
64
|
+
timestamp: now,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
incrementSessionCount(): void {
|
|
69
|
+
this.provider.run(
|
|
70
|
+
"UPDATE dream_meta SET sessions_since_last_dream = sessions_since_last_dream + 1, updated_at = datetime('now') WHERE id = 1",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getStatus(): DreamStatus {
|
|
75
|
+
const meta = this.getMeta();
|
|
76
|
+
return {
|
|
77
|
+
sessionsSinceLastDream: meta.sessions_since_last_dream as number,
|
|
78
|
+
lastDreamAt: meta.last_dream_at as string | null,
|
|
79
|
+
lastDreamDurationMs: meta.last_dream_duration_ms as number | null,
|
|
80
|
+
totalDreams: meta.total_dreams as number,
|
|
81
|
+
gateEligible: this.isGateEligible(meta),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
checkGate(): { eligible: boolean; reason: string } {
|
|
86
|
+
const meta = this.getMeta();
|
|
87
|
+
const sessions = meta.sessions_since_last_dream as number;
|
|
88
|
+
const lastDream = meta.last_dream_at as string | null;
|
|
89
|
+
if (sessions < this.sessionThreshold) {
|
|
90
|
+
return {
|
|
91
|
+
eligible: false,
|
|
92
|
+
reason: `Only ${sessions}/${this.sessionThreshold} sessions since last dream`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (lastDream) {
|
|
96
|
+
const hoursSince = (Date.now() - new Date(lastDream).getTime()) / (1000 * 60 * 60);
|
|
97
|
+
if (hoursSince < this.hourThreshold) {
|
|
98
|
+
return {
|
|
99
|
+
eligible: false,
|
|
100
|
+
reason: `Only ${Math.round(hoursSince)}h/${this.hourThreshold}h since last dream`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { eligible: true, reason: 'Gate conditions met' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private isGateEligible(meta: Record<string, unknown>): boolean {
|
|
108
|
+
const sessions = meta.sessions_since_last_dream as number;
|
|
109
|
+
const lastDream = meta.last_dream_at as string | null;
|
|
110
|
+
if (sessions < this.sessionThreshold) return false;
|
|
111
|
+
if (!lastDream) return true;
|
|
112
|
+
const hoursSince = (Date.now() - new Date(lastDream).getTime()) / (1000 * 60 * 60);
|
|
113
|
+
return hoursSince >= this.hourThreshold;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getMeta(): Record<string, unknown> {
|
|
117
|
+
return this.provider.get('SELECT * FROM dream_meta WHERE id = 1') as Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dream runtime ops — facade operations for the dream engine.
|
|
3
|
+
* dream_run, dream_status, dream_check_gate.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
8
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
9
|
+
import { DreamEngine } from './dream-engine.js';
|
|
10
|
+
import { ensureDreamSchema } from './schema.js';
|
|
11
|
+
|
|
12
|
+
export function createDreamOps(runtime: AgentRuntime): OpDefinition[] {
|
|
13
|
+
const { vault, curator } = runtime;
|
|
14
|
+
ensureDreamSchema(vault.getProvider());
|
|
15
|
+
const engine = new DreamEngine(vault, curator);
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
name: 'dream_run',
|
|
20
|
+
description:
|
|
21
|
+
'Run a dream cycle — consolidate vault knowledge (duplicates, stale entries, contradictions). Checks gate unless force=true.',
|
|
22
|
+
auth: 'write',
|
|
23
|
+
schema: z.object({
|
|
24
|
+
force: z.boolean().optional().describe('Skip gate check. Default false.'),
|
|
25
|
+
}),
|
|
26
|
+
handler: async (params) => {
|
|
27
|
+
const force = (params.force as boolean) ?? false;
|
|
28
|
+
if (!force) {
|
|
29
|
+
const gate = engine.checkGate();
|
|
30
|
+
if (!gate.eligible) {
|
|
31
|
+
return { skipped: true, reason: gate.reason, status: engine.getStatus() };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return engine.run();
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'dream_status',
|
|
39
|
+
description:
|
|
40
|
+
'Dream status — sessions since last dream, last dream timestamp, gate eligibility.',
|
|
41
|
+
auth: 'read',
|
|
42
|
+
handler: async () => {
|
|
43
|
+
return engine.getStatus();
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'dream_check_gate',
|
|
48
|
+
description:
|
|
49
|
+
'Check whether dream gate conditions are met (session threshold + time threshold).',
|
|
50
|
+
auth: 'read',
|
|
51
|
+
handler: async () => {
|
|
52
|
+
return engine.checkGate();
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Vault } from '../vault/vault.js';
|
|
3
|
+
import { Curator } from '../curator/curator.js';
|
|
4
|
+
import { ensureDreamSchema } from './schema.js';
|
|
5
|
+
import { DreamEngine } from './dream-engine.js';
|
|
6
|
+
import { createDreamOps } from './dream-ops.js';
|
|
7
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
8
|
+
|
|
9
|
+
describe('dream schema', () => {
|
|
10
|
+
let vault: Vault;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vault = new Vault(':memory:');
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vault.close();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('creates dream_meta table', () => {
|
|
19
|
+
ensureDreamSchema(vault.getProvider());
|
|
20
|
+
const info = vault
|
|
21
|
+
.getProvider()
|
|
22
|
+
.get("SELECT name FROM sqlite_master WHERE type='table' AND name='dream_meta'");
|
|
23
|
+
expect(info).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('initializes single row with defaults', () => {
|
|
27
|
+
ensureDreamSchema(vault.getProvider());
|
|
28
|
+
const row = vault.getProvider().get('SELECT * FROM dream_meta WHERE id = 1') as Record<
|
|
29
|
+
string,
|
|
30
|
+
unknown
|
|
31
|
+
>;
|
|
32
|
+
expect(row.sessions_since_last_dream).toBe(0);
|
|
33
|
+
expect(row.total_dreams).toBe(0);
|
|
34
|
+
expect(row.last_dream_at).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('DreamEngine', () => {
|
|
39
|
+
let vault: Vault;
|
|
40
|
+
let curator: Curator;
|
|
41
|
+
let engine: DreamEngine;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vault = new Vault(':memory:');
|
|
45
|
+
ensureDreamSchema(vault.getProvider());
|
|
46
|
+
curator = new Curator(vault);
|
|
47
|
+
engine = new DreamEngine(vault, curator);
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vault.close();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('run() returns a dream report', () => {
|
|
54
|
+
const report = engine.run();
|
|
55
|
+
expect(report).toHaveProperty('durationMs');
|
|
56
|
+
expect(report).toHaveProperty('duplicatesFound');
|
|
57
|
+
expect(report).toHaveProperty('staleArchived');
|
|
58
|
+
expect(report).toHaveProperty('contradictionsFound');
|
|
59
|
+
expect(report).toHaveProperty('totalDreams');
|
|
60
|
+
expect(report.totalDreams).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('run() resets sessions_since_last_dream to 0', () => {
|
|
64
|
+
for (let i = 0; i < 5; i++) engine.incrementSessionCount();
|
|
65
|
+
expect(engine.getStatus().sessionsSinceLastDream).toBe(5);
|
|
66
|
+
engine.run();
|
|
67
|
+
expect(engine.getStatus().sessionsSinceLastDream).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('incrementSessionCount increases counter', () => {
|
|
71
|
+
expect(engine.getStatus().sessionsSinceLastDream).toBe(0);
|
|
72
|
+
engine.incrementSessionCount();
|
|
73
|
+
expect(engine.getStatus().sessionsSinceLastDream).toBe(1);
|
|
74
|
+
engine.incrementSessionCount();
|
|
75
|
+
expect(engine.getStatus().sessionsSinceLastDream).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('getStatus returns current dream state', () => {
|
|
79
|
+
const status = engine.getStatus();
|
|
80
|
+
expect(status.sessionsSinceLastDream).toBe(0);
|
|
81
|
+
expect(status.lastDreamAt).toBeNull();
|
|
82
|
+
expect(status.totalDreams).toBe(0);
|
|
83
|
+
expect(status.gateEligible).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('gate conditions', () => {
|
|
87
|
+
it('not eligible with 0 sessions', () => {
|
|
88
|
+
const gate = engine.checkGate();
|
|
89
|
+
expect(gate.eligible).toBe(false);
|
|
90
|
+
expect(gate.reason).toContain('0/5');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('eligible after 5 sessions and no prior dream', () => {
|
|
94
|
+
for (let i = 0; i < 5; i++) engine.incrementSessionCount();
|
|
95
|
+
const gate = engine.checkGate();
|
|
96
|
+
expect(gate.eligible).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('not eligible if dreamed less than 24h ago', () => {
|
|
100
|
+
for (let i = 0; i < 5; i++) engine.incrementSessionCount();
|
|
101
|
+
engine.run();
|
|
102
|
+
for (let i = 0; i < 5; i++) engine.incrementSessionCount();
|
|
103
|
+
const gate = engine.checkGate();
|
|
104
|
+
expect(gate.eligible).toBe(false);
|
|
105
|
+
expect(gate.reason).toContain('h/24h');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('force run works regardless of gate', () => {
|
|
109
|
+
const gate = engine.checkGate();
|
|
110
|
+
expect(gate.eligible).toBe(false);
|
|
111
|
+
const report = engine.run();
|
|
112
|
+
expect(report.totalDreams).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('dream ops', () => {
|
|
118
|
+
let vault: Vault;
|
|
119
|
+
let ops: ReturnType<typeof createDreamOps>;
|
|
120
|
+
|
|
121
|
+
function findOp(name: string) {
|
|
122
|
+
const op = ops.find((o) => o.name === name);
|
|
123
|
+
if (!op) throw new Error(`Op ${name} not found`);
|
|
124
|
+
return op;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
vault = new Vault(':memory:');
|
|
129
|
+
const curator = new Curator(vault);
|
|
130
|
+
const runtime = { vault, curator } as unknown as AgentRuntime;
|
|
131
|
+
ops = createDreamOps(runtime);
|
|
132
|
+
});
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
vault.close();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('creates 3 ops with correct names', () => {
|
|
138
|
+
expect(ops).toHaveLength(3);
|
|
139
|
+
expect(ops.map((o) => o.name).sort()).toEqual(
|
|
140
|
+
['dream_check_gate', 'dream_run', 'dream_status'].sort(),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('dream_status returns status', async () => {
|
|
145
|
+
const result = (await findOp('dream_status').handler({})) as Record<string, unknown>;
|
|
146
|
+
expect(result).toHaveProperty('sessionsSinceLastDream');
|
|
147
|
+
expect(result).toHaveProperty('gateEligible');
|
|
148
|
+
expect(result.sessionsSinceLastDream).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('dream_check_gate returns gate info', async () => {
|
|
152
|
+
const result = (await findOp('dream_check_gate').handler({})) as Record<string, unknown>;
|
|
153
|
+
expect(result).toHaveProperty('eligible');
|
|
154
|
+
expect(result).toHaveProperty('reason');
|
|
155
|
+
expect(result.eligible).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('dream_run skips when gate not met and force=false', async () => {
|
|
159
|
+
const result = (await findOp('dream_run').handler({})) as Record<string, unknown>;
|
|
160
|
+
expect(result.skipped).toBe(true);
|
|
161
|
+
expect(result.reason).toBeDefined();
|
|
162
|
+
expect(result.status).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('dream_run executes when force=true', async () => {
|
|
166
|
+
const result = (await findOp('dream_run').handler({ force: true })) as Record<string, unknown>;
|
|
167
|
+
expect(result.skipped).toBeUndefined();
|
|
168
|
+
expect(result).toHaveProperty('durationMs');
|
|
169
|
+
expect(result).toHaveProperty('totalDreams');
|
|
170
|
+
expect(result.totalDreams).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('dream_run executes when gate is met', async () => {
|
|
174
|
+
// Increment sessions to meet threshold (default 5)
|
|
175
|
+
const engine = new DreamEngine(vault, new Curator(vault));
|
|
176
|
+
for (let i = 0; i < 5; i++) engine.incrementSessionCount();
|
|
177
|
+
|
|
178
|
+
const result = (await findOp('dream_run').handler({})) as Record<string, unknown>;
|
|
179
|
+
expect(result.skipped).toBeUndefined();
|
|
180
|
+
expect(result).toHaveProperty('totalDreams');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ensureDreamSchema } from './schema.js';
|
|
2
|
+
export { DreamEngine } from './dream-engine.js';
|
|
3
|
+
export type { DreamReport, DreamStatus } from './dream-engine.js';
|
|
4
|
+
export { createDreamOps } from './dream-ops.js';
|
|
5
|
+
export { getSchedule, schedule, unschedule } from './cron-manager.js';
|
|
6
|
+
export type { CronSchedule } from './cron-manager.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PersistenceProvider } from '../persistence/types.js';
|
|
2
|
+
|
|
3
|
+
export function ensureDreamSchema(provider: PersistenceProvider): void {
|
|
4
|
+
provider.execSql(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS dream_meta (
|
|
6
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
7
|
+
sessions_since_last_dream INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
last_dream_at TEXT,
|
|
9
|
+
last_dream_duration_ms INTEGER,
|
|
10
|
+
last_dream_report TEXT,
|
|
11
|
+
total_dreams INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14
|
+
)
|
|
15
|
+
`);
|
|
16
|
+
provider.run('INSERT OR IGNORE INTO dream_meta (id) VALUES (1)');
|
|
17
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Embedding Provider — generates dense vector embeddings via the
|
|
3
|
+
* OpenAI embeddings API. Supports key pool rotation and batch chunking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EmbeddingProvider, EmbeddingResult, EmbeddingConfig } from './types.js';
|
|
7
|
+
import type { KeyPool } from '../llm/key-pool.js';
|
|
8
|
+
import { LLMError } from '../llm/types.js';
|
|
9
|
+
import { retry } from '../llm/utils.js';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CONSTANTS
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MODEL = 'text-embedding-3-small';
|
|
16
|
+
const DEFAULT_DIMENSIONS = 1536;
|
|
17
|
+
const DEFAULT_BATCH_SIZE = 2048;
|
|
18
|
+
const DEFAULT_BASE_URL = 'https://api.openai.com';
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// RESPONSE TYPES
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
interface OpenAIEmbeddingResponse {
|
|
26
|
+
data: Array<{ embedding: number[]; index: number }>;
|
|
27
|
+
usage: { prompt_tokens: number; total_tokens: number };
|
|
28
|
+
model: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// PROVIDER
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Embedding provider that calls the OpenAI `/v1/embeddings` endpoint.
|
|
37
|
+
*
|
|
38
|
+
* Supports optional {@link KeyPool} for key rotation — on 429 responses the
|
|
39
|
+
* pool is rotated so the next call uses a fresh key. If no pool is provided,
|
|
40
|
+
* falls back to `config.apiKey`.
|
|
41
|
+
*/
|
|
42
|
+
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
43
|
+
readonly providerName = 'openai';
|
|
44
|
+
readonly model: string;
|
|
45
|
+
readonly dimensions: number;
|
|
46
|
+
|
|
47
|
+
private readonly apiUrl: string;
|
|
48
|
+
private readonly batchSize: number;
|
|
49
|
+
private readonly apiKey?: string;
|
|
50
|
+
private readonly keyPool?: KeyPool;
|
|
51
|
+
|
|
52
|
+
constructor(config: EmbeddingConfig, keyPool?: KeyPool) {
|
|
53
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
54
|
+
this.dimensions = DEFAULT_DIMENSIONS;
|
|
55
|
+
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
56
|
+
this.apiKey = config.apiKey;
|
|
57
|
+
this.keyPool = keyPool;
|
|
58
|
+
|
|
59
|
+
const baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
60
|
+
this.apiUrl = `${baseUrl}/v1/embeddings`;
|
|
61
|
+
|
|
62
|
+
if (!this.keyPool?.hasKeys && !this.apiKey) {
|
|
63
|
+
throw new LLMError(
|
|
64
|
+
'OpenAI embedding provider requires an API key — provide config.apiKey or a KeyPool with keys',
|
|
65
|
+
{ retryable: false },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Embed one or more texts, returning one vector per input text.
|
|
72
|
+
* Automatically chunks requests that exceed {@link batchSize}.
|
|
73
|
+
*/
|
|
74
|
+
async embed(texts: string[]): Promise<EmbeddingResult> {
|
|
75
|
+
if (texts.length === 0) {
|
|
76
|
+
return { vectors: [], tokensUsed: 0, model: this.model };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Single batch — no chunking needed
|
|
80
|
+
if (texts.length <= this.batchSize) {
|
|
81
|
+
return this.callApi(texts);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Chunk into batches and concatenate results
|
|
85
|
+
const allVectors: number[][] = [];
|
|
86
|
+
let totalTokens = 0;
|
|
87
|
+
|
|
88
|
+
for (let offset = 0; offset < texts.length; offset += this.batchSize) {
|
|
89
|
+
const chunk = texts.slice(offset, offset + this.batchSize);
|
|
90
|
+
// oxlint-disable-next-line eslint(no-await-in-loop)
|
|
91
|
+
const result = await this.callApi(chunk);
|
|
92
|
+
allVectors.push(...result.vectors);
|
|
93
|
+
totalTokens += result.tokensUsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { vectors: allVectors, tokensUsed: totalTokens, model: this.model };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ===========================================================================
|
|
100
|
+
// INTERNALS
|
|
101
|
+
// ===========================================================================
|
|
102
|
+
|
|
103
|
+
private resolveApiKey(): string {
|
|
104
|
+
if (this.keyPool?.hasKeys) {
|
|
105
|
+
return this.keyPool.getActiveKey().expose();
|
|
106
|
+
}
|
|
107
|
+
if (this.apiKey) {
|
|
108
|
+
return this.apiKey;
|
|
109
|
+
}
|
|
110
|
+
throw new LLMError('No API key available for OpenAI embeddings', { retryable: false });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async callApi(texts: string[]): Promise<EmbeddingResult> {
|
|
114
|
+
const doRequest = async (): Promise<EmbeddingResult> => {
|
|
115
|
+
const apiKey = this.resolveApiKey();
|
|
116
|
+
|
|
117
|
+
const response = await fetch(this.apiUrl, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
Authorization: `Bearer ${apiKey}`,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
model: this.model,
|
|
125
|
+
input: texts,
|
|
126
|
+
}),
|
|
127
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
// Rotate key on rate limit
|
|
132
|
+
if (response.status === 429 && this.keyPool && this.keyPool.poolSize > 1) {
|
|
133
|
+
this.keyPool.rotateOnError();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const errorBody = await response.text();
|
|
137
|
+
throw new LLMError(`OpenAI Embeddings API error: ${response.status} - ${errorBody}`, {
|
|
138
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
139
|
+
statusCode: response.status,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = (await response.json()) as OpenAIEmbeddingResponse;
|
|
144
|
+
|
|
145
|
+
// OpenAI returns data sorted by index, but sort defensively
|
|
146
|
+
const sorted = [...data.data].sort((a, b) => a.index - b.index);
|
|
147
|
+
const vectors = sorted.map((d) => d.embedding);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
vectors,
|
|
151
|
+
tokensUsed: data.usage?.total_tokens ?? 0,
|
|
152
|
+
model: this.model,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return retry(doRequest, { maxAttempts: 3 });
|
|
157
|
+
}
|
|
158
|
+
}
|