@stackbilt/aegis-core 0.6.5 → 0.8.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 (121) hide show
  1. package/cli/aegis.mjs +356 -0
  2. package/package.json +21 -3
  3. package/public/assets/index-CQHn03rW.css +1 -0
  4. package/public/assets/index-CTKpNJEr.js +74 -0
  5. package/public/index.html +14 -0
  6. package/schema.sql +4 -0
  7. package/src/adapters/voice/cloudflare-agent.ts +0 -0
  8. package/src/agent-routing.ts +38 -0
  9. package/src/assets.ts +6 -0
  10. package/src/auth.ts +14 -5
  11. package/src/bluesky.ts +0 -0
  12. package/src/claude-tools/content.ts +0 -0
  13. package/src/claude-tools/email.ts +0 -0
  14. package/src/claude.ts +133 -268
  15. package/src/codebeast.ts +0 -0
  16. package/src/composite.ts +49 -79
  17. package/src/content/column.ts +0 -0
  18. package/src/content/hero-image.ts +0 -0
  19. package/src/content/index.ts +0 -0
  20. package/src/content/journal.ts +0 -0
  21. package/src/content/roundtable.ts +0 -0
  22. package/src/contracts/agenda-item.contract.ts +0 -0
  23. package/src/contracts/cc-task.contract.ts +0 -0
  24. package/src/contracts/goal.contract.ts +0 -0
  25. package/src/contracts/memory-entry.contract.ts +0 -0
  26. package/src/core.ts +5 -0
  27. package/src/dashboard.ts +0 -0
  28. package/src/decision-docs.ts +0 -0
  29. package/src/dispatch.ts +0 -0
  30. package/src/durable-objects/chat-session-auth.ts +20 -0
  31. package/src/durable-objects/chat-session.ts +251 -0
  32. package/src/edge-env.ts +0 -0
  33. package/src/exports.ts +0 -0
  34. package/src/github-projects.ts +0 -0
  35. package/src/groq.ts +61 -113
  36. package/src/index.ts +11 -1
  37. package/src/kernel/argus-actions.ts +0 -0
  38. package/src/kernel/argus-correlation.ts +0 -0
  39. package/src/kernel/board.ts +0 -0
  40. package/src/kernel/classify-memory-topic.ts +0 -0
  41. package/src/kernel/disambiguation.ts +55 -0
  42. package/src/kernel/dispatch.ts +59 -44
  43. package/src/kernel/dynamic-tools.ts +30 -52
  44. package/src/kernel/executor-port.ts +0 -0
  45. package/src/kernel/executor-router.ts +0 -0
  46. package/src/kernel/executors/claude.ts +1 -0
  47. package/src/kernel/executors/direct.ts +14 -0
  48. package/src/kernel/executors/workers-ai.ts +5 -0
  49. package/src/kernel/grounding/fabrication-detector.ts +0 -0
  50. package/src/kernel/grounding/fanout.ts +0 -0
  51. package/src/kernel/grounding/semantic-sanhedrin.ts +0 -0
  52. package/src/kernel/grounding/verify.ts +0 -0
  53. package/src/kernel/grounding-layer.ts +0 -0
  54. package/src/kernel/insight-cache.ts +0 -0
  55. package/src/kernel/memory/episodic.ts +3 -1
  56. package/src/kernel/memory/insights.ts +0 -0
  57. package/src/kernel/memory-guardrails.ts +0 -0
  58. package/src/kernel/memory-service.ts +0 -0
  59. package/src/kernel/patterns.ts +0 -0
  60. package/src/kernel/port.ts +0 -0
  61. package/src/kernel/provider-factory.ts +0 -0
  62. package/src/kernel/resilience.ts +0 -0
  63. package/src/kernel/router.ts +33 -11
  64. package/src/kernel/scheduled/agent-dispatch.ts +0 -0
  65. package/src/kernel/scheduled/argus-analytics.ts +0 -0
  66. package/src/kernel/scheduled/argus-heartbeat.ts +0 -0
  67. package/src/kernel/scheduled/argus-notify.ts +0 -0
  68. package/src/kernel/scheduled/board-sync.ts +0 -0
  69. package/src/kernel/scheduled/ci-watcher.ts +0 -0
  70. package/src/kernel/scheduled/content-drip.ts +0 -0
  71. package/src/kernel/scheduled/content.ts +0 -0
  72. package/src/kernel/scheduled/conversation-facts.ts +9 -7
  73. package/src/kernel/scheduled/cost-report.ts +0 -0
  74. package/src/kernel/scheduled/dev-activity.ts +0 -0
  75. package/src/kernel/scheduled/digest.ts +30 -3
  76. package/src/kernel/scheduled/dreaming/agenda-triage.ts +0 -0
  77. package/src/kernel/scheduled/dreaming/facts.ts +0 -0
  78. package/src/kernel/scheduled/dreaming/index.ts +0 -0
  79. package/src/kernel/scheduled/dreaming/llm.ts +9 -5
  80. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +0 -0
  81. package/src/kernel/scheduled/dreaming/persona.ts +0 -0
  82. package/src/kernel/scheduled/dreaming/symbolic.ts +0 -0
  83. package/src/kernel/scheduled/dreaming/task-proposals.ts +0 -0
  84. package/src/kernel/scheduled/entropy.ts +0 -0
  85. package/src/kernel/scheduled/feed-watcher.ts +0 -0
  86. package/src/kernel/scheduled/inbox-processor.ts +0 -0
  87. package/src/kernel/scheduled/issue-proposer.ts +0 -0
  88. package/src/kernel/scheduled/issue-watcher.ts +0 -0
  89. package/src/kernel/scheduled/pr-automerge.ts +0 -0
  90. package/src/kernel/scheduled/product-health.ts +0 -0
  91. package/src/kernel/scheduled/self-improvement.ts +0 -0
  92. package/src/kernel/scheduled/social-engage.ts +12 -8
  93. package/src/kernel/scheduled/task-audit.ts +0 -0
  94. package/src/kernel/types.ts +6 -0
  95. package/src/landing.ts +0 -0
  96. package/src/lib/audit-chain/chain.ts +0 -0
  97. package/src/lib/audit-chain/types.ts +0 -0
  98. package/src/lib/observability/errors.ts +0 -0
  99. package/src/operator/config.ts +0 -0
  100. package/src/operator/persona.ts +0 -0
  101. package/src/operator/prompt-builder.ts +3 -0
  102. package/src/pulse.ts +0 -0
  103. package/src/routes/bluesky.ts +0 -0
  104. package/src/routes/chat-ws.ts +17 -0
  105. package/src/routes/codebeast.ts +0 -0
  106. package/src/routes/content.ts +0 -0
  107. package/src/routes/dynamic-tools.ts +0 -0
  108. package/src/routes/messages.ts +11 -6
  109. package/src/routes/observability.ts +0 -0
  110. package/src/routes/operator-logs.ts +0 -0
  111. package/src/routes/pages.ts +12 -1
  112. package/src/schema-enums.ts +0 -0
  113. package/src/task-intelligence.ts +0 -0
  114. package/src/types.ts +8 -0
  115. package/src/ui/index.html +13 -0
  116. package/src/ui/main.tsx +356 -0
  117. package/src/ui/styles.css +391 -0
  118. package/src/ui.ts +594 -2
  119. package/src/version.ts +3 -3
  120. package/src/wiki/client.ts +0 -0
  121. package/src/wiki/types.ts +0 -0
