@ouraihub/aether-worker 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,257 @@
1
+ import type { ImagePart, ModelRef, RunRequest } from '@ouraihub/aether-contracts'
2
+ import type { HistoryTurn } from '@ouraihub/aether-core/types'
3
+ import { badRequest } from '@ouraihub/aether-core/error'
4
+ import { canonicalJson, charCount } from '@ouraihub/aether-core/guard'
5
+ import {
6
+ checkId,
7
+ checkText,
8
+ conflict,
9
+ historyPath,
10
+ idle,
11
+ imageParts,
12
+ notFound,
13
+ target,
14
+ validateImages,
15
+ visibleText,
16
+ } from './model.ts'
17
+ import type { ConversationDocument, StoredImage, StoredTurn } from './model.ts'
18
+
19
+ export type Action =
20
+ | { kind: 'reply'; parentId: string | null }
21
+ | { kind: 'retry'; sourceId: string }
22
+ | { kind: 'translation'; sourceId: string; targetLanguage: string }
23
+ export type SelectionGuard =
24
+ | { kind: 'explicit' }
25
+ | { kind: 'selected'; turnId: string }
26
+ | { kind: 'latest'; turnId: string | null }
27
+ export interface ReservationInput {
28
+ input: string
29
+ images: ImagePart[]
30
+ historyPath: StoredTurn[]
31
+ history: HistoryTurn[]
32
+ sourceModel: ModelRef | null
33
+ }
34
+ export interface PreparedAction extends ReservationInput {
35
+ request: RunRequest
36
+ effectiveRequest: RunRequest
37
+ action: Action
38
+ selectionGuard: SelectionGuard
39
+ expected: string
40
+ requiresSourceModel: boolean
41
+ }
42
+ function latest(document: ConversationDocument): string | null {
43
+ return document.turns.findLast((t) => t.status === 'completed')?.id ?? null
44
+ }
45
+ export function authoritative(
46
+ request: RunRequest,
47
+ action: Action,
48
+ guard: SelectionGuard,
49
+ document: ConversationDocument | null,
50
+ ): ReservationInput {
51
+ if ((request.conversationId ?? null) !== (document?.summary.id ?? null))
52
+ throw notFound('conversation')
53
+ if (document) {
54
+ idle(document)
55
+ if (guard.kind === 'selected' && document.summary.selectedTurnId !== guard.turnId)
56
+ throw conflict('preparation_changed')
57
+ if (
58
+ guard.kind === 'latest' &&
59
+ (document.summary.selectedTurnId !== null || latest(document) !== guard.turnId)
60
+ )
61
+ throw conflict('preparation_changed')
62
+ }
63
+ let input = request.input,
64
+ images = request.images ?? [],
65
+ head: string | null,
66
+ sourceModel: ModelRef | null = null
67
+ if (action.kind === 'reply') {
68
+ head = action.parentId
69
+ if (head !== null && (!document || target(document, head).status !== 'completed'))
70
+ throw conflict('turn_not_completed')
71
+ } else {
72
+ if (!document) throw notFound('conversation')
73
+ const source = target(document, action.sourceId)
74
+ if (source.status === 'running') throw conflict('turn_running')
75
+ sourceModel =
76
+ source.endpointId !== null && source.modelId !== null
77
+ ? { endpointId: source.endpointId, modelId: source.modelId }
78
+ : null
79
+ if (action.kind === 'retry') {
80
+ input = source.input
81
+ images = imageParts(source.images)
82
+ head = source.parentId
83
+ } else {
84
+ const text = visibleText(source)
85
+ if (!text.trim()) throw badRequest('translateTurnId', 'empty_reply')
86
+ input = `Translate the following reply into ${action.targetLanguage}. Return only the translation, preserving Markdown formatting.\n\n${text}`
87
+ images = []
88
+ head = source.id
89
+ }
90
+ }
91
+ checkText('input', input, 100_000)
92
+ const path = document ? historyPath(document, head) : []
93
+ return {
94
+ input,
95
+ images,
96
+ historyPath: path,
97
+ history: path.map((turn) => ({
98
+ status: turn.status,
99
+ input: turn.input,
100
+ messages: turn.messages,
101
+ images: imageParts(turn.images),
102
+ })),
103
+ sourceModel,
104
+ }
105
+ }
106
+ async function digest(value: string): Promise<string> {
107
+ const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
108
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')
109
+ }
110
+ async function imageBasis(
111
+ turnId: string,
112
+ kind: string,
113
+ images: readonly StoredImage[],
114
+ ): Promise<unknown[]> {
115
+ return Promise.all(
116
+ images.map(async (image, index) => ({
117
+ turnId,
118
+ kind,
119
+ index,
120
+ mime: image.mime,
121
+ data: image.data === null ? null : await digest(image.data),
122
+ })),
123
+ )
124
+ }
125
+ export async function fingerprint(
126
+ request: RunRequest,
127
+ action: Action,
128
+ guard: SelectionGuard,
129
+ document: ConversationDocument | null,
130
+ input: ReservationInput,
131
+ ): Promise<string> {
132
+ const source = action.kind !== 'reply' && document ? target(document, action.sourceId) : null
133
+ const sourceBasis = source
134
+ ? {
135
+ id: source.id,
136
+ status: source.status,
137
+ parentId: source.parentId,
138
+ questionId: source.questionId,
139
+ supersedesId: source.supersedesId,
140
+ kind: source.kind,
141
+ modelId: source.modelId,
142
+ endpointId: source.endpointId,
143
+ input: source.input,
144
+ visibleText: action.kind === 'translation' ? visibleText(source) : null,
145
+ images: await imageBasis(source.id, 'input', source.images),
146
+ generated: await imageBasis(source.id, 'generated', source.generated),
147
+ }
148
+ : null
149
+ const path = await Promise.all(
150
+ input.historyPath.map(async (turn) => ({
151
+ id: turn.id,
152
+ parentId: turn.parentId,
153
+ status: turn.status,
154
+ input: turn.input,
155
+ messages: turn.messages,
156
+ endpointId: turn.endpointId,
157
+ modelId: turn.modelId,
158
+ reasoningEffort: turn.reasoningEffort,
159
+ images: await imageBasis(turn.id, 'input', turn.images),
160
+ generated: await imageBasis(turn.id, 'generated', turn.generated),
161
+ })),
162
+ )
163
+ const currentImages = await Promise.all(
164
+ input.images.map(async (image, index) => ({
165
+ index,
166
+ mime: image.mime,
167
+ data: await digest(image.data),
168
+ })),
169
+ )
170
+ return digest(
171
+ canonicalJson({
172
+ version: 1,
173
+ conversationId: request.conversationId ?? null,
174
+ scope: document?.summary.scope ?? request.scope ?? null,
175
+ action,
176
+ guard,
177
+ input: input.input,
178
+ images: currentImages,
179
+ source: sourceBasis,
180
+ path,
181
+ }),
182
+ )
183
+ }
184
+ export async function resolveAction(
185
+ request: RunRequest,
186
+ document: ConversationDocument | null,
187
+ ): Promise<PreparedAction> {
188
+ for (const [field, id] of [
189
+ ['conversationId', request.conversationId],
190
+ ['replyToId', request.replyToId],
191
+ ['retryTurnId', request.retryTurnId],
192
+ ['translateTurnId', request.translateTurnId],
193
+ ])
194
+ if (id !== undefined && id !== null) {
195
+ if (typeof id !== 'string') throw badRequest(String(field), 'invalid')
196
+ checkId(String(field), id)
197
+ }
198
+ if (request.scope !== undefined && request.scope !== null) checkText('scope', request.scope, 200)
199
+ checkText('input', request.input, 100_000)
200
+ validateImages(request.images ?? [])
201
+ const sourced = Boolean(request.retryTurnId || request.translateTurnId || request.regenerate)
202
+ if (
203
+ Number(Boolean(request.retryTurnId)) +
204
+ Number(Boolean(request.translateTurnId)) +
205
+ Number(request.regenerate) >
206
+ 1
207
+ )
208
+ throw badRequest('action', 'multiple')
209
+ if (sourced && request.replyToId) throw badRequest('replyToId', 'action_conflict')
210
+ if (sourced && (request.images?.length ?? 0) > 0) throw badRequest('images', 'action_conflict')
211
+ if (
212
+ Boolean(request.translateTurnId) !==
213
+ (request.targetLanguage !== null && request.targetLanguage !== undefined)
214
+ )
215
+ throw badRequest('targetLanguage', 'translation_required')
216
+ if (!request.conversationId && (sourced || request.replyToId))
217
+ throw badRequest('conversationId', 'required')
218
+ if ((request.retryTurnId || request.translateTurnId) && request.webSearch)
219
+ throw badRequest('webSearch', 'action_conflict')
220
+ let guard: SelectionGuard = { kind: 'explicit' },
221
+ head: string | null = request.replyToId ?? null
222
+ if (document && !request.replyToId && !request.retryTurnId && !request.translateTurnId) {
223
+ if (document.summary.selectedTurnId !== null) {
224
+ head = document.summary.selectedTurnId
225
+ guard = { kind: 'selected', turnId: head }
226
+ } else {
227
+ head = latest(document)
228
+ guard = { kind: 'latest', turnId: head }
229
+ }
230
+ }
231
+ let action: Action
232
+ if (request.translateTurnId) {
233
+ const language = request.targetLanguage?.trim() ?? ''
234
+ if (!language || charCount(language) > 80) throw badRequest('targetLanguage', 'invalid')
235
+ action = { kind: 'translation', sourceId: request.translateTurnId, targetLanguage: language }
236
+ } else if (request.retryTurnId) action = { kind: 'retry', sourceId: request.retryTurnId }
237
+ else if (request.regenerate) {
238
+ if (head === null) throw notFound('turn')
239
+ action = { kind: 'retry', sourceId: head }
240
+ } else action = { kind: 'reply', parentId: head }
241
+ const input = authoritative(request, action, guard, document)
242
+ const effectiveRequest: RunRequest = {
243
+ ...request,
244
+ input: input.input,
245
+ images: input.images,
246
+ ...(request.model ? {} : { model: input.sourceModel }),
247
+ }
248
+ return {
249
+ ...input,
250
+ request,
251
+ effectiveRequest,
252
+ action,
253
+ selectionGuard: guard,
254
+ expected: await fingerprint(request, action, guard, document, input),
255
+ requiresSourceModel: action.kind !== 'reply' && !request.model,
256
+ }
257
+ }