@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.
- package/LICENSE +789 -0
- package/migrations/0001.sql +22 -0
- package/package.json +38 -3
- package/src/app.ts +363 -0
- package/src/cloudflare.ts +49 -0
- package/src/context.ts +55 -0
- package/src/conversations/action.ts +257 -0
- package/src/conversations/index.ts +480 -0
- package/src/conversations/model.ts +427 -0
- package/src/conversations/routes.ts +101 -0
- package/src/coordinator-helpers.ts +131 -0
- package/src/coordinator.ts +488 -0
- package/src/credentials.ts +171 -0
- package/src/files.ts +117 -0
- package/src/host.ts +25 -0
- package/src/identity.ts +227 -0
- package/src/images/index.ts +73 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +98 -0
- package/src/mcp/auth.ts +398 -0
- package/src/mcp/client.ts +491 -0
- package/src/mcp/common.ts +124 -0
- package/src/mcp/index.ts +292 -0
- package/src/mcp/oauth.ts +343 -0
- package/src/mcp/types.ts +81 -0
- package/src/outbound.ts +320 -0
- package/src/providers/catalog.ts +63 -0
- package/src/providers/http.ts +88 -0
- package/src/providers/index.ts +445 -0
- package/src/providers/model-options.ts +28 -0
- package/src/runtime-input.ts +44 -0
- package/src/runtime-tools.ts +298 -0
- package/src/runtime.ts +476 -0
- package/src/search/index.ts +497 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/persistence.ts +153 -0
- package/src/skills/service.ts +250 -0
- package/src/storage.ts +293 -0
- package/src/testing/fakes.ts +97 -0
- package/README.md +0 -3
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { ImagePart, RunRequest } from '@ouraihub/aether-contracts'
|
|
2
|
+
import { badRequest, cancelled, KitFailure, safeError } from '@ouraihub/aether-core/error'
|
|
3
|
+
import { charCount } from '@ouraihub/aether-core/guard'
|
|
4
|
+
import { ImageFailure } from '@ouraihub/aether-core/images'
|
|
5
|
+
import type { McpSession } from '@ouraihub/aether-core/mcp'
|
|
6
|
+
import type { ResolvedSearchConfig } from '@ouraihub/aether-core/search'
|
|
7
|
+
import type { ToolCall, ToolExecution, ToolSpec, Connection } from '@ouraihub/aether-core/types'
|
|
8
|
+
import type { Actor, WorkerContext } from './context.js'
|
|
9
|
+
import type { AcceptedHandle, ConversationService, PreparedAction } from './conversations/index.js'
|
|
10
|
+
import type { WorkerHost } from './host.js'
|
|
11
|
+
import type { ImageService } from './images/index.js'
|
|
12
|
+
import type { McpService } from './mcp/index.js'
|
|
13
|
+
import type { ProviderService } from './providers/index.js'
|
|
14
|
+
import type { SearchService } from './search/index.js'
|
|
15
|
+
|
|
16
|
+
export interface Candidate {
|
|
17
|
+
id: string
|
|
18
|
+
image: ImagePart
|
|
19
|
+
origin: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ToolPreparationOptions {
|
|
23
|
+
host: WorkerHost
|
|
24
|
+
providers: ProviderService
|
|
25
|
+
search?: SearchService
|
|
26
|
+
mcp?: McpService
|
|
27
|
+
images?: ImageService
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PreparedTools {
|
|
31
|
+
tools: ToolSpec[]
|
|
32
|
+
mcp: McpSession | null
|
|
33
|
+
painting: Connection | null
|
|
34
|
+
candidates: Candidate[]
|
|
35
|
+
search: ResolvedSearchConfig | null
|
|
36
|
+
nativeWebSearch: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ToolRuntimeState {
|
|
40
|
+
input: { tools: readonly ToolSpec[] }
|
|
41
|
+
context: WorkerContext
|
|
42
|
+
mcp: McpSession | null
|
|
43
|
+
painting: Connection | null
|
|
44
|
+
candidates: readonly Candidate[]
|
|
45
|
+
paintingCalls: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const assertActive = (signal: AbortSignal): void => {
|
|
49
|
+
if (signal.aborted) throw cancelled()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const missing = (field: string): KitFailure => badRequest(field, 'missing_instance')
|
|
53
|
+
|
|
54
|
+
function candidatesFor(action: PreparedAction, enabled: boolean): Candidate[] {
|
|
55
|
+
if (!enabled) return []
|
|
56
|
+
const result: Candidate[] = []
|
|
57
|
+
const add = (images: readonly { mime: string; data: string | null }[], origin: string): void => {
|
|
58
|
+
for (let i = images.length - 1; i >= 0 && result.length < 8; i--) {
|
|
59
|
+
const image = images[i]
|
|
60
|
+
if (image && image.data !== null)
|
|
61
|
+
result.push({
|
|
62
|
+
id: `image-${result.length + 1}`,
|
|
63
|
+
image: { mime: image.mime, data: image.data },
|
|
64
|
+
origin,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
add(action.images, 'current attachment')
|
|
69
|
+
for (let i = action.historyPath.length - 1; i >= 0 && result.length < 8; i--) {
|
|
70
|
+
const turn = action.historyPath[i]
|
|
71
|
+
if (turn) {
|
|
72
|
+
add(turn.generated, 'historical generated image')
|
|
73
|
+
add(turn.images, 'historical attachment')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function paintingTool(candidates: readonly Candidate[]): ToolSpec {
|
|
80
|
+
const properties: Record<string, unknown> = {
|
|
81
|
+
prompt: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
82
|
+
size: { type: 'string', enum: ['1024x1024', '1024x1536', '1536x1024'] },
|
|
83
|
+
}
|
|
84
|
+
if (candidates.length)
|
|
85
|
+
properties.image_ids = {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: { type: 'string', enum: candidates.map((item) => item.id) },
|
|
88
|
+
minItems: 1,
|
|
89
|
+
maxItems: 4,
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
name: 'generate_image',
|
|
93
|
+
description: `Generate or edit an image. At most two calls per run.${candidates.length ? ` Images you can pass in image_ids (newest first): ${candidates.map((item) => `${item.id}: ${item.origin}`).join('; ')}` : ''}`,
|
|
94
|
+
parameters: { type: 'object', properties, required: ['prompt'], additionalProperties: false },
|
|
95
|
+
approval: 'ask',
|
|
96
|
+
effect: 'other',
|
|
97
|
+
source: { kind: 'host' },
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function prepareTools(
|
|
102
|
+
options: ToolPreparationOptions,
|
|
103
|
+
effective: RunRequest,
|
|
104
|
+
runId: string,
|
|
105
|
+
preparationContext: WorkerContext,
|
|
106
|
+
signal: AbortSignal,
|
|
107
|
+
actor: Actor,
|
|
108
|
+
action: PreparedAction,
|
|
109
|
+
connection: Connection,
|
|
110
|
+
): Promise<PreparedTools> {
|
|
111
|
+
const tools = structuredClone(await options.host.tools(effective, runId, preparationContext)).map(
|
|
112
|
+
(tool) => {
|
|
113
|
+
if (tool.source.kind !== 'host') throw badRequest('tools', 'host_source')
|
|
114
|
+
return tool
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
assertActive(signal)
|
|
118
|
+
let mcp: McpSession | null = null
|
|
119
|
+
if (options.mcp && (options.host.mcpTools?.(effective, preparationContext) ?? true)) {
|
|
120
|
+
assertActive(signal)
|
|
121
|
+
mcp = await options.mcp.client(preparationContext).session(signal)
|
|
122
|
+
}
|
|
123
|
+
assertActive(signal)
|
|
124
|
+
if (mcp)
|
|
125
|
+
tools.push(
|
|
126
|
+
...mcp.tools().map(
|
|
127
|
+
(tool) =>
|
|
128
|
+
({
|
|
129
|
+
name: tool.prefixed,
|
|
130
|
+
description: tool.description,
|
|
131
|
+
parameters: { ...tool.schema },
|
|
132
|
+
approval: tool.approval,
|
|
133
|
+
effect: 'other',
|
|
134
|
+
source: { kind: 'mcp', server: tool.server },
|
|
135
|
+
}) satisfies ToolSpec,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
let painting: Connection | null = null
|
|
139
|
+
if (effective.generateImage) {
|
|
140
|
+
if (!options.images) throw missing('images')
|
|
141
|
+
const reference = await options.host.paintingModel?.(effective, preparationContext)
|
|
142
|
+
assertActive(signal)
|
|
143
|
+
if (!reference)
|
|
144
|
+
throw new KitFailure('needs_config', 'Painting model unavailable', {
|
|
145
|
+
reason: 'painting_model',
|
|
146
|
+
})
|
|
147
|
+
painting = structuredClone(await options.providers.resolve(actor, reference, 'image'))
|
|
148
|
+
assertActive(signal)
|
|
149
|
+
}
|
|
150
|
+
const candidates = candidatesFor(action, painting?.canVision ?? false)
|
|
151
|
+
if (painting && connection.supportsTools !== false) tools.push(paintingTool(candidates))
|
|
152
|
+
let search: ResolvedSearchConfig | null = null
|
|
153
|
+
let nativeWebSearch = false
|
|
154
|
+
if (effective.webSearch) {
|
|
155
|
+
if (!options.search) throw missing('search')
|
|
156
|
+
const preferences = await options.search.preferences(actor)
|
|
157
|
+
assertActive(signal)
|
|
158
|
+
nativeWebSearch =
|
|
159
|
+
preferences.modelToolsPreferred &&
|
|
160
|
+
connection.nativeWebSearch === true &&
|
|
161
|
+
connection.protocol !== 'openai-chat'
|
|
162
|
+
if (!nativeWebSearch) {
|
|
163
|
+
search = {
|
|
164
|
+
...structuredClone(await options.search.resolve(actor, 'search')),
|
|
165
|
+
preferences: structuredClone(preferences),
|
|
166
|
+
}
|
|
167
|
+
assertActive(signal)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { tools, mcp, painting, candidates, search, nativeWebSearch }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function callTool(
|
|
174
|
+
handle: AcceptedHandle | null,
|
|
175
|
+
state: ToolRuntimeState,
|
|
176
|
+
call: ToolCall,
|
|
177
|
+
signal: AbortSignal,
|
|
178
|
+
options: ToolPreparationOptions & { conversations: ConversationService | null },
|
|
179
|
+
): Promise<ToolExecution> {
|
|
180
|
+
assertActive(signal)
|
|
181
|
+
const tool = state.input.tools.find((item) => item.name === call.name)
|
|
182
|
+
if (!tool) return { kind: 'error', code: 'not_found', message: 'Tool not in this run' }
|
|
183
|
+
if (tool.source.kind === 'mcp') {
|
|
184
|
+
if (!state.mcp) return { kind: 'error', code: 'not_found', message: 'MCP tool not in this run' }
|
|
185
|
+
const result = await state.mcp.call(call.name, call.args, signal)
|
|
186
|
+
if (result.kind === 'unknown') return result
|
|
187
|
+
if (result.kind === 'failed')
|
|
188
|
+
return { kind: 'error', code: result.code, message: result.message }
|
|
189
|
+
const text = result.content
|
|
190
|
+
.map((item) =>
|
|
191
|
+
item.type === 'text' && typeof item.text === 'string' ? item.text : JSON.stringify(item),
|
|
192
|
+
)
|
|
193
|
+
.join('\n')
|
|
194
|
+
const payload = {
|
|
195
|
+
content: result.content,
|
|
196
|
+
...(result.structuredContent ? { structuredContent: result.structuredContent } : {}),
|
|
197
|
+
}
|
|
198
|
+
return result.isError
|
|
199
|
+
? { kind: 'error', code: 'mcp_error', message: text, payload }
|
|
200
|
+
: { kind: 'ok', text, payload }
|
|
201
|
+
}
|
|
202
|
+
if (call.name === 'generate_image' && state.painting)
|
|
203
|
+
return paint(handle, state, call, signal, options)
|
|
204
|
+
return options.host.callTool(call, signal, { ...state.context, signal })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function paint(
|
|
208
|
+
handle: AcceptedHandle | null,
|
|
209
|
+
state: ToolRuntimeState,
|
|
210
|
+
call: ToolCall,
|
|
211
|
+
signal: AbortSignal,
|
|
212
|
+
options: Pick<ToolPreparationOptions, 'images'> & { conversations: ConversationService | null },
|
|
213
|
+
): Promise<ToolExecution> {
|
|
214
|
+
try {
|
|
215
|
+
const { prompt, size = '1024x1024', image_ids: ids } = call.args
|
|
216
|
+
if (
|
|
217
|
+
Object.keys(call.args).some((key) => !['prompt', 'size', 'image_ids'].includes(key)) ||
|
|
218
|
+
typeof prompt !== 'string' ||
|
|
219
|
+
!prompt.trim() ||
|
|
220
|
+
charCount(prompt) > 4000
|
|
221
|
+
)
|
|
222
|
+
throw badRequest('prompt', 'invalid')
|
|
223
|
+
if (size !== '1024x1024' && size !== '1024x1536' && size !== '1536x1024')
|
|
224
|
+
throw badRequest('size', 'invalid')
|
|
225
|
+
const images: ImagePart[] = []
|
|
226
|
+
if (ids !== undefined) {
|
|
227
|
+
if (!state.painting?.canVision || !state.candidates.length)
|
|
228
|
+
throw badRequest('image_ids', 'unsupported')
|
|
229
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 4)
|
|
230
|
+
throw badRequest('image_ids', 'too_many')
|
|
231
|
+
for (const id of ids) {
|
|
232
|
+
const candidate = state.candidates.find((item) => item.id === id)
|
|
233
|
+
if (!candidate) throw badRequest('image_ids', 'unknown')
|
|
234
|
+
images.push(candidate.image)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (state.paintingCalls >= 2) throw badRequest('generateImage', 'call_limit')
|
|
238
|
+
assertActive(signal)
|
|
239
|
+
if (!state.painting || !options.images) throw missing('images')
|
|
240
|
+
state.paintingCalls++
|
|
241
|
+
const result = await options.images.generate(
|
|
242
|
+
state.painting,
|
|
243
|
+
{ prompt, size, images },
|
|
244
|
+
{ ...state.context, signal },
|
|
245
|
+
)
|
|
246
|
+
let references
|
|
247
|
+
try {
|
|
248
|
+
references =
|
|
249
|
+
handle && options.conversations
|
|
250
|
+
? await options.conversations.addGenerated(handle, [result.image], state.context.actor)
|
|
251
|
+
: [result.reference]
|
|
252
|
+
} catch {
|
|
253
|
+
return {
|
|
254
|
+
kind: 'error',
|
|
255
|
+
code: 'generated_save_failed',
|
|
256
|
+
message:
|
|
257
|
+
'Image was generated, but its persistence could not be confirmed. Do not automatically regenerate.',
|
|
258
|
+
payload: {
|
|
259
|
+
generated: true,
|
|
260
|
+
persisted: false,
|
|
261
|
+
usage: result.usage,
|
|
262
|
+
totalTokens: result.totalTokens,
|
|
263
|
+
revisedPrompt: result.revisedPrompt,
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind: 'ok',
|
|
269
|
+
text: 'Generated 1 image(s).',
|
|
270
|
+
payload: {
|
|
271
|
+
images: references,
|
|
272
|
+
usage: result.usage,
|
|
273
|
+
totalTokens: result.totalTokens,
|
|
274
|
+
revisedPrompt: result.revisedPrompt,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error instanceof ImageFailure && error.dispatch === 'unknown')
|
|
279
|
+
return {
|
|
280
|
+
kind: 'unknown',
|
|
281
|
+
reason:
|
|
282
|
+
'Image generation was dispatched; its result could not be confirmed. Do not automatically retry.',
|
|
283
|
+
}
|
|
284
|
+
if (error instanceof ImageFailure && error.dispatch === 'generated')
|
|
285
|
+
return {
|
|
286
|
+
kind: 'error',
|
|
287
|
+
code: 'generated_save_failed',
|
|
288
|
+
message: 'Image was generated, but its persistence could not be confirmed.',
|
|
289
|
+
payload: { generated: true, persisted: false },
|
|
290
|
+
}
|
|
291
|
+
const failure = safeError(error)
|
|
292
|
+
return {
|
|
293
|
+
kind: 'error',
|
|
294
|
+
code: failure.code,
|
|
295
|
+
message: error instanceof KitFailure ? error.message : 'Image operation failed',
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|