@ouraihub/aether-worker 0.1.0 → 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.
@@ -13,6 +13,8 @@ import {
13
13
  type ProviderSave,
14
14
  } from '@ouraihub/aether-contracts'
15
15
  import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
16
+ import { inferReasoning } from '@ouraihub/aether-core/providers/infer'
17
+ import { probe as probeEndpoint } from '@ouraihub/aether-core/providers/probe'
16
18
  import {
17
19
  prepare,
18
20
  validateHeaders,
@@ -22,75 +24,26 @@ import {
22
24
  charCount,
23
25
  type EndpointSnapshot,
24
26
  } from '@ouraihub/aether-core/providers/connection'
25
- import { inferReasoning } from '@ouraihub/aether-core/providers/infer'
26
- import { probe as probeEndpoint } from '@ouraihub/aether-core/providers/probe'
27
27
  import type { Connection } from '@ouraihub/aether-core/types'
28
28
  import type { OutboundHttp } from '@ouraihub/aether-core/ports'
29
29
  import { ownerKey, type Actor, type WorkerContext } from '../context.js'
30
- import { isSealedCredential, type CredentialPort, type CredentialScope, type SealedCredential } from '../credentials.js'
30
+ import type { CredentialPort, CredentialScope, SealedCredential } from '../credentials.js'
31
31
  import type { DocumentStore, StoredDocument } from '../storage.js'
32
+ import { parseCatalog, type Catalog, type Secret } from './catalog.js'
33
+ import { modelOptions } from './model-options.js'
34
+ import { handleProviderRequest } from './http.js'
32
35
 
33
- interface Secret {
34
- endpointId: string
35
- keyId: string
36
- sealed: SealedCredential
37
- }
38
- interface Catalog {
39
- providers: Provider[]
40
- secrets: Secret[]
41
- inferred: string[]
42
- }
43
36
  export interface ProviderServiceOptions {
44
37
  documents: DocumentStore
45
38
  credentials: CredentialPort
46
39
  outbound: OutboundHttp
47
40
  authorizeReveal: (request: Request, context: WorkerContext) => Promise<void>
48
41
  }
49
- function record(value: unknown): value is Record<string, unknown> {
50
- return typeof value === 'object' && value !== null && !Array.isArray(value)
51
- }
52
- function parseCatalog(value: unknown): Catalog {
53
- if (
54
- !record(value) ||
55
- !Array.isArray(value.providers) ||
56
- !Array.isArray(value.secrets) ||
57
- !Array.isArray(value.inferred)
58
- )
59
- throw new KitFailure('incompatible_db', 'Invalid provider catalog')
60
- const providers: Provider[] = []
61
- const secrets: Secret[] = []
62
- const inferred: string[] = []
63
- for (const item of value.providers) {
64
- if (!isContract('Provider', item)) throw new KitFailure('incompatible_db', 'Invalid provider document')
65
- providers.push(item)
66
- }
67
- for (const item of value.secrets) {
68
- if (
69
- !record(item) ||
70
- typeof item.endpointId !== 'string' ||
71
- typeof item.keyId !== 'string' ||
72
- !isSealedCredential(item.sealed)
73
- )
74
- throw new KitFailure('incompatible_db', 'Invalid provider credential')
75
- secrets.push({ endpointId: item.endpointId, keyId: item.keyId, sealed: item.sealed })
76
- }
77
- for (const item of value.inferred) {
78
- if (typeof item !== 'string') throw new KitFailure('incompatible_db', 'Invalid reasoning declaration')
79
- inferred.push(item)
80
- }
81
- const automatic = new Set(inferred)
82
- for (const provider of providers)
83
- for (const endpoint of provider.endpoints)
84
- for (const model of endpoint.models)
85
- if (automatic.has(JSON.stringify([endpoint.id, model.id])))
86
- model.reasoning =
87
- model.modelType === 'chat'
88
- ? inferReasoning(model.id, endpoint.protocol, endpoint.baseUrl, model.maxOutput)
89
- : null
90
- return { providers, secrets, inferred }
91
- }
92
- const missing = (entity: string): KitFailure => new KitFailure('not_found', `Unknown ${entity}`, { entity })
93
- const reasoningKey = (endpointId: string, modelId: string): string => JSON.stringify([endpointId, modelId])
42
+
43
+ const missing = (entity: string): KitFailure =>
44
+ new KitFailure('not_found', `Unknown ${entity}`, { entity })
45
+ const reasoningKey = (endpointId: string, modelId: string): string =>
46
+ JSON.stringify([endpointId, modelId])
94
47
  export class ProviderService {
95
48
  private readonly cursors = new Map<string, number>()
96
49
  private readonly options: ProviderServiceOptions
@@ -108,13 +61,31 @@ export class ProviderService {
108
61
  }
109
62
  private async load(actor: Actor): Promise<{ document: StoredDocument | null; catalog: Catalog }> {
110
63
  const document = await this.options.documents.get(actor, 'providers', 'catalog')
111
- return { document, catalog: document ? parseCatalog(document.value) : { providers: [], secrets: [], inferred: [] } }
64
+ return {
65
+ document,
66
+ catalog: document
67
+ ? parseCatalog(document.value)
68
+ : { providers: [], secrets: [], inferred: [] },
69
+ }
112
70
  }
113
- private async commit(actor: Actor, document: StoredDocument | null, catalog: Catalog): Promise<Provider[]> {
114
- await this.options.documents.compareAndSet(actor, 'providers', 'catalog', document?.revision ?? null, catalog)
71
+ private async commit(
72
+ actor: Actor,
73
+ document: StoredDocument | null,
74
+ catalog: Catalog,
75
+ ): Promise<Provider[]> {
76
+ await this.options.documents.compareAndSet(
77
+ actor,
78
+ 'providers',
79
+ 'catalog',
80
+ document?.revision ?? null,
81
+ catalog,
82
+ )
115
83
  return this.list(actor)
116
84
  }
117
- private endpoint(catalog: Catalog, endpointId: string): { provider: Provider; endpoint: Endpoint } {
85
+ private endpoint(
86
+ catalog: Catalog,
87
+ endpointId: string,
88
+ ): { provider: Provider; endpoint: Endpoint } {
118
89
  for (const provider of catalog.providers) {
119
90
  const endpoint = provider.endpoints.find((item) => item.id === endpointId)
120
91
  if (endpoint) return { provider, endpoint }
@@ -122,7 +93,10 @@ export class ProviderService {
122
93
  throw missing('endpoint')
123
94
  }
124
95
  private async open(actor: Actor, secret: Secret): Promise<string> {
125
- const bytes = await this.options.credentials.open(this.scope(actor, secret.keyId), secret.sealed)
96
+ const bytes = await this.options.credentials.open(
97
+ this.scope(actor, secret.keyId),
98
+ secret.sealed,
99
+ )
126
100
  try {
127
101
  return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
128
102
  } finally {
@@ -134,7 +108,9 @@ export class ProviderService {
134
108
  for (const provider of catalog.providers)
135
109
  for (const endpoint of provider.endpoints)
136
110
  for (const key of endpoint.keys) {
137
- const secret = catalog.secrets.find((item) => item.endpointId === endpoint.id && item.keyId === key.id)
111
+ const secret = catalog.secrets.find(
112
+ (item) => item.endpointId === endpoint.id && item.keyId === key.id,
113
+ )
138
114
  let plain: string | null = null
139
115
  if (secret)
140
116
  try {
@@ -143,7 +119,12 @@ export class ProviderService {
143
119
  /* Unreadable credentials remain visible but cannot execute. */
144
120
  }
145
121
  key.readable = plain !== null
146
- key.masked = plain === null ? null : plain.length > 8 ? `${plain.slice(0, 4)}…${plain.slice(-4)}` : '********'
122
+ key.masked =
123
+ plain === null
124
+ ? null
125
+ : plain.length > 8
126
+ ? `${plain.slice(0, 4)}…${plain.slice(-4)}`
127
+ : '********'
147
128
  }
148
129
  return catalog.providers
149
130
  }
@@ -152,10 +133,12 @@ export class ProviderService {
152
133
  const { document, catalog } = await this.load(actor)
153
134
  const name = input.name.trim()
154
135
  if (!name || charCount(name) > 80) throw badRequest('name', 'invalid')
155
- if (input.notes !== undefined && charCount(input.notes) > 10000) throw badRequest('notes', 'too_long', 10000)
136
+ if (input.notes !== undefined && charCount(input.notes) > 10000)
137
+ throw badRequest('notes', 'too_long', 10000)
156
138
  if (input.endpoints.length > 20) throw badRequest('endpoints', 'too_many', 20)
157
139
  if (input.presetId && !input.id) {
158
- if (catalog.providers.some((item) => item.presetId === input.presetId)) return this.list(actor)
140
+ if (catalog.providers.some((item) => item.presetId === input.presetId))
141
+ return this.list(actor)
159
142
  const adopted = catalog.providers.find((item) => item.name === name && item.presetId === null)
160
143
  if (adopted) {
161
144
  adopted.presetId = input.presetId
@@ -164,7 +147,8 @@ export class ProviderService {
164
147
  }
165
148
  const old = input.id ? catalog.providers.find((item) => item.id === input.id) : undefined
166
149
  if (input.id && !old) throw missing('provider')
167
- if (input.presetId && old && input.presetId !== old.presetId) throw badRequest('presetId', 'immutable')
150
+ if (input.presetId && old && input.presetId !== old.presetId)
151
+ throw badRequest('presetId', 'immutable')
168
152
  const provider: Provider = {
169
153
  id: old?.id ?? crypto.randomUUID(),
170
154
  name,
@@ -178,7 +162,8 @@ export class ProviderService {
178
162
  const endpointIds = new Set<string>()
179
163
  const inferred = new Set(catalog.inferred)
180
164
  for (const saved of input.endpoints) {
181
- if (!/^[a-z0-9-]{1,40}$/.test(saved.slug) || slugs.has(saved.slug)) throw badRequest('slug', 'invalid')
165
+ if (!/^[a-z0-9-]{1,40}$/.test(saved.slug) || slugs.has(saved.slug))
166
+ throw badRequest('slug', 'invalid')
182
167
  slugs.add(saved.slug)
183
168
  const previous = saved.id ? old?.endpoints.find((item) => item.id === saved.id) : undefined
184
169
  if (saved.id && !previous) throw missing('endpoint')
@@ -237,11 +222,15 @@ export class ProviderService {
237
222
  catalog.providers = old
238
223
  ? catalog.providers.map((item) => (item.id === old.id ? provider : item))
239
224
  : [...catalog.providers, provider]
240
- const allEndpoints = new Set(catalog.providers.flatMap((item) => item.endpoints.map((endpoint) => endpoint.id)))
225
+ const allEndpoints = new Set(
226
+ catalog.providers.flatMap((item) => item.endpoints.map((endpoint) => endpoint.id)),
227
+ )
241
228
  catalog.secrets = catalog.secrets.filter((item) => allEndpoints.has(item.endpointId))
242
229
  catalog.inferred = [...inferred].filter((key) =>
243
230
  catalog.providers.some((item) =>
244
- item.endpoints.some((endpoint) => endpoint.models.some((model) => reasoningKey(endpoint.id, model.id) === key)),
231
+ item.endpoints.some((endpoint) =>
232
+ endpoint.models.some((model) => reasoningKey(endpoint.id, model.id) === key),
233
+ ),
245
234
  ),
246
235
  )
247
236
  return this.commit(actor, document, catalog)
@@ -285,7 +274,9 @@ export class ProviderService {
285
274
  if (!plain) throw badRequest('apiKey', 'empty')
286
275
  if (
287
276
  new TextEncoder().encode(plain).length > 4096 ||
288
- [...plain].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)
277
+ [...plain].some(
278
+ (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
279
+ )
289
280
  )
290
281
  throw badRequest('apiKey', 'too_long', 4096)
291
282
  for (const secret of catalog.secrets)
@@ -294,7 +285,9 @@ export class ProviderService {
294
285
  try {
295
286
  existing = await this.open(actor, secret)
296
287
  } catch {
297
- throw new KitFailure('needs_config', 'Credential cannot be read', { reason: 'key_unreadable' })
288
+ throw new KitFailure('needs_config', 'Credential cannot be read', {
289
+ reason: 'key_unreadable',
290
+ })
298
291
  }
299
292
  if (existing === plain) throw badRequest('apiKey', 'duplicate')
300
293
  }
@@ -317,7 +310,9 @@ export class ProviderService {
317
310
  exhaustedUntil: input.restore ? null : (old?.exhaustedUntil ?? null),
318
311
  exhaustedReason: input.restore ? null : (old?.exhaustedReason ?? null),
319
312
  }
320
- endpoint.keys = old ? endpoint.keys.map((item) => (item.id === id ? key : item)) : [...endpoint.keys, key]
313
+ endpoint.keys = old
314
+ ? endpoint.keys.map((item) => (item.id === id ? key : item))
315
+ : [...endpoint.keys, key]
321
316
  return this.commit(actor, document, catalog)
322
317
  }
323
318
  async removeKey(actor: Actor, endpointId: string, keyId: string): Promise<Provider[]> {
@@ -325,7 +320,9 @@ export class ProviderService {
325
320
  const { endpoint } = this.endpoint(catalog, endpointId)
326
321
  if (!endpoint.keys.some((item) => item.id === keyId)) throw missing('key')
327
322
  endpoint.keys = endpoint.keys.filter((item) => item.id !== keyId)
328
- catalog.secrets = catalog.secrets.filter((item) => !(item.keyId === keyId && item.endpointId === endpointId))
323
+ catalog.secrets = catalog.secrets.filter(
324
+ (item) => !(item.keyId === keyId && item.endpointId === endpointId),
325
+ )
329
326
  return this.commit(actor, document, catalog)
330
327
  }
331
328
  async revealKey(actor: Actor, keyId: string): Promise<string> {
@@ -335,7 +332,9 @@ export class ProviderService {
335
332
  try {
336
333
  return await this.open(actor, secret)
337
334
  } catch {
338
- throw new KitFailure('needs_config', 'Credential cannot be read', { reason: 'key_unreadable' })
335
+ throw new KitFailure('needs_config', 'Credential cannot be read', {
336
+ reason: 'key_unreadable',
337
+ })
339
338
  }
340
339
  }
341
340
  private async snapshotFrom(
@@ -346,16 +345,25 @@ export class ProviderService {
346
345
  ): Promise<EndpointSnapshot> {
347
346
  const { endpoint } = this.endpoint(catalog, endpointId)
348
347
  const chosen =
349
- keyId === undefined ? endpoint.keys.filter((key) => key.enabled) : endpoint.keys.filter((key) => key.id === keyId)
348
+ keyId === undefined
349
+ ? endpoint.keys.filter((key) => key.enabled)
350
+ : endpoint.keys.filter((key) => key.id === keyId)
350
351
  if (keyId !== undefined && !chosen.length) throw missing('key')
351
352
  const keys: string[] = []
352
353
  for (const key of chosen) {
353
- const secret = catalog.secrets.find((item) => item.keyId === key.id && item.endpointId === endpoint.id)
354
- if (!secret) throw new KitFailure('needs_config', 'Credential cannot be read', { reason: 'key_unreadable' })
354
+ const secret = catalog.secrets.find(
355
+ (item) => item.keyId === key.id && item.endpointId === endpoint.id,
356
+ )
357
+ if (!secret)
358
+ throw new KitFailure('needs_config', 'Credential cannot be read', {
359
+ reason: 'key_unreadable',
360
+ })
355
361
  try {
356
362
  keys.push(await this.open(actor, secret))
357
363
  } catch {
358
- throw new KitFailure('needs_config', 'Credential cannot be read', { reason: 'key_unreadable' })
364
+ throw new KitFailure('needs_config', 'Credential cannot be read', {
365
+ reason: 'key_unreadable',
366
+ })
359
367
  }
360
368
  }
361
369
  if (keyId === undefined && keys.length) {
@@ -381,12 +389,14 @@ export class ProviderService {
381
389
  async resolve(actor: Actor, reference: ModelRef, requiredType?: ModelType): Promise<Connection> {
382
390
  const { catalog } = await this.load(actor)
383
391
  const provider = catalog.providers.find(
384
- (item) => item.enabled && item.endpoints.some((endpoint) => endpoint.id === reference.endpointId),
392
+ (item) =>
393
+ item.enabled && item.endpoints.some((endpoint) => endpoint.id === reference.endpointId),
385
394
  )
386
395
  const endpoint = provider?.endpoints.find((item) => item.id === reference.endpointId)
387
396
  const model = endpoint?.models.find((item) => item.id === reference.modelId)
388
397
  if (!endpoint || !model) throw missing('model')
389
- if (requiredType !== undefined && model.modelType !== requiredType) throw badRequest('model', 'unsupported_type')
398
+ if (requiredType !== undefined && model.modelType !== requiredType)
399
+ throw badRequest('model', 'unsupported_type')
390
400
  prepare({
391
401
  endpoint: {
392
402
  endpointId: endpoint.id,
@@ -403,35 +413,14 @@ export class ProviderService {
403
413
  }
404
414
  async modelOptions(actor: Actor, filter: ModelFilter = {}): Promise<ModelOption[]> {
405
415
  const { catalog } = await this.load(actor)
406
- return catalog.providers
407
- .filter((provider) => provider.enabled)
408
- .flatMap((provider) =>
409
- provider.endpoints.flatMap((endpoint) =>
410
- endpoint.models
411
- .filter(
412
- (model) =>
413
- (filter.modelType === undefined || model.modelType === filter.modelType) &&
414
- (filter.supportsTools === undefined || model.supportsTools === filter.supportsTools),
415
- )
416
- .map((model) => ({
417
- endpointId: endpoint.id,
418
- modelId: model.id,
419
- label: `${provider.name} · ${endpoint.slug} · ${model.id}`,
420
- name: model.name,
421
- modelType: model.modelType,
422
- supportsTools: model.supportsTools,
423
- reasoning: model.reasoning,
424
- nativeWebSearch: model.nativeWebSearch,
425
- canVision: model.canVision,
426
- })),
427
- ),
428
- )
416
+ return modelOptions(catalog, filter)
429
417
  }
430
418
  async probe(actor: Actor, target: ProbeTarget, signal: AbortSignal): Promise<ProbeResult> {
431
419
  if (!isContract('ProbeTarget', target)) throw badRequest('target', 'invalid')
432
420
  const { catalog } = await this.load(actor)
433
421
  let endpoint: EndpointSnapshot
434
- if (target.kind === 'saved') endpoint = await this.snapshotFrom(actor, catalog, target.endpointId)
422
+ if (target.kind === 'saved')
423
+ endpoint = await this.snapshotFrom(actor, catalog, target.endpointId)
435
424
  else {
436
425
  if (target.keyId !== undefined && target.endpointId === undefined) throw missing('key')
437
426
  endpoint = target.endpointId
@@ -440,66 +429,17 @@ export class ProviderService {
440
429
  baseUrl: target.baseUrl,
441
430
  protocol: target.protocol,
442
431
  }
443
- : { endpointId: null, baseUrl: target.baseUrl, protocol: target.protocol, keys: [], headers: {} }
432
+ : {
433
+ endpointId: null,
434
+ baseUrl: target.baseUrl,
435
+ protocol: target.protocol,
436
+ keys: [],
437
+ headers: {},
438
+ }
444
439
  }
445
440
  return probeEndpoint(this.options.outbound, { endpoint, signal })
446
441
  }
447
442
  async handle(request: Request, context: WorkerContext): Promise<Response | null> {
448
- const url = new URL(request.url)
449
- const path = url.pathname
450
- const root = '/api/v1/providers'
451
- if (path !== root && !path.startsWith(`${root}/`)) return null
452
- const actor = context.actor
453
- const json = (value: unknown): Response => Response.json(value)
454
- if (path === root && request.method === 'GET') return json(await this.list(actor))
455
- if (path === root && request.method === 'POST') {
456
- const value: unknown = await request.json()
457
- if (!isContract('ProviderSave', value)) throw badRequest('provider', 'invalid')
458
- return json(await this.save(actor, value))
459
- }
460
- if (path === `${root}/model-options` && request.method === 'GET') {
461
- const value: unknown = {
462
- ...(url.searchParams.has('modelType') ? { modelType: url.searchParams.get('modelType') } : {}),
463
- ...(url.searchParams.has('supportsTools')
464
- ? { supportsTools: url.searchParams.get('supportsTools') === 'true' }
465
- : {}),
466
- }
467
- if (
468
- url.searchParams.has('supportsTools') &&
469
- !['true', 'false'].includes(url.searchParams.get('supportsTools') ?? '')
470
- )
471
- throw badRequest('supportsTools', 'invalid')
472
- if (!isContract('ModelFilter', value)) throw badRequest('filter', 'invalid')
473
- return json(await this.modelOptions(actor, value))
474
- }
475
- if (path === `${root}/probe` && request.method === 'POST') {
476
- const value: unknown = await request.json()
477
- if (!isContract('ProbeTarget', value)) throw badRequest('target', 'invalid')
478
- return json(await this.probe(actor, value, context.signal))
479
- }
480
- if (path === `${root}/keys` && request.method === 'POST') {
481
- const value: unknown = await request.json()
482
- if (!isContract('EndpointKeySave', value)) throw badRequest('key', 'invalid')
483
- return json(await this.saveKey(actor, value))
484
- }
485
- const reveal = path.match(/^\/api\/v1\/providers\/keys\/([^/]+)\/reveal$/)
486
- if (reveal && request.method === 'POST') {
487
- await this.options.authorizeReveal(request, context)
488
- return json(await this.revealKey(actor, decodeURIComponent(reveal[1] ?? '')))
489
- }
490
- const key = path.match(/^\/api\/v1\/providers\/([^/]+)\/keys\/([^/]+)$/)
491
- if (key && request.method === 'DELETE')
492
- return json(await this.removeKey(actor, decodeURIComponent(key[1] ?? ''), decodeURIComponent(key[2] ?? '')))
493
- const enabled = path.match(/^\/api\/v1\/providers\/([^/]+)\/enabled$/)
494
- if (enabled && request.method === 'POST') {
495
- const value: unknown = await request.json()
496
- if (!record(value) || Object.keys(value).length !== 1 || typeof value.enabled !== 'boolean')
497
- throw badRequest('enabled', 'invalid')
498
- return json(await this.setEnabled(actor, decodeURIComponent(enabled[1] ?? ''), value.enabled))
499
- }
500
- const provider = path.match(/^\/api\/v1\/providers\/([^/]+)$/)
501
- if (provider && request.method === 'DELETE')
502
- return json(await this.remove(actor, decodeURIComponent(provider[1] ?? '')))
503
- throw missing('route')
443
+ return handleProviderRequest(this, request, context, this.options.authorizeReveal)
504
444
  }
505
445
  }
@@ -0,0 +1,28 @@
1
+ import { type ModelFilter, type ModelOption } from '@ouraihub/aether-contracts'
2
+ import type { Catalog } from './catalog.js'
3
+
4
+ export function modelOptions(catalog: Catalog, filter: ModelFilter): ModelOption[] {
5
+ return catalog.providers
6
+ .filter((provider) => provider.enabled)
7
+ .flatMap((provider) =>
8
+ provider.endpoints.flatMap((endpoint) =>
9
+ endpoint.models
10
+ .filter(
11
+ (model) =>
12
+ (filter.modelType === undefined || model.modelType === filter.modelType) &&
13
+ (filter.supportsTools === undefined || model.supportsTools === filter.supportsTools),
14
+ )
15
+ .map((model) => ({
16
+ endpointId: endpoint.id,
17
+ modelId: model.id,
18
+ label: `${provider.name} · ${endpoint.slug} · ${model.id}`,
19
+ name: model.name,
20
+ modelType: model.modelType,
21
+ supportsTools: model.supportsTools,
22
+ reasoning: model.reasoning,
23
+ nativeWebSearch: model.nativeWebSearch,
24
+ canVision: model.canVision,
25
+ })),
26
+ ),
27
+ )
28
+ }
@@ -0,0 +1,44 @@
1
+ import type { EndStatus, KitError } from '@ouraihub/aether-contracts'
2
+ import { safeError } from '@ouraihub/aether-core/error'
3
+ import { charCount } from '@ouraihub/aether-core/guard'
4
+ import { renderSystem, validate } from '@ouraihub/aether-core/engine'
5
+ import { trimHistory } from '@ouraihub/aether-core/history'
6
+ import type { HistoryTurn, RunInput, RunSummary } from '@ouraihub/aether-core/types'
7
+
8
+ export const emptySummary = (status: EndStatus, error?: KitError): RunSummary => ({
9
+ status,
10
+ messages: [],
11
+ usage: {
12
+ inputTokens: null,
13
+ outputTokens: null,
14
+ cacheReadTokens: null,
15
+ cacheWriteTokens: null,
16
+ reasoningTokens: null,
17
+ },
18
+ cost: null,
19
+ contextUsage: null,
20
+ exhausted: false,
21
+ ...(error ? { error } : {}),
22
+ })
23
+
24
+ /** Final snapshots never copy arbitrary host/upstream exception text or details. */
25
+ export const publicError = (error: unknown): KitError => ({
26
+ code: safeError(error).code,
27
+ message: 'Run could not be completed',
28
+ })
29
+
30
+ export function assemble(input: RunInput, history: readonly HistoryTurn[]): RunInput {
31
+ const checked = validate({ ...input, history: [] })
32
+ const systemChars = charCount(renderSystem(checked.system, checked.permissionMode === 'plan'))
33
+ const available =
34
+ checked.budget.contextWindow -
35
+ checked.budget.maxOutput -
36
+ Math.ceil((systemChars + charCount(checked.input)) / 3)
37
+ return {
38
+ ...checked,
39
+ history:
40
+ checked.purpose === 'writing'
41
+ ? []
42
+ : trimHistory(history, available, checked.connection.canVision),
43
+ }
44
+ }