@ouraihub/aether-worker 0.0.0-stage → 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/LICENSE +789 -0
- package/migrations/0001.sql +22 -0
- package/package.json +37 -3
- package/src/app.ts +327 -0
- package/src/cloudflare.ts +49 -0
- package/src/context.ts +49 -0
- package/src/conversations/action.ts +234 -0
- package/src/conversations/index.ts +477 -0
- package/src/conversations/model.ts +402 -0
- package/src/coordinator.ts +560 -0
- package/src/credentials.ts +153 -0
- package/src/files.ts +105 -0
- package/src/host.ts +17 -0
- package/src/identity.ts +198 -0
- package/src/images/index.ts +46 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +87 -0
- package/src/mcp/auth.ts +350 -0
- package/src/mcp/client.ts +426 -0
- package/src/mcp/common.ts +120 -0
- package/src/mcp/index.ts +239 -0
- package/src/mcp/oauth.ts +284 -0
- package/src/mcp/types.ts +74 -0
- package/src/outbound.ts +305 -0
- package/src/providers/index.ts +505 -0
- package/src/runtime.ts +635 -0
- package/src/search/index.ts +431 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/persistence.ts +140 -0
- package/src/skills/service.ts +203 -0
- package/src/storage.ts +198 -0
- package/README.md +0 -3
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextUsage,
|
|
3
|
+
Conversation,
|
|
4
|
+
ConversationSummary,
|
|
5
|
+
Cost,
|
|
6
|
+
ImagePart,
|
|
7
|
+
ImageRef,
|
|
8
|
+
ReasoningEffort,
|
|
9
|
+
SkillRef,
|
|
10
|
+
TokenUsage,
|
|
11
|
+
TurnStatus,
|
|
12
|
+
} from '@ouraihub/aether-contracts'
|
|
13
|
+
import type { Message, ReasoningBlock, ToolCall } from '@ouraihub/aether-core/types'
|
|
14
|
+
import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
|
|
15
|
+
import { charCount, isRecord } from '@ouraihub/aether-core/guard'
|
|
16
|
+
import { decodeImage, IMAGE_MIMES, parseImage } from '@ouraihub/aether-core/images'
|
|
17
|
+
|
|
18
|
+
export const COLLECTION = 'conversations'
|
|
19
|
+
export const IMAGE_TTL_DAYS = 7
|
|
20
|
+
export interface StoredImage {
|
|
21
|
+
mime: string
|
|
22
|
+
data: string | null
|
|
23
|
+
createdAt: number
|
|
24
|
+
}
|
|
25
|
+
export interface StoredTurn {
|
|
26
|
+
id: string
|
|
27
|
+
seq: number
|
|
28
|
+
runId: string
|
|
29
|
+
reservationId: string
|
|
30
|
+
parentId: string | null
|
|
31
|
+
questionId: string
|
|
32
|
+
supersedesId: string | null
|
|
33
|
+
kind: 'reply' | 'translation'
|
|
34
|
+
status: TurnStatus
|
|
35
|
+
input: string
|
|
36
|
+
messages: Message[]
|
|
37
|
+
modelId: string | null
|
|
38
|
+
endpointId: string | null
|
|
39
|
+
skills: SkillRef[]
|
|
40
|
+
errorCode: string | null
|
|
41
|
+
usage: TokenUsage
|
|
42
|
+
contextUsage: ContextUsage | null
|
|
43
|
+
reasoningEffort: ReasoningEffort | null
|
|
44
|
+
cost: Cost | null
|
|
45
|
+
createdAt: number
|
|
46
|
+
images: StoredImage[]
|
|
47
|
+
generated: StoredImage[]
|
|
48
|
+
}
|
|
49
|
+
export interface ConversationDocument {
|
|
50
|
+
version: 1
|
|
51
|
+
summary: ConversationSummary
|
|
52
|
+
turns: StoredTurn[]
|
|
53
|
+
}
|
|
54
|
+
export const conflict = (reason: string): KitFailure =>
|
|
55
|
+
new KitFailure('conflict', 'Conversation changed or unavailable', { reason })
|
|
56
|
+
export const notFound = (entity: string): KitFailure => new KitFailure('not_found', 'Resource not found', { entity })
|
|
57
|
+
const corrupt = (): never => {
|
|
58
|
+
throw new KitFailure('incompatible_db', 'Invalid stored conversation')
|
|
59
|
+
}
|
|
60
|
+
export const emptyUsage = (): TokenUsage => ({
|
|
61
|
+
inputTokens: null,
|
|
62
|
+
outputTokens: null,
|
|
63
|
+
cacheReadTokens: null,
|
|
64
|
+
cacheWriteTokens: null,
|
|
65
|
+
reasoningTokens: null,
|
|
66
|
+
})
|
|
67
|
+
export function checkId(field: string, id: string): void {
|
|
68
|
+
if (!id.trim() || charCount(id) > 128) throw badRequest(field, 'invalid')
|
|
69
|
+
}
|
|
70
|
+
export function checkText(field: string, text: string, limit: number, nonempty = false): void {
|
|
71
|
+
if (charCount(text) > limit) throw badRequest(field, 'too_long', limit)
|
|
72
|
+
if (nonempty && !text.trim()) throw badRequest(field, 'empty')
|
|
73
|
+
}
|
|
74
|
+
function record(value: unknown): Record<string, unknown> {
|
|
75
|
+
return isRecord(value) ? value : corrupt()
|
|
76
|
+
}
|
|
77
|
+
function text(value: unknown): string {
|
|
78
|
+
return typeof value === 'string' ? value : corrupt()
|
|
79
|
+
}
|
|
80
|
+
function nullableText(value: unknown): string | null {
|
|
81
|
+
return value === null ? null : text(value)
|
|
82
|
+
}
|
|
83
|
+
function integer(value: unknown): number {
|
|
84
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : corrupt()
|
|
85
|
+
}
|
|
86
|
+
function count(value: unknown): number | null {
|
|
87
|
+
const parsed = value === null ? null : integer(value)
|
|
88
|
+
return parsed === null || parsed <= 0xffff_ffff ? parsed : corrupt()
|
|
89
|
+
}
|
|
90
|
+
function array<T>(value: unknown, parse: (item: unknown) => T): T[] {
|
|
91
|
+
return Array.isArray(value) ? value.map(parse) : corrupt()
|
|
92
|
+
}
|
|
93
|
+
function effort(value: unknown): ReasoningEffort | null {
|
|
94
|
+
switch (value) {
|
|
95
|
+
case null:
|
|
96
|
+
case 'default':
|
|
97
|
+
case 'none':
|
|
98
|
+
case 'auto':
|
|
99
|
+
case 'minimal':
|
|
100
|
+
case 'low':
|
|
101
|
+
case 'medium':
|
|
102
|
+
case 'high':
|
|
103
|
+
case 'xhigh':
|
|
104
|
+
case 'max':
|
|
105
|
+
return value
|
|
106
|
+
default:
|
|
107
|
+
return corrupt()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function usage(value: unknown): TokenUsage {
|
|
111
|
+
const v = record(value)
|
|
112
|
+
return {
|
|
113
|
+
inputTokens: count(v.inputTokens),
|
|
114
|
+
outputTokens: count(v.outputTokens),
|
|
115
|
+
cacheReadTokens: count(v.cacheReadTokens),
|
|
116
|
+
cacheWriteTokens: count(v.cacheWriteTokens),
|
|
117
|
+
reasoningTokens: count(v.reasoningTokens),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function context(value: unknown): ContextUsage | null {
|
|
121
|
+
if (value === null) return null
|
|
122
|
+
const v = record(value),
|
|
123
|
+
source = v.source
|
|
124
|
+
if (source !== 'reported' && source !== 'estimated' && source !== 'unknown') return corrupt()
|
|
125
|
+
return {
|
|
126
|
+
tokens: count(v.tokens),
|
|
127
|
+
contextWindow: count(v.contextWindow),
|
|
128
|
+
source,
|
|
129
|
+
endpointId: nullableText(v.endpointId),
|
|
130
|
+
modelId: nullableText(v.modelId),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function cost(value: unknown): Cost | null {
|
|
134
|
+
if (value === null) return null
|
|
135
|
+
const v = record(value),
|
|
136
|
+
currency = v.currency
|
|
137
|
+
if (
|
|
138
|
+
typeof v.amount !== 'number' ||
|
|
139
|
+
!Number.isFinite(v.amount) ||
|
|
140
|
+
v.amount < 0 ||
|
|
141
|
+
(currency !== 'USD' && currency !== 'CNY')
|
|
142
|
+
)
|
|
143
|
+
return corrupt()
|
|
144
|
+
return { amount: v.amount, currency }
|
|
145
|
+
}
|
|
146
|
+
function image(value: unknown): StoredImage {
|
|
147
|
+
const v = record(value),
|
|
148
|
+
mime = text(v.mime),
|
|
149
|
+
data = nullableText(v.data)
|
|
150
|
+
if (!IMAGE_MIMES.some((value) => value === mime)) return corrupt()
|
|
151
|
+
if (data !== null) {
|
|
152
|
+
try {
|
|
153
|
+
decodeImage({ mime, data })
|
|
154
|
+
} catch {
|
|
155
|
+
return corrupt()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { mime, data, createdAt: integer(v.createdAt) }
|
|
159
|
+
}
|
|
160
|
+
export function validateImages(images: readonly ImagePart[]): void {
|
|
161
|
+
if (images.length > 4) throw badRequest('images', 'too_many', 4)
|
|
162
|
+
for (const image of images) parseImage(image)
|
|
163
|
+
}
|
|
164
|
+
function reasoning(value: unknown): ReasoningBlock {
|
|
165
|
+
const v = record(value),
|
|
166
|
+
protocol = v.protocol
|
|
167
|
+
if (protocol !== 'openai-chat' && protocol !== 'openai-responses' && protocol !== 'anthropic-messages')
|
|
168
|
+
return corrupt()
|
|
169
|
+
return { protocol, endpointId: nullableText(v.endpointId), modelId: nullableText(v.modelId), value: json(v.value) }
|
|
170
|
+
}
|
|
171
|
+
function json(value: unknown, depth = 0): unknown {
|
|
172
|
+
if (depth > 64) return corrupt()
|
|
173
|
+
if (
|
|
174
|
+
value === null ||
|
|
175
|
+
typeof value === 'string' ||
|
|
176
|
+
typeof value === 'boolean' ||
|
|
177
|
+
(typeof value === 'number' && Number.isFinite(value))
|
|
178
|
+
)
|
|
179
|
+
return value
|
|
180
|
+
if (Array.isArray(value)) return value.map((item: unknown) => json(item, depth + 1))
|
|
181
|
+
if (isRecord(value))
|
|
182
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, json(item, depth + 1)]))
|
|
183
|
+
return corrupt()
|
|
184
|
+
}
|
|
185
|
+
function call(value: unknown): ToolCall {
|
|
186
|
+
const v = record(value)
|
|
187
|
+
return { id: text(v.id), name: text(v.name), args: jsonRecord(v.args) }
|
|
188
|
+
}
|
|
189
|
+
function jsonRecord(value: unknown): Record<string, unknown> {
|
|
190
|
+
const v = record(value)
|
|
191
|
+
return Object.fromEntries(Object.entries(v).map(([key, item]) => [key, json(item)]))
|
|
192
|
+
}
|
|
193
|
+
export function parseMessage(value: unknown): Message {
|
|
194
|
+
const v = record(value)
|
|
195
|
+
if (['user', 'assistant', 'tool'].filter((key) => Object.hasOwn(v, key)).length !== 1) return corrupt()
|
|
196
|
+
if (typeof v.user === 'string') {
|
|
197
|
+
if (v.images === undefined) return { user: v.user }
|
|
198
|
+
return {
|
|
199
|
+
user: v.user,
|
|
200
|
+
images: array(v.images, (item) => {
|
|
201
|
+
try {
|
|
202
|
+
return parseImage(item)
|
|
203
|
+
} catch {
|
|
204
|
+
return corrupt()
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (isRecord(v.assistant)) {
|
|
210
|
+
const a = v.assistant
|
|
211
|
+
return {
|
|
212
|
+
assistant: {
|
|
213
|
+
text: text(a.text),
|
|
214
|
+
tool_calls: array(a.tool_calls, call),
|
|
215
|
+
...(a.reasoning === undefined ? {} : { reasoning: array(a.reasoning, reasoning) }),
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (isRecord(v.tool)) return { tool: { call_id: text(v.tool.call_id), text: text(v.tool.text) } }
|
|
220
|
+
return corrupt()
|
|
221
|
+
}
|
|
222
|
+
export function parseDocument(value: unknown, key: string): ConversationDocument {
|
|
223
|
+
const v = record(value),
|
|
224
|
+
s = record(v.summary)
|
|
225
|
+
if (v.version !== 1 || s.id !== key) return corrupt()
|
|
226
|
+
const summary: ConversationSummary = {
|
|
227
|
+
id: text(s.id),
|
|
228
|
+
title: text(s.title),
|
|
229
|
+
scope: nullableText(s.scope),
|
|
230
|
+
selectedTurnId: nullableText(s.selectedTurnId),
|
|
231
|
+
createdAt: integer(s.createdAt),
|
|
232
|
+
updatedAt: integer(s.updatedAt),
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
!summary.id.trim() ||
|
|
236
|
+
charCount(summary.id) > 128 ||
|
|
237
|
+
charCount(summary.title) > 200 ||
|
|
238
|
+
(summary.scope !== null && charCount(summary.scope) > 200)
|
|
239
|
+
)
|
|
240
|
+
return corrupt()
|
|
241
|
+
const turns = array(v.turns, (item): StoredTurn => {
|
|
242
|
+
const t = record(item),
|
|
243
|
+
status = t.status,
|
|
244
|
+
kind = t.kind
|
|
245
|
+
if (
|
|
246
|
+
(status !== 'running' && status !== 'completed' && status !== 'failed' && status !== 'cancelled') ||
|
|
247
|
+
(kind !== 'reply' && kind !== 'translation')
|
|
248
|
+
)
|
|
249
|
+
return corrupt()
|
|
250
|
+
return {
|
|
251
|
+
id: text(t.id),
|
|
252
|
+
seq: integer(t.seq),
|
|
253
|
+
runId: text(t.runId),
|
|
254
|
+
reservationId: text(t.reservationId),
|
|
255
|
+
parentId: nullableText(t.parentId),
|
|
256
|
+
questionId: text(t.questionId),
|
|
257
|
+
supersedesId: nullableText(t.supersedesId),
|
|
258
|
+
kind,
|
|
259
|
+
status,
|
|
260
|
+
input: text(t.input),
|
|
261
|
+
messages: array(t.messages, parseMessage),
|
|
262
|
+
modelId: nullableText(t.modelId),
|
|
263
|
+
endpointId: nullableText(t.endpointId),
|
|
264
|
+
skills: array(t.skills, (item) => {
|
|
265
|
+
const skill = record(item)
|
|
266
|
+
return { id: text(skill.id), revision: integer(skill.revision) }
|
|
267
|
+
}),
|
|
268
|
+
errorCode: nullableText(t.errorCode),
|
|
269
|
+
usage: usage(t.usage),
|
|
270
|
+
contextUsage: context(t.contextUsage),
|
|
271
|
+
reasoningEffort: effort(t.reasoningEffort),
|
|
272
|
+
cost: cost(t.cost),
|
|
273
|
+
createdAt: integer(t.createdAt),
|
|
274
|
+
images: array(t.images, image),
|
|
275
|
+
generated: array(t.generated, image),
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
const ids = new Set<string>(),
|
|
279
|
+
seqs = new Set<number>()
|
|
280
|
+
let running = 0,
|
|
281
|
+
last = 0
|
|
282
|
+
for (const t of turns) {
|
|
283
|
+
if (!t.id.trim() || charCount(t.id) > 128 || ids.has(t.id) || seqs.has(t.seq) || t.seq <= last) return corrupt()
|
|
284
|
+
if (
|
|
285
|
+
!t.questionId.trim() ||
|
|
286
|
+
charCount(t.questionId) > 128 ||
|
|
287
|
+
!t.runId.trim() ||
|
|
288
|
+
!t.reservationId.trim() ||
|
|
289
|
+
charCount(t.input) > 100_000 ||
|
|
290
|
+
t.images.length > 4 ||
|
|
291
|
+
t.skills.length > 20
|
|
292
|
+
)
|
|
293
|
+
return corrupt()
|
|
294
|
+
ids.add(t.id)
|
|
295
|
+
seqs.add(t.seq)
|
|
296
|
+
last = t.seq
|
|
297
|
+
if (t.status === 'running') running++
|
|
298
|
+
}
|
|
299
|
+
if (running > 1 || (summary.selectedTurnId !== null && !ids.has(summary.selectedTurnId))) return corrupt()
|
|
300
|
+
for (const t of turns) {
|
|
301
|
+
if (
|
|
302
|
+
(t.parentId !== null && !ids.has(t.parentId)) ||
|
|
303
|
+
(t.supersedesId !== null && (!ids.has(t.supersedesId) || t.supersedesId === t.id))
|
|
304
|
+
)
|
|
305
|
+
return corrupt()
|
|
306
|
+
const seen = new Set<string>()
|
|
307
|
+
let node: StoredTurn | undefined = t
|
|
308
|
+
while (node) {
|
|
309
|
+
if (seen.has(node.id)) return corrupt()
|
|
310
|
+
seen.add(node.id)
|
|
311
|
+
node = turns.find((p) => p.id === node?.parentId)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { version: 1, summary, turns }
|
|
315
|
+
}
|
|
316
|
+
export function idle(document: ConversationDocument): void {
|
|
317
|
+
if (document.turns.some((t) => t.status === 'running')) throw conflict('turn_running')
|
|
318
|
+
}
|
|
319
|
+
export function target(document: ConversationDocument, id: string): StoredTurn {
|
|
320
|
+
checkId('turnId', id)
|
|
321
|
+
const turn = document.turns.find((t) => t.id === id)
|
|
322
|
+
if (!turn) throw notFound('turn')
|
|
323
|
+
return turn
|
|
324
|
+
}
|
|
325
|
+
export function visibleText(turn: StoredTurn): string {
|
|
326
|
+
return turn.messages.flatMap((m) => ('assistant' in m && m.assistant.text ? [m.assistant.text] : [])).join('\n\n')
|
|
327
|
+
}
|
|
328
|
+
export function reasoningText(turn: StoredTurn): string {
|
|
329
|
+
let text = ''
|
|
330
|
+
for (const message of turn.messages) {
|
|
331
|
+
if (!('assistant' in message)) continue
|
|
332
|
+
for (const block of message.assistant.reasoning ?? []) {
|
|
333
|
+
if (block.protocol === 'openai-chat' && typeof block.value === 'string') text += block.value
|
|
334
|
+
else if (
|
|
335
|
+
block.protocol === 'anthropic-messages' &&
|
|
336
|
+
isRecord(block.value) &&
|
|
337
|
+
block.value.type === 'thinking' &&
|
|
338
|
+
typeof block.value.thinking === 'string'
|
|
339
|
+
)
|
|
340
|
+
text += block.value.thinking
|
|
341
|
+
else if (block.protocol === 'openai-responses' && isRecord(block.value) && Array.isArray(block.value.summary)) {
|
|
342
|
+
for (const part of block.value.summary)
|
|
343
|
+
if (isRecord(part) && part.type === 'summary_text' && typeof part.text === 'string') text += part.text
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return text
|
|
348
|
+
}
|
|
349
|
+
export function historyPath(document: ConversationDocument, head: string | null): StoredTurn[] {
|
|
350
|
+
const path: StoredTurn[] = [],
|
|
351
|
+
seen = new Set<string>()
|
|
352
|
+
while (head !== null) {
|
|
353
|
+
if (seen.has(head)) throw conflict('history_cycle')
|
|
354
|
+
seen.add(head)
|
|
355
|
+
const turn = target(document, head)
|
|
356
|
+
path.push(turn)
|
|
357
|
+
head = turn.parentId
|
|
358
|
+
}
|
|
359
|
+
return path.reverse()
|
|
360
|
+
}
|
|
361
|
+
export function imageParts(images: readonly StoredImage[]): ImagePart[] {
|
|
362
|
+
return images.flatMap((i) => (i.data === null ? [] : [{ mime: i.mime, data: i.data }]))
|
|
363
|
+
}
|
|
364
|
+
export function imageRefs(
|
|
365
|
+
conversationId: string,
|
|
366
|
+
turnId: string,
|
|
367
|
+
kind: 'input' | 'generated',
|
|
368
|
+
images: readonly StoredImage[],
|
|
369
|
+
): ImageRef[] {
|
|
370
|
+
return images.map((image, index) => ({
|
|
371
|
+
mime: image.mime,
|
|
372
|
+
url:
|
|
373
|
+
image.data === null
|
|
374
|
+
? null
|
|
375
|
+
: `/api/v1/conversations/${encodeURIComponent(conversationId)}/turns/${encodeURIComponent(turnId)}/images/${kind}/${index}`,
|
|
376
|
+
}))
|
|
377
|
+
}
|
|
378
|
+
export function project(document: ConversationDocument): Conversation {
|
|
379
|
+
return {
|
|
380
|
+
summary: document.summary,
|
|
381
|
+
turns: document.turns.map((t) => ({
|
|
382
|
+
id: t.id,
|
|
383
|
+
parentId: t.parentId,
|
|
384
|
+
questionId: t.questionId,
|
|
385
|
+
supersedesId: t.supersedesId,
|
|
386
|
+
kind: t.kind,
|
|
387
|
+
status: t.status,
|
|
388
|
+
input: t.input,
|
|
389
|
+
text: visibleText(t),
|
|
390
|
+
modelId: t.modelId,
|
|
391
|
+
endpointId: t.endpointId,
|
|
392
|
+
errorCode: t.errorCode,
|
|
393
|
+
usage: t.usage,
|
|
394
|
+
contextUsage: t.contextUsage,
|
|
395
|
+
reasoningEffort: t.reasoningEffort,
|
|
396
|
+
cost: t.cost,
|
|
397
|
+
reasoningText: reasoningText(t),
|
|
398
|
+
images: imageRefs(document.summary.id, t.id, 'input', t.images),
|
|
399
|
+
generated: imageRefs(document.summary.id, t.id, 'generated', t.generated),
|
|
400
|
+
})),
|
|
401
|
+
}
|
|
402
|
+
}
|