@stackbilt/aegis-core 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbilt/aegis-core",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
package/src/groq.ts CHANGED
@@ -30,11 +30,16 @@ export async function askGroq(
30
30
  }
31
31
 
32
32
  const data = await response.json<{
33
- choices: { message: { content: string } }[];
33
+ choices: { message: { content: unknown } }[];
34
34
  usage?: { total_tokens: number };
35
35
  }>();
36
36
 
37
- return data.choices[0]?.message?.content ?? '';
37
+ const content = data.choices[0]?.message?.content;
38
+ if (typeof content === 'string') return content;
39
+ if (content == null) return '';
40
+ // Some Groq-routed models (notably gpt-oss tool-calling variants) return content
41
+ // as an array of content blocks. Coerce so downstream string operations don't crash.
42
+ return typeof content === 'object' ? JSON.stringify(content) : String(content);
38
43
  }
39
44
 
40
45
  // ─── Logprobs-enabled classification ─────────────────────────
@@ -54,6 +54,7 @@ export interface EdgeEnv {
54
54
  codebeastFetcher?: Fetcher;
55
55
  mindspringFetcher?: Fetcher;
56
56
  mindspringToken?: string;
57
+ mindspringIngestToken?: string;
57
58
  devtoApiKey?: string;
58
59
  gaCredentials?: string;
59
60
  blueskyHandle?: string;
@@ -43,6 +43,8 @@ interface MindSpringResult {
43
43
  title: string;
44
44
  text: string;
45
45
  score: number;
46
+ notebook_id?: string;
47
+ notebook_title?: string;
46
48
  }
47
49
 
48
50
  // ─── RRF (Reciprocal Rank Fusion) ────────────────────────────
@@ -143,8 +145,12 @@ export async function recallForQuery(
143
145
  : query;
144
146
 
145
147
  const msResponse = await env.mindspringFetcher.fetch(
146
- `https://mindspring/api/search?q=${encodeURIComponent(msQuery)}&limit=5&threshold=0.4`,
147
- { headers: { 'Authorization': `Bearer ${env.mindspringToken}` } },
148
+ 'https://mindspring/api/v2/workspaces/aegis-daemon/search',
149
+ {
150
+ method: 'POST',
151
+ headers: { 'Authorization': `Bearer ${env.mindspringToken}`, 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({ query: msQuery, limit: 5, threshold: 0.4 }),
153
+ },
148
154
  );
149
155
 
150
156
  if (msResponse.ok) {
@@ -79,8 +79,14 @@ async function classifyWithWorkersAI(
79
79
  ],
80
80
  max_tokens: 200,
81
81
  temperature: 0.1,
82
- }) as { response?: string };
83
- return result.response ?? '';
82
+ }) as { response?: unknown };
83
+ const raw = result.response;
84
+ if (typeof raw === 'string') return raw;
85
+ if (raw == null) return '';
86
+ // Workers AI sometimes returns structured responses (objects with tool_calls,
87
+ // arrays of segments, etc.). Coerce to string so downstream .trim()/JSON.parse
88
+ // callers don't crash on non-string payloads.
89
+ return typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
84
90
  }
85
91
 
86
92
 
@@ -21,7 +21,15 @@ import type { CorrelationResult, IncidentCluster, ArgusDiagnosis } from '../argu
21
21
  // ─── Configuration ───────────────────────────────────────────
22
22
 
23
23
  const RUN_CADENCE_HOURS = 3;
24
- const COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h cooldown per pattern alert
24
+ const COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h default cooldown per pattern alert
25
+
26
+ // Per-pattern cooldown overrides. Drought patterns are expected to persist in
27
+ // pre-revenue or low-activity states — a longer cooldown prevents daily noise
28
+ // from a condition that isn't going to self-resolve on a 12h cycle.
29
+ const PATTERN_COOLDOWN_MS: Record<string, number> = {
30
+ drought_stripe: 72 * 60 * 60 * 1000, // 72h — expected in pre-revenue
31
+ drought_github: 48 * 60 * 60 * 1000, // 48h
32
+ };
25
33
 