package/src/groq.ts CHANGED
@@ -1,4 +1,24 @@
1
- // Edge-native Groq client fetch-based, no OpenAI SDK dependency
1
+ // Edge-native Groq helpers backed by @stackbilt/llm-providers
2
+
3
+ import { createLLMProviderFactory, type LLMMessage } from '@stackbilt/llm-providers';
4
+ import { tokenize, jaccardSimilarity } from './kernel/memory/index.js';
5
+ import { cosineSimilarity } from './kernel/memory/semantic.js';
6
+ import type { MemoryServiceBinding } from './types.js';
7
+
8
+ function buildGroqFactory(apiKey: string, baseUrl: string) {
9
+ return createLLMProviderFactory({
10
+ groq: { apiKey, baseUrl },
11
+ fallbackRules: [],
12
+ enableCircuitBreaker: true,
13
+ enableRetries: true,
14
+ });
15
+ }
16
+
17
+ function coerceText(content: unknown): string {
18
+ if (typeof content === 'string') return content;
19
+ if (content == null) return '';
20
+ return typeof content === 'object' ? JSON.stringify(content) : String(content);
21
+ }
2
22
 
3
23
  export async function askGroq(
4
24
  apiKey: string,
@@ -7,45 +27,27 @@ export async function askGroq(
7
27
  userPrompt: string,
8
28
  baseUrl = 'https://api.groq.com',
9
29
  ): Promise<string> {
10
- const response = await fetch(`${baseUrl}/openai/v1/chat/completions`, {
11
- method: 'POST',
12
- headers: {
13
- 'Content-Type': 'application/json',
14
- 'Authorization': `Bearer ${apiKey}`,
15
- },
16
- body: JSON.stringify({
30
+ try {
31
+ const result = await buildGroqFactory(apiKey, baseUrl).generateResponse({
17
32
  model,
33
+ systemPrompt,
18
34
  temperature: 0.3,
19
- max_tokens: 500,
35
+ maxTokens: 500,
20
36
  messages: [
21
- { role: 'system', content: systemPrompt },
22
37
  { role: 'user', content: userPrompt },
23
38
  ],
24
- }),
25
- });
26
-
27
- if (!response.ok) {
28
- const errText = await response.text();
29
- throw new Error(`Groq API error ${response.status}: ${errText}`);
39
+ });
40
+ return coerceText(result.message);
41
+ } catch (err) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ throw new Error(`Groq API error: ${msg}`);
30
44
  }
31
-
32
- const data = await response.json<{
33
- choices: { message: { content: unknown } }[];
34
- usage?: { total_tokens: number };
35
- }>();
36
-
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);
43
45
  }
