@stackbilt/aegis-core 0.1.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.
Files changed (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,587 @@
1
+ import { route } from './router.js';
2
+ import { recordEpisode, retrogradeEpisode, upsertProcedure, addRefinement, degradeProcedure, getProcedure, procedureKey } from './memory/index.js';
3
+ import { searchMemoryByKeywords, getAllMemoryForContext } from './memory-adapter.js';
4
+ import { generateBriefing, formatBriefing } from './briefing.js';
5
+ import { fetchRelevantInsights } from './insight-cache.js';
6
+ import { probeConsistency } from '../groq.js';
7
+ import { executeComposite } from '../composite.js';
8
+ import { buildGroqSystemPrompt } from '../operator/prompt-builder.js';
9
+ import type { KernelIntent, DispatchResult, Executor } from './types.js';
10
+ import {
11
+ executeClaude,
12
+ executeClaudeOpus,
13
+ executeClaudeStream,
14
+ executeGroq,
15
+ executeWorkersAi,
16
+ executeGptOss,
17
+ executeDirect,
18
+ executeCodeTask,
19
+ executeWithAnthropicFailover,
20
+ executeTarotScript,
21
+ buildMcpRegistry,
22
+ } from './executors/index.js';
23
+ // ─── Edge Environment ────────────────────────────────────────
24
+
25
+ export interface EdgeEnv {
26
+ db: D1Database;
27
+ anthropicApiKey: string;
28
+ claudeModel: string;
29
+ opusModel: string;
30
+ gptOssModel: string;
31
+ groqApiKey: string;
32
+ groqModel: string;
33
+ groqResponseModel: string;
34
+ groqGptOssModel: string;
35
+ bizopsFetcher: Fetcher;
36
+ bizopsToken: string;
37
+ resendApiKey: string;
38
+ resendApiKeyPersonal: string;
39
+ githubToken: string;
40
+ githubRepo: string;
41
+ braveApiKey: string;
42
+ notifyEmail: string;
43
+ baseUrl: string;
44
+ ai?: Ai;
45
+ aiGatewayId?: string;
46
+ anthropicBaseUrl: string;
47
+ groqBaseUrl: string;
48
+ ctx?: ExecutionContext; // optional — used to background long-running tasks
49
+ roundtableDb?: D1Database;
50
+ cfAnalyticsToken?: string;
51
+ imgForgeFetcher?: Fetcher;
52
+ imgForgeSbSecret?: string;
53
+ memoryBinding?: import('../types.js').MemoryServiceBinding;
54
+ tarotscriptFetcher?: Fetcher;
55
+ maraFetcher?: Fetcher;
56
+ maraToken?: string;
57
+ codebeastFetcher?: Fetcher;
58
+ mindspringFetcher?: Fetcher;
59
+ mindspringToken?: string;
60
+ devtoApiKey?: string;
61
+ gaCredentials?: string;
62
+ blueskyHandle?: string;
63
+ blueskyAppPassword?: string;
64
+ authBinding?: import('../types.js').AuthServiceBinding;
65
+ }
66
+
67
+ // ─── Intent Construction ─────────────────────────────────────
68
+
69
+ export function createIntent(
70
+ threadId: string,
71
+ text: string,
72
+ overrides?: Partial<KernelIntent>,
73
+ ): KernelIntent {
74
+ return {
75
+ source: { channel: 'web', threadId },
76
+ raw: text,
77
+ timestamp: Date.now(),
78
+ costCeiling: 'expensive',
79
+ ...overrides,
80
+ };
81
+ }
82
+
83
+ // ─── Self-consistency probe (#51) ────────────────────────────
84
+ // Before executing via workers_ai or gpt_oss for no-tool queries,
85
+ // fire 3 parallel Groq 8B calls. If all agree, skip the expensive executor.
86
+
87
+ const PROBE_SKIP_CLASSIFICATIONS = new Set([
88
+ 'heartbeat', 'greeting', 'code_task', 'self_improvement', 'goal_execution',
89
+ ]);
90
+
91
+ function probeEligible(
92
+ executor: string,
93
+ intent: KernelIntent,
94
+ classification: string,
95
+ ): boolean {
96
+ if (intent.needsTools) return false;
97
+ if (intent.source.channel === 'internal') return false;
98
+ if (PROBE_SKIP_CLASSIFICATIONS.has(classification)) return false;
99
+ return executor === 'workers_ai' || executor === 'gpt_oss';
100
+ }
101
+
102
+ // ─── Shared post-execution bookkeeping ───────────────────────
103
+
104
+ async function recordOutcome(
105
+ env: EdgeEnv,
106
+ intent: KernelIntent,
107
+ classification: string,
108
+ procKey: string,
109
+ plan: Awaited<ReturnType<typeof route>>['plan'],
110
+ existingProcedure: Awaited<ReturnType<typeof getProcedure>>,
111
+ text: string,
112
+ cost: number,
113
+ latencyMs: number,
114
+ nearMiss: string | null | undefined,
115
+ outcome: 'success' | 'failure' | 'partial_failure',
116
+ reclassified?: boolean,
117
+ ): Promise<void> {
118
+ await recordEpisode(env.db, {
119
+ intent_class: classification,
120
+ channel: intent.source.channel,
121
+ summary: `${classification} via ${plan.executor}${intent.classifierSource ? ` [clf:${intent.classifierSource}]` : ''}${intent.domain ? ` [domain:${intent.domain}/${(intent.domainConfidence ?? 0).toFixed(2)}]` : ''}: ${text.slice(0, 500)}`,
122
+ outcome: outcome === 'partial_failure' ? 'failure' : outcome,
123
+ cost,
124
+ latency_ms: latencyMs,
125
+ near_miss: nearMiss,
126
+ classifier_confidence: intent.confidence,
127
+ reclassified,
128
+ thread_id: intent.source.threadId,
129
+ executor: plan.executor,
130
+ });
131
+
132
+ await upsertProcedure(env.db, procKey, plan.executor, JSON.stringify({ executor: plan.executor }), outcome, latencyMs, cost);
133
+
134
+ const isReplan = existingProcedure
135
+ && (existingProcedure.status === 'degraded' || existingProcedure.status === 'broken')
136
+ && !plan.procedureId;
137
+
138
+ if (isReplan && outcome === 'success' && existingProcedure) {
139
+ await addRefinement(env.db, procKey, {
140
+ timestamp: Date.now(),
141
+ what: `Replanned from ${existingProcedure.executor} to ${plan.executor}`,
142
+ why: `Previous executor had ${existingProcedure.consecutive_failures} consecutive failures (status: ${existingProcedure.status})`,
143
+ impact: 'pending',
144
+ });
145
+ }
146
+ }
147
+
148
+ // ─── Shared Augmentation ─────────────────────────────────────
149
+ // Common pre-dispatch logic: insight injection, memory recall,
150
+ // implicit feedback, self-improvement ACK, and probe.
151
+
152
+ interface AugmentedContext {
153
+ plan: Awaited<ReturnType<typeof route>>['plan'];
154
+ nearMiss: string | null | undefined;
155
+ reclassified: boolean | undefined;
156
+ classification: string;
157
+ procKey: string;
158
+ existingProcedure: Awaited<ReturnType<typeof getProcedure>>;
159
+ }
160
+
161
+ async function augmentIntent(intent: KernelIntent, env: EdgeEnv): Promise<AugmentedContext> {
162
+ // 1. Route
163
+ const { plan, nearMiss, reclassified } = await route(intent, env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl, env.ai, env.tarotscriptFetcher);
164
+ const classification = intent.classified ?? 'unknown';
165
+ const procKey = procedureKey(classification, intent.complexity);
166
+ const existingProcedure = await getProcedure(env.db, procKey);
167
+
168
+ // 1.3. Cross-repo insight augmentation (CRIX #106)
169
+ if (env.memoryBinding && classification !== 'greeting' && classification !== 'heartbeat') {
170
+ try {
171
+ const insightContext = await fetchRelevantInsights(env, classification, intent.raw);
172
+ if (insightContext) {
173
+ intent.raw = `${insightContext}\n\n${intent.raw}`;
174
+ }
175
+ } catch (err) {
176
+ console.warn('[dispatch] Insight fetch failed (non-fatal):', err instanceof Error ? err.message : String(err));
177
+ }
178
+ }
179
+
180
+ // 1.5. Memory-recall augmentation
181
+ if (classification === 'memory_recall') {
182
+ try {
183
+ if (env.memoryBinding) {
184
+ const relevant = await searchMemoryByKeywords(env.memoryBinding, intent.raw, 10);
185
+ if (relevant.length > 0) {
186
+ const memLines = relevant.map(m => `- [${m.topic}] ${m.fact} (confidence: ${m.confidence})`).join('\n');
187
+ intent.raw = `[Relevant memory entries matching this query]\n${memLines}\n\n[User's question]\n${intent.raw}`;
188
+ }
189
+ } else {
190
+ console.warn('[dispatch] Memory binding unavailable — skipping memory recall augmentation');
191
+ }
192
+ } catch (err) {
193
+ console.warn('[dispatch] Memory search failed (non-fatal):', err instanceof Error ? err.message : String(err));
194
+ }
195
+ }
196
+
197
+ // 1.6. Implicit feedback — retrograde previous episode on user correction
198
+ if (classification === 'user_correction' && intent.source.threadId) {
199
+ try {
200
+ const retrograded = await retrogradeEpisode(env.db, intent.source.threadId);
201
+ if (retrograded) {
202
+ const badProcKey = procedureKey(retrograded.intent_class, intent.complexity);
203
+ await degradeProcedure(env.db, badProcKey, retrograded.executor ?? 'unknown');
204
+ console.log(`[dispatch] Implicit feedback: retrograded episode ${retrograded.id} (${retrograded.intent_class}/${retrograded.executor})`);
205
+ }
206
+ } catch (err) {
207
+ console.warn('[dispatch] Implicit feedback failed (non-fatal):', err instanceof Error ? err.message : String(err));
208
+ }
209
+ }
210
+
211
+ // 1.7. Greeting → Re-entry Briefing
212
+ // Instead of a dumb "hi!", generate a situational briefing so AEGIS acts like
213
+ // a co-founder catching you up: what happened, what needs you, what's active.
214
+ if (classification === 'greeting') {
215
+ try {
216
+ const briefing = await generateBriefing(env);
217
+ const briefingText = formatBriefing(briefing);
218
+ intent.raw = `[The operator just opened a session. Generate a re-entry briefing — not a greeting. Lead with what needs their attention, then what shipped, then what you're working on. Be direct, no fluff. Use the data below.]\n\n${briefingText}\n\n[Operator said: ${intent.raw}]`;
219
+ } catch (err) {
220
+ console.warn('[dispatch] Briefing generation failed (non-fatal):', err instanceof Error ? err.message : String(err));
221
+ }
222
+ }
223
+
224
+ return { plan, nearMiss, reclassified, classification, procKey, existingProcedure };
225
+ }
226
+
227
+ // ─── Self-Improvement ACK ────────────────────────────────────
228
+ // self_improvement is long-running — return immediate ACK, background the work.
229
+
230
+ function trySelfImprovementAck(
231
+ classification: string,
232
+ intent: KernelIntent,
233
+ env: EdgeEnv,
234
+ startMs: number,
235
+ ): DispatchResult | null {
236
+ if (classification !== 'self_improvement' || intent.source.channel === 'internal') return null;
237
+
238
+ if (env.ctx) {
239
+ env.ctx.waitUntil(
240
+ (async () => {
241
+ const start = Date.now();
242
+ try {
243
+ await executeComposite(intent, env, buildMcpRegistry(env));
244
+ await env.db.prepare(
245
+ "INSERT INTO task_runs (task_name, status, duration_ms) VALUES ('self_improvement', 'ok', ?)"
246
+ ).bind(Date.now() - start).run().catch(() => {});
247
+ } catch (err) {
248
+ const msg = err instanceof Error ? err.message : String(err);
249
+ console.error('[self_improvement] background run failed:', msg);
250
+ await env.db.prepare(
251
+ "INSERT INTO task_runs (task_name, status, duration_ms, error_message) VALUES ('self_improvement', 'error', ?, ?)"
252
+ ).bind(Date.now() - start, msg).run().catch(() => {});
253
+ }
254
+ })(),
255
+ );
256
+ }
257
+
258
+ return {
259
+ text: 'Self-improvement analysis queued. I\'ll review the codebase, check existing issues, and post any findings as GitHub issues + agenda items. This usually takes 1-2 minutes.',
260
+ executor: 'composite',
261
+ cost: 0,
262
+ latency_ms: Date.now() - startMs,
263
+ procedureHit: false,
264
+ classification,
265
+ };
266
+ }
267
+
268
+ // ─── Probe + Execute ─────────────────────────────────────────
269
+ // Self-consistency probe, then executor dispatch. Shared between
270
+ // streaming and non-streaming paths.
271
+
272
+ interface ExecuteResult {
273
+ text: string;
274
+ cost: number;
275
+ meta?: unknown;
276
+ outcome: 'success' | 'failure' | 'partial_failure';
277
+ probeResult?: 'agreed' | 'split' | 'escalated';
278
+ earlyReturn?: DispatchResult; // probe agreed → skip executor
279
+ }
280
+
281
+ async function probeAndExecute(
282
+ ctx: AugmentedContext,
283
+ intent: KernelIntent,
284
+ env: EdgeEnv,
285
+ startMs: number,
286
+ onDelta?: (text: string) => void,
287
+ ): Promise<ExecuteResult> {
288
+ const { plan, nearMiss, classification, procKey, existingProcedure } = ctx;
289
+ let probeResult: 'agreed' | 'split' | 'escalated' | undefined;
290
+
291
+ // Self-consistency probe
292
+ if (probeEligible(plan.executor, intent, classification)) {
293
+ try {
294
+ const probe = await probeConsistency(
295
+ env.groqApiKey, env.groqResponseModel, buildGroqSystemPrompt(), intent.raw, env.groqBaseUrl,
296
+ env.memoryBinding,
297
+ );
298
+ if (probe.sigma === 0 && probe.agreedText) {
299
+ console.log(`[dispatch] Probe agreed for "${classification}" — skipping ${plan.executor}`);
300
+ const text = probe.agreedText;
301
+ const cost = 0.0003;
302
+ probeResult = 'agreed';
303
+ plan.executor = 'groq';
304
+ if (onDelta) onDelta(text);
305
+ const latencyMs = Date.now() - startMs;
306
+ await recordOutcome(env, intent, classification, procKey, plan, existingProcedure, text, cost, latencyMs, nearMiss, 'success', ctx.reclassified);
307
+ return {
308
+ text, cost, outcome: 'success', probeResult,
309
+ earlyReturn: { text, executor: plan.executor, cost, latency_ms: latencyMs, procedureHit: !!plan.procedureId, classification, confidence: intent.confidence, reclassified: ctx.reclassified, probeResult, meta: undefined },
310
+ };
311
+ } else if (probe.sigma === 1.0) {
312
+ const prev = plan.executor;
313
+ plan.executor = plan.executor === 'workers_ai' ? 'gpt_oss' : 'composite';
314
+ probeResult = 'escalated';
315
+ console.log(`[dispatch] Probe disagreed for "${classification}" — escalating ${prev} → ${plan.executor}`);
316
+ } else {
317
+ probeResult = 'split';
318
+ }
319
+ } catch (err) {
320
+ console.warn('[dispatch] Probe failed (non-fatal):', err instanceof Error ? err.message : String(err));
321
+ }
322
+ }
323
+
324
+ // Execute
325
+ let text: string;
326
+ let cost: number;
327
+ let meta: unknown;
328
+ let outcome: 'success' | 'failure' | 'partial_failure' = 'success';
329
+
330
+ try {
331
+ let result: { text: string; cost: number; meta?: unknown };
332
+
333
+ // Streaming Claude path — uses executeClaudeStream for real-time deltas
334
+ if (onDelta && (plan.executor === 'claude' || plan.executor === 'claude_opus')) {
335
+ try {
336
+ // Tag intent so executeClaudeStream picks the right model
337
+ const savedClassified = intent.classified;
338
+ intent.classified = plan.executor;
339
+ const streamResult = await executeClaudeStream(intent, env, onDelta);
340
+ intent.classified = savedClassified;
341
+ result = streamResult;
342
+ } catch (streamErr) {
343
+ const msg = streamErr instanceof Error ? streamErr.message : String(streamErr);
344
+ if (msg.includes('Anthropic API error') || msg.includes('credit balance')) {
345
+ console.warn(`[dispatch] streaming ${plan.executor} failed, failover → gpt_oss: ${msg.slice(0, 120)}`);
346
+ const fallback = await executeGptOss(intent, env);
347
+ result = fallback;
348
+ plan.executor = 'gpt_oss';
349
+ onDelta(result.text);
350
+ } else {
351
+ throw streamErr;
352
+ }
353
+ }
354
+ } else {
355
+ // Non-streaming or non-Claude executors
356
+ switch (plan.executor) {
357
+ case 'claude':
358
+ case 'claude_opus': {
359
+ const r = await executeWithAnthropicFailover(plan.executor, intent, env);
360
+ result = r;
361
+ plan.executor = r.actualExecutor;
362
+ break;
363
+ }
364
+ case 'composite':
365
+ result = await executeComposite(intent, env, buildMcpRegistry(env));
366
+ break;
367
+ case 'gpt_oss':
368
+ result = await executeGptOss(intent, env);
369
+ break;
370
+ case 'workers_ai':
371
+ result = await executeWorkersAi(intent, env);
372
+ break;
373
+ case 'groq':
374
+ result = await executeGroq(intent, env);
375
+ break;
376
+ case 'direct':
377
+ result = await executeDirect(intent, env);
378
+ break;
379
+ case 'claude_code':
380
+ result = await executeCodeTask(intent, env);
381
+ break;
382
+ case 'tarotscript':
383
+ result = await executeTarotScript(intent, env);
384
+ break;
385
+ default:
386
+ throw new Error(`Unknown executor: ${plan.executor}`);
387
+ }
388
+
389
+ // For streaming non-Claude executors, emit full text as single delta
390
+ if (onDelta) onDelta(result.text);
391
+ }
392
+
393
+ text = result.text;
394
+ cost = result.cost;
395
+ meta = result.meta;
396
+ // Detect partial failure from composite executor (#71)
397
+ const partialMeta = result.meta as { partialFailure?: boolean } | undefined;
398
+ if (partialMeta?.partialFailure) {
399
+ outcome = 'partial_failure';
400
+ }
401
+ } catch (err) {
402
+ outcome = 'failure';
403
+ cost = 0;
404
+ text = `Error: ${err instanceof Error ? err.message : String(err)}`;
405
+ if (onDelta) onDelta(text);
406
+ }
407
+
408
+ return { text, cost, meta, outcome, probeResult };
409
+ }
410
+
411
+ // ─── Shadow Exploration (#290) ───────────────────────────────
412
+ // On ~5% of expensive dispatches, background-test a cheaper executor.
413
+ // If the cheaper tier produces quality output, record it as a candidate
414
+ // in procedural memory. Promotion happens automatically via the
415
+ // candidate_executor probation system in upsertProcedure().
416
+
417
+ const SHADOW_EXPLORATION_RATE = 0.05; // 5% of eligible dispatches
418
+
419
+ // Expensive → cheaper executor demotion map
420
+ const SHADOW_DEMOTION: Partial<Record<Executor, Executor>> = {
421
+ claude_opus: 'claude',
422
+ claude: 'gpt_oss',
423
+ composite: 'gpt_oss',
424
+ gpt_oss: 'workers_ai',
425
+ };
426
+
427
+ // Skip shadow exploration for these classifications
428
+ const SHADOW_SKIP = new Set([
429
+ 'heartbeat', 'greeting', 'code_task', 'self_improvement',
430
+ 'goal_execution', 'user_correction',
431
+ ]);
432
+
433
+ function shadowEligible(executor: Executor, classification: string, intent: KernelIntent): boolean {
434
+ if (SHADOW_SKIP.has(classification)) return false;
435
+ if (intent.source.channel === 'internal') return false;
436
+ if (!SHADOW_DEMOTION[executor]) return false;
437
+ return Math.random() < SHADOW_EXPLORATION_RATE;
438
+ }
439
+
440
+ // Quality gate: is the shadow output good enough to count as a success?
441
+ function shadowQualityPass(primaryText: string, shadowText: string): boolean {
442
+ // Too short = likely error or refusal
443
+ if (shadowText.length < 20) return false;
444
+ // Length ratio: shadow should be at least 30% of primary length
445
+ if (shadowText.length < primaryText.length * 0.3) return false;
446
+ // Error patterns
447
+ const errorPatterns = /^error:|i cannot|i'm unable|as an ai|i don't have/i;
448
+ if (errorPatterns.test(shadowText.trim())) return false;
449
+ return true;
450
+ }
451
+
452
+ async function tryShadowExploration(
453
+ ctx: AugmentedContext,
454
+ intent: KernelIntent,
455
+ env: EdgeEnv,
456
+ primaryText: string,
457
+ ): Promise<void> {
458
+ const primaryExecutor = ctx.plan.executor;
459
+ const shadowExecutor = SHADOW_DEMOTION[primaryExecutor];
460
+ if (!shadowExecutor) return;
461
+
462
+ try {
463
+ // Clone intent to avoid mutation
464
+ const shadowIntent: KernelIntent = { ...intent, classified: shadowExecutor };
465
+ let result: { text: string; cost: number };
466
+
467
+ switch (shadowExecutor) {
468
+ case 'gpt_oss':
469
+ result = await executeGptOss(shadowIntent, env);
470
+ break;
471
+ case 'workers_ai':
472
+ result = await executeWorkersAi(shadowIntent, env);
473
+ break;
474
+ case 'claude':
475
+ result = await executeClaude(shadowIntent, env);
476
+ break;
477
+ default:
478
+ return;
479
+ }
480
+
481
+ const passed = shadowQualityPass(primaryText, result.text);
482
+ const outcome = passed ? 'success' : 'failure';
483
+
484
+ // Record to procedural memory as the shadow executor —
485
+ // upsertProcedure's probation system handles candidate tracking
486
+ await upsertProcedure(
487
+ env.db, ctx.procKey, shadowExecutor,
488
+ JSON.stringify({ executor: shadowExecutor }),
489
+ outcome, 0, result.cost,
490
+ );
491
+
492
+ console.log(
493
+ `[shadow] ${ctx.classification}: ${primaryExecutor} → ${shadowExecutor} = ${outcome}` +
494
+ ` (primary: ${primaryText.length} chars, shadow: ${result.text.length} chars)`,
495
+ );
496
+ } catch (err) {
497
+ console.warn('[shadow] exploration failed (non-fatal):', err instanceof Error ? err.message : String(err));
498
+ }
499
+ }
500
+
501
+ // ─── Shared Finalization ─────────────────────────────────────
502
+
503
+ function buildResult(
504
+ ctx: AugmentedContext,
505
+ intent: KernelIntent,
506
+ exec: ExecuteResult,
507
+ latencyMs: number,
508
+ ): DispatchResult {
509
+ return {
510
+ text: exec.text,
511
+ executor: ctx.plan.executor,
512
+ cost: exec.cost,
513
+ latency_ms: latencyMs,
514
+ procedureHit: !!ctx.plan.procedureId,
515
+ classification: ctx.classification,
516
+ confidence: intent.confidence,
517
+ reclassified: ctx.reclassified,
518
+ probeResult: exec.probeResult,
519
+ meta: exec.meta,
520
+ };
521
+ }
522
+
523
+ // ─── Main Dispatch Loop ──────────────────────────────────────
524
+
525
+ export async function dispatch(intent: KernelIntent, env: EdgeEnv): Promise<DispatchResult> {
526
+ const startMs = Date.now();
527
+ const ctx = await augmentIntent(intent, env);
528
+
529
+ // Self-improvement ACK (backgrounded)
530
+ const ack = trySelfImprovementAck(ctx.classification, intent, env, startMs);
531
+ if (ack) return ack;
532
+
533
+ const exec = await probeAndExecute(ctx, intent, env, startMs);
534
+ if (exec.earlyReturn) return exec.earlyReturn;
535
+
536
+ const latencyMs = Date.now() - startMs;
537
+ await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, exec.outcome, ctx.reclassified);
538
+
539
+ // Background: shadow exploration + shadow read
540
+ if (env.ctx) {
541
+ if (exec.outcome === 'success' && shadowEligible(ctx.plan.executor as Executor, ctx.classification, intent)) {
542
+ env.ctx.waitUntil(tryShadowExploration(ctx, intent, env, exec.text));
543
+ }
544
+ if (env.memoryBinding) {
545
+ env.ctx.waitUntil(
546
+ getAllMemoryForContext(env.memoryBinding).catch(err => {
547
+ console.warn('[dispatch] Background shadow read failed:', err instanceof Error ? err.message : String(err));
548
+ }),
549
+ );
550
+ }
551
+ }
552
+
553
+ return buildResult(ctx, intent, exec, latencyMs);
554
+ }
555
+
556
+ // ─── Streaming Dispatch ──────────────────────────────────────
557
+
558
+ export async function dispatchStream(
559
+ intent: KernelIntent,
560
+ env: EdgeEnv,
561
+ onDelta: (text: string) => void,
562
+ ): Promise<DispatchResult> {
563
+ const startMs = Date.now();
564
+ const ctx = await augmentIntent(intent, env);
565
+
566
+ const exec = await probeAndExecute(ctx, intent, env, startMs, onDelta);
567
+ if (exec.earlyReturn) return exec.earlyReturn;
568
+
569
+ const latencyMs = Date.now() - startMs;
570
+ await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, exec.outcome, ctx.reclassified);
571
+
572
+ // Background: shadow exploration + shadow read
573
+ if (env.ctx) {
574
+ if (exec.outcome === 'success' && shadowEligible(ctx.plan.executor as Executor, ctx.classification, intent)) {
575
+ env.ctx.waitUntil(tryShadowExploration(ctx, intent, env, exec.text));
576
+ }
577
+ if (env.memoryBinding) {
578
+ env.ctx.waitUntil(
579
+ getAllMemoryForContext(env.memoryBinding).catch(err => {
580
+ console.warn('[dispatch] Background shadow read failed:', err instanceof Error ? err.message : String(err));
581
+ }),
582
+ );
583
+ }
584
+ }
585
+
586
+ return buildResult(ctx, intent, exec, latencyMs);
587
+ }
@@ -0,0 +1,50 @@
1
+ // ─── Domain Pre-Filter (Phase 0 — observe only) ────────────
2
+ // Tags queries with a domain slug from a fixed taxonomy.
3
+ // Does NOT affect routing — purely observational for data collection.
4
+
5
+ export interface DomainTag {
6
+ domain: string; // slug from taxonomy
7
+ confidence: number; // 0.0-1.0
8
+ }
9
+
10
+ const DOMAIN_SIGNALS: Record<string, RegExp[]> = {
11
+ legal: [/\bllc\b/i, /\bcompliance\b/i, /\bfiling\b/i, /\bcontract\b/i, /\bcorporat/i, /\bein\b/i, /\bregistered agent/i, /\btrademark\b/i],
12
+ finance: [/\brevenue\b/i, /\bcost\b/i, /\bcogs\b/i, /\bmargin\b/i, /\btax\b/i, /\binvoice\b/i, /\bstripe\b/i, /\brunway\b/i, /\bburn\b/i],
13
+ technical: [/\binfra/i, /\bdevops\b/i, /\bdeploy/i, /\bworker\b/i, /\bcloudflare\b/i, /\bd1\b/i, /\bkv\b/i, /\bdurable object/i],
14
+ software: [/\brefactor/i, /\bfunction\b/i, /\bclass\b/i, /\bapi\b/i, /\bendpoint\b/i, /\btypescript\b/i, /\bbug\b/i],
15
+ ai_ml: [/\bmodel\b/i, /\bllm\b/i, /\binference\b/i, /\btraining\b/i, /\bagent\b/i, /\bprompt\b/i, /\bembedding/i, /\bvector/i],
16
+ creative: [/\bblog\b/i, /\bwriting\b/i, /\bdesign\b/i, /\bcontent\b/i, /\bmarketing\b/i],
17
+ operations: [/\bproject\b/i, /\bsprint\b/i, /\bpipeline\b/i, /\bprocess\b/i, /\bworkflow\b/i],
18
+ general: [], // fallback
19
+ };
20
+
21
+ export function domainPreFilter(query: string): DomainTag {
22
+ let bestDomain = 'general';
23
+ let bestCount = 0;
24
+ let tied = false;
25
+
26
+ for (const [domain, patterns] of Object.entries(DOMAIN_SIGNALS)) {
27
+ if (patterns.length === 0) continue;
28
+ let matchCount = 0;
29
+ for (const pattern of patterns) {
30
+ if (pattern.test(query)) matchCount++;
31
+ }
32
+ if (matchCount > bestCount) {
33
+ bestDomain = domain;
34
+ bestCount = matchCount;
35
+ tied = false;
36
+ } else if (matchCount > 0 && matchCount === bestCount) {
37
+ tied = true;
38
+ }
39
+ }
40
+
41
+ // No matches or tie → general with low confidence
42
+ if (bestCount === 0 || tied) {
43
+ return { domain: 'general', confidence: 0.3 };
44
+ }
45
+
46
+ const totalPossible = DOMAIN_SIGNALS[bestDomain].length;
47
+ const confidence = totalPossible > 0 ? bestCount / totalPossible : 0.3;
48
+
49
+ return { domain: bestDomain, confidence };
50
+ }