26
34
  // Pattern thresholds
27
35
  const CI_FAILURE_WINDOW_HOURS = 6;
@@ -179,7 +187,8 @@ async function isOnCooldown(db: D1Database, pattern: string): Promise<boolean> {
179
187
  ).bind(key).first<{ received_at: string }>();
180
188
 
181
189
  if (!last) return false;
182
- return (Date.now() - new Date(last.received_at + 'Z').getTime()) < COOLDOWN_MS;
190
+ const cooldown = PATTERN_COOLDOWN_MS[pattern] ?? COOLDOWN_MS;
191
+ return (Date.now() - new Date(last.received_at + 'Z').getTime()) < cooldown;
183
192
  }
184
193
 
185
194
  async function recordCooldown(db: D1Database, pattern: string): Promise<void> {
@@ -1,11 +1,13 @@
1
1
  // Per-conversation fact extraction (#324)
2
2
  // Complements the dreaming cycle (daily, batch) with near-real-time
3
3
  // fact capture from operator chat sessions. Runs every 2 hours,
4
- // processes conversations updated since last run. Uses Workers AI (free).
4
+ // processes conversations updated since last run. Uses Groq (free) with
5
+ // Workers AI llama-3.1-8b (free tier) as fallback.
5
6
 
6
7
  import { type EdgeEnv } from '../dispatch.js';
7
8
  import { recordMemory as recordMemoryAdapter } from '../memory-adapter.js';
8
9
  import { askGroq } from '../../groq.js';
10
+ import { pushFactsToMindSpring, type FactEntry } from './mindspring-notebook.js';
9
11
 
10
12
  const WATERMARK_KEY = 'conversation_facts_watermark';
11
13
  const MAX_CONVERSATIONS = 5;
@@ -85,16 +87,25 @@ async function askAi(
85
87
  system: string,
86
88
  user: string,
87
89
  ): Promise<string> {
90
+ // Groq first — free, same 70B quality, no neuron consumption
91
+ if (env.groqApiKey) {
92
+ try {
93
+ return await askGroq(env.groqApiKey, env.groqResponseModel, system, user, env.groqBaseUrl);
94
+ } catch {
95
+ // fall through to Workers AI
96
+ }
97
+ }
98
+ // Workers AI fallback — llama-3.1-8b is on the genuine free tier
88
99
  if (env.ai) {
89
100
  const result = await env.ai.run(
90
- '@cf/meta/llama-3.3-70b-instruct-fp8-fast' as Parameters<Ai['run']>[0],
101
+ '@cf/meta/llama-3.1-8b-instruct' as Parameters<Ai['run']>[0],
91
102
  { messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
92
103
  );
93
104
  if (typeof result === 'string') return result;
94
105
  const obj = result as { response?: string; choices?: Array<{ message?: { content?: string } }> };
95
106
  return obj.choices?.[0]?.message?.content ?? obj.response ?? '';
96
107
  }
97
- return askGroq(env.groqApiKey, env.groqResponseModel, system, user, env.groqBaseUrl);
108
+ throw new Error('[conv-facts] No LLM provider available (groqApiKey and env.ai both missing)');
98
109
  }
99
110
 
100
111
  export async function runConversationFactExtraction(env: EdgeEnv): Promise<void> {
@@ -128,6 +139,7 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
128
139
  }
129
140
 
130
141
  let totalFacts = 0;
142
+ const allFacts: FactEntry[] = [];
131
143
 
132
144
  for (const conv of conversations.results) {
133
145
  const messages = await env.db.prepare(`
@@ -185,6 +197,7 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
185
197
  fact.confidence ?? 0.8,
186
198
  'conversation_extraction',
187
199
  );
200
+ allFacts.push({ topic: topicLower, fact: fact.fact, confidence: fact.confidence ?? 0.8 });
188
201
  totalFacts++;
189
202
  console.log(`[conv-facts] Extracted: [${topicLower}] ${fact.fact.slice(0, 80)}`);
190
203
  } catch (err) {
@@ -195,6 +208,12 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
195
208
 
196
209
  await advanceWatermark(env.db);
197
210
  console.log(`[conv-facts] Processed ${conversations.results.length} conversations, extracted ${totalFacts} facts`);
211
+
212
+ // Push to MindSpring topic notebooks (non-blocking, never throws)
213
+ if (allFacts.length > 0) {
214
+ const runTag = `conv-facts-${Date.now().toString(36)}`;
215
+ await pushFactsToMindSpring(allFacts, runTag, env);
216
+ }
198
217
  }
199
218
 
200
219
  async function advanceWatermark(db: D1Database): Promise<void> {
@@ -8,11 +8,12 @@ import { type HeartbeatCheck } from './heartbeat.js';
8
8
  export interface CuriosityCandidate {
9
9
  topic: string;
10
10
  reason: string;
11
- source: 'memory_gap' | 'low_confidence' | 'failure_rate' | 'heartbeat_warn' | 'goal_failure' | 'self_interest';
11
+ source: 'memory_gap' | 'low_confidence' | 'failure_rate' | 'heartbeat_warn' | 'goal_failure' | 'self_interest' | 'conversation_gap';
12
12
  }
13
13
 
14
14
  export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCandidate[]> {
15
15
  const candidates: CuriosityCandidate[] = [];
16
+ const thinTopicSeeds: string[] = [];
16
17
 
17
18
  // Source 1: Memory gaps — topics with few entries relative to others
18
19
  if (env.memoryBinding) {
@@ -20,6 +21,7 @@ export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCand
20
21
  const stats = await env.memoryBinding.stats('aegis');
21
22
  const thinTopics = stats.topics.filter(t => t.count <= 2).slice(0, 5);
22
23
  for (const t of thinTopics) {
24
+ thinTopicSeeds.push(t.topic);
23
25
  candidates.push({
24
26
  topic: `What more should I know about "${t.topic}"?`,
25
27
  reason: `Only ${t.count} memory entries — thin coverage`,
@@ -154,6 +156,38 @@ export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCand
154
156
  }
155
157
  }
156
158
 
159
+ // Source 8: MindSpring conversation gap — topics MW barely knows but that appear in
160
+ // conversation history signal a consolidation pipeline failure, not just a knowledge gap.
161
+ const { mindspringFetcher, mindspringToken } = env;
162
+ if (mindspringFetcher && mindspringToken && thinTopicSeeds.length > 0) {
163
+ try {
164
+ const queryResults = await Promise.allSettled(
165
+ thinTopicSeeds.slice(0, 3).map(async (seed) => {
166
+ const res = await mindspringFetcher.fetch('https://mindspring/api/v2/workspaces/aegis-daemon/search', {
167
+ method: 'POST',
168
+ signal: AbortSignal.timeout(1500),
169
+ headers: { Authorization: `Bearer ${mindspringToken}`, 'Content-Type': 'application/json' },
170
+ body: JSON.stringify({ query: seed, limit: 5, threshold: 0.5 }),
171
+ });
172
+ if (!res.ok) return { seed, count: 0 };
173
+ const data = await res.json<{ results: Array<{ title: string; score: number }> }>();
174
+ return { seed, count: (data.results ?? []).length };
175
+ })
176
+ );
177
+ for (const r of queryResults) {
178
+ if (r.status === 'fulfilled' && r.value.count > 0) {
179
+ candidates.push({
180
+ topic: `"${r.value.seed}" appears in conversation history but has thin memory coverage`,
181
+ reason: `${r.value.count} MindSpring matches vs ≤2 memory entries — consolidation gap`,
182
+ source: 'conversation_gap',
183
+ });
184
+ }
185
+ }
186
+ } catch (err) {
187
+ console.warn('[curiosity] MindSpring gap scan failed:', err instanceof Error ? err.message : String(err));
188
+ }
189
+ }
190
+
157
191
  return candidates;
158
192
  }
159
193
 
@@ -1,4 +1,4 @@
1
- // Shared LLM helper — Workers AI with Groq fallback (zero-cost primary, paid fallback)
1
+ // Shared LLM helper — Groq first (free, 70B quality), Workers AI 70B fallback
2
2
 
3
3
  import type { EdgeEnv } from '../../dispatch.js';
4
4
  import { askGroq } from '../../../groq.js';
@@ -9,18 +9,24 @@ export async function askWorkersAiOrGroq(
9
9
  user: string,
10
10
  useResponseModel = false,
11
11
  ): Promise<string> {
12
+ const groqModel = useResponseModel ? env.groqResponseModel : env.groqModel;
13
+ // Groq first — free tier, same 70B quality, eliminates neuron consumption
14
+ if (env.groqApiKey) {
15
+ try {
16
+ return await askGroq(env.groqApiKey, groqModel, system, user, env.groqBaseUrl);
17
+ } catch {
18
+ // fall through to Workers AI
19
+ }
20
+ }
21
+ // Workers AI fallback — only fires if Groq is unavailable or throws
12
22
  if (env.ai) {
13
- const model = useResponseModel
14
- ? '@cf/meta/llama-3.3-70b-instruct-fp8-fast'
15
- : (env.gptOssModel || '@cf/meta/llama-3.3-70b-instruct-fp8-fast');
16
23
  const result = await env.ai.run(
17
- model as Parameters<Ai['run']>[0],
24
+ '@cf/meta/llama-3.3-70b-instruct-fp8-fast' as Parameters<Ai['run']>[0],
18
25
  { messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
19
26
  ) as { response?: string; choices?: Array<{ message?: { content?: string } }> };
20
27
  return result.choices?.[0]?.message?.content ?? result.response ?? '';
21
28
  }
22
- const groqModel = useResponseModel ? env.groqResponseModel : env.groqModel;
23
- return askGroq(env.groqApiKey, groqModel, system, user, env.groqBaseUrl);
29
+ throw new Error('[dreaming] No LLM provider available (groqApiKey and env.ai both missing)');
24
30
  }
25
31
 
26
32
  export function parseJsonResponse<T>(raw: string): T | null {
@@ -0,0 +1,132 @@
1
+ // MindSpring v2 write pipeline — push extracted facts to topic notebooks
2
+
3
+ import type { EdgeEnv } from '../dispatch.js';
4
+
5
+ const WORKSPACE_ID = 'aegis-daemon';
6
+ const MS_BASE = 'https://mindspring';
7
+
8
+ interface MsNotebook { id: string; title: string }
9
+ interface UploadAccepted { uploadId: string; status: string }
10
+
11
+ export interface FactEntry {
12
+ topic: string;
13
+ fact: string;
14
+ confidence: number;
15
+ }
16
+
17
+ function msHeaders(token: string, extra?: Record<string, string>): Headers {
18
+ const h = new Headers(extra);
19
+ h.set('Authorization', `Bearer ${token}`);
20
+ return h;
21
+ }
22
+
23
+ async function findOrCreateNotebook(topic: string, env: EdgeEnv): Promise<string> {
24
+ const token = env.mindspringIngestToken!;
25
+ const fetcher = env.mindspringFetcher!;
26
+
27
+ const listResp = await fetcher.fetch(
28
+ `${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks`,
29
+ { headers: msHeaders(token) },
30
+ );
31
+ if (listResp.ok) {
32
+ const data = await listResp.json<{ notebooks: MsNotebook[] }>();
33
+ const existing = data.notebooks?.find((nb) => nb.title === topic);
34
+ if (existing) return existing.id;
35
+ }
36
+
37
+ const createResp = await fetcher.fetch(
38
+ `${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks`,
39
+ {
40
+ method: 'POST',
41
+ headers: msHeaders(token, { 'Content-Type': 'application/json' }),
42
+ body: JSON.stringify({ title: topic, type: 'research' }),
43
+ },
44
+ );
45
+ if (!createResp.ok) {
46
+ const msg = await createResp.text().catch(() => '');
47
+ throw new Error(`create notebook failed: ${createResp.status} ${msg.slice(0, 120)}`);
48
+ }
49
+ const nb = await createResp.json<MsNotebook>();
50
+ return nb.id;
51
+ }
52
+
53
+ async function uploadContent(content: string, filename: string, env: EdgeEnv): Promise<string> {
54
+ const token = env.mindspringIngestToken!;
55
+ const fetcher = env.mindspringFetcher!;
56
+
57
+ const resp = await fetcher.fetch(`${MS_BASE}/api/uploads/simple`, {
58
+ method: 'POST',
59
+ headers: msHeaders(token, {
60
+ 'Content-Type': 'text/plain',
61
+ 'X-File-Name': filename,
62
+ }),
63
+ body: content,
64
+ });
65
+ if (!resp.ok) {
66
+ const msg = await resp.text().catch(() => '');
67
+ throw new Error(`upload failed: ${resp.status} ${msg.slice(0, 120)}`);
68
+ }
69
+ const { uploadId } = await resp.json<UploadAccepted>();
70
+ return uploadId;
71
+ }
72
+
73
+ async function registerSource(notebookId: string, title: string, uploadId: string, env: EdgeEnv): Promise<void> {
74
+ const token = env.mindspringIngestToken!;
75
+ const fetcher = env.mindspringFetcher!;
76
+
77
+ const resp = await fetcher.fetch(
78
+ `${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks/${notebookId}/sources`,
79
+ {
80
+ method: 'POST',
81
+ headers: msHeaders(token, { 'Content-Type': 'application/json' }),
82
+ body: JSON.stringify({ title, type: 'txt', sourceUploadId: uploadId, parserType: 'txt' }),
83
+ },
84
+ );
85
+ if (!resp.ok && resp.status !== 202) {
86
+ const msg = await resp.text().catch(() => '');
87
+ throw new Error(`register source failed: ${resp.status} ${msg.slice(0, 120)}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Push extracted facts to MindSpring v2 topic notebooks.
93
+ * Groups facts by topic, creates notebooks as needed.
94
+ * Never throws — all errors are logged as warnings.
95
+ */
96
+ export async function pushFactsToMindSpring(
97
+ facts: FactEntry[],
98
+ sourceTag: string,
99
+ env: EdgeEnv,
100
+ ): Promise<void> {
101
+ if (!env.mindspringFetcher || !env.mindspringIngestToken || facts.length === 0) return;
102
+
103
+ // Group by topic
104
+ const byTopic = new Map<string, string[]>();
105
+ for (const { topic, fact } of facts) {
106
+ const arr = byTopic.get(topic) ?? [];
107
+ arr.push(fact);
108
+ byTopic.set(topic, arr);
109
+ }
110
+
111
+ const date = new Date().toISOString().slice(0, 10);
112
+
113
+ for (const [topic, topicFacts] of byTopic.entries()) {
114
+ try {
115
+ const content = [
116
+ `Topic: ${topic}`,
117
+ `Source: ${sourceTag}`,
118
+ `Date: ${date}`,
119
+ '',
120
+ ...topicFacts.map((f) => `- ${f}`),
121
+ ].join('\n');
122
+
123
+ const notebookId = await findOrCreateNotebook(topic, env);
124
+ const uploadId = await uploadContent(content, `${topic}-facts.txt`, env);
125
+ await registerSource(notebookId, `facts-${date}-${sourceTag.slice(0, 12)}`, uploadId, env);
126
+
127
+ console.log(`[mindspring-nb] pushed ${topicFacts.length} fact(s) → notebook '${topic}' (${notebookId.slice(0, 8)})`);
128
+ } catch (err) {
129
+ console.warn(`[mindspring-nb] topic '${topic}' push failed:`, err instanceof Error ? err.message : String(err));
130
+ }
131
+ }
132
+ }