synapse-gateway 2.0.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 (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. package/tsconfig.json +34 -0
@@ -0,0 +1,139 @@
1
+ import type { Message, NormalizedRequest } from '../providers/types'
2
+
3
+ export interface AnthropicMessage {
4
+ role: 'user' | 'assistant'
5
+ content: string | AnthropicContentBlock[]
6
+ }
7
+
8
+ export interface AnthropicContentBlock {
9
+ type: 'text' | 'image' | 'tool_use' | 'tool_result'
10
+ text?: string
11
+ tool_use_id?: string
12
+ name?: string
13
+ input?: unknown
14
+ content?: string
15
+ }
16
+
17
+ export interface AnthropicRequest {
18
+ model: string
19
+ messages: AnthropicMessage[]
20
+ system?: string
21
+ max_tokens: number
22
+ temperature?: number
23
+ stream?: boolean
24
+ tools?: unknown[]
25
+ }
26
+
27
+ export function toAnthropicRequest(req: NormalizedRequest): AnthropicRequest {
28
+ const messages: AnthropicMessage[] = []
29
+ let system: string | undefined
30
+
31
+ for (const msg of req.messages) {
32
+ if (msg.role === 'system') {
33
+ system = (system ? system + '\n\n' : '') + msg.content
34
+ continue
35
+ }
36
+
37
+ const anthropicMsg: AnthropicMessage = {
38
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
39
+ content: msg.content,
40
+ }
41
+
42
+ if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
43
+ anthropicMsg.content = [
44
+ { type: 'text', text: msg.content },
45
+ ...msg.toolCalls.map((tc) => ({
46
+ type: 'tool_use' as const,
47
+ id: tc.id,
48
+ name: tc.function.name,
49
+ input: JSON.parse(tc.function.arguments),
50
+ })),
51
+ ]
52
+ }
53
+
54
+ if (msg.role === 'tool') {
55
+ anthropicMsg.content = [
56
+ {
57
+ type: 'tool_result' as const,
58
+ tool_use_id: msg.toolCallId,
59
+ content: msg.content,
60
+ },
61
+ ]
62
+ }
63
+
64
+ messages.push(anthropicMsg)
65
+ }
66
+
67
+ if (messages.length === 0) {
68
+ messages.push({ role: 'user', content: ' ' })
69
+ }
70
+
71
+ if (messages[0].role !== 'user') {
72
+ messages.unshift({ role: 'user', content: ' ' })
73
+ }
74
+
75
+ let filtered = messages.filter((m) => m.role === 'user' || m.role === 'assistant')
76
+ const merged: AnthropicMessage[] = []
77
+ for (const msg of filtered) {
78
+ if (merged.length > 0 && merged[merged.length - 1].role === msg.role) {
79
+ const last = merged[merged.length - 1]
80
+ const lastText = typeof last.content === 'string' ? last.content : ''
81
+ const currText = typeof msg.content === 'string' ? msg.content : ''
82
+ merged[merged.length - 1] = { ...last, content: lastText + '\n' + currText }
83
+ } else {
84
+ merged.push(msg)
85
+ }
86
+ }
87
+
88
+ return {
89
+ model: req.model,
90
+ messages: merged,
91
+ system,
92
+ max_tokens: req.maxTokens || 4096,
93
+ temperature: req.temperature,
94
+ stream: req.stream,
95
+ tools: req.tools,
96
+ }
97
+ }
98
+
99
+ export function fromAnthropicResponse(data: Record<string, unknown>) {
100
+ const content = data.content as Array<{ type: string; text?: string }>
101
+ const textContent = content?.filter((c) => c.type === 'text').map((c) => c.text || '').join('') || ''
102
+
103
+ return {
104
+ id: data.id as string,
105
+ model: data.model as string,
106
+ choices: [
107
+ {
108
+ index: 0,
109
+ message: { role: 'assistant' as const, content: textContent },
110
+ finishReason: (data.stop_reason as string) || 'end_turn',
111
+ },
112
+ ],
113
+ usage: {
114
+ inputTokens: (data.usage as Record<string, number>)?.input_tokens || 0,
115
+ outputTokens: (data.usage as Record<string, number>)?.output_tokens || 0,
116
+ totalTokens:
117
+ ((data.usage as Record<string, number>)?.input_tokens || 0) +
118
+ ((data.usage as Record<string, number>)?.output_tokens || 0),
119
+ },
120
+ }
121
+ }
122
+
123
+ export function fromAnthropicChunk(data: Record<string, unknown>) {
124
+ const delta = data.delta as { type: string; text?: string; stop_reason?: string } | undefined
125
+ const isContent = delta?.type === 'text_delta'
126
+ const isStop = delta?.type === 'message_stop' || delta?.stop_reason
127
+
128
+ return {
129
+ id: (data.id as string) || '',
130
+ model: data.model as string,
131
+ choices: [
132
+ {
133
+ index: 0,
134
+ delta: isContent ? { role: 'assistant' as const, content: delta?.text || '' } : {},
135
+ finishReason: isStop ? (delta?.stop_reason as string) || 'end_turn' : null,
136
+ },
137
+ ],
138
+ }
139
+ }
@@ -0,0 +1,130 @@
1
+ import type { NormalizedRequest } from '../providers/types'
2
+
3
+ export interface GeminiContent {
4
+ role: 'user' | 'model'
5
+ parts: GeminiPart[]
6
+ }
7
+
8
+ export interface GeminiPart {
9
+ text?: string
10
+ functionCall?: { name: string; args: Record<string, unknown> }
11
+ functionResponse?: { name: string; response: Record<string, unknown> }
12
+ }
13
+
14
+ export interface GeminiRequest {
15
+ model: string
16
+ contents: GeminiContent[]
17
+ systemInstruction?: { parts: Array<{ text: string }> }
18
+ generationConfig: {
19
+ maxOutputTokens?: number
20
+ temperature?: number
21
+ }
22
+ tools?: unknown[]
23
+ }
24
+
25
+ export function toGeminiRequest(req: NormalizedRequest): GeminiRequest {
26
+ const contents: GeminiContent[] = []
27
+ const systemParts: Array<{ text: string }> = []
28
+
29
+ for (const msg of req.messages) {
30
+ if (msg.role === 'system') {
31
+ systemParts.push({ text: msg.content })
32
+ continue
33
+ }
34
+
35
+ const parts: GeminiPart[] = []
36
+
37
+ if (msg.role === 'assistant' && msg.toolCalls) {
38
+ for (const tc of msg.toolCalls) {
39
+ parts.push({
40
+ functionCall: {
41
+ name: tc.function.name,
42
+ args: JSON.parse(tc.function.arguments),
43
+ },
44
+ })
45
+ }
46
+ if (msg.content) {
47
+ parts.unshift({ text: msg.content })
48
+ }
49
+ } else if (msg.role === 'tool') {
50
+ parts.push({
51
+ functionResponse: {
52
+ name: msg.toolCallId || '',
53
+ response: { content: msg.content },
54
+ },
55
+ })
56
+ } else {
57
+ parts.push({ text: msg.content })
58
+ }
59
+
60
+ if (parts.length > 0) {
61
+ contents.push({
62
+ role: msg.role === 'assistant' ? 'model' : 'user',
63
+ parts,
64
+ })
65
+ }
66
+ }
67
+
68
+ const merged: GeminiContent[] = []
69
+ for (const c of contents) {
70
+ if (merged.length > 0 && merged[merged.length - 1].role === c.role) {
71
+ merged[merged.length - 1] = { ...merged[merged.length - 1], parts: [...merged[merged.length - 1].parts, ...c.parts] }
72
+ } else {
73
+ merged.push(c)
74
+ }
75
+ }
76
+
77
+ return {
78
+ model: req.model,
79
+ contents: merged,
80
+ ...(systemParts.length > 0 ? { systemInstruction: { parts: systemParts } } : {}),
81
+ generationConfig: {
82
+ maxOutputTokens: req.maxTokens,
83
+ temperature: req.temperature,
84
+ },
85
+ tools: req.tools,
86
+ }
87
+ }
88
+
89
+ export function fromGeminiResponse(data: Record<string, unknown>) {
90
+ const candidates = data.candidates as Array<{ content: { parts: GeminiPart[] }; finishReason: string }> | undefined
91
+ const candidate = candidates?.[0]
92
+ const text = candidate?.content?.parts?.map((p) => p.text || '').join('') || ''
93
+
94
+ const usage = data.usageMetadata as { promptTokenCount: number; candidatesTokenCount: number; totalTokenCount: number } | undefined
95
+
96
+ return {
97
+ id: crypto.randomUUID(),
98
+ model: data.model as string,
99
+ choices: [
100
+ {
101
+ index: 0,
102
+ message: { role: 'assistant' as const, content: text },
103
+ finishReason: candidate?.finishReason || 'STOP',
104
+ },
105
+ ],
106
+ usage: {
107
+ inputTokens: usage?.promptTokenCount || 0,
108
+ outputTokens: usage?.candidatesTokenCount || 0,
109
+ totalTokens: usage?.totalTokenCount || 0,
110
+ },
111
+ }
112
+ }
113
+
114
+ export function fromGeminiChunk(data: Record<string, unknown>) {
115
+ const candidates = data.candidates as Array<{ content: { parts: GeminiPart[] }; finishReason: string }> | undefined
116
+ const candidate = candidates?.[0]
117
+ const text = candidate?.content?.parts?.map((p) => p.text || '').join('') || ''
118
+
119
+ return {
120
+ id: '',
121
+ model: '',
122
+ choices: [
123
+ {
124
+ index: 0,
125
+ delta: text ? { role: 'assistant' as const, content: text } : {},
126
+ finishReason: candidate?.finishReason === 'STOP' ? 'stop' : null,
127
+ },
128
+ ],
129
+ }
130
+ }
@@ -0,0 +1,3 @@
1
+ export { toAnthropicRequest, fromAnthropicResponse, fromAnthropicChunk } from './anthropic'
2
+ export { toGeminiRequest, fromGeminiResponse, fromGeminiChunk } from './gemini'
3
+ export { toOpenAIRequest, fromOpenAIResponse, fromOpenAIChunk } from './openai'
@@ -0,0 +1,78 @@
1
+ export interface OpenAIMessage {
2
+ role: 'system' | 'user' | 'assistant' | 'tool'
3
+ content: string | null
4
+ tool_call_id?: string
5
+ tool_calls?: Array<{
6
+ id: string
7
+ type: 'function'
8
+ function: { name: string; arguments: string }
9
+ }>
10
+ }
11
+
12
+ export interface OpenAIRequest {
13
+ model: string
14
+ messages: OpenAIMessage[]
15
+ temperature?: number
16
+ max_tokens?: number
17
+ stream?: boolean
18
+ tools?: unknown[]
19
+ }
20
+
21
+ export function toOpenAIRequest(req: import('../providers/types').NormalizedRequest): OpenAIRequest {
22
+ return {
23
+ model: req.model,
24
+ messages: req.messages.map((m) => ({
25
+ role: m.role,
26
+ content: m.content,
27
+ ...(m.toolCallId ? { tool_call_id: m.toolCallId } : {}),
28
+ ...(m.toolCalls ? { tool_calls: m.toolCalls } : {}),
29
+ })),
30
+ temperature: req.temperature,
31
+ max_tokens: req.maxTokens,
32
+ stream: req.stream,
33
+ tools: req.tools,
34
+ }
35
+ }
36
+
37
+ export function fromOpenAIResponse(data: Record<string, unknown>) {
38
+ const choices = data.choices as Array<{
39
+ index: number
40
+ message: { role: string; content: string }
41
+ finish_reason: string
42
+ }>
43
+
44
+ const usage = data.usage as { prompt_tokens: number; completion_tokens: number; total_tokens: number }
45
+
46
+ return {
47
+ id: data.id as string,
48
+ model: data.model as string,
49
+ choices: choices.map((c) => ({
50
+ index: c.index,
51
+ message: { role: c.message.role as 'assistant', content: c.message.content },
52
+ finishReason: c.finish_reason,
53
+ })),
54
+ usage: {
55
+ inputTokens: usage?.prompt_tokens || 0,
56
+ outputTokens: usage?.completion_tokens || 0,
57
+ totalTokens: usage?.total_tokens || 0,
58
+ },
59
+ }
60
+ }
61
+
62
+ export function fromOpenAIChunk(data: Record<string, unknown>) {
63
+ const choices = data.choices as Array<{
64
+ index: number
65
+ delta: { role?: string; content?: string }
66
+ finish_reason: string | null
67
+ }>
68
+
69
+ return {
70
+ id: data.id as string,
71
+ model: data.model as string,
72
+ choices: choices.map((c) => ({
73
+ index: c.index,
74
+ delta: c.delta.content ? { role: 'assistant' as const, content: c.delta.content } : {},
75
+ finishReason: c.finish_reason,
76
+ })),
77
+ }
78
+ }
@@ -0,0 +1,158 @@
1
+ import { db } from '../db'
2
+ import { providerHealth, providers } from '../db/schema'
3
+ import { eq } from 'drizzle-orm'
4
+ import { registry } from '../providers/registry'
5
+ import { logger } from '../utils/logger'
6
+ import { v4 as uuid } from 'uuid'
7
+
8
+ interface HealthCheckResult {
9
+ providerId: string
10
+ status: 'healthy' | 'degraded' | 'down' | 'disabled'
11
+ latencyMs: number
12
+ timestamp: string
13
+ }
14
+
15
+ class HealthChecker {
16
+ private intervalHandle: ReturnType<typeof setInterval> | null = null
17
+
18
+ start(intervalMs = 5 * 60 * 1000) {
19
+ if (this.intervalHandle) return
20
+
21
+ logger.info({ intervalMinutes: intervalMs / 60000 }, 'Health checker started')
22
+
23
+ this.intervalHandle = setInterval(async () => {
24
+ try {
25
+ await this.checkAll()
26
+ } catch (err) {
27
+ logger.error({ error: (err as Error).message }, 'Health check failed')
28
+ }
29
+ }, intervalMs)
30
+
31
+ setTimeout(() => this.checkAll(), 10000)
32
+ }
33
+
34
+ stop() {
35
+ if (this.intervalHandle) {
36
+ clearInterval(this.intervalHandle)
37
+ this.intervalHandle = null
38
+ logger.info('Health checker stopped')
39
+ }
40
+ }
41
+
42
+ async checkAll(): Promise<HealthCheckResult[]> {
43
+ const adapters = registry.list()
44
+ const results: HealthCheckResult[] = []
45
+
46
+ for (const adapter of adapters) {
47
+ try {
48
+ const result = await adapter.healthCheck()
49
+ await this.recordResult(adapter.info.id, result.status, result.latencyMs)
50
+ results.push({
51
+ providerId: adapter.info.id,
52
+ status: result.status,
53
+ latencyMs: result.latencyMs,
54
+ timestamp: new Date().toISOString(),
55
+ })
56
+ } catch (err) {
57
+ await this.recordResult(adapter.info.id, 'down', 0)
58
+ results.push({
59
+ providerId: adapter.info.id,
60
+ status: 'down',
61
+ latencyMs: 0,
62
+ timestamp: new Date().toISOString(),
63
+ })
64
+ logger.warn({ provider: adapter.info.id, error: (err as Error).message }, 'Health check failed')
65
+ }
66
+ }
67
+
68
+ return results
69
+ }
70
+
71
+ async checkProvider(providerId: string): Promise<HealthCheckResult | null> {
72
+ const adapters = registry.list()
73
+ const adapter = adapters.find((a) => a.info.id === providerId)
74
+ if (!adapter) return null
75
+
76
+ try {
77
+ const result = await adapter.healthCheck()
78
+ await this.recordResult(providerId, result.status, result.latencyMs)
79
+ return {
80
+ providerId,
81
+ status: result.status,
82
+ latencyMs: result.latencyMs,
83
+ timestamp: new Date().toISOString(),
84
+ }
85
+ } catch {
86
+ await this.recordResult(providerId, 'down', 0)
87
+ return { providerId, status: 'down', latencyMs: 0, timestamp: new Date().toISOString() }
88
+ }
89
+ }
90
+
91
+ private async recordResult(providerId: string, status: 'healthy' | 'degraded' | 'down' | 'disabled', latencyMs: number) {
92
+ try {
93
+ const existing = await db.select().from(providerHealth)
94
+ .where(eq(providerHealth.providerId, providerId)).limit(1)
95
+
96
+ const now = new Date().toISOString()
97
+
98
+ if (existing.length > 0) {
99
+ const row = existing[0]
100
+ const consecutiveFailures = status === 'down'
101
+ ? (row.consecutiveFailures || 0) + 1
102
+ : 0
103
+
104
+ const totalChecks = (row.successRate || 0) * 100 + 1
105
+ const newSuccessRate = status === 'healthy'
106
+ ? ((row.successRate || 0) * 100 + 1) / totalChecks
107
+ : (row.successRate || 0) * 100 / totalChecks
108
+
109
+ const totalLatencyChecks = 100
110
+ const newAvgLatency = Math.round(
111
+ ((row.avgLatencyMs || 0) * (totalLatencyChecks - 1) + latencyMs) / totalLatencyChecks
112
+ )
113
+
114
+ await db.update(providerHealth).set({
115
+ status: status as 'healthy' | 'degraded' | 'down' | 'disabled',
116
+ avgLatencyMs: newAvgLatency,
117
+ errorRate: 1 - newSuccessRate,
118
+ successRate: newSuccessRate,
119
+ consecutiveFailures,
120
+ lastCheckAt: now,
121
+ ...(status === 'healthy' ? { lastSuccessAt: now } : {}),
122
+ updatedAt: now,
123
+ }).where(eq(providerHealth.id, row.id))
124
+ } else {
125
+ await db.insert(providerHealth).values({
126
+ id: uuid(),
127
+ providerId,
128
+ status: status as 'healthy' | 'degraded' | 'down' | 'disabled',
129
+ avgLatencyMs: latencyMs,
130
+ p95LatencyMs: latencyMs,
131
+ errorRate: status === 'healthy' ? 0 : 1,
132
+ successRate: status === 'healthy' ? 1 : 0,
133
+ consecutiveFailures: status === 'down' ? 1 : 0,
134
+ lastCheckAt: now,
135
+ lastSuccessAt: status === 'healthy' ? now : null,
136
+ updatedAt: now,
137
+ })
138
+ }
139
+ } catch (err) {
140
+ logger.warn({ error: (err as Error).message }, 'Failed to record health result')
141
+ }
142
+ }
143
+
144
+ async getProviderHealth(providerId?: string) {
145
+ try {
146
+ if (providerId) {
147
+ const rows = await db.select().from(providerHealth)
148
+ .where(eq(providerHealth.providerId, providerId)).limit(1)
149
+ return rows[0] || null
150
+ }
151
+ return await db.select().from(providerHealth)
152
+ } catch {
153
+ return []
154
+ }
155
+ }
156
+ }
157
+
158
+ export const healthChecker = new HealthChecker()
@@ -0,0 +1,83 @@
1
+ import { registerTool, registerResource } from './registry'
2
+ import { analytics } from '../analytics'
3
+ import { memoryStore } from '../memory'
4
+ import { semanticCacheStore } from '../cache'
5
+ import { healthChecker } from '../health'
6
+
7
+ registerTool({
8
+ name: 'get_usage_stats',
9
+ description: 'Get usage statistics for the last N days',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: { days: { type: 'number', description: 'Number of days (default: 7)' } },
13
+ },
14
+ handler: async (args) => analytics.getUsageStats((args.days as number) || 7),
15
+ })
16
+
17
+ registerTool({
18
+ name: 'get_health_status',
19
+ description: 'Get health status of all providers',
20
+ inputSchema: { type: 'object', properties: {} },
21
+ handler: async () => healthChecker.getProviderHealth(),
22
+ })
23
+
24
+ registerTool({
25
+ name: 'get_cache_stats',
26
+ description: 'Get semantic cache statistics',
27
+ inputSchema: { type: 'object', properties: {} },
28
+ handler: async () => semanticCacheStore.getStats(),
29
+ })
30
+
31
+ registerTool({
32
+ name: 'search_memory',
33
+ description: 'Search persistent memory for information',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ query: { type: 'string', description: 'Search query' },
38
+ types: { type: 'array', items: { type: 'string' }, description: 'Memory types to search' },
39
+ },
40
+ required: ['query'],
41
+ },
42
+ handler: async (args) => memoryStore.search(args.query as string, args.types as import('../memory/types').MemoryType[] | undefined),
43
+ })
44
+
45
+ registerTool({
46
+ name: 'clear_cache',
47
+ description: 'Clear the semantic cache',
48
+ inputSchema: { type: 'object', properties: {} },
49
+ handler: async () => { await semanticCacheStore.clear(); return { success: true } },
50
+ })
51
+
52
+ registerTool({
53
+ name: 'trigger_distillation',
54
+ description: 'Trigger experience distillation run',
55
+ inputSchema: { type: 'object', properties: {} },
56
+ handler: async () => {
57
+ const { experienceDistiller } = await import('../distiller')
58
+ return experienceDistiller.run()
59
+ },
60
+ })
61
+
62
+ registerResource({
63
+ uri: 'synapse://config',
64
+ name: 'Synapse Configuration',
65
+ description: 'Current gateway configuration',
66
+ mimeType: 'application/json',
67
+ read: async () => {
68
+ const { loadConfig } = await import('../config')
69
+ const config = await loadConfig()
70
+ return JSON.stringify(config, null, 2)
71
+ },
72
+ })
73
+
74
+ registerResource({
75
+ uri: 'synapse://providers',
76
+ name: 'Provider List',
77
+ description: 'List of registered providers',
78
+ mimeType: 'application/json',
79
+ read: async () => {
80
+ const { registry } = await import('../providers/registry')
81
+ return JSON.stringify(registry.list().map((a) => ({ id: a.info.id, name: a.info.name, prefix: a.info.prefix })), null, 2)
82
+ },
83
+ })
@@ -0,0 +1 @@
1
+ export { registerTool, registerResource, listTools, listResources, callTool, readResource } from './registry'
@@ -0,0 +1,49 @@
1
+ export interface MCPTool {
2
+ name: string
3
+ description: string
4
+ inputSchema: Record<string, unknown>
5
+ handler: (args: Record<string, unknown>) => Promise<unknown>
6
+ }
7
+
8
+ export interface MCPResource {
9
+ uri: string
10
+ name: string
11
+ description: string
12
+ mimeType: string
13
+ read: () => Promise<string>
14
+ }
15
+
16
+ const tools: MCPTool[] = []
17
+ const resources: MCPResource[] = []
18
+
19
+ export function registerTool(tool: MCPTool) {
20
+ const existing = tools.findIndex((t) => t.name === tool.name)
21
+ if (existing >= 0) tools[existing] = tool
22
+ else tools.push(tool)
23
+ }
24
+
25
+ export function registerResource(resource: MCPResource) {
26
+ const existing = resources.findIndex((r) => r.uri === resource.uri)
27
+ if (existing >= 0) resources[existing] = resource
28
+ else resources.push(resource)
29
+ }
30
+
31
+ export function listTools(): Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> {
32
+ return tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
33
+ }
34
+
35
+ export function listResources(): Array<{ uri: string; name: string; description: string; mimeType: string }> {
36
+ return resources.map((r) => ({ uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType }))
37
+ }
38
+
39
+ export async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
40
+ const tool = tools.find((t) => t.name === name)
41
+ if (!tool) throw new Error(`Tool not found: ${name}`)
42
+ return tool.handler(args)
43
+ }
44
+
45
+ export async function readResource(uri: string): Promise<string> {
46
+ const resource = resources.find((r) => r.uri === uri)
47
+ if (!resource) throw new Error(`Resource not found: ${uri}`)
48
+ return resource.read()
49
+ }
@@ -0,0 +1,3 @@
1
+ export type { MemoryType, EpisodicMemory, SemanticMemory, ProceduralMemory, MemorySearchResult } from './types'
2
+ export type { EpisodeInput, KnowledgeInput, RuleInput } from './store'
3
+ export { memoryStore } from './store'