44
46
 
45
- // ─── Logprobs-enabled classification ─────────────────────────
46
- // Requests logprobs from Groq's OpenAI-compatible API and computes
47
- // geometric mean of token probabilities for the classification value.
48
- // Returns both self-reported confidence and token-level confidence.
47
+ // ─── Classification confidence helper ────────────────────────
48
+ // Provider-backed replacement for the former raw Groq logprobs call.
49
+ // llm-providers does not expose provider logprob request options yet, so
50
+ // tokenConfidence mirrors self-reported confidence until that contract lands.
49
51
 
50
52
  export interface LogprobClassification {
51
53
  pattern: string;
@@ -62,65 +64,20 @@ export async function askGroqWithLogprobs(
62
64
  userPrompt: string,
63
65
  baseUrl = 'https://api.groq.com',
64
66
  ): Promise<LogprobClassification> {
65
- const response = await fetch(`${baseUrl}/openai/v1/chat/completions`, {
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- 'Authorization': `Bearer ${apiKey}`,
70
- },
71
- body: JSON.stringify({
72
- model,
73
- temperature: 0.1,
74
- max_tokens: 200,
75
- response_format: { type: 'json_object' },
76
- logprobs: true,
77
- top_logprobs: 3,
78
- messages: [
79
- { role: 'system', content: systemPrompt },
80
- { role: 'user', content: userPrompt },
81
- ],
82
- }),
83
- });
84
-
85
- if (!response.ok) {
86
- const errText = await response.text();
87
- throw new Error(`Groq logprobs API error ${response.status}: ${errText}`);
88
- }
89
-
90
- interface LogprobToken {
91
- token: string;
92
- logprob: number;
93
- }
94
-
95
- const data = await response.json<{
96
- choices: {
97
- message: { content: string };
98
- logprobs?: { content?: LogprobToken[] };
99
- }[];
100
- }>();
101
-
102
- const raw = data.choices[0]?.message?.content ?? '{}';
103
- const parsed = JSON.parse(raw) as {
67
+ const { parsed } = await askGroqJson<{
104
68
  pattern?: string;
105
69
  complexity?: number;
106
70
  needs_tools?: boolean;
107
71
  confidence?: number;
108
- };
109
-
110
- // Compute geometric mean of token logprobs
111
- let tokenConfidence = 0.5; // fallback if no logprobs
112
- const logprobTokens = data.choices[0]?.logprobs?.content;
113
- if (logprobTokens && logprobTokens.length > 0) {
114
- const sumLogprobs = logprobTokens.reduce((sum, t) => sum + t.logprob, 0);
115
- tokenConfidence = Math.exp(sumLogprobs / logprobTokens.length);
116
- }
72
+ }>(apiKey, model, systemPrompt, userPrompt, baseUrl, { maxTokens: 200, temperature: 0.1 });
73
+ const confidence = parsed.confidence ?? 0.5;
117
74
 
118
75
  return {
119
76
  pattern: (parsed.pattern ?? 'general_knowledge').toLowerCase().replace(/[^a-z_]/g, ''),
120
77
  complexity: parsed.complexity ?? 2,
121
78
  needs_tools: parsed.needs_tools ?? false,
122
- selfReportedConfidence: parsed.confidence ?? 0.5,
123
- tokenConfidence,
79
+ selfReportedConfidence: confidence,
80
+ tokenConfidence: confidence,
124
81
  };
125
82
  }
126
83
 
