@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.
- package/cli/aegis.mjs +356 -0
- package/package.json +21 -3
- package/public/assets/index-CQHn03rW.css +1 -0
- package/public/assets/index-CTKpNJEr.js +74 -0
- package/public/index.html +14 -0
- package/schema.sql +4 -0
- package/src/adapters/voice/cloudflare-agent.ts +0 -0
- package/src/agent-routing.ts +38 -0
- package/src/assets.ts +6 -0
- package/src/auth.ts +14 -5
- package/src/bluesky.ts +0 -0
- package/src/claude-tools/content.ts +0 -0
- package/src/claude-tools/email.ts +0 -0
- package/src/claude.ts +133 -268
- package/src/codebeast.ts +0 -0
- package/src/composite.ts +49 -79
- package/src/content/column.ts +0 -0
- package/src/content/hero-image.ts +0 -0
- package/src/content/index.ts +0 -0
- package/src/content/journal.ts +0 -0
- package/src/content/roundtable.ts +0 -0
- package/src/contracts/agenda-item.contract.ts +0 -0
- package/src/contracts/cc-task.contract.ts +0 -0
- package/src/contracts/goal.contract.ts +0 -0
- package/src/contracts/memory-entry.contract.ts +0 -0
- package/src/core.ts +5 -0
- package/src/dashboard.ts +0 -0
- package/src/decision-docs.ts +0 -0
- package/src/dispatch.ts +0 -0
- package/src/durable-objects/chat-session-auth.ts +20 -0
- package/src/durable-objects/chat-session.ts +251 -0
- package/src/edge-env.ts +0 -0
- package/src/exports.ts +0 -0
- package/src/github-projects.ts +0 -0
- package/src/groq.ts +61 -113
- package/src/index.ts +11 -1
- package/src/kernel/argus-actions.ts +0 -0
- package/src/kernel/argus-correlation.ts +0 -0
- package/src/kernel/board.ts +0 -0
- package/src/kernel/classify-memory-topic.ts +0 -0
- package/src/kernel/disambiguation.ts +55 -0
- package/src/kernel/dispatch.ts +59 -44
- package/src/kernel/dynamic-tools.ts +30 -52
- package/src/kernel/executor-port.ts +0 -0
- package/src/kernel/executor-router.ts +0 -0
- package/src/kernel/executors/claude.ts +1 -0
- package/src/kernel/executors/direct.ts +14 -0
- package/src/kernel/executors/workers-ai.ts +5 -0
- package/src/kernel/grounding/fabrication-detector.ts +0 -0
- package/src/kernel/grounding/fanout.ts +0 -0
- package/src/kernel/grounding/semantic-sanhedrin.ts +0 -0
- package/src/kernel/grounding/verify.ts +0 -0
- package/src/kernel/grounding-layer.ts +0 -0
- package/src/kernel/insight-cache.ts +0 -0
- package/src/kernel/memory/episodic.ts +3 -1
- package/src/kernel/memory/insights.ts +0 -0
- package/src/kernel/memory-guardrails.ts +0 -0
- package/src/kernel/memory-service.ts +0 -0
- package/src/kernel/patterns.ts +0 -0
- package/src/kernel/port.ts +0 -0
- package/src/kernel/provider-factory.ts +0 -0
- package/src/kernel/resilience.ts +0 -0
- package/src/kernel/router.ts +33 -11
- package/src/kernel/scheduled/agent-dispatch.ts +0 -0
- package/src/kernel/scheduled/argus-analytics.ts +0 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +0 -0
- package/src/kernel/scheduled/argus-notify.ts +0 -0
- package/src/kernel/scheduled/board-sync.ts +0 -0
- package/src/kernel/scheduled/ci-watcher.ts +0 -0
- package/src/kernel/scheduled/content-drip.ts +0 -0
- package/src/kernel/scheduled/content.ts +0 -0
- package/src/kernel/scheduled/conversation-facts.ts +9 -7
- package/src/kernel/scheduled/cost-report.ts +0 -0
- package/src/kernel/scheduled/dev-activity.ts +0 -0
- package/src/kernel/scheduled/digest.ts +30 -3
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +0 -0
- package/src/kernel/scheduled/dreaming/facts.ts +0 -0
- package/src/kernel/scheduled/dreaming/index.ts +0 -0
- package/src/kernel/scheduled/dreaming/llm.ts +9 -5
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +0 -0
- package/src/kernel/scheduled/dreaming/persona.ts +0 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +0 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +0 -0
- package/src/kernel/scheduled/entropy.ts +0 -0
- package/src/kernel/scheduled/feed-watcher.ts +0 -0
- package/src/kernel/scheduled/inbox-processor.ts +0 -0
- package/src/kernel/scheduled/issue-proposer.ts +0 -0
- package/src/kernel/scheduled/issue-watcher.ts +0 -0
- package/src/kernel/scheduled/pr-automerge.ts +0 -0
- package/src/kernel/scheduled/product-health.ts +0 -0
- package/src/kernel/scheduled/self-improvement.ts +0 -0
- package/src/kernel/scheduled/social-engage.ts +12 -8
- package/src/kernel/scheduled/task-audit.ts +0 -0
- package/src/kernel/types.ts +6 -0
- package/src/landing.ts +0 -0
- package/src/lib/audit-chain/chain.ts +0 -0
- package/src/lib/audit-chain/types.ts +0 -0
- package/src/lib/observability/errors.ts +0 -0
- package/src/operator/config.ts +0 -0
- package/src/operator/persona.ts +0 -0
- package/src/operator/prompt-builder.ts +3 -0
- package/src/pulse.ts +0 -0
- package/src/routes/bluesky.ts +0 -0
- package/src/routes/chat-ws.ts +17 -0
- package/src/routes/codebeast.ts +0 -0
- package/src/routes/content.ts +0 -0
- package/src/routes/dynamic-tools.ts +0 -0
- package/src/routes/messages.ts +11 -6
- package/src/routes/observability.ts +0 -0
- package/src/routes/operator-logs.ts +0 -0
- package/src/routes/pages.ts +12 -1
- package/src/schema-enums.ts +0 -0
- package/src/task-intelligence.ts +0 -0
- package/src/types.ts +8 -0
- package/src/ui/index.html +13 -0
- package/src/ui/main.tsx +356 -0
- package/src/ui/styles.css +391 -0
- package/src/ui.ts +594 -2
- package/src/version.ts +3 -3
- package/src/wiki/client.ts +0 -0
- package/src/wiki/types.ts +0 -0
package/src/composite.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
// Claude Sonnet: $3/$15 per MTok (synthesize)
|
|
8
8
|
// Expected total: $0.01-0.03 per composite query
|
|
9
9
|
|
|
10
|
+
import type { LLMMessage, ToolResult as LLMToolResult } from '@stackbilt/llm-providers';
|
|
10
11
|
import { askGroqJson } from './groq.js';
|
|
11
12
|
import { buildContext, handleInProcessTool, callMcpWithRetry, resolveMcpTool } from './claude.js';
|
|
12
|
-
import { toOpenAiTools
|
|
13
|
+
import { toOpenAiTools } from './workers-ai-chat.js';
|
|
13
14
|
import { McpClient, McpRegistry } from './mcp-client.js';
|
|
14
15
|
import { operatorConfig } from './operator/index.js';
|
|
15
16
|
import { buildPersonaPreamble } from './operator/prompt-builder.js';
|
|
@@ -17,6 +18,7 @@ import { getCognitiveState, formatCognitiveContext } from './kernel/cognition.js
|
|
|
17
18
|
import { getAttachedBlocks, assembleBlockContext } from './kernel/memory/blocks.js';
|
|
18
19
|
import { getConversationHistory, budgetConversationHistory } from './kernel/memory/index.js';
|
|
19
20
|
import { classifyCourtCard, type CourtCard, type CourtCardProfile } from './kernel/court-cards.js';
|
|
21
|
+
import { buildLLMProviderFactory } from './kernel/provider-factory.js';
|
|
20
22
|
import type { KernelIntent } from './kernel/types.js';
|
|
21
23
|
import type { EdgeEnv } from './kernel/dispatch.js';
|
|
22
24
|
|
|
@@ -43,10 +45,6 @@ interface SubtaskResult {
|
|
|
43
45
|
// ─── Cost rates ─────────────────────────────────────────────
|
|
44
46
|
|
|
45
47
|
const GROQ_GPT_OSS_RATES = { input: 0.15, output: 0.60 };
|
|
46
|
-
const CF_GPT_OSS_RATES = { input: 0.35, output: 0.75 };
|
|
47
|
-
const CLAUDE_SONNET_RATES = { input: 3, output: 15 };
|
|
48
|
-
const CLAUDE_OPUS_RATES = { input: 15, output: 75 };
|
|
49
|
-
|
|
50
48
|
// ─── Phase 1: Orchestrate ───────────────────────────────────
|
|
51
49
|
|
|
52
50
|
const ORCHESTRATOR_SYSTEM = `You are a task decomposition engine. Given a user query and a list of available tools, decompose the query into subtasks that can be executed independently.
|
|
@@ -160,13 +158,7 @@ function validateDagIntent(userQuery: string, dag: ExecutionDAG): boolean {
|
|
|
160
158
|
return true;
|
|
161
159
|
}
|
|
162
160
|
|
|
163
|
-
// ─── Phase 2: Gather (
|
|
164
|
-
|
|
165
|
-
type ChatMessage =
|
|
166
|
-
| { role: 'system'; content: string }
|
|
167
|
-
| { role: 'user'; content: string }
|
|
168
|
-
| { role: 'assistant'; content: string; tool_calls?: Array<{ id: string; type: string; function: { name: string; arguments: string } }> }
|
|
169
|
-
| { role: 'tool'; tool_call_id: string; content: string };
|
|
161
|
+
// ─── Phase 2: Gather (provider-backed Workers AI tool loop) ──
|
|
170
162
|
|
|
171
163
|
const MAX_GATHER_ROUNDS = 6;
|
|
172
164
|
|
|
@@ -210,7 +202,6 @@ async function synthesize(
|
|
|
210
202
|
courtCard?: CourtCardProfile,
|
|
211
203
|
): Promise<{ text: string; cost: number }> {
|
|
212
204
|
const model = useOpus ? env.opusModel : env.claudeModel;
|
|
213
|
-
const rates = useOpus ? CLAUDE_OPUS_RATES : CLAUDE_SONNET_RATES;
|
|
214
205
|
|
|
215
206
|
const subtaskSummary = subtaskResults.map(r => {
|
|
216
207
|
// Include raw gathered data so synthesis can recover structured values the analysis step may have dropped
|
|
@@ -233,38 +224,17 @@ async function synthesize(
|
|
|
233
224
|
}
|
|
234
225
|
} catch { /* non-fatal — synthesize without cognitive context */ }
|
|
235
226
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
model,
|
|
245
|
-
max_tokens: 4096,
|
|
246
|
-
system: `${buildPersonaPreamble()} Synthesize the analyzed subtask results into a coherent, actionable answer. Speak as AEGIS — the co-founder who knows the business inside-out. Be thorough but concise. Reference specific products, numbers, and context from the portfolio below. Never give generic consultant advice; give the answer a co-founder would give.${courtCard ? ` ${courtCard.synthesisVoice}` : ''}${contextSuffix}`,
|
|
247
|
-
messages: [{
|
|
248
|
-
role: 'user',
|
|
249
|
-
content: `Original query: ${intent.raw}\n\nSynthesis instruction: ${synthesisInstruction}\n\nSubtask results:\n${subtaskSummary}`,
|
|
250
|
-
}],
|
|
251
|
-
}),
|
|
227
|
+
const result = await buildLLMProviderFactory(env).generateResponse({
|
|
228
|
+
model,
|
|
229
|
+
maxTokens: 4096,
|
|
230
|
+
systemPrompt: `${buildPersonaPreamble()} Synthesize the analyzed subtask results into a coherent, actionable answer. Speak as AEGIS — the co-founder who knows the business inside-out. Be thorough but concise. Reference specific products, numbers, and context from the portfolio below. Never give generic consultant advice; give the answer a co-founder would give.${courtCard ? ` ${courtCard.synthesisVoice}` : ''}${contextSuffix}`,
|
|
231
|
+
messages: [{
|
|
232
|
+
role: 'user',
|
|
233
|
+
content: `Original query: ${intent.raw}\n\nSynthesis instruction: ${synthesisInstruction}\n\nSubtask results:\n${subtaskSummary}`,
|
|
234
|
+
}],
|
|
252
235
|
});
|
|
253
236
|
|
|
254
|
-
|
|
255
|
-
const err = await response.text();
|
|
256
|
-
throw new Error(`Anthropic API error ${response.status}: ${err}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const data = await response.json<{
|
|
260
|
-
content: Array<{ type: string; text?: string }>;
|
|
261
|
-
usage: { input_tokens: number; output_tokens: number };
|
|
262
|
-
}>();
|
|
263
|
-
|
|
264
|
-
const text = data.content.filter(b => b.type === 'text').map(b => b.text ?? '').join('');
|
|
265
|
-
const cost = (data.usage.input_tokens * rates.input + data.usage.output_tokens * rates.output) / 1_000_000;
|
|
266
|
-
|
|
267
|
-
return { text: text || '(no synthesis)', cost };
|
|
237
|
+
return { text: result.message || '(no synthesis)', cost: result.usage.cost };
|
|
268
238
|
}
|
|
269
239
|
|
|
270
240
|
// ─── Groq synthesis fallback ────────────────────────────────
|
|
@@ -363,7 +333,10 @@ export async function executeComposite(
|
|
|
363
333
|
githubRepo: env.githubRepo,
|
|
364
334
|
braveApiKey: env.braveApiKey,
|
|
365
335
|
roundtableDb: env.roundtableDb,
|
|
336
|
+
memoryBinding: env.memoryBinding,
|
|
337
|
+
resendApiKeys: { resendApiKey: env.resendApiKey, resendApiKeyPersonal: env.resendApiKeyPersonal },
|
|
366
338
|
userQuery: intent.raw,
|
|
339
|
+
edgeEnv: env,
|
|
367
340
|
}, env.roundtableDb);
|
|
368
341
|
|
|
369
342
|
// Load conversation history for context continuity
|
|
@@ -602,7 +575,7 @@ async function gatherSubtaskInstrumented(
|
|
|
602
575
|
? `Original request: ${originalQuery}\n\nYour subtask: ${subtask.description}\n\nIMPORTANT: Use exact identifiers (UUIDs, IDs, enum values) from the original request when calling tools.`
|
|
603
576
|
: subtask.description;
|
|
604
577
|
|
|
605
|
-
const messages:
|
|
578
|
+
const messages: LLMMessage[] = [
|
|
606
579
|
{ role: 'system', content: `${systemPrompt}\n\nFocus: ${subtask.description}\nGather the data needed and return your findings.` },
|
|
607
580
|
{ role: 'user', content: userContent },
|
|
608
581
|
];
|
|
@@ -613,29 +586,26 @@ async function gatherSubtaskInstrumented(
|
|
|
613
586
|
// Tool loop — up to MAX_GATHER_ROUNDS
|
|
614
587
|
for (let round = 0; round < MAX_GATHER_ROUNDS; round++) {
|
|
615
588
|
subrequestCount += 1; // 1 AI call per round
|
|
616
|
-
const result = await env.
|
|
617
|
-
messages,
|
|
618
|
-
|
|
619
|
-
|
|
589
|
+
const result = await buildLLMProviderFactory(env).generateResponse({
|
|
590
|
+
messages: [...messages],
|
|
591
|
+
model: env.gptOssModel,
|
|
592
|
+
...(openAiTools.length > 0 ? { tools: openAiTools as Parameters<ReturnType<typeof buildLLMProviderFactory>['generateResponse']>[0]['tools'] } : {}),
|
|
593
|
+
maxTokens: 2048,
|
|
620
594
|
temperature: 0.2,
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const usage = extractUsage(result);
|
|
626
|
-
if (usage) {
|
|
627
|
-
totalCost += (usage.prompt_tokens * CF_GPT_OSS_RATES.input
|
|
628
|
-
+ usage.completion_tokens * CF_GPT_OSS_RATES.output) / 1_000_000;
|
|
629
|
-
}
|
|
595
|
+
topP: 0.9,
|
|
596
|
+
frequencyPenalty: 0.3,
|
|
597
|
+
});
|
|
630
598
|
|
|
631
|
-
|
|
632
|
-
const
|
|
599
|
+
totalCost += result.usage.cost;
|
|
600
|
+
const toolCalls = result.toolCalls ?? [];
|
|
601
|
+
const responseText = result.message;
|
|
633
602
|
|
|
634
603
|
if (toolCalls.length === 0) {
|
|
635
604
|
return { gathered: responseText ?? '(no data gathered)', cost: totalCost, subrequestCount };
|
|
636
605
|
}
|
|
637
606
|
|
|
638
|
-
messages.push({ role: 'assistant', content: responseText ?? '',
|
|
607
|
+
messages.push({ role: 'assistant', content: responseText ?? '', toolCalls });
|
|
608
|
+
const toolResults: LLMToolResult[] = [];
|
|
639
609
|
|
|
640
610
|
for (const call of toolCalls) {
|
|
641
611
|
subrequestCount += 1; // 1 subrequest per tool call (external fetch or DB query)
|
|
@@ -650,6 +620,7 @@ async function gatherSubtaskInstrumented(
|
|
|
650
620
|
{ apiKey: env.anthropicApiKey, model: env.claudeModel, baseUrl: env.anthropicBaseUrl },
|
|
651
621
|
env.memoryBinding,
|
|
652
622
|
{ resendApiKey: env.resendApiKey, resendApiKeyPersonal: env.resendApiKeyPersonal },
|
|
623
|
+
env,
|
|
653
624
|
);
|
|
654
625
|
|
|
655
626
|
if (inProcess !== null) {
|
|
@@ -663,26 +634,29 @@ async function gatherSubtaskInstrumented(
|
|
|
663
634
|
}
|
|
664
635
|
}
|
|
665
636
|
|
|
666
|
-
|
|
637
|
+
toolResults.push({ id: call.id, output: toolResult });
|
|
667
638
|
}
|
|
639
|
+
messages.push({ role: 'user', content: '', toolResults });
|
|
668
640
|
}
|
|
669
641
|
|
|
670
642
|
// Force summary if tool rounds exhausted
|
|
671
643
|
// Condense messages: strip tool_calls metadata and truncate tool results
|
|
672
644
|
// to prevent context overflow when sending to GPT-OSS without tools definition
|
|
673
|
-
const condensedGather:
|
|
645
|
+
const condensedGather: LLMMessage[] = [messages[0]]; // system
|
|
674
646
|
const gatherFindings: string[] = [];
|
|
675
647
|
for (let i = 1; i < messages.length; i++) {
|
|
676
648
|
const msg = messages[i];
|
|
677
|
-
if (msg.role === 'user' && !
|
|
678
|
-
condensedGather.push(msg);
|
|
649
|
+
if (msg.role === 'user' && !msg.toolResults) {
|
|
650
|
+
condensedGather.push({ role: 'user', content: msg.content });
|
|
679
651
|
} else if (msg.role === 'assistant' && msg.content) {
|
|
680
652
|
gatherFindings.push(msg.content);
|
|
681
|
-
} else if (msg.
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
653
|
+
} else if (msg.toolResults) {
|
|
654
|
+
for (const toolResult of msg.toolResults) {
|
|
655
|
+
const truncated = toolResult.output.length > 2000
|
|
656
|
+
? toolResult.output.slice(0, 2000) + '... [truncated]'
|
|
657
|
+
: toolResult.output;
|
|
658
|
+
gatherFindings.push(truncated);
|
|
659
|
+
}
|
|
686
660
|
}
|
|
687
661
|
}
|
|
688
662
|
if (gatherFindings.length > 0) {
|
|
@@ -699,17 +673,13 @@ async function gatherSubtaskInstrumented(
|
|
|
699
673
|
condensedGather.push({ role: 'user', content: 'Summarize all data gathered so far. Return the raw findings.' });
|
|
700
674
|
|
|
701
675
|
subrequestCount += 1;
|
|
702
|
-
const summaryResult = await env.
|
|
676
|
+
const summaryResult = await buildLLMProviderFactory(env).generateResponse({
|
|
703
677
|
messages: condensedGather,
|
|
704
|
-
|
|
678
|
+
model: env.gptOssModel,
|
|
679
|
+
maxTokens: 2048,
|
|
705
680
|
temperature: 0.2,
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const summaryUsage = extractUsage(summaryResult);
|
|
709
|
-
if (summaryUsage) {
|
|
710
|
-
totalCost += (summaryUsage.prompt_tokens * CF_GPT_OSS_RATES.input
|
|
711
|
-
+ summaryUsage.completion_tokens * CF_GPT_OSS_RATES.output) / 1_000_000;
|
|
712
|
-
}
|
|
681
|
+
});
|
|
713
682
|
|
|
714
|
-
|
|
683
|
+
totalCost += summaryResult.usage.cost;
|
|
684
|
+
return { gathered: summaryResult.message ?? '(gather exhausted)', cost: totalCost, subrequestCount };
|
|
715
685
|
}
|
package/src/content/column.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/content/index.ts
CHANGED
|
File without changes
|
package/src/content/journal.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/core.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { observability } from './routes/observability.js';
|
|
|
27
27
|
import { pages } from './routes/pages.js';
|
|
28
28
|
import { ccTasks } from './routes/cc-tasks.js';
|
|
29
29
|
import { messages } from './routes/messages.js';
|
|
30
|
+
import { chatWs } from './routes/chat-ws.js';
|
|
30
31
|
import { dynamicToolsRoutes } from './routes/dynamic-tools.js';
|
|
31
32
|
|
|
32
33
|
// ─── Scheduled Task Plugin ──────────────────────────────────
|
|
@@ -213,6 +214,7 @@ export function createAegisApp(config: AegisAppConfig): AegisApp {
|
|
|
213
214
|
app.route('/', pages);
|
|
214
215
|
app.route('/', ccTasks);
|
|
215
216
|
app.route('/', messages);
|
|
217
|
+
app.route('/', chatWs);
|
|
216
218
|
app.route('/', dynamicToolsRoutes);
|
|
217
219
|
|
|
218
220
|
// ── Extension routes ──
|
|
@@ -268,6 +270,7 @@ export function createAegisApp(config: AegisAppConfig): AegisApp {
|
|
|
268
270
|
pages,
|
|
269
271
|
ccTasks,
|
|
270
272
|
messages,
|
|
273
|
+
chatWs,
|
|
271
274
|
dynamicTools: dynamicToolsRoutes,
|
|
272
275
|
};
|
|
273
276
|
|
|
@@ -288,6 +291,8 @@ export type {
|
|
|
288
291
|
MessageMetadata,
|
|
289
292
|
} from './types.js';
|
|
290
293
|
|
|
294
|
+
export { ChatSession } from './durable-objects/chat-session.js';
|
|
295
|
+
|
|
291
296
|
export type { EdgeEnv } from './kernel/dispatch.js';
|
|
292
297
|
|
|
293
298
|
export type {
|
package/src/dashboard.ts
CHANGED
|
File without changes
|
package/src/decision-docs.ts
CHANGED
|
File without changes
|
package/src/dispatch.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
|
|
3
|
+
export type ConversationOwnership = 'owned' | 'not_owned' | 'not_found';
|
|
4
|
+
|
|
5
|
+
export function isValidConversationId(id: string): boolean {
|
|
6
|
+
return UUID_RE.test(id);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function verifyConversationOwnership(
|
|
10
|
+
db: D1Database,
|
|
11
|
+
conversationId: string,
|
|
12
|
+
userId: string,
|
|
13
|
+
): Promise<ConversationOwnership> {
|
|
14
|
+
const row = await db.prepare(
|
|
15
|
+
'SELECT user_id FROM conversations WHERE id = ?',
|
|
16
|
+
).bind(conversationId).first<{ user_id: string | null }>();
|
|
17
|
+
|
|
18
|
+
if (!row) return 'not_found';
|
|
19
|
+
return (row.user_id ?? 'operator') === userId ? 'owned' : 'not_owned';
|
|
20
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Env, MessageMetadata } from '../types.js';
|
|
2
|
+
import { buildEdgeEnv } from '../edge-env.js';
|
|
3
|
+
import { createIntent, dispatchStream } from '../kernel/dispatch.js';
|
|
4
|
+
import type { DispatchResult, Executor } from '../kernel/types.js';
|
|
5
|
+
import { isValidConversationId, verifyConversationOwnership } from './chat-session-auth.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
const ExecutorSchema = z.enum([
|
|
9
|
+
'claude',
|
|
10
|
+
'groq',
|
|
11
|
+
'workers_ai',
|
|
12
|
+
'claude_opus',
|
|
13
|
+
'gpt_oss',
|
|
14
|
+
'composite',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const MessageFrameSchema = z.object({
|
|
18
|
+
type: z.literal('message'),
|
|
19
|
+
text: z.string().trim().min(1),
|
|
20
|
+
conversationId: z.string().optional(),
|
|
21
|
+
eventId: z.string().trim().min(1).optional(),
|
|
22
|
+
executor: ExecutorSchema.optional(),
|
|
23
|
+
}).passthrough();
|
|
24
|
+
|
|
25
|
+
type StoredMessage = {
|
|
26
|
+
id: string;
|
|
27
|
+
role: 'user' | 'assistant';
|
|
28
|
+
content: string;
|
|
29
|
+
metadata: string | null;
|
|
30
|
+
created_at: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class ChatSession implements DurableObject {
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly state: DurableObjectState,
|
|
36
|
+
private readonly env: Env,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
async fetch(request: Request): Promise<Response> {
|
|
40
|
+
if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
|
41
|
+
return new Response('Expected WebSocket upgrade', { status: 426 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pair = new WebSocketPair();
|
|
45
|
+
const client = pair[0];
|
|
46
|
+
const server = pair[1];
|
|
47
|
+
const url = new URL(request.url);
|
|
48
|
+
const userId = this.userId();
|
|
49
|
+
const conversationId = url.searchParams.get('conversationId');
|
|
50
|
+
|
|
51
|
+
this.state.acceptWebSocket(server, ['aegis-chat']);
|
|
52
|
+
this.state.waitUntil(this.sendHistory(server, conversationId, userId));
|
|
53
|
+
|
|
54
|
+
const headers = new Headers();
|
|
55
|
+
if (request.headers.get('Sec-WebSocket-Protocol')?.split(',').map((p) => p.trim()).includes('aegis-chat')) {
|
|
56
|
+
headers.set('Sec-WebSocket-Protocol', 'aegis-chat');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new Response(null, {
|
|
60
|
+
status: 101,
|
|
61
|
+
webSocket: client,
|
|
62
|
+
headers,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
67
|
+
let payload: unknown;
|
|
68
|
+
try {
|
|
69
|
+
payload = JSON.parse(typeof message === 'string' ? message : new TextDecoder().decode(message));
|
|
70
|
+
} catch {
|
|
71
|
+
this.send(ws, { type: 'error', error: 'Invalid JSON frame' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isRecord(payload) || payload.type !== 'message') return;
|
|
76
|
+
|
|
77
|
+
const parsed = MessageFrameSchema.safeParse(payload);
|
|
78
|
+
if (!parsed.success) {
|
|
79
|
+
this.send(ws, { type: 'error', error: 'Invalid message frame' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const eventId = parsed.data.eventId ?? crypto.randomUUID();
|
|
84
|
+
const eventClaimed = await this.claimEvent(eventId);
|
|
85
|
+
if (!eventClaimed) {
|
|
86
|
+
this.send(ws, { type: 'error', error: 'duplicate event' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const text = parsed.data.text;
|
|
91
|
+
const userId = this.userId();
|
|
92
|
+
const conversationId = await this.resolveConversationId(ws, parsed.data.conversationId, userId, text);
|
|
93
|
+
if (!conversationId) return;
|
|
94
|
+
|
|
95
|
+
const userMessageId = crypto.randomUUID();
|
|
96
|
+
await this.env.DB.prepare(
|
|
97
|
+
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)',
|
|
98
|
+
).bind(userMessageId, conversationId, 'user', text).run();
|
|
99
|
+
|
|
100
|
+
this.send(ws, { type: 'start', conversationId });
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const edgeEnv = buildEdgeEnv(this.env);
|
|
104
|
+
const intent = createIntent(conversationId, text, {
|
|
105
|
+
forcedExecutor: parsed.data.executor as Executor | undefined,
|
|
106
|
+
});
|
|
107
|
+
const result = await dispatchStream(intent, edgeEnv, (delta) => {
|
|
108
|
+
this.send(ws, { type: 'delta', text: delta });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const assistantMessageId = crypto.randomUUID();
|
|
112
|
+
const metadata = buildMessageMetadata(result);
|
|
113
|
+
|
|
114
|
+
await this.env.DB.prepare(
|
|
115
|
+
'INSERT INTO messages (id, conversation_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)',
|
|
116
|
+
).bind(assistantMessageId, conversationId, 'assistant', result.text, JSON.stringify(metadata)).run();
|
|
117
|
+
|
|
118
|
+
await this.env.DB.prepare(
|
|
119
|
+
"UPDATE conversations SET updated_at = datetime('now') WHERE id = ?",
|
|
120
|
+
).bind(conversationId).run();
|
|
121
|
+
|
|
122
|
+
this.send(ws, { type: 'done', conversationId, metadata: { id: assistantMessageId, ...metadata } });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
125
|
+
this.send(ws, { type: 'error', error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async resolveConversationId(
|
|
130
|
+
ws: WebSocket,
|
|
131
|
+
rawConversationId: unknown,
|
|
132
|
+
userId: string,
|
|
133
|
+
firstMessage: string,
|
|
134
|
+
): Promise<string | null> {
|
|
135
|
+
if (rawConversationId !== undefined && typeof rawConversationId !== 'string') {
|
|
136
|
+
this.send(ws, { type: 'error', error: 'conversationId must be a UUID string' });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const conversationId = rawConversationId ?? crypto.randomUUID();
|
|
141
|
+
if (!isValidConversationId(conversationId)) {
|
|
142
|
+
this.send(ws, { type: 'error', error: 'conversationId must be a UUID string' });
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ownership = await verifyConversationOwnership(this.env.DB, conversationId, userId);
|
|
147
|
+
if (ownership === 'not_owned') {
|
|
148
|
+
this.send(ws, { type: 'error', error: 'conversation not found' });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (ownership === 'owned') return conversationId;
|
|
152
|
+
|
|
153
|
+
await this.env.DB.prepare(
|
|
154
|
+
'INSERT INTO conversations (id, title, user_id) VALUES (?, ?, ?)',
|
|
155
|
+
).bind(conversationId, firstMessage.slice(0, 100), userId).run();
|
|
156
|
+
return conversationId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async claimEvent(eventId: string): Promise<boolean> {
|
|
160
|
+
const existing = await this.env.DB.prepare(
|
|
161
|
+
'SELECT event_id FROM web_events WHERE event_id = ?',
|
|
162
|
+
).bind(eventId).first();
|
|
163
|
+
if (existing) return false;
|
|
164
|
+
|
|
165
|
+
await this.env.DB.prepare(
|
|
166
|
+
'INSERT INTO web_events (event_id) VALUES (?)',
|
|
167
|
+
).bind(eventId).run();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async sendHistory(ws: WebSocket, conversationId: string | null, userId: string): Promise<void> {
|
|
172
|
+
if (!conversationId) {
|
|
173
|
+
this.send(ws, { type: 'history', conversationId: null, messages: [] });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!isValidConversationId(conversationId)) {
|
|
177
|
+
this.send(ws, { type: 'error', error: 'conversationId must be a UUID string' });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ownership = await verifyConversationOwnership(this.env.DB, conversationId, userId);
|
|
182
|
+
if (ownership === 'not_owned') {
|
|
183
|
+
this.send(ws, { type: 'error', error: 'conversation not found' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (ownership === 'not_found') {
|
|
187
|
+
this.send(ws, { type: 'history', conversationId, messages: [] });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rows = await this.env.DB.prepare(
|
|
192
|
+
`SELECT m.id, m.role, m.content, m.metadata, m.created_at
|
|
193
|
+
FROM messages m
|
|
194
|
+
JOIN conversations c ON c.id = m.conversation_id
|
|
195
|
+
WHERE m.conversation_id = ? AND c.user_id = ?
|
|
196
|
+
ORDER BY m.created_at ASC`,
|
|
197
|
+
).bind(conversationId, userId).all<StoredMessage>();
|
|
198
|
+
|
|
199
|
+
this.send(ws, {
|
|
200
|
+
type: 'history',
|
|
201
|
+
conversationId,
|
|
202
|
+
messages: rows.results.map((message) => ({
|
|
203
|
+
...message,
|
|
204
|
+
metadata: parseMetadata(message.metadata),
|
|
205
|
+
})),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private userId(): string {
|
|
210
|
+
return this.state.id.name ?? 'operator';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private send(ws: WebSocket, frame: Record<string, unknown>): void {
|
|
214
|
+
try {
|
|
215
|
+
ws.send(JSON.stringify(frame));
|
|
216
|
+
} catch {
|
|
217
|
+
// Ignore writes after the client disconnects.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildMessageMetadata(result: DispatchResult): MessageMetadata {
|
|
223
|
+
return {
|
|
224
|
+
classification: result.classification,
|
|
225
|
+
executor: result.executor,
|
|
226
|
+
procHit: result.procedureHit,
|
|
227
|
+
latencyMs: result.latency_ms,
|
|
228
|
+
cost: result.cost,
|
|
229
|
+
confidence: result.confidence,
|
|
230
|
+
reclassified: result.reclassified,
|
|
231
|
+
probeResult: result.probeResult,
|
|
232
|
+
grounded: result.grounded,
|
|
233
|
+
sources: result.sources,
|
|
234
|
+
unknowns: result.unknowns,
|
|
235
|
+
searched: result.searched,
|
|
236
|
+
unverifiedClaims: result.unverified_claims,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseMetadata(value: string | null): MessageMetadata | null {
|
|
241
|
+
if (!value) return null;
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(value) as MessageMetadata;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
250
|
+
return typeof value === 'object' && value !== null;
|
|
251
|
+
}
|
package/src/edge-env.ts
CHANGED
|
File without changes
|
package/src/exports.ts
CHANGED
|
File without changes
|
package/src/github-projects.ts
CHANGED
|
File without changes
|