@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.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- package/src/workers-ai-chat.ts +333 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Dynamic Tools API — CRUD + invocation for runtime-created tools
|
|
2
|
+
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
5
|
+
import type { Env } from '../types.js';
|
|
6
|
+
import {
|
|
7
|
+
createDynamicTool,
|
|
8
|
+
getDynamicTool,
|
|
9
|
+
listDynamicTools,
|
|
10
|
+
updateDynamicTool,
|
|
11
|
+
retireDynamicTool,
|
|
12
|
+
executeDynamicTool,
|
|
13
|
+
invalidateToolCache,
|
|
14
|
+
} from '../kernel/dynamic-tools.js';
|
|
15
|
+
import { buildEdgeEnv } from '../edge-env.js';
|
|
16
|
+
import type { ToolExecutor, ToolStatus } from '../schema-enums.js';
|
|
17
|
+
|
|
18
|
+
const DYNAMIC_TOOLS_BODY_LIMIT = 100 * 1024;
|
|
19
|
+
|
|
20
|
+
const dynamicToolsRoutes = new Hono<{ Bindings: Env }>();
|
|
21
|
+
|
|
22
|
+
// GET /api/dynamic-tools — list active tools
|
|
23
|
+
dynamicToolsRoutes.get('/api/dynamic-tools', async (c) => {
|
|
24
|
+
const status = c.req.query('status');
|
|
25
|
+
const limit = Math.min(Number(c.req.query('limit') ?? 50), 100);
|
|
26
|
+
const tools = await listDynamicTools(c.env.DB, { status: status ?? undefined, limit });
|
|
27
|
+
return c.json({ tools, count: tools.length });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// POST /api/dynamic-tools — create a new tool
|
|
31
|
+
dynamicToolsRoutes.post('/api/dynamic-tools', bodyLimit({ maxSize: DYNAMIC_TOOLS_BODY_LIMIT }), async (c) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = await c.req.json<{
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
input_schema?: string;
|
|
37
|
+
prompt_template: string;
|
|
38
|
+
executor?: ToolExecutor;
|
|
39
|
+
created_by?: string;
|
|
40
|
+
ttl_days?: number;
|
|
41
|
+
status?: 'active' | 'draft';
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
if (!body.name || !body.description || !body.prompt_template) {
|
|
45
|
+
return c.json({ error: 'name, description, and prompt_template are required' }, 400);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const id = await createDynamicTool(c.env.DB, body);
|
|
49
|
+
invalidateToolCache();
|
|
50
|
+
return c.json({ id, name: body.name, status: body.status ?? 'active' }, 201);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return c.json({ error: msg }, 400);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// GET /api/dynamic-tools/:id — get tool details
|
|
58
|
+
dynamicToolsRoutes.get('/api/dynamic-tools/:id', async (c) => {
|
|
59
|
+
const tool = await getDynamicTool(c.env.DB, c.req.param('id'));
|
|
60
|
+
if (!tool) return c.json({ error: 'Not found' }, 404);
|
|
61
|
+
return c.json(tool);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// PUT /api/dynamic-tools/:id — update a tool
|
|
65
|
+
dynamicToolsRoutes.put('/api/dynamic-tools/:id', async (c) => {
|
|
66
|
+
const id = c.req.param('id');
|
|
67
|
+
const tool = await getDynamicTool(c.env.DB, id);
|
|
68
|
+
if (!tool) return c.json({ error: 'Not found' }, 404);
|
|
69
|
+
|
|
70
|
+
const body = await c.req.json<{
|
|
71
|
+
description?: string;
|
|
72
|
+
prompt_template?: string;
|
|
73
|
+
executor?: ToolExecutor;
|
|
74
|
+
input_schema?: string;
|
|
75
|
+
status?: ToolStatus;
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
await updateDynamicTool(c.env.DB, tool.id, body);
|
|
79
|
+
invalidateToolCache();
|
|
80
|
+
return c.json({ updated: true, id: tool.id });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// DELETE /api/dynamic-tools/:id — retire a tool
|
|
84
|
+
dynamicToolsRoutes.delete('/api/dynamic-tools/:id', async (c) => {
|
|
85
|
+
const id = c.req.param('id');
|
|
86
|
+
const tool = await getDynamicTool(c.env.DB, id);
|
|
87
|
+
if (!tool) return c.json({ error: 'Not found' }, 404);
|
|
88
|
+
|
|
89
|
+
await retireDynamicTool(c.env.DB, tool.id);
|
|
90
|
+
invalidateToolCache();
|
|
91
|
+
return c.json({ retired: true, id: tool.id });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// POST /api/dynamic-tools/:id/invoke — execute a dynamic tool
|
|
95
|
+
dynamicToolsRoutes.post('/api/dynamic-tools/:id/invoke', bodyLimit({ maxSize: DYNAMIC_TOOLS_BODY_LIMIT }), async (c) => {
|
|
96
|
+
const tool = await getDynamicTool(c.env.DB, c.req.param('id'));
|
|
97
|
+
if (!tool) return c.json({ error: 'Not found' }, 404);
|
|
98
|
+
if (tool.status === 'draft') return c.json({ error: 'Tool is in draft status — activate it first' }, 400);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const body = await c.req.json<{ inputs?: Record<string, unknown> }>();
|
|
102
|
+
const env = buildEdgeEnv(c.env);
|
|
103
|
+
const result = await executeDynamicTool(tool, body.inputs ?? {}, env);
|
|
104
|
+
return c.json(result);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
return c.json({ error: msg }, 500);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export { dynamicToolsRoutes };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import type { Env } from '../types.js';
|
|
4
|
+
import { McpClient } from '../mcp-client.js';
|
|
5
|
+
import { addAgendaItem } from '../kernel/memory/agenda.js';
|
|
6
|
+
import { FEEDBACK_CATEGORIES, validateEnum } from '../schema-enums.js';
|
|
7
|
+
|
|
8
|
+
const FEEDBACK_BODY_LIMIT = 100 * 1024;
|
|
9
|
+
|
|
10
|
+
export const feedback = new Hono<{ Bindings: Env }>();
|
|
11
|
+
|
|
12
|
+
// CORS preflight for cross-origin feedback submissions (Client App UI → AEGIS)
|
|
13
|
+
feedback.options('/api/feedback', (c) => {
|
|
14
|
+
return new Response(null, {
|
|
15
|
+
status: 204,
|
|
16
|
+
headers: {
|
|
17
|
+
'Access-Control-Allow-Origin': '*',
|
|
18
|
+
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
|
19
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
20
|
+
'Access-Control-Max-Age': '86400',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Middleware: add CORS headers to all feedback responses
|
|
26
|
+
feedback.use('/api/feedback', async (c, next) => {
|
|
27
|
+
await next();
|
|
28
|
+
c.header('Access-Control-Allow-Origin', '*');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
feedback.post('/api/feedback', bodyLimit({ maxSize: FEEDBACK_BODY_LIMIT }), async (c) => {
|
|
32
|
+
const body = await c.req.json<{ email?: string; category?: string; message?: string }>().catch(() => null);
|
|
33
|
+
if (!body?.message || body.message.length < 5 || body.message.length > 5000) {
|
|
34
|
+
return c.json({ error: 'message required (5-5000 chars)' }, 400);
|
|
35
|
+
}
|
|
36
|
+
const category = validateEnum(FEEDBACK_CATEGORIES, body.category, 'general');
|
|
37
|
+
const id = crypto.randomUUID();
|
|
38
|
+
const userAgent = c.req.header('User-Agent') ?? null;
|
|
39
|
+
await c.env.DB.prepare(
|
|
40
|
+
'INSERT INTO feedback (id, email, category, message, source, user_agent) VALUES (?, ?, ?, ?, ?, ?)'
|
|
41
|
+
).bind(id, body.email ?? null, category, body.message, 'web', userAgent).run();
|
|
42
|
+
|
|
43
|
+
// Notify operator via email
|
|
44
|
+
if (c.env.RESEND_API_KEY) {
|
|
45
|
+
const subject = `[Feedback] ${category}: ${body.message.slice(0, 60)}${body.message.length > 60 ? '…' : ''}`;
|
|
46
|
+
const html = `<p><strong>Category:</strong> ${category}</p>
|
|
47
|
+
<p><strong>From:</strong> ${body.email ?? 'anonymous'}</p>
|
|
48
|
+
<p><strong>Message:</strong></p><p>${body.message.replace(/</g, '<').replace(/>/g, '>')}</p>`;
|
|
49
|
+
try {
|
|
50
|
+
await fetch('https://api.resend.com/emails', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { Authorization: `Bearer ${c.env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ from: 'AEGIS <agent@example.com>', to: 'admin@example.com', subject, html }),
|
|
54
|
+
});
|
|
55
|
+
} catch { /* best-effort */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── BizOps CRM integration ────────────────────────────────
|
|
59
|
+
// Wire feedback to CRM contacts + interactions (mirrors voice funnel pattern)
|
|
60
|
+
// TarotScript triage-cast runs first for deterministic classification
|
|
61
|
+
const email = body.email;
|
|
62
|
+
const message = body.message;
|
|
63
|
+
if (email && c.env.BIZOPS && c.env.BIZOPS_TOKEN) {
|
|
64
|
+
const crmWork = async () => {
|
|
65
|
+
try {
|
|
66
|
+
// ─── TarotScript triage-cast (zero-inference classification) ──
|
|
67
|
+
let triage: { ticket_category?: string; urgency_level?: string; sentiment_signal?: string; complexity_tier?: string; needs_escalation?: boolean } = {};
|
|
68
|
+
if (c.env.TAROTSCRIPT) {
|
|
69
|
+
try {
|
|
70
|
+
const triageRes = await c.env.TAROTSCRIPT.fetch('https://internal/run', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
spreadType: 'triage-cast',
|
|
75
|
+
querent: {
|
|
76
|
+
id: email,
|
|
77
|
+
intention: message,
|
|
78
|
+
state: { source: 'human', channel: 'feedback_form', account_tier: 'free' },
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (triageRes.ok) {
|
|
83
|
+
const triageData = await triageRes.json<{ facts?: typeof triage }>();
|
|
84
|
+
triage = triageData.facts ?? {};
|
|
85
|
+
console.log(`[feedback] triage-cast: category=${triage.ticket_category} urgency=${triage.urgency_level} sentiment=${triage.sentiment_signal} escalation=${triage.needs_escalation}`);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn('[feedback] triage-cast failed:', err instanceof Error ? err.message : String(err));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const bizops = new McpClient({
|
|
93
|
+
url: 'https://your-bizops.example.com/mcp',
|
|
94
|
+
token: c.env.BIZOPS_TOKEN,
|
|
95
|
+
prefix: 'bizops',
|
|
96
|
+
fetcher: c.env.BIZOPS,
|
|
97
|
+
rpcPath: '/rpc',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const STACKBILT_ORG_ID = 'f876b6eb-332f-44a8-9683-342b2147d98b';
|
|
101
|
+
|
|
102
|
+
// Upsert contact
|
|
103
|
+
let contactId: string | undefined;
|
|
104
|
+
try {
|
|
105
|
+
const searchResult = await bizops.callTool('search_contacts', { query: email });
|
|
106
|
+
const parsed = JSON.parse(searchResult);
|
|
107
|
+
if (parsed.contacts?.length > 0) {
|
|
108
|
+
contactId = parsed.contacts[0].id;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Contact search failed — proceed to create
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!contactId) {
|
|
115
|
+
try {
|
|
116
|
+
const name = email.split('@')[0] || 'Anonymous';
|
|
117
|
+
const createResult = await bizops.callTool('create_contact', {
|
|
118
|
+
org_id: STACKBILT_ORG_ID,
|
|
119
|
+
name,
|
|
120
|
+
email,
|
|
121
|
+
source: 'FEEDBACK',
|
|
122
|
+
});
|
|
123
|
+
const parsed = JSON.parse(createResult);
|
|
124
|
+
if (parsed.id) contactId = parsed.id;
|
|
125
|
+
} catch {
|
|
126
|
+
// Contact creation failed
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Log interaction — enriched with triage-cast classification
|
|
131
|
+
if (contactId) {
|
|
132
|
+
const triageCategory = triage.ticket_category ?? category;
|
|
133
|
+
const interactionBody = [
|
|
134
|
+
`**Category:** ${triageCategory}`,
|
|
135
|
+
triage.urgency_level ? `**Urgency:** ${triage.urgency_level}` : '',
|
|
136
|
+
triage.sentiment_signal ? `**Sentiment:** ${triage.sentiment_signal}` : '',
|
|
137
|
+
triage.complexity_tier ? `**Complexity:** ${triage.complexity_tier}` : '',
|
|
138
|
+
triage.needs_escalation ? '**⚠ ESCALATION FLAGGED**' : '',
|
|
139
|
+
`**Message:** ${message}`,
|
|
140
|
+
userAgent ? `**User-Agent:** ${userAgent}` : '',
|
|
141
|
+
].filter(Boolean).join('\n');
|
|
142
|
+
|
|
143
|
+
await bizops.callTool('log_interaction', {
|
|
144
|
+
org_id: STACKBILT_ORG_ID,
|
|
145
|
+
contact_id: contactId,
|
|
146
|
+
type: 'FEEDBACK',
|
|
147
|
+
direction: 'INBOUND',
|
|
148
|
+
subject: `[${triageCategory}] ${message.slice(0, 80)}`,
|
|
149
|
+
body: interactionBody.slice(0, 5000),
|
|
150
|
+
channel: 'web_form',
|
|
151
|
+
metadata_json: JSON.stringify({
|
|
152
|
+
category: triageCategory,
|
|
153
|
+
triage,
|
|
154
|
+
user_agent: userAgent,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create agenda item — priority informed by triage
|
|
160
|
+
const triagePriority = triage.needs_escalation || triage.urgency_level === 'critical'
|
|
161
|
+
? 'high' as const
|
|
162
|
+
: (category === 'bug' || triage.urgency_level === 'high')
|
|
163
|
+
? 'high' as const
|
|
164
|
+
: 'medium' as const;
|
|
165
|
+
await addAgendaItem(
|
|
166
|
+
c.env.DB,
|
|
167
|
+
`Follow up on ${triage.ticket_category ?? category} feedback from ${email}`,
|
|
168
|
+
JSON.stringify({ feedback_id: id, category: triage.ticket_category ?? category, urgency: triage.urgency_level, sentiment: triage.sentiment_signal, email, message_preview: message.slice(0, 200) }),
|
|
169
|
+
triagePriority,
|
|
170
|
+
);
|
|
171
|
+
} catch {
|
|
172
|
+
// Non-fatal — feedback already stored in D1 + email sent
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (c.executionCtx) {
|
|
177
|
+
c.executionCtx.waitUntil(crmWork());
|
|
178
|
+
} else {
|
|
179
|
+
await crmWork();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return c.json({ id, received: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
feedback.get('/api/feedback', async (c) => {
|
|
187
|
+
const limit = Math.min(parseInt(c.req.query('limit') ?? '50', 10), 200);
|
|
188
|
+
const rows = await c.env.DB.prepare(
|
|
189
|
+
'SELECT id, email, category, message, source, created_at FROM feedback ORDER BY created_at DESC LIMIT ?'
|
|
190
|
+
).bind(limit).all();
|
|
191
|
+
return c.json({ feedback: rows.results });
|
|
192
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { Env } from '../types.js';
|
|
3
|
+
import { getAllProcedures } from '../kernel/memory/index.js';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { healthPage, type HealthData } from '../health-page.js';
|
|
6
|
+
|
|
7
|
+
/** Allow consuming apps to override the reported version (set by createAegisApp). */
|
|
8
|
+
let appVersion: string | undefined;
|
|
9
|
+
export function setAppVersion(v: string): void { appVersion = v; }
|
|
10
|
+
|
|
11
|
+
interface CostHealthEntry {
|
|
12
|
+
spend_usd: number;
|
|
13
|
+
monthly_budget: number;
|
|
14
|
+
threshold_tier: string;
|
|
15
|
+
projected_depletion_days: number | null;
|
|
16
|
+
burn_rate_per_hour: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function loadCostHealth(db: D1Database): Promise<Record<string, CostHealthEntry> | null> {
|
|
20
|
+
// Tables are owned by cost-monitor scheduled task; may not exist in fresh installs.
|
|
21
|
+
const budgets = await db
|
|
22
|
+
.prepare('SELECT provider, monthly_budget, current_spend, threshold_tier, current_period_start FROM cost_budgets')
|
|
23
|
+
.all<{ provider: string; monthly_budget: number; current_spend: number; threshold_tier: string; current_period_start: string }>()
|
|
24
|
+
.catch(() => null);
|
|
25
|
+
if (!budgets || budgets.results.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const result: Record<string, CostHealthEntry> = {};
|
|
28
|
+
for (const b of budgets.results) {
|
|
29
|
+
// Latest snapshot = best burn-rate signal. Fall back to spend/hours_elapsed.
|
|
30
|
+
const snap = await db
|
|
31
|
+
.prepare(
|
|
32
|
+
'SELECT burn_rate_per_hour FROM cost_snapshots WHERE provider = ?1 ORDER BY created_at DESC LIMIT 1'
|
|
33
|
+
)
|
|
34
|
+
.bind(b.provider)
|
|
35
|
+
.first<{ burn_rate_per_hour: number }>()
|
|
36
|
+
.catch(() => null);
|
|
37
|
+
|
|
38
|
+
let burn = snap?.burn_rate_per_hour ?? 0;
|
|
39
|
+
if (!burn && b.current_spend > 0) {
|
|
40
|
+
const hoursElapsed = Math.max(
|
|
41
|
+
1,
|
|
42
|
+
(Date.now() - new Date(b.current_period_start + 'Z').getTime()) / 3_600_000
|
|
43
|
+
);
|
|
44
|
+
burn = b.current_spend / hoursElapsed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let depletion: number | null = null;
|
|
48
|
+
if (b.monthly_budget > 0 && burn > 0) {
|
|
49
|
+
const remaining = b.monthly_budget - b.current_spend;
|
|
50
|
+
depletion = remaining <= 0 ? 0 : remaining / burn / 24;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result[b.provider] = {
|
|
54
|
+
spend_usd: Number(b.current_spend.toFixed(4)),
|
|
55
|
+
monthly_budget: b.monthly_budget,
|
|
56
|
+
threshold_tier: b.threshold_tier,
|
|
57
|
+
projected_depletion_days: depletion != null ? Number(depletion.toFixed(2)) : null,
|
|
58
|
+
burn_rate_per_hour: Number(burn.toFixed(6)),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const health = new Hono<{ Bindings: Env }>();
|
|
65
|
+
|
|
66
|
+
health.get('/health', async (c) => {
|
|
67
|
+
const procedures = await getAllProcedures(c.env.DB);
|
|
68
|
+
|
|
69
|
+
// Last 24h task run stats
|
|
70
|
+
const taskStats = await c.env.DB.prepare(`
|
|
71
|
+
SELECT task_name,
|
|
72
|
+
COUNT(*) as runs,
|
|
73
|
+
SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END) as ok,
|
|
74
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors,
|
|
75
|
+
MAX(created_at) as last_run
|
|
76
|
+
FROM task_runs
|
|
77
|
+
WHERE created_at > datetime('now', '-24 hours')
|
|
78
|
+
GROUP BY task_name
|
|
79
|
+
ORDER BY errors DESC, task_name
|
|
80
|
+
`).all<{ task_name: string; runs: number; ok: number; errors: number; last_run: string }>().catch(() => ({ results: [] }));
|
|
81
|
+
|
|
82
|
+
const kernel = {
|
|
83
|
+
learned: procedures.filter(p => p.status === 'learned').length,
|
|
84
|
+
learning: procedures.filter(p => p.status === 'learning').length,
|
|
85
|
+
degraded: procedures.filter(p => p.status === 'degraded').length,
|
|
86
|
+
broken: procedures.filter(p => p.status === 'broken').length,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Docs sync staleness watermark
|
|
90
|
+
const lastDocSync = await c.env.DB.prepare(
|
|
91
|
+
"SELECT received_at FROM web_events WHERE event_id = 'last_docs_sync_at'"
|
|
92
|
+
).first<{ received_at: string }>().catch(() => null);
|
|
93
|
+
const docsSyncAgeMs = lastDocSync ? Date.now() - new Date(lastDocSync.received_at + 'Z').getTime() : null;
|
|
94
|
+
const docsSyncAgeHours = docsSyncAgeMs != null ? Math.round(docsSyncAgeMs / 3_600_000) : null;
|
|
95
|
+
const docsSyncStatus = {
|
|
96
|
+
lastSyncAge: docsSyncAgeHours,
|
|
97
|
+
status: (docsSyncAgeHours == null || docsSyncAgeHours > 168 ? 'alert' : docsSyncAgeHours > 48 ? 'warn' : 'ok') as 'ok' | 'warn' | 'alert',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// JSON response for programmatic consumers (?format=json, explicit Accept, curl)
|
|
101
|
+
const accept = c.req.header('accept') ?? '';
|
|
102
|
+
const wantsJson = c.req.query('format') === 'json'
|
|
103
|
+
|| (accept.includes('application/json') && !accept.includes('text/html'));
|
|
104
|
+
|
|
105
|
+
if (wantsJson) {
|
|
106
|
+
const costHealth = await loadCostHealth(c.env.DB);
|
|
107
|
+
return c.json({
|
|
108
|
+
status: 'ok',
|
|
109
|
+
service: 'aegis-web',
|
|
110
|
+
version: appVersion ?? VERSION,
|
|
111
|
+
mode: 'edge-native',
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
kernel,
|
|
114
|
+
tasks_24h: taskStats.results,
|
|
115
|
+
docs_sync_status: docsSyncStatus,
|
|
116
|
+
cost_health: costHealth,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Gather extra stats for the HTML dashboard (non-sensitive counts only)
|
|
121
|
+
const [memoryRow, agendaRow, goalRow, uptimeRow] = await Promise.all([
|
|
122
|
+
// Memory count from Memory Worker (sole knowledge store)
|
|
123
|
+
c.env.MEMORY
|
|
124
|
+
? c.env.MEMORY.health().then(h => ({ c: h.active_fragments })).catch(() => ({ c: 0 }))
|
|
125
|
+
: Promise.resolve({ c: 0 }),
|
|
126
|
+
c.env.DB.prepare("SELECT COUNT(*) as c FROM agent_agenda WHERE status = 'active'").first<{ c: number }>().catch(() => ({ c: 0 })),
|
|
127
|
+
c.env.DB.prepare("SELECT COUNT(*) as c FROM agent_goals WHERE status = 'active'").first<{ c: number }>().catch(() => ({ c: 0 })),
|
|
128
|
+
c.env.DB.prepare("SELECT MIN(created_at) as first_run FROM task_runs").first<{ first_run: string | null }>().catch(() => ({ first_run: null })),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const uptimeHours = uptimeRow?.first_run
|
|
132
|
+
? Math.round((Date.now() - new Date(uptimeRow.first_run + 'Z').getTime()) / 3_600_000)
|
|
133
|
+
: 0;
|
|
134
|
+
|
|
135
|
+
const healthData: HealthData = {
|
|
136
|
+
version: appVersion ?? VERSION,
|
|
137
|
+
kernel,
|
|
138
|
+
tasks: taskStats.results,
|
|
139
|
+
memoryCount: memoryRow?.c ?? 0,
|
|
140
|
+
agendaCount: agendaRow?.c ?? 0,
|
|
141
|
+
goalCount: goalRow?.c ?? 0,
|
|
142
|
+
uptimeHours,
|
|
143
|
+
docsSyncStatus,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return c.html(healthPage(healthData));
|
|
147
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import { createIntent, dispatch, dispatchStream } from '../kernel/dispatch.js';
|
|
4
|
+
import { askGroq } from '../groq.js';
|
|
5
|
+
import type { Env, MessageMetadata } from '../types.js';
|
|
6
|
+
import { buildEdgeEnv } from '../edge-env.js';
|
|
7
|
+
|
|
8
|
+
// 100 KB — generous for chat text, blocks payload abuse
|
|
9
|
+
const MESSAGE_BODY_LIMIT = 100 * 1024;
|
|
10
|
+
|
|
11
|
+
const messages = new Hono<{ Bindings: Env }>();
|
|
12
|
+
|
|
13
|
+
// ─── Gateway URL helpers ──────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function groqBaseUrl(env: Env): string | undefined {
|
|
16
|
+
if (!env.AI_GATEWAY_ID) return undefined;
|
|
17
|
+
if (!env.CF_ACCOUNT_ID) return undefined;
|
|
18
|
+
return `https://gateway.ai.cloudflare.com/v1/${env.CF_ACCOUNT_ID}/${env.AI_GATEWAY_ID}/groq`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Auto-generate conversation title (#21) ──────────────
|
|
22
|
+
|
|
23
|
+
async function generateConversationTitle(
|
|
24
|
+
db: D1Database,
|
|
25
|
+
conversationId: string,
|
|
26
|
+
firstMessage: string,
|
|
27
|
+
groqApiKey: string,
|
|
28
|
+
groqModel: string,
|
|
29
|
+
groqBase?: string,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const title = await askGroq(
|
|
33
|
+
groqApiKey,
|
|
34
|
+
groqModel,
|
|
35
|
+
'Generate a concise 3-6 word title for a conversation that starts with the following message. Return ONLY the title, no quotes, no punctuation at the end.',
|
|
36
|
+
firstMessage,
|
|
37
|
+
groqBase,
|
|
38
|
+
);
|
|
39
|
+
const cleaned = title.trim().slice(0, 100);
|
|
40
|
+
if (cleaned) {
|
|
41
|
+
await db.prepare('UPDATE conversations SET title = ? WHERE id = ?').bind(cleaned, conversationId).run();
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Non-fatal — leaves first-message slice as title
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Send Message (edge-native kernel) ───────────────────────
|
|
49
|
+
messages.post('/api/message', bodyLimit({ maxSize: MESSAGE_BODY_LIMIT }), async (c) => {
|
|
50
|
+
const body = await c.req.json<{ text: string; conversationId?: string }>();
|
|
51
|
+
if (!body.text?.trim()) {
|
|
52
|
+
return c.json({ error: 'text is required' }, 400);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const text = body.text.trim();
|
|
56
|
+
const conversationId = body.conversationId ?? crypto.randomUUID();
|
|
57
|
+
const userMessageId = crypto.randomUUID();
|
|
58
|
+
const eventId = crypto.randomUUID();
|
|
59
|
+
|
|
60
|
+
// Event dedup
|
|
61
|
+
const existing = await c.env.DB.prepare(
|
|
62
|
+
'SELECT event_id FROM web_events WHERE event_id = ?'
|
|
63
|
+
).bind(eventId).first();
|
|
64
|
+
if (existing) {
|
|
65
|
+
return c.json({ error: 'duplicate event' }, 409);
|
|
66
|
+
}
|
|
67
|
+
await c.env.DB.prepare(
|
|
68
|
+
'INSERT INTO web_events (event_id) VALUES (?)'
|
|
69
|
+
).bind(eventId).run();
|
|
70
|
+
|
|
71
|
+
// Ensure conversation exists
|
|
72
|
+
const convInsert = await c.env.DB.prepare(
|
|
73
|
+
'INSERT OR IGNORE INTO conversations (id, title) VALUES (?, ?)'
|
|
74
|
+
).bind(conversationId, text.slice(0, 100)).run();
|
|
75
|
+
const isNewConversation = (convInsert.meta.changes ?? 0) > 0;
|
|
76
|
+
|
|
77
|
+
// Save user message
|
|
78
|
+
await c.env.DB.prepare(
|
|
79
|
+
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)'
|
|
80
|
+
).bind(userMessageId, conversationId, 'user', text).run();
|
|
81
|
+
|
|
82
|
+
// Fire-and-forget title generation for new conversations (#21)
|
|
83
|
+
if (isNewConversation && c.executionCtx) {
|
|
84
|
+
c.executionCtx.waitUntil(
|
|
85
|
+
generateConversationTitle(c.env.DB, conversationId, text, c.env.GROQ_API_KEY, c.env.GROQ_MODEL || 'llama-3.3-70b-versatile', groqBaseUrl(c.env)),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dispatch through edge kernel
|
|
90
|
+
const edgeEnv = buildEdgeEnv(c.env, c.executionCtx);
|
|
91
|
+
const intent = createIntent(conversationId, text);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await dispatch(intent, edgeEnv);
|
|
95
|
+
|
|
96
|
+
// Save assistant message
|
|
97
|
+
const assistantMessageId = crypto.randomUUID();
|
|
98
|
+
const metadata: MessageMetadata = {
|
|
99
|
+
classification: result.classification,
|
|
100
|
+
executor: result.executor,
|
|
101
|
+
procHit: result.procedureHit,
|
|
102
|
+
latencyMs: result.latency_ms,
|
|
103
|
+
cost: result.cost,
|
|
104
|
+
confidence: result.confidence,
|
|
105
|
+
reclassified: result.reclassified,
|
|
106
|
+
probeResult: result.probeResult,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await c.env.DB.prepare(
|
|
110
|
+
'INSERT INTO messages (id, conversation_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)'
|
|
111
|
+
).bind(assistantMessageId, conversationId, 'assistant', result.text, JSON.stringify(metadata)).run();
|
|
112
|
+
|
|
113
|
+
await c.env.DB.prepare(
|
|
114
|
+
"UPDATE conversations SET updated_at = datetime('now') WHERE id = ?"
|
|
115
|
+
).bind(conversationId).run();
|
|
116
|
+
|
|
117
|
+
return c.json({
|
|
118
|
+
conversationId,
|
|
119
|
+
message: {
|
|
120
|
+
id: assistantMessageId,
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: result.text,
|
|
123
|
+
metadata,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
128
|
+
const errMessageId = crypto.randomUUID();
|
|
129
|
+
|
|
130
|
+
await c.env.DB.prepare(
|
|
131
|
+
'INSERT INTO messages (id, conversation_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)'
|
|
132
|
+
).bind(errMessageId, conversationId, 'assistant', `Error: ${errMsg}`, JSON.stringify({ error: true })).run();
|
|
133
|
+
|
|
134
|
+
console.error('Kernel error:', errMsg);
|
|
135
|
+
return c.json({
|
|
136
|
+
conversationId,
|
|
137
|
+
message: {
|
|
138
|
+
id: errMessageId,
|
|
139
|
+
role: 'assistant',
|
|
140
|
+
content: 'An error occurred processing your message',
|
|
141
|
+
metadata: { error: true },
|
|
142
|
+
},
|
|
143
|
+
}, 500);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ─── Streaming Message (SSE) ─────────────────────────────────
|
|
148
|
+
messages.post('/api/message/stream', bodyLimit({ maxSize: MESSAGE_BODY_LIMIT }), async (c) => {
|
|
149
|
+
const body = await c.req.json<{ text: string; conversationId?: string }>();
|
|
150
|
+
if (!body.text?.trim()) {
|
|
151
|
+
return c.json({ error: 'text is required' }, 400);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const text = body.text.trim();
|
|
155
|
+
const conversationId = body.conversationId ?? crypto.randomUUID();
|
|
156
|
+
const userMessageId = crypto.randomUUID();
|
|
157
|
+
const eventId = crypto.randomUUID();
|
|
158
|
+
|
|
159
|
+
// Event dedup
|
|
160
|
+
const existing = await c.env.DB.prepare(
|
|
161
|
+
'SELECT event_id FROM web_events WHERE event_id = ?'
|
|
162
|
+
).bind(eventId).first();
|
|
163
|
+
if (existing) return c.json({ error: 'duplicate event' }, 409);
|
|
164
|
+
await c.env.DB.prepare('INSERT INTO web_events (event_id) VALUES (?)').bind(eventId).run();
|
|
165
|
+
|
|
166
|
+
// Ensure conversation + save user message
|
|
167
|
+
const convInsertStream = await c.env.DB.prepare('INSERT OR IGNORE INTO conversations (id, title) VALUES (?, ?)').bind(conversationId, text.slice(0, 100)).run();
|
|
168
|
+
const isNewConvStream = (convInsertStream.meta.changes ?? 0) > 0;
|
|
169
|
+
await c.env.DB.prepare('INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)').bind(userMessageId, conversationId, 'user', text).run();
|
|
170
|
+
|
|
171
|
+
// Fire-and-forget title generation for new conversations (#21)
|
|
172
|
+
if (isNewConvStream && c.executionCtx) {
|
|
173
|
+
c.executionCtx.waitUntil(
|
|
174
|
+
generateConversationTitle(c.env.DB, conversationId, text, c.env.GROQ_API_KEY, c.env.GROQ_MODEL || 'llama-3.3-70b-versatile', groqBaseUrl(c.env)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const edgeEnv = buildEdgeEnv(c.env, c.executionCtx);
|
|
179
|
+
const intent = createIntent(conversationId, text);
|
|
180
|
+
|
|
181
|
+
const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
|
|
182
|
+
const writer = writable.getWriter();
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
|
|
185
|
+
const writeSSE = (data: unknown) => writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
186
|
+
|
|
187
|
+
// Run dispatch async — stream stays open until writer.close()
|
|
188
|
+
(async () => {
|
|
189
|
+
try {
|
|
190
|
+
await writeSSE({ type: 'start', conversationId });
|
|
191
|
+
|
|
192
|
+
const result = await dispatchStream(intent, edgeEnv, async (delta) => {
|
|
193
|
+
await writeSSE({ type: 'delta', text: delta });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Persist assistant message
|
|
197
|
+
const assistantMessageId = crypto.randomUUID();
|
|
198
|
+
const metadata: MessageMetadata = {
|
|
199
|
+
classification: result.classification,
|
|
200
|
+
executor: result.executor,
|
|
201
|
+
procHit: result.procedureHit,
|
|
202
|
+
latencyMs: result.latency_ms,
|
|
203
|
+
cost: result.cost,
|
|
204
|
+
confidence: result.confidence,
|
|
205
|
+
reclassified: result.reclassified,
|
|
206
|
+
probeResult: result.probeResult,
|
|
207
|
+
};
|
|
208
|
+
await c.env.DB.prepare('INSERT INTO messages (id, conversation_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)').bind(assistantMessageId, conversationId, 'assistant', result.text, JSON.stringify(metadata)).run();
|
|
209
|
+
await c.env.DB.prepare("UPDATE conversations SET updated_at = datetime('now') WHERE id = ?").bind(conversationId).run();
|
|
210
|
+
|
|
211
|
+
await writeSSE({ type: 'done', conversationId, metadata: { id: assistantMessageId, ...metadata } });
|
|
212
|
+
} catch (err) {
|
|
213
|
+
await writeSSE({ type: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
214
|
+
} finally {
|
|
215
|
+
await writer.close();
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
|
|
219
|
+
return new Response(readable, {
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'text/event-stream',
|
|
222
|
+
'Cache-Control': 'no-cache',
|
|
223
|
+
'X-Accel-Buffering': 'no',
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
export { messages };
|