@@ -130,10 +87,6 @@ export async function askGroqWithLogprobs(
130
87
  // Jaccard when memoryBinding is unavailable.
131
88
  // Returns σ metric: 0=all agree, 0.5=partial, 1.0=disagree.
132
89
 
133
- import { tokenize, jaccardSimilarity } from './kernel/memory/index.js';
134
- import { cosineSimilarity } from './kernel/memory/semantic.js';
135
- import type { MemoryServiceBinding } from './types.js';
136
-
137
90
  const PROBE_TIMEOUT_MS = 3_000;
138
91
  const JACCARD_AGREEMENT_THRESHOLD = 0.5;
139
92
  const COSINE_AGREEMENT_THRESHOLD = 0.85;
@@ -210,8 +163,7 @@ export async function askGroqJson<T = unknown>(
210
163
  baseUrl = 'https://api.groq.com',
211
164
  options?: { maxTokens?: number; temperature?: number; prefill?: string },
212
165
  ): Promise<{ parsed: T; raw: string; usage?: { prompt_tokens: number; completion_tokens: number } }> {
213
- const messages: Array<{ role: string; content: string }> = [
214
- { role: 'system', content: systemPrompt },
166
+ const messages: LLMMessage[] = [
215
167
  { role: 'user', content: userPrompt },
216
168
  ];
217
169
  // Prefilling: seed the assistant response to steer tone/format
@@ -219,34 +171,30 @@ export async function askGroqJson<T = unknown>(
219
171
  messages.push({ role: 'assistant', content: options.prefill });
220
172
  }
221
173
 
222
- const response = await fetch(`${baseUrl}/openai/v1/chat/completions`, {
223
- method: 'POST',
224
- headers: {
225
- 'Content-Type': 'application/json',
226
- 'Authorization': `Bearer ${apiKey}`,
227
- },
228
- body: JSON.stringify({
174
+ try {
175
+ const result = await buildGroqFactory(apiKey, baseUrl).generateResponse({
229
176
  model,
177
+ systemPrompt,
230
178
  temperature: options?.temperature ?? 0.2,
231
- max_tokens: options?.maxTokens ?? 2000,
179
+ maxTokens: options?.maxTokens ?? 2000,
232
180
  response_format: { type: 'json_object' },
233
181
  messages,
234
- }),
235
- });
236
-
237
- if (!response.ok) {
238
- const errText = await response.text();
239
- throw new Error(`Groq API error ${response.status}: ${errText}`);
182
+ });
183
+
184
+ const completion = result.message ?? '{}';
185
+ // If prefilled, the model continues from the prefill — concatenate for valid JSON
186
+ const raw = options?.prefill ? options.prefill + completion : completion;
187
+ const parsed = JSON.parse(raw) as T;
188
+ return {
189
+ parsed,
190
+ raw,
191
+ usage: {
192
+ prompt_tokens: result.usage.inputTokens,
193
+ completion_tokens: result.usage.outputTokens,
194
+ },
195
+ };
196
+ } catch (err) {
197
+ const msg = err instanceof Error ? err.message : String(err);
198
+ throw new Error(`Groq API error: ${msg}`);
240
199
  }
241
-
242
- const data = await response.json<{
243
- choices: { message: { content: string } }[];
244
- usage?: { prompt_tokens: number; completion_tokens: number };
245
- }>();
246
-
247
- const completion = data.choices[0]?.message?.content ?? '{}';
248
- // If prefilled, the model continues from the prefill — concatenate for valid JSON
249
- const raw = options?.prefill ? options.prefill + completion : completion;
250
- const parsed = JSON.parse(raw) as T;
251
- return { parsed, raw, usage: data.usage };
252
200
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { Hono } from 'hono';
10
10
  import OAuthProvider from '@cloudflare/workers-oauth-provider';
11
11
  import { bearerAuth } from './auth.js';
12
+ import { routeAegisAgentRequest } from './agent-routing.js';
12
13
  import { runScheduledTasks } from './kernel/scheduled/index.js';
13
14
  import type { Env } from './types.js';
14
15
  import { handleMcpRequest } from './mcp-server.js';
@@ -24,6 +25,7 @@ import { observability } from './routes/observability.js';
24
25
  import { pages } from './routes/pages.js';
25
26
  import { ccTasks } from './routes/cc-tasks.js';
26
27
  import { messages } from './routes/messages.js';
28
+ import { chatWs } from './routes/chat-ws.js';
27
29
  import { content } from './routes/content.js';
28
30
  import { codebeast } from './routes/codebeast.js';
29
31
  import { bluesky } from './routes/bluesky.js';
@@ -44,6 +46,7 @@ app.route('/', observability);
44
46
  app.route('/', pages);
45
47
  app.route('/', ccTasks);
46
48
  app.route('/', messages);
49
+ app.route('/', chatWs);
47
50
  app.route('/', content);
48
51
  app.route('/', codebeast);
49
52
  app.route('/', bluesky);
@@ -82,8 +85,15 @@ const oauthProvider = new OAuthProvider<Env>({
82
85
  });
83
86
 
84
87
  export default {
85
- fetch: oauthProvider.fetch.bind(oauthProvider),
88
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
89
+ const agentResponse = await routeAegisAgentRequest(request, env);
90
+ if (agentResponse) return agentResponse;
91
+ return oauthProvider.fetch(request, env, ctx);
92
+ },
86
93
  async scheduled(_event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
87
94
  ctx.waitUntil(runScheduledTasks(buildEdgeEnv(env)));
88
95
  },
89
96
  };
97
+
98
+ export { ChatSession } from './durable-objects/chat-session.js';
99
+ export { AegisVoiceAdapter } from './adapters/voice/cloudflare-agent.js';
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,55 @@
1
+ export interface DisambiguationRequest {
2
+ concept: string;
3
+ question: string;
4
+ }
5
+
6
+ const DATA_INTENT =
7
+ /\b(what|show|calculate|report|compare|analy[sz]e|trend|metric|dashboard|how many|how much|rate)\b/i;
8
+
9
+ const KNOWN_DATA_SURFACES =
10
+ /\b(compliance|deadline|deadlines|filing|filings|document|documents|project|projects|agenda|goal|goals|task|tasks|health|heartbeat)\b/i;
11
+
12
+ const DISAMBIGUATORS =
13
+ /\b(by|defined as|definition|seat count|seats|mrr|arr|gross|net|monthly|annual|period|last \d+|past \d+|count|rate)\b/i;
14
+
15
+ const AMBIGUOUS_CONCEPTS: Array<{ name: string; pattern: RegExp; question: string }> = [
16
+ {
17
+ name: 'churn',
18
+ pattern: /\bchurn\b/i,
19
+ question: 'Do you mean churn by seat count, account count, revenue, MRR, or another defined basis?',
20
+ },
21
+ {
22
+ name: 'retention',
23
+ pattern: /\bretention\b/i,
24
+ question: 'Do you mean retention by user, account, seat, revenue, cohort, or another defined basis?',
25
+ },
26
+ {
27
+ name: 'active users',
28
+ pattern: /\b(active users?|users?)\b/i,
29
+ question: 'Do you mean active users by login, event activity, paid seat, account membership, or another defined rule?',
30
+ },
31
+ {
32
+ name: 'conversion',
33
+ pattern: /\b(conversion|activation)\b/i,
34
+ question: 'Do you mean conversion by visit, signup, qualified lead, paid account, or another funnel step?',
35
+ },
36
+ {
37
+ name: 'revenue',
38
+ pattern: /\b(revenue|growth)\b/i,
39
+ question: 'Do you mean revenue by gross, net, MRR, ARR, cash receipts, or another defined basis?',
40
+ },
41
+ ];
42
+
43
+ export function detectDisambiguationNeed(message: string): DisambiguationRequest | null {
44
+ if (!DATA_INTENT.test(message)) return null;
45
+ if (KNOWN_DATA_SURFACES.test(message)) return null;
46
+ if (DISAMBIGUATORS.test(message)) return null;
47
+
48
+ for (const concept of AMBIGUOUS_CONCEPTS) {
49
+ if (concept.pattern.test(message)) {
50
+ return { concept: concept.name, question: concept.question };
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
@@ -1,12 +1,20 @@
1
1
  import { route } from './router.js';
2
2
  import { recordEpisode, retrogradeEpisode, upsertProcedure, addRefinement, degradeProcedure, getProcedure, procedureKey } from './memory/index.js';
3
- import { searchMemoryByKeywords, getAllMemoryForContext } from './memory-adapter.js';
3
+ import { getAllMemoryForContext } from './memory-adapter.js';
4
4
  import { generateBriefing, formatBriefing } from './briefing.js';
5
- import { fetchRelevantInsights } from './insight-cache.js';
6
5
  import { probeConsistency } from '../groq.js';
7
6
  import { executeComposite } from '../composite.js';
8
7
  import { buildGroqSystemPrompt } from '../operator/prompt-builder.js';
9
8
  import type { KernelIntent, DispatchResult, Executor } from './types.js';
9
+ import type { GroundingEnvelope } from './grounding/fanout.js';
10
+ import {
11
+ augmentWithEntityGrounding,
12
+ augmentWithInsights,
13
+ augmentWithMemoryRecall,
14
+ applyFabricationCheck,
15
+ applyGapSignal,
16
+ applyGroundingProof,
17
+ } from './grounding-layer.js';
10
18
  import {
11
19
  executeGptOss,
12
20
  executeClaudeStream,
@@ -113,6 +121,7 @@ async function recordOutcome(
113
121
  latencyMs: number,
114
122
  nearMiss: string | null | undefined,
115
123
  outcome: 'success' | 'failure' | 'partial_failure',
124
+ groundingGap: boolean,
116
125
  reclassified?: boolean,
117
126
  ): Promise<void> {
118
127
  await recordEpisode(env.db, {
@@ -127,6 +136,7 @@ async function recordOutcome(
127
136
  reclassified,
128
137
  thread_id: intent.source.threadId,
129
138
  executor: plan.executor,
139
+ grounding_gap: groundingGap,
130
140
  });
131
141
 
132
142
  await upsertProcedure(env.db, procKey, plan.executor, JSON.stringify({ executor: plan.executor }), outcome, latencyMs, cost);
@@ -156,43 +166,29 @@ interface AugmentedContext {
156
166
  classification: string;
157
167
  procKey: string;
158
168
  existingProcedure: Awaited<ReturnType<typeof getProcedure>>;
169
+ grounding?: GroundingEnvelope;
159
170
  }
160
171
 
161
172
  async function augmentIntent(intent: KernelIntent, env: EdgeEnv): Promise<AugmentedContext> {
162
173
  // 1. Route
163
174
  const { plan, nearMiss, reclassified } = await route(intent, env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl, env.ai, env.tarotscriptFetcher);
175
+ if (intent.forcedExecutor) {
176
+ plan.executor = intent.forcedExecutor;
177
+ plan.reasoning = `Forced by channel frame: ${intent.forcedExecutor}`;
178
+ plan.costCeiling = intent.forcedExecutor === 'direct' ? 'free'
179
+ : intent.forcedExecutor === 'workers_ai' || intent.forcedExecutor === 'gpt_oss' || intent.forcedExecutor === 'groq'
180
+ ? 'cheap'
181
+ : 'expensive';
182
+ }
164
183
  const classification = intent.classified ?? 'unknown';
165
184
  const procKey = procedureKey(classification, intent.complexity);
166
185
  const existingProcedure = await getProcedure(env.db, procKey);
167
186
 
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
- }
187
+ // 1.3-1.5. Grounding-layer augmentations: cross-repo insights, entity
188
+ // grounding, and memory recall. Each helper is non-fatal by contract.
189
+ await augmentWithInsights(intent, classification, env);
190
+ const grounding = await augmentWithEntityGrounding(intent, classification, env);
191
+ await augmentWithMemoryRecall(intent, classification, env);
196
192
 
197
193
  // 1.6. Implicit feedback — retrograde previous episode on user correction
198
194
  if (classification === 'user_correction' && intent.source.threadId) {
@@ -221,7 +217,7 @@ async function augmentIntent(intent: KernelIntent, env: EdgeEnv): Promise<Augmen
221
217
  }
222
218
  }
223
219
 
224
- return { plan, nearMiss, reclassified, classification, procKey, existingProcedure };
220
+ return { plan, nearMiss, reclassified, classification, procKey, existingProcedure, grounding };
225
221
  }
226
222
 
227
223
  // ─── Self-Improvement ACK ────────────────────────────────────
@@ -275,7 +271,6 @@ interface ExecuteResult {
275
271
  meta?: unknown;
276
272
  outcome: 'success' | 'failure' | 'partial_failure';
277
273
  probeResult?: 'agreed' | 'split' | 'escalated';
278
- earlyReturn?: DispatchResult; // probe agreed → skip executor
279
274
  }
280
275
 
281
276
  async function probeAndExecute(
@@ -302,11 +297,8 @@ async function probeAndExecute(
302
297
  probeResult = 'agreed';
303
298
  plan.executor = 'groq';
304
299
  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
300
  return {
308
301
  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
302
  };
311
303
  } else if (probe.sigma === 1.0) {
312
304
  const prev = plan.executor;
@@ -489,7 +481,7 @@ function buildResult(
489
481
  exec: ExecuteResult,
490
482
  latencyMs: number,
491
483
  ): DispatchResult {
492
- return {
484
+ const result: DispatchResult = {
493
485
  text: exec.text,
494
486
  executor: ctx.plan.executor,
495
487
  cost: exec.cost,
@@ -501,6 +493,29 @@ function buildResult(
501
493
  probeResult: exec.probeResult,
502
494
  meta: exec.meta,
503
495
  };
496
+ if (ctx.grounding) {
497
+ result.grounded = ctx.grounding.grounded;
498
+ result.sources = ctx.grounding.sources;
499
+ result.unknowns = ctx.grounding.unknowns;
500
+ result.searched = ctx.grounding.searched;
501
+ }
502
+ return result;
503
+ }
504
+
505
+ async function finalizeGrounding(
506
+ ctx: AugmentedContext,
507
+ intent: KernelIntent,
508
+ env: EdgeEnv,
509
+ exec: ExecuteResult,
510
+ latencyMs: number,
511
+ ): Promise<{ result: DispatchResult; outcome: 'success' | 'failure' | 'partial_failure'; groundingGap: boolean }> {
512
+ const result = buildResult(ctx, intent, exec, latencyMs);
513
+ await applyFabricationCheck(result, exec.text, env);
514
+ const outcome = applyGroundingProof(exec.outcome, ctx.classification, result);
515
+ const groundingGap =
516
+ (result.unverified_claims?.length ?? 0) > 0 ||
517
+ (result.unknowns?.length ?? 0) > 0;
518
+ return { result, outcome, groundingGap };
504
519
  }
505
520
 
506
521
  // ─── Main Dispatch Loop ──────────────────────────────────────
@@ -514,10 +529,10 @@ export async function dispatch(intent: KernelIntent, env: EdgeEnv): Promise<Disp
514
529
  if (ack) return ack;
515
530
 
516
531
  const exec = await probeAndExecute(ctx, intent, env, startMs);
517
- if (exec.earlyReturn) return exec.earlyReturn;
518
-
519
532
  const latencyMs = Date.now() - startMs;
520
- await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, exec.outcome, ctx.reclassified);
533
+ const { result, outcome, groundingGap } = await finalizeGrounding(ctx, intent, env, exec, latencyMs);
534
+ await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, outcome, groundingGap, ctx.reclassified);
535
+ await applyGapSignal(result, ctx.procKey, ctx.classification, env);
521
536
 
522
537
  // Background: shadow exploration + shadow read
523
538
  if (env.ctx) {
@@ -533,7 +548,7 @@ export async function dispatch(intent: KernelIntent, env: EdgeEnv): Promise<Disp
533
548
  }
534
549
  }
535
550
 
536
- return buildResult(ctx, intent, exec, latencyMs);
551
+ return result;
537
552
  }
538
553
 
539
554
  // ─── Streaming Dispatch ──────────────────────────────────────
@@ -547,10 +562,10 @@ export async function dispatchStream(
547
562
  const ctx = await augmentIntent(intent, env);
548
563
 
549
564
  const exec = await probeAndExecute(ctx, intent, env, startMs, onDelta);
550
- if (exec.earlyReturn) return exec.earlyReturn;
551
-
552
565
  const latencyMs = Date.now() - startMs;
553
- await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, exec.outcome, ctx.reclassified);
566
+ const { result, outcome, groundingGap } = await finalizeGrounding(ctx, intent, env, exec, latencyMs);
567
+ await recordOutcome(env, intent, ctx.classification, ctx.procKey, ctx.plan, ctx.existingProcedure, exec.text, exec.cost, latencyMs, ctx.nearMiss, outcome, groundingGap, ctx.reclassified);
568
+ await applyGapSignal(result, ctx.procKey, ctx.classification, env);
554
569
 
555
570
  // Background: shadow exploration + shadow read
556
571
  if (env.ctx) {
@@ -566,5 +581,5 @@ export async function dispatchStream(
566
581
  }
567
582
  }
568
583
 
569
- return buildResult(ctx, intent, exec, latencyMs);
584
+ return result;
570
585
  }
@@ -3,6 +3,7 @@
3
3
  // No eval(). No code execution. Just parameterized prompts.
4
4
 
5
5
  import { type EdgeEnv } from './dispatch.js';
6
+ import { buildLLMProviderFactory } from './provider-factory.js';
6
7
  import type { ToolExecutor, ToolStatus } from '../schema-enums.js';
7
8
 
8
9
  // ─── Types ──────────────────────────────────────────────────
@@ -160,58 +161,20 @@ export async function executeDynamicTool(
160
161
  ): Promise<ToolInvocationResult> {
161
162
  const rendered = renderPrompt(tool.prompt_template, inputs);
162
163
  const start = Date.now();
163
- let text = '';
164
- let cost = 0;
165
-
166
- if (tool.executor === 'workers_ai' && env.ai) {
167
- // Workers AI — free inference
168
- const result = await env.ai.run('@cf/meta/llama-3.1-8b-instruct' as Parameters<Ai['run']>[0], {
169
- messages: [
170
- { role: 'system', content: 'You are a focused tool. Answer precisely. No preamble.' },
171
- { role: 'user', content: rendered },
172
- ],
173
- max_tokens: 1024,
174
- }) as { response?: string };
175
- text = result.response ?? '';
176
- cost = 0;
177
- } else if (tool.executor === 'groq' && env.groqApiKey) {
178
- // Groq — fast, cheap
179
- const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
180
- method: 'POST',
181
- headers: {
182
- 'Authorization': `Bearer ${env.groqApiKey}`,
183
- 'Content-Type': 'application/json',
184
- },
185
- body: JSON.stringify({
186
- model: env.groqModel ?? 'llama-3.3-70b-versatile',
187
- messages: [
188
- { role: 'system', content: 'You are a focused tool. Answer precisely. No preamble.' },
189
- { role: 'user', content: rendered },
190
- ],
191
- max_tokens: 1024,
192
- }),
193
- signal: AbortSignal.timeout(15_000),
194
- });
195
- if (!res.ok) throw new Error(`Groq error: ${res.status}`);
196
- const data = await res.json() as { choices: Array<{ message: { content: string } }> };
197
- text = data.choices[0]?.message?.content ?? '';
198
- cost = 0.001; // ~$0.001 per Groq call
199
- } else {
200
- // Default: Workers AI fallback (always available on CF Workers)
201
- if (env.ai) {
202
- const result = await env.ai.run('@cf/meta/llama-3.1-8b-instruct' as Parameters<Ai['run']>[0], {
203
- messages: [
204
- { role: 'system', content: 'You are a focused tool. Answer precisely. No preamble.' },
205
- { role: 'user', content: rendered },
206
- ],
207
- max_tokens: 1024,
208
- }) as { response?: string };
209
- text = result.response ?? '';
210
- cost = 0;
211
- } else {
212
- throw new Error(`Executor "${tool.executor}" not available — no API key or binding`);
213
- }
214
- }
164
+ const executor = tool.executor as ToolExecutor;
165
+ const model = resolveDynamicToolModel(executor, env);
166
+ const factory = buildLLMProviderFactory(env);
167
+ const result = await factory.generateResponse({
168
+ model,
169
+ messages: [
170
+ { role: 'system', content: 'You are a focused tool. Answer precisely. No preamble.' },
171
+ { role: 'user', content: rendered },
172
+ ],
173
+ maxTokens: 1024,
174
+ temperature: 0.2,
175
+ });
176
+ const text = result.message ?? '';
177
+ const cost = result.usage.cost;
215
178
 
216
179
  const latencyMs = Date.now() - start;
217
180
 
@@ -229,6 +192,21 @@ export async function executeDynamicTool(
229
192
  return { text, cost, latency_ms: latencyMs, executor: tool.executor };
230
193
  }
231
194
 
195
+ function resolveDynamicToolModel(executor: ToolExecutor, env: EdgeEnv): string {
196
+ if (executor === 'workers_ai') {
197
+ if (!env.ai) throw new Error('Executor "workers_ai" not available — no AI binding');
198
+ return '@cf/meta/llama-3.1-8b-instruct';
199
+ }
200
+
201
+ if (executor === 'groq') {
202
+ if (!env.groqApiKey) throw new Error('Executor "groq" not available — no Groq API key');
203
+ return env.groqModel ?? 'llama-3.3-70b-versatile';
204
+ }
205
+
206
+ if (!env.ai) throw new Error(`Executor "${executor}" not available — no AI binding`);
207
+ return env.gptOssModel;
208
+ }
209
+
232
210
  // ─── Lifecycle (GC + Promotion) ─────────────────────────────
233
211
 
234
212
  const MAX_ACTIVE_TOOLS = 50;
File without changes
File without changes
@@ -28,6 +28,7 @@ function buildClaudeConfig(env: EdgeEnv, intent: KernelIntent, model: string, re
28
28
  braveApiKey: env.braveApiKey,
29
29
  roundtableDb: env.roundtableDb,
30
30
  memoryBinding: env.memoryBinding,
31
+ edgeEnv: env,
31
32
  };
32
33
  }
33
34