@klarkxy/dsh-ai-services 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/lib/index.js ADDED
@@ -0,0 +1,1299 @@
1
+ import { AI_RPC_CHANNEL, CHAT_EVENTS_SLOT, MODEL_SETTINGS_SLOT, producerMessageSource } from "./contracts.js";
2
+ import { registerHostRpc } from "./host-rpc.js";
3
+ import { createInsertCollector } from "./insert-output.js";
4
+ import { BlockAssembler, LlmError, ReasoningEffortId, createUserMessage, isAgentLoopRequest } from "@deepseek-ai/dsh-llm";
5
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
6
+ import { z } from "zod";
7
+ import { randomUUID } from "node:crypto";
8
+ //#region src/errors.ts
9
+ var AiServicesError = class extends Error {
10
+ code;
11
+ constructor(message, code) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = "AiServicesError";
15
+ }
16
+ };
17
+ const AI_UNKNOWN_PURPOSE = "AI_UNKNOWN_PURPOSE";
18
+ const AI_UNKNOWN_ROLE = "AI_UNKNOWN_ROLE";
19
+ const AI_ROLE_UNSET = "AI_ROLE_UNSET";
20
+ const AI_INVALID_ROUTE = "AI_INVALID_ROUTE";
21
+ const AI_SESSION_UNAVAILABLE = "AI_SESSION_UNAVAILABLE";
22
+ const AI_SESSION_INVALID = "AI_SESSION_INVALID";
23
+ const AI_POLICY_CONFLICT = "AI_POLICY_CONFLICT";
24
+ const AI_POLICY_INVALID = "AI_POLICY_INVALID";
25
+ const AI_POLICY_SAVE_FAILED = "AI_POLICY_SAVE_FAILED";
26
+ const AI_DUPLICATE_PURPOSE = "AI_DUPLICATE_PURPOSE";
27
+ const AI_INACTIVE = "AI_INACTIVE";
28
+ function fail$1(code, message) {
29
+ throw new AiServicesError(message, code);
30
+ }
31
+ function isAbortError(error) {
32
+ if (!error || typeof error !== "object") return false;
33
+ const name = "name" in error ? String(error.name) : "";
34
+ const code = "code" in error ? String(error.code) : "";
35
+ return name === "AbortError" || name === "TimeoutError" || code === "ABORT_ERR" || code === "ABORTED";
36
+ }
37
+ async function abortable(work, signal) {
38
+ signal.throwIfAborted();
39
+ let off = () => {};
40
+ const aborted = new Promise((_, reject) => {
41
+ const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : Object.assign(/* @__PURE__ */ new Error("This operation was aborted"), { name: "AbortError" }));
42
+ signal.addEventListener("abort", onAbort, { once: true });
43
+ off = () => signal.removeEventListener("abort", onAbort);
44
+ });
45
+ try {
46
+ return await Promise.race([Promise.resolve().then(work), aborted]);
47
+ } finally {
48
+ off();
49
+ }
50
+ }
51
+ function publicCallError(code) {
52
+ if (code === "NO_ADAPTER") return "指定的模型供应不可用,未改用其他模型。";
53
+ if (code === "UNSUPPORTED_REASONING_EFFORT") return "该模型不支持所选推理强度,未改用其他设置。";
54
+ if (code === "INVALID_CREDENTIAL" || code === "AUTH" || code === "MISSING_CREDENTIAL") return "模型凭据不可用。";
55
+ if (code === "QUOTA") return "模型额度不足。";
56
+ if (code === "CONTEXT_WINDOW_EXCEEDED") return "输入超出模型上下文。";
57
+ return "模型调用失败。";
58
+ }
59
+ //#endregion
60
+ //#region src/storage.ts
61
+ const POLICY_KEY = "current";
62
+ const DEFAULT_LIMITS = {
63
+ concurrency: 1,
64
+ timeoutMs: 6e4,
65
+ maxInputChars: 32e3,
66
+ maxOutputTokens: 2048,
67
+ maxAttempts: 2
68
+ };
69
+ const roleSchema = z.enum([
70
+ "normal",
71
+ "weak",
72
+ "strong",
73
+ "fantasy"
74
+ ]);
75
+ const modelRouteSchema = z.object({
76
+ provider: z.string().trim().min(1).max(128),
77
+ model: z.string().trim().min(1).max(256),
78
+ reasoningEffort: z.string().trim().min(1).max(64).optional()
79
+ }).strict();
80
+ const modelTargetSchema = z.discriminatedUnion("kind", [
81
+ z.object({
82
+ kind: z.literal("role"),
83
+ role: roleSchema
84
+ }).strict(),
85
+ z.object({ kind: z.literal("session") }).strict(),
86
+ z.object({ kind: z.literal("model") }).merge(modelRouteSchema).strict()
87
+ ]);
88
+ const limitsSchema = z.object({
89
+ concurrency: z.number().int().min(1).max(8),
90
+ timeoutMs: z.number().int().min(1e3).max(3e5),
91
+ maxInputChars: z.number().int().min(1).max(2e5),
92
+ maxOutputTokens: z.number().int().min(1).max(8192),
93
+ maxAttempts: z.number().int().min(1).max(5)
94
+ }).strict();
95
+ const policyDataSchema = z.object({
96
+ roles: z.object({
97
+ normal: modelRouteSchema.optional(),
98
+ weak: modelRouteSchema.optional(),
99
+ strong: modelRouteSchema.optional(),
100
+ fantasy: modelRouteSchema.optional()
101
+ }).strict(),
102
+ purposes: z.record(z.string().min(1).max(80), modelTargetSchema).refine((value) => Object.keys(value).length <= 200),
103
+ limits: limitsSchema
104
+ }).strict();
105
+ const storedPolicySchema = policyDataSchema.extend({ revision: z.number().int().nonnegative() }).strict().extend({ imports: z.array(z.string().min(1).max(80)).max(100).optional() }).strict();
106
+ const updatePolicySchema = z.object({
107
+ expectedRevision: z.number().int().nonnegative(),
108
+ policy: policyDataSchema
109
+ }).strict();
110
+ const resolveRpcSchema = z.object({
111
+ purpose: z.string().min(1).max(80),
112
+ sessionId: z.string().min(1).max(128).optional(),
113
+ override: modelTargetSchema.optional()
114
+ }).strict();
115
+ const usageReceiptSchema = z.object({
116
+ id: z.string().min(1).max(80),
117
+ plugin: z.string().min(1).max(80),
118
+ purpose: z.string().min(1).max(80),
119
+ sessionId: z.string().max(128).optional(),
120
+ sourceVersion: z.string().max(128),
121
+ contractVersion: z.number().int().optional(),
122
+ promptVersion: z.string().max(80).optional(),
123
+ schemaVersion: z.string().max(80).optional(),
124
+ route: z.object({
125
+ provider: z.string(),
126
+ model: z.string(),
127
+ reasoningEffort: z.string().optional(),
128
+ source: z.enum([
129
+ "override",
130
+ "purpose",
131
+ "default"
132
+ ]),
133
+ target: modelTargetSchema,
134
+ policyRevision: z.number().int().nonnegative(),
135
+ inheritedRole: roleSchema.optional()
136
+ }).strict().optional(),
137
+ status: z.enum([
138
+ "success",
139
+ "failed",
140
+ "cancelled",
141
+ "superseded",
142
+ "skipped"
143
+ ]),
144
+ attempts: z.number().int().nonnegative(),
145
+ inputTokens: z.number().int().nonnegative().optional(),
146
+ outputTokens: z.number().int().nonnegative().optional(),
147
+ cost: z.null(),
148
+ startedAt: z.number(),
149
+ finishedAt: z.number(),
150
+ error: z.string().max(240).optional()
151
+ }).strict();
152
+ const storedReceiptsSchema = z.object({ items: z.array(usageReceiptSchema).max(100) }).strict();
153
+ const purposeSpecSchema = z.object({
154
+ id: z.string().regex(/^[A-Za-z][A-Za-z0-9._:-]{0,79}$/),
155
+ label: z.string().trim().min(1).max(80),
156
+ defaultTarget: modelTargetSchema,
157
+ maxOutputTokens: z.number().int().min(1).max(8192).optional(),
158
+ maxInputChars: z.number().int().min(1).max(2e5).optional(),
159
+ timeoutMs: z.number().int().min(1e3).max(3e5).optional()
160
+ }).strict();
161
+ function defaultPolicy() {
162
+ return {
163
+ revision: 0,
164
+ roles: {},
165
+ purposes: {},
166
+ limits: { ...DEFAULT_LIMITS }
167
+ };
168
+ }
169
+ function clonePolicy(policy) {
170
+ return structuredClone(policy);
171
+ }
172
+ function parsePolicyData(value) {
173
+ const parsed = policyDataSchema.safeParse(value);
174
+ if (!parsed.success) fail$1(AI_POLICY_INVALID, "模型策略格式无效。");
175
+ return parsed.data;
176
+ }
177
+ function sanitizeReceipt(receipt) {
178
+ return usageReceiptSchema.parse({
179
+ ...receipt,
180
+ cost: null,
181
+ error: receipt.error?.slice(0, 240)
182
+ });
183
+ }
184
+ function boundedReceipts(items) {
185
+ return storedReceiptsSchema.parse({ items: items.map(sanitizeReceipt).slice(-100) }).items;
186
+ }
187
+ const aiServicesDomain = defineDomain({
188
+ name: "dsh_editor_ai_services",
189
+ version: 1,
190
+ tables: {
191
+ policy: domainTable(storedPolicySchema),
192
+ receipts: domainTable(storedReceiptsSchema)
193
+ }
194
+ });
195
+ function isModelRole(value) {
196
+ return value === "normal" || value === "weak" || value === "strong" || value === "fantasy";
197
+ }
198
+ function cloneRoute(route) {
199
+ return route.reasoningEffort ? {
200
+ provider: route.provider,
201
+ model: route.model,
202
+ reasoningEffort: route.reasoningEffort
203
+ } : {
204
+ provider: route.provider,
205
+ model: route.model
206
+ };
207
+ }
208
+ function clonePurpose(spec) {
209
+ return structuredClone(spec);
210
+ }
211
+ //#endregion
212
+ //#region src/rpc.ts
213
+ function fail(code, message) {
214
+ return {
215
+ ok: false,
216
+ error: {
217
+ code,
218
+ message
219
+ }
220
+ };
221
+ }
222
+ function mapError(error) {
223
+ if (error instanceof AiServicesError) return fail(error.code, error.message);
224
+ return fail("AI_REQUEST_FAILED", "操作失败,请刷新后重试。");
225
+ }
226
+ async function handleAiRpc(service, endpoint, payload, signal) {
227
+ try {
228
+ signal.throwIfAborted();
229
+ if (endpoint === "status") {
230
+ if (payload !== void 0 && payload !== null && (typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length > 0)) return fail("AI_INVALID_REQUEST", "未知操作。");
231
+ return {
232
+ ok: true,
233
+ value: {
234
+ policy: service.getPolicy(),
235
+ purposes: service.purposes(),
236
+ storageFailed: Boolean(service.storageFailedFlag)
237
+ }
238
+ };
239
+ }
240
+ if (endpoint === "update") {
241
+ const parsed = updatePolicySchema.safeParse(payload);
242
+ if (!parsed.success) return fail("AI_INVALID_REQUEST", "策略格式无效。");
243
+ return {
244
+ ok: true,
245
+ value: await service.updatePolicy(parsed.data.policy, parsed.data.expectedRevision)
246
+ };
247
+ }
248
+ if (endpoint === "resolve") {
249
+ const parsed = resolveRpcSchema.safeParse(payload);
250
+ if (!parsed.success) return fail("AI_INVALID_REQUEST", "路由请求无效。");
251
+ return {
252
+ ok: true,
253
+ value: await service.resolve(parsed.data.purpose, parsed.data.sessionId, parsed.data.override)
254
+ };
255
+ }
256
+ if (endpoint === "usage") {
257
+ if (payload !== void 0 && payload !== null && (typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length > 0)) return fail("AI_INVALID_REQUEST", "未知操作。");
258
+ return {
259
+ ok: true,
260
+ value: service.usage()
261
+ };
262
+ }
263
+ return fail("AI_INVALID_REQUEST", "未知操作。");
264
+ } catch (error) {
265
+ if (signal.aborted) return fail("AI_CANCELLED", "请求已取消。");
266
+ return mapError(error);
267
+ }
268
+ }
269
+ //#endregion
270
+ //#region src/generate.ts
271
+ const PERMANENT_CODES = /* @__PURE__ */ new Set([
272
+ "NO_ADAPTER",
273
+ "UNSUPPORTED_REASONING_EFFORT",
274
+ "INVALID_PREPARED_CALL",
275
+ "INVALID_CREDENTIAL",
276
+ "AUTH",
277
+ "MISSING_CREDENTIAL",
278
+ "QUOTA",
279
+ "CONTEXT_WINDOW_EXCEEDED"
280
+ ]);
281
+ function joinText(assembler) {
282
+ return assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
283
+ }
284
+ async function consume(stream, signal, live, insert) {
285
+ const assembler = new BlockAssembler();
286
+ let sawFinish = false;
287
+ const collector = insert ? createInsertCollector(insert.maxChars) : void 0;
288
+ let boundedText;
289
+ const iterator = stream[Symbol.asyncIterator]();
290
+ try {
291
+ while (live()) {
292
+ const item = await abortable(() => iterator.next(), signal);
293
+ if (item.done) break;
294
+ if (item.value.type === "finish") sawFinish = true;
295
+ assembler.push(item.value);
296
+ if (collector && item.value.type === "text-delta") {
297
+ collector.append(item.value.text);
298
+ if (collector.full && !sawFinish && !assembler.blocks().some((block) => block.type === "tool-call")) {
299
+ boundedText = collector.text();
300
+ insert.stop();
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ } finally {
306
+ try {
307
+ const closing = iterator.return?.();
308
+ if (closing) closing.catch(() => {});
309
+ } catch {}
310
+ }
311
+ return {
312
+ assembler,
313
+ sawFinish,
314
+ boundedText,
315
+ visibleText: collector?.text()
316
+ };
317
+ }
318
+ function outcomeFromAssembler(assembler, signal, sawFinish) {
319
+ if (signal.aborted) return {
320
+ text: "",
321
+ status: "cancelled",
322
+ error: "调用已取消。"
323
+ };
324
+ const finish = assembler.finish;
325
+ const usage = assembler.usage;
326
+ const tokens = usage ? {
327
+ inputTokens: usage.inputTokens,
328
+ outputTokens: usage.outputTokens
329
+ } : {};
330
+ if (finish.kind === "aborted") return {
331
+ text: "",
332
+ status: "cancelled",
333
+ error: "调用已取消。",
334
+ ...tokens
335
+ };
336
+ if (finish.kind === "error") return {
337
+ text: "",
338
+ status: "failed",
339
+ error: publicCallError(finish.failure.code),
340
+ ...tokens
341
+ };
342
+ if (!sawFinish) return {
343
+ text: "",
344
+ status: "failed",
345
+ error: "模型调用未完成。",
346
+ ...tokens
347
+ };
348
+ if (finish.kind === "max-tokens") return {
349
+ text: "",
350
+ status: "failed",
351
+ error: "输出被截断。",
352
+ ...tokens
353
+ };
354
+ if (finish.kind === "tool-calls" || assembler.blocks().some((block) => block.type === "tool-call")) return {
355
+ text: "",
356
+ status: "failed",
357
+ error: "辅助调用不使用工具。",
358
+ ...tokens
359
+ };
360
+ if (finish.kind !== "stop") return {
361
+ text: "",
362
+ status: "failed",
363
+ error: "模型调用失败。",
364
+ ...tokens
365
+ };
366
+ return {
367
+ text: joinText(assembler),
368
+ status: "success",
369
+ ...tokens
370
+ };
371
+ }
372
+ async function generateAuxiliary(input) {
373
+ const { llm, plugin, route, system, text, maxTokens, maxAttempts, signal, sessionId, live } = input;
374
+ let last = {
375
+ text: "",
376
+ attempts: 0,
377
+ status: "failed",
378
+ error: "模型调用失败。"
379
+ };
380
+ const boundAttempts = Math.max(1, Math.min(5, maxAttempts));
381
+ for (let attempt = 1; attempt <= boundAttempts; attempt += 1) {
382
+ if (signal.aborted) return {
383
+ text: "",
384
+ attempts: attempt - 1,
385
+ status: "cancelled",
386
+ error: "调用已取消。"
387
+ };
388
+ if (!live()) return {
389
+ ...last,
390
+ attempts: attempt - 1
391
+ };
392
+ try {
393
+ const effort = route.reasoningEffort ? ReasoningEffortId(route.reasoningEffort) : void 0;
394
+ const prepared = await llm.prepareCall({
395
+ provider: route.provider,
396
+ model: route.model,
397
+ ...effort ? { reasoningEffort: effort } : {},
398
+ maxTokens
399
+ }, signal);
400
+ if (signal.aborted) return {
401
+ text: "",
402
+ attempts: attempt - 1,
403
+ status: "cancelled",
404
+ error: "调用已取消。"
405
+ };
406
+ if (!live()) return {
407
+ ...last,
408
+ attempts: attempt - 1
409
+ };
410
+ const outputStop = new AbortController();
411
+ const outputSignal = AbortSignal.any([signal, outputStop.signal]);
412
+ const options = {
413
+ provider: prepared.config.provider,
414
+ model: prepared.config.model,
415
+ ...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
416
+ ...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(maxTokens, prepared.config.maxTokens) } : { maxTokens },
417
+ ...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
418
+ ...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
419
+ system,
420
+ messages: [createUserMessage({
421
+ source: producerMessageSource(plugin),
422
+ content: [{
423
+ type: "text",
424
+ text
425
+ }]
426
+ })],
427
+ signal: outputSignal,
428
+ ...sessionId ? { sessionId } : {}
429
+ };
430
+ const { assembler, sawFinish, boundedText, visibleText } = await consume(prepared.stream(options), signal, live, input.insert ? {
431
+ ...input.insert,
432
+ stop: () => outputStop.abort()
433
+ } : void 0);
434
+ if (boundedText !== void 0 && !signal.aborted && live()) return {
435
+ text: boundedText,
436
+ status: "success",
437
+ attempts: attempt,
438
+ ...assembler.usage ? {
439
+ inputTokens: assembler.usage.inputTokens,
440
+ outputTokens: assembler.usage.outputTokens
441
+ } : {}
442
+ };
443
+ last = {
444
+ ...outcomeFromAssembler(assembler, signal, sawFinish),
445
+ attempts: attempt
446
+ };
447
+ if (last.status === "success" && visibleText !== void 0) last.text = visibleText;
448
+ if (last.status !== "failed") return last;
449
+ if (assembler.finish.kind !== "error") return last;
450
+ const code = assembler.finish.failure.code;
451
+ if (PERMANENT_CODES.has(code)) return last;
452
+ } catch (error) {
453
+ if (signal.aborted || isAbortError(error)) return {
454
+ text: "",
455
+ attempts: attempt,
456
+ status: "cancelled",
457
+ error: "调用已取消。"
458
+ };
459
+ if (error instanceof LlmError) {
460
+ last = {
461
+ text: "",
462
+ attempts: attempt,
463
+ status: "failed",
464
+ error: publicCallError(error.code)
465
+ };
466
+ if (PERMANENT_CODES.has(error.code)) return last;
467
+ } else last = {
468
+ text: "",
469
+ attempts: attempt,
470
+ status: "failed",
471
+ error: "模型调用失败。"
472
+ };
473
+ }
474
+ }
475
+ return last;
476
+ }
477
+ //#endregion
478
+ //#region src/queue.ts
479
+ function abortError(signal) {
480
+ if (signal.reason instanceof Error) return signal.reason;
481
+ return Object.assign(/* @__PURE__ */ new Error("This operation was aborted"), { name: "AbortError" });
482
+ }
483
+ /** Per-provider limiter: interactive waiters before background; background yields to in-flight agent streams. */
484
+ var ProviderQueue = class {
485
+ limit;
486
+ running = 0;
487
+ agent = 0;
488
+ waiters = [];
489
+ constructor(limit) {
490
+ this.limit = limit;
491
+ }
492
+ noteAgent(delta) {
493
+ if (delta !== 0) this.agent = Math.max(0, this.agent + delta);
494
+ this.pump();
495
+ }
496
+ wake() {
497
+ this.pump();
498
+ }
499
+ get agentCount() {
500
+ return this.agent;
501
+ }
502
+ get runningCount() {
503
+ return this.running;
504
+ }
505
+ get waiterCount() {
506
+ return this.waiters.length;
507
+ }
508
+ async acquire(priority, signal) {
509
+ signal.throwIfAborted();
510
+ if (this.tryAcquire(priority)) return () => this.release();
511
+ await new Promise((resolve, reject) => {
512
+ let waiter;
513
+ const onAbort = () => {
514
+ const index = this.waiters.indexOf(waiter);
515
+ if (index < 0) return;
516
+ this.waiters.splice(index, 1);
517
+ waiter.dispose();
518
+ reject(abortError(signal));
519
+ };
520
+ waiter = {
521
+ priority,
522
+ signal,
523
+ resume: resolve,
524
+ reject,
525
+ dispose: () => signal.removeEventListener("abort", onAbort)
526
+ };
527
+ signal.addEventListener("abort", onAbort, { once: true });
528
+ this.enqueue(waiter);
529
+ this.pump();
530
+ });
531
+ return () => this.release();
532
+ }
533
+ enqueue(waiter) {
534
+ if (waiter.priority === "interactive") {
535
+ const index = this.waiters.findIndex((item) => item.priority === "background");
536
+ this.waiters.splice(index === -1 ? this.waiters.length : index, 0, waiter);
537
+ return;
538
+ }
539
+ this.waiters.push(waiter);
540
+ }
541
+ canStart(priority) {
542
+ if (this.running >= this.limit()) return false;
543
+ if (priority === "background" && this.agent > 0) return false;
544
+ return true;
545
+ }
546
+ tryAcquire(priority) {
547
+ if (!this.canStart(priority)) return false;
548
+ this.running += 1;
549
+ return true;
550
+ }
551
+ release() {
552
+ this.running = Math.max(0, this.running - 1);
553
+ this.pump();
554
+ }
555
+ pump() {
556
+ while (true) {
557
+ const index = this.waiters.findIndex((waiter) => this.canStart(waiter.priority));
558
+ if (index < 0) return;
559
+ const waiter = this.waiters[index];
560
+ if (!this.tryAcquire(waiter.priority)) return;
561
+ this.waiters.splice(index, 1);
562
+ waiter.dispose();
563
+ waiter.resume();
564
+ }
565
+ }
566
+ };
567
+ var ProviderQueues = class {
568
+ limit;
569
+ queues = /* @__PURE__ */ new Map();
570
+ constructor(limit) {
571
+ this.limit = limit;
572
+ }
573
+ forProvider(provider) {
574
+ let queue = this.queues.get(provider);
575
+ if (!queue) {
576
+ queue = new ProviderQueue(this.limit);
577
+ this.queues.set(provider, queue);
578
+ }
579
+ return queue;
580
+ }
581
+ noteAgent(provider, delta) {
582
+ this.forProvider(provider).noteAgent(delta);
583
+ }
584
+ wake() {
585
+ for (const queue of this.queues.values()) queue.wake();
586
+ }
587
+ };
588
+ //#endregion
589
+ //#region src/routing.ts
590
+ function streamReasoningEffort(value) {
591
+ if (typeof value !== "string") return void 0;
592
+ const id = value.trim();
593
+ if (!id) return void 0;
594
+ return id;
595
+ }
596
+ function asCallConfig(route) {
597
+ const effort = streamReasoningEffort(route.reasoningEffort);
598
+ return effort ? {
599
+ provider: route.provider,
600
+ model: route.model,
601
+ reasoningEffort: ReasoningEffortId(effort)
602
+ } : {
603
+ provider: route.provider,
604
+ model: route.model
605
+ };
606
+ }
607
+ async function validateRoute(llm, route, signal) {
608
+ if (!route.provider.trim() || !route.model.trim()) fail$1(AI_INVALID_ROUTE, "指定的模型路由不完整,未改用其他模型。");
609
+ try {
610
+ const work = () => llm.resolveCallConfig(asCallConfig(route), signal);
611
+ await (signal ? abortable(work, signal) : work());
612
+ } catch (error) {
613
+ if (signal?.aborted || isAbortError(error)) throw error;
614
+ if (error instanceof LlmError) fail$1(AI_INVALID_ROUTE, publicCallError(error.code));
615
+ fail$1(AI_INVALID_ROUTE, "指定的模型路由无效,未改用其他模型。");
616
+ }
617
+ }
618
+ function configuredRole(policy, role) {
619
+ const route = policy.roles[role];
620
+ if (!route) return void 0;
621
+ if (!route.provider.trim() || !route.model.trim()) return void 0;
622
+ return cloneRoute(route);
623
+ }
624
+ async function resolveTarget(input) {
625
+ const { llm, policy, target, source, sessionId, sessionModels, defaultModel, modelCenterAvailable, signal } = input;
626
+ const policyRevision = policy.revision;
627
+ if (target.kind === "model") {
628
+ const route = cloneRoute({
629
+ provider: target.provider,
630
+ model: target.model,
631
+ ...streamReasoningEffort(target.reasoningEffort) ? { reasoningEffort: streamReasoningEffort(target.reasoningEffort) } : {}
632
+ });
633
+ await validateRoute(llm, route, signal);
634
+ return {
635
+ ...route,
636
+ source,
637
+ target,
638
+ policyRevision
639
+ };
640
+ }
641
+ if (target.kind === "session") {
642
+ if (!sessionId) fail$1(AI_SESSION_UNAVAILABLE, "会话模型需要当前会话。");
643
+ if (!sessionModels) fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
644
+ let selected;
645
+ try {
646
+ selected = await sessionModels(sessionId, signal);
647
+ } catch (error) {
648
+ if (signal?.aborted || isAbortError(error)) throw error;
649
+ if (error instanceof AiServicesError) throw error;
650
+ if (error instanceof Error && error.message) fail$1(AI_SESSION_INVALID, error.message);
651
+ fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
652
+ }
653
+ const effort = streamReasoningEffort(selected.reasoningEffort);
654
+ const route = effort ? {
655
+ provider: selected.provider,
656
+ model: selected.model,
657
+ reasoningEffort: effort
658
+ } : {
659
+ provider: selected.provider,
660
+ model: selected.model
661
+ };
662
+ await validateRoute(llm, route, signal);
663
+ return {
664
+ ...route,
665
+ source,
666
+ target,
667
+ policyRevision
668
+ };
669
+ }
670
+ if (!isModelRole(target.role)) fail$1(AI_UNKNOWN_ROLE, "未知模型角色。");
671
+ let inheritedRole;
672
+ let route = modelCenterAvailable === false ? void 0 : configuredRole(policy, target.role);
673
+ if (!route) {
674
+ route = configuredRole(policy, "normal") ?? defaultModel?.();
675
+ if (!route) fail$1(AI_ROLE_UNSET, "请先设置默认对话模型。");
676
+ route = cloneRoute(route);
677
+ if (target.role !== "normal") inheritedRole = "normal";
678
+ }
679
+ await validateRoute(llm, route, signal);
680
+ return inheritedRole ? {
681
+ ...route,
682
+ source,
683
+ target,
684
+ policyRevision,
685
+ inheritedRole
686
+ } : {
687
+ ...route,
688
+ source,
689
+ target,
690
+ policyRevision
691
+ };
692
+ }
693
+ async function resolvePurposeRoute(input) {
694
+ const { llm, policy, purpose, spec, sessionId, override, sessionModels, defaultModel, modelCenterAvailable, signal } = input;
695
+ if (override) return resolveTarget({
696
+ llm,
697
+ policy,
698
+ target: override,
699
+ source: "override",
700
+ sessionId,
701
+ sessionModels,
702
+ defaultModel,
703
+ modelCenterAvailable,
704
+ signal
705
+ });
706
+ const mapped = policy.purposes[purpose];
707
+ if (mapped) return resolveTarget({
708
+ llm,
709
+ policy,
710
+ target: mapped,
711
+ source: "purpose",
712
+ sessionId,
713
+ sessionModels,
714
+ defaultModel,
715
+ modelCenterAvailable,
716
+ signal
717
+ });
718
+ if (spec) return resolveTarget({
719
+ llm,
720
+ policy,
721
+ target: spec.defaultTarget,
722
+ source: "default",
723
+ sessionId,
724
+ sessionModels,
725
+ defaultModel,
726
+ modelCenterAvailable,
727
+ signal
728
+ });
729
+ fail$1(AI_UNKNOWN_PURPOSE, "用途未注册。");
730
+ }
731
+ function routeFromSelection(value, message) {
732
+ if (typeof value?.provider !== "string" || typeof value.model !== "string") fail$1(AI_SESSION_INVALID, message);
733
+ const effort = streamReasoningEffort(value.reasoningEffort);
734
+ return effort ? {
735
+ provider: value.provider,
736
+ model: value.model,
737
+ reasoningEffort: effort
738
+ } : {
739
+ provider: value.provider,
740
+ model: value.model
741
+ };
742
+ }
743
+ /**
744
+ * Read the live host picker. A durable pending choice wins, then the route
745
+ * recorded by the current request, then the host's default model.
746
+ */
747
+ function sessionModelsFromHost(getHost) {
748
+ return async (sessionId, signal) => {
749
+ signal?.throwIfAborted();
750
+ const host = getHost();
751
+ if (!host?.agents || !host.sessionProjections) fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
752
+ const session = host.agents.get(sessionId)?.session;
753
+ if (!session) fail$1(AI_SESSION_UNAVAILABLE, "当前会话不在 Host 中,无法读取模型。");
754
+ let projection;
755
+ try {
756
+ projection = host.sessionProjections.stateOf(session, "modelSelection");
757
+ } catch (error) {
758
+ if (signal?.aborted || isAbortError(error)) throw error;
759
+ fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
760
+ }
761
+ signal?.throwIfAborted();
762
+ if (!projection) fail$1(AI_SESSION_UNAVAILABLE, "当前会话模型投影不可用。");
763
+ if (projection.pending !== null && projection.pending !== void 0) return routeFromSelection(projection.pending, "当前会话模型选择无效。");
764
+ const header = session.requestHeader?.();
765
+ const recorded = header?.config;
766
+ if (recorded !== void 0) return routeFromSelection(header?.adapterDefaults?.reasoningEffort === true ? {
767
+ provider: recorded.provider,
768
+ model: recorded.model
769
+ } : recorded, "当前会话模型记录无效。");
770
+ return routeFromSelection(host.agentDefaultModel?.currentSelection(), "默认对话模型不可用。");
771
+ };
772
+ }
773
+ //#endregion
774
+ //#region src/service.ts
775
+ const PLUGIN_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
776
+ function pluginName(value) {
777
+ const id = value.trim();
778
+ if (!id || id.length > 80 || id.includes("..") || !PLUGIN_NAME.test(id)) fail$1(AI_POLICY_INVALID, "插件标识无效。");
779
+ return id;
780
+ }
781
+ function minLimit(policyValue, purposeValue) {
782
+ return purposeValue === void 0 ? policyValue : Math.min(policyValue, purposeValue);
783
+ }
784
+ var AiServicesRuntime = class {
785
+ options;
786
+ policy;
787
+ imports;
788
+ receipts;
789
+ storageFailed = false;
790
+ disposed = false;
791
+ pending = Promise.resolve();
792
+ purposeRegistry = /* @__PURE__ */ new Map();
793
+ lifetimes = /* @__PURE__ */ new Set();
794
+ queues;
795
+ now;
796
+ id;
797
+ constructor(options) {
798
+ this.options = options;
799
+ this.imports = [...options.initialImports ?? []];
800
+ this.policy = clonePolicy(options.initialPolicy ?? defaultPolicy());
801
+ this.receipts = boundedReceipts(options.initialReceipts ?? []);
802
+ this.now = options.now ?? Date.now;
803
+ this.id = options.id ?? (() => randomUUID());
804
+ this.queues = new ProviderQueues(() => this.policy.limits.concurrency);
805
+ }
806
+ get storageFailedFlag() {
807
+ return this.storageFailed;
808
+ }
809
+ noteAgent(provider, delta) {
810
+ this.queues.noteAgent(provider, delta);
811
+ }
812
+ getPolicy() {
813
+ return clonePolicy(this.policy);
814
+ }
815
+ purposes() {
816
+ return [...this.purposeRegistry.values()].map((item) => ({
817
+ ...clonePurpose(item),
818
+ plugin: item.plugin
819
+ }));
820
+ }
821
+ usage() {
822
+ return this.receipts.map((item) => structuredClone(item));
823
+ }
824
+ importPurposes(migrationId, defaults, roles = {}) {
825
+ const task = this.pending.then(async () => {
826
+ this.assertOpen();
827
+ if (!/^[A-Za-z][A-Za-z0-9._:-]{0,79}$/.test(migrationId)) fail$1(AI_POLICY_INVALID, "迁移标识无效。");
828
+ if (this.imports.includes(migrationId)) return this.getPolicy();
829
+ if (this.imports.length >= 100) fail$1(AI_POLICY_INVALID, "迁移记录已满。");
830
+ const { revision: _revision, ...current } = this.policy;
831
+ const parsed = parsePolicyData({
832
+ ...current,
833
+ roles: {
834
+ ...roles,
835
+ ...current.roles
836
+ },
837
+ purposes: {
838
+ ...defaults,
839
+ ...this.policy.purposes
840
+ }
841
+ });
842
+ const purposes = {
843
+ ...parsed.purposes,
844
+ ...this.policy.purposes
845
+ };
846
+ const next = {
847
+ ...this.getPolicy(),
848
+ roles: parsed.roles,
849
+ purposes,
850
+ revision: this.policy.revision + 1
851
+ };
852
+ const imports = [...this.imports, migrationId];
853
+ try {
854
+ await this.options.store.savePolicy(next, imports);
855
+ } catch {
856
+ this.storageFailed = true;
857
+ fail$1(AI_POLICY_SAVE_FAILED, "旧模型配置迁移失败,已保留原设置。");
858
+ }
859
+ this.policy = next;
860
+ this.imports = imports;
861
+ this.storageFailed = false;
862
+ return this.getPolicy();
863
+ });
864
+ this.pending = task.then(() => {}, () => {});
865
+ return task;
866
+ }
867
+ updatePolicy(policy, expectedRevision) {
868
+ const task = this.pending.then(async () => {
869
+ if (this.disposed) fail$1(AI_INACTIVE, "AI 服务已停止。");
870
+ if (expectedRevision !== this.policy.revision) fail$1(AI_POLICY_CONFLICT, "策略已被其他页面修改,请刷新后重试。");
871
+ const parsed = parsePolicyData(policy);
872
+ const next = {
873
+ ...clonePolicy({
874
+ revision: expectedRevision + 1,
875
+ ...parsed
876
+ }),
877
+ revision: expectedRevision + 1
878
+ };
879
+ try {
880
+ await this.options.store.savePolicy(next, this.imports);
881
+ } catch {
882
+ this.storageFailed = true;
883
+ fail$1(AI_POLICY_SAVE_FAILED, "策略保存失败,已保留原设置。");
884
+ }
885
+ this.policy = next;
886
+ this.storageFailed = false;
887
+ this.queues.wake();
888
+ return clonePolicy(this.policy);
889
+ });
890
+ this.pending = task.then(() => {}, () => {});
891
+ return task;
892
+ }
893
+ async resolve(purpose, sessionId, override) {
894
+ this.assertOpen();
895
+ const spec = this.purposeRegistry.get(purpose);
896
+ return resolvePurposeRoute({
897
+ llm: this.options.llm,
898
+ policy: this.policy,
899
+ purpose,
900
+ spec,
901
+ sessionId,
902
+ override,
903
+ sessionModels: this.options.sessionModels,
904
+ defaultModel: this.options.defaultModel,
905
+ modelCenterAvailable: this.options.modelCenterAvailable?.()
906
+ });
907
+ }
908
+ activate(plugin) {
909
+ this.assertOpen();
910
+ const name = pluginName(plugin);
911
+ const lifetime = {
912
+ plugin: name,
913
+ controller: new AbortController(),
914
+ active: true,
915
+ purposes: /* @__PURE__ */ new Map()
916
+ };
917
+ this.lifetimes.add(lifetime);
918
+ return {
919
+ plugin: name,
920
+ get signal() {
921
+ return lifetime.controller.signal;
922
+ },
923
+ get active() {
924
+ return lifetime.active && !lifetime.controller.signal.aborted;
925
+ },
926
+ registerPurpose: (spec) => this.registerPurpose(lifetime, spec),
927
+ run: (request) => this.run(lifetime, request),
928
+ dispose: () => this.disposeLifetime(lifetime)
929
+ };
930
+ }
931
+ async dispose() {
932
+ this.disposed = true;
933
+ for (const lifetime of [...this.lifetimes]) this.disposeLifetime(lifetime);
934
+ await this.pending;
935
+ }
936
+ assertOpen() {
937
+ if (this.disposed) fail$1(AI_INACTIVE, "AI 服务已停止。");
938
+ }
939
+ registerPurpose(lifetime, spec) {
940
+ if (!lifetime.active) fail$1(AI_INACTIVE, "插件范围已停用。");
941
+ let parsed;
942
+ try {
943
+ parsed = purposeSpecSchema.parse(spec);
944
+ } catch {
945
+ fail$1(AI_POLICY_INVALID, "用途声明无效。");
946
+ }
947
+ const existing = this.purposeRegistry.get(parsed.id);
948
+ if (existing && existing.plugin !== lifetime.plugin) fail$1(AI_DUPLICATE_PURPOSE, "用途已被其他插件注册。");
949
+ const owned = {
950
+ ...clonePurpose(parsed),
951
+ plugin: lifetime.plugin
952
+ };
953
+ lifetime.purposes.set(parsed.id, owned);
954
+ this.purposeRegistry.set(parsed.id, owned);
955
+ let removed = false;
956
+ return () => {
957
+ if (removed) return;
958
+ removed = true;
959
+ lifetime.purposes.delete(parsed.id);
960
+ this.refreshPurpose(parsed.id);
961
+ };
962
+ }
963
+ disposeLifetime(lifetime) {
964
+ if (!lifetime.active && !this.lifetimes.has(lifetime)) return;
965
+ lifetime.active = false;
966
+ if (!lifetime.controller.signal.aborted) lifetime.controller.abort();
967
+ const ids = [...lifetime.purposes.keys()];
968
+ lifetime.purposes.clear();
969
+ this.lifetimes.delete(lifetime);
970
+ for (const id of ids) this.refreshPurpose(id);
971
+ }
972
+ refreshPurpose(id) {
973
+ for (const other of this.lifetimes) {
974
+ const spec = other.purposes.get(id);
975
+ if (spec && other.active) {
976
+ this.purposeRegistry.set(id, {
977
+ ...spec,
978
+ plugin: other.plugin
979
+ });
980
+ return;
981
+ }
982
+ }
983
+ this.purposeRegistry.delete(id);
984
+ }
985
+ live(lifetime, request) {
986
+ if (!lifetime.active || lifetime.controller.signal.aborted || request.signal?.aborted) return false;
987
+ try {
988
+ if (request.isCurrent && !request.isCurrent()) return false;
989
+ } catch {
990
+ return false;
991
+ }
992
+ return true;
993
+ }
994
+ classify(lifetime, request, queued) {
995
+ const aborted = lifetime.controller.signal.aborted || Boolean(request.signal?.aborted);
996
+ let current = true;
997
+ try {
998
+ current = !request.isCurrent || request.isCurrent();
999
+ } catch {
1000
+ current = false;
1001
+ }
1002
+ if (aborted) return "cancelled";
1003
+ if (!lifetime.active || !current) return queued ? "superseded" : "skipped";
1004
+ }
1005
+ async run(lifetime, request) {
1006
+ const startedAt = this.now();
1007
+ const base = {
1008
+ id: this.id(),
1009
+ plugin: lifetime.plugin,
1010
+ purpose: request.purpose,
1011
+ ...request.sessionId ? { sessionId: request.sessionId } : {},
1012
+ sourceVersion: request.sourceVersion,
1013
+ ...request.contractVersion !== void 0 ? { contractVersion: request.contractVersion } : {},
1014
+ ...request.promptVersion ? { promptVersion: request.promptVersion } : {},
1015
+ ...request.schemaVersion ? { schemaVersion: request.schemaVersion } : {},
1016
+ cost: null,
1017
+ startedAt
1018
+ };
1019
+ let queued = false;
1020
+ let timeout;
1021
+ const finish = async (receipt, text = "") => {
1022
+ let sealed = sanitizeReceipt({
1023
+ ...receipt,
1024
+ finishedAt: this.now(),
1025
+ cost: null
1026
+ });
1027
+ await this.record(sealed);
1028
+ const again = this.classify(lifetime, request, queued);
1029
+ if (again && (sealed.status === "success" || text !== "")) {
1030
+ const { error: _error, ...rest } = sealed;
1031
+ const timedOut = Boolean(timeout?.aborted) && !request.signal?.aborted && !lifetime.controller.signal.aborted;
1032
+ sealed = sanitizeReceipt({
1033
+ ...rest,
1034
+ status: again,
1035
+ finishedAt: this.now(),
1036
+ cost: null,
1037
+ ...again === "cancelled" ? { error: timedOut ? "调用超时。" : "调用已取消。" } : {}
1038
+ });
1039
+ await this.record(sealed);
1040
+ return {
1041
+ text: "",
1042
+ receipt: sealed
1043
+ };
1044
+ }
1045
+ return {
1046
+ text: sealed.status === "success" ? text : "",
1047
+ receipt: sealed
1048
+ };
1049
+ };
1050
+ const early = this.classify(lifetime, request, false);
1051
+ if (early) return finish({
1052
+ ...base,
1053
+ status: early,
1054
+ attempts: 0,
1055
+ finishedAt: this.now(),
1056
+ ...early === "cancelled" ? { error: "调用已取消。" } : {}
1057
+ });
1058
+ const spec = lifetime.purposes.get(request.purpose);
1059
+ if (!spec) return finish({
1060
+ ...base,
1061
+ status: "failed",
1062
+ attempts: 0,
1063
+ finishedAt: this.now(),
1064
+ error: "用途未注册。"
1065
+ });
1066
+ if (request.insert && (!Number.isInteger(request.insert.maxChars) || request.insert.maxChars < 1 || request.insert.maxChars > 12e3)) return finish({
1067
+ ...base,
1068
+ status: "failed",
1069
+ attempts: 0,
1070
+ finishedAt: this.now(),
1071
+ error: "输出字符上限无效。"
1072
+ });
1073
+ const policy = this.policy;
1074
+ const maxInputChars = minLimit(policy.limits.maxInputChars, spec.maxInputChars);
1075
+ if (request.input.length + request.system.length > maxInputChars) return finish({
1076
+ ...base,
1077
+ status: "failed",
1078
+ attempts: 0,
1079
+ finishedAt: this.now(),
1080
+ error: "输入超出上限。"
1081
+ });
1082
+ const timeoutMs = minLimit(policy.limits.timeoutMs, spec.timeoutMs);
1083
+ timeout = AbortSignal.timeout(timeoutMs);
1084
+ const combined = AbortSignal.any([
1085
+ lifetime.controller.signal,
1086
+ timeout,
1087
+ ...request.signal ? [request.signal] : []
1088
+ ]);
1089
+ let route;
1090
+ try {
1091
+ route = await abortable(() => resolvePurposeRoute({
1092
+ llm: this.options.llm,
1093
+ policy,
1094
+ purpose: request.purpose,
1095
+ spec,
1096
+ sessionId: request.sessionId,
1097
+ override: request.override,
1098
+ sessionModels: this.options.sessionModels,
1099
+ defaultModel: this.options.defaultModel,
1100
+ modelCenterAvailable: this.options.modelCenterAvailable?.(),
1101
+ signal: combined
1102
+ }), combined);
1103
+ } catch (error) {
1104
+ const classified = this.classify(lifetime, request, false) ?? (isAbortError(error) || combined.aborted ? "cancelled" : void 0);
1105
+ if (classified) {
1106
+ const timedOut = timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted;
1107
+ return finish({
1108
+ ...base,
1109
+ status: classified,
1110
+ attempts: 0,
1111
+ finishedAt: this.now(),
1112
+ ...classified === "cancelled" ? { error: timedOut ? "调用超时。" : "调用已取消。" } : {}
1113
+ });
1114
+ }
1115
+ const message = error instanceof AiServicesError ? error.message : "模型路由无效,未改用其他模型。";
1116
+ return finish({
1117
+ ...base,
1118
+ status: "failed",
1119
+ attempts: 0,
1120
+ finishedAt: this.now(),
1121
+ error: message
1122
+ });
1123
+ }
1124
+ const afterResolve = this.classify(lifetime, request, false);
1125
+ if (afterResolve) return finish({
1126
+ ...base,
1127
+ status: afterResolve,
1128
+ attempts: 0,
1129
+ finishedAt: this.now(),
1130
+ route,
1131
+ ...afterResolve === "cancelled" ? { error: "调用已取消。" } : {}
1132
+ });
1133
+ const maxOutputTokens = minLimit(policy.limits.maxOutputTokens, spec.maxOutputTokens);
1134
+ const maxAttempts = policy.limits.maxAttempts;
1135
+ const priority = request.priority === "interactive" ? "interactive" : "background";
1136
+ queued = true;
1137
+ let release = () => {};
1138
+ try {
1139
+ release = await this.queues.forProvider(route.provider).acquire(priority, combined);
1140
+ } catch (error) {
1141
+ const classified = this.classify(lifetime, request, true) ?? (isAbortError(error) || combined.aborted ? "cancelled" : "failed");
1142
+ return finish({
1143
+ ...base,
1144
+ status: classified,
1145
+ attempts: 0,
1146
+ finishedAt: this.now(),
1147
+ route,
1148
+ error: classified === "cancelled" ? timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted ? "调用超时。" : "调用已取消。" : "排队失败。"
1149
+ });
1150
+ }
1151
+ try {
1152
+ const afterQueue = this.classify(lifetime, request, true);
1153
+ if (afterQueue) return finish({
1154
+ ...base,
1155
+ status: afterQueue,
1156
+ attempts: 0,
1157
+ finishedAt: this.now(),
1158
+ route,
1159
+ ...afterQueue === "cancelled" ? { error: "调用已取消。" } : {}
1160
+ });
1161
+ const generated = await generateAuxiliary({
1162
+ llm: this.options.llm,
1163
+ plugin: lifetime.plugin,
1164
+ route,
1165
+ system: request.system,
1166
+ text: request.input,
1167
+ maxTokens: maxOutputTokens,
1168
+ maxAttempts,
1169
+ insert: request.insert,
1170
+ signal: combined,
1171
+ sessionId: request.sessionId,
1172
+ live: () => this.live(lifetime, request)
1173
+ });
1174
+ const classified = this.classify(lifetime, request, true);
1175
+ if (classified === "superseded") return finish({
1176
+ ...base,
1177
+ status: "superseded",
1178
+ attempts: generated.attempts,
1179
+ finishedAt: this.now(),
1180
+ route,
1181
+ ...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
1182
+ ...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {}
1183
+ });
1184
+ const timedOut = timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted;
1185
+ if (classified === "cancelled" || generated.status === "cancelled") return finish({
1186
+ ...base,
1187
+ status: "cancelled",
1188
+ attempts: generated.attempts,
1189
+ finishedAt: this.now(),
1190
+ route,
1191
+ ...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
1192
+ ...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {},
1193
+ error: timedOut ? "调用超时。" : "调用已取消。"
1194
+ });
1195
+ if (generated.status !== "success") return finish({
1196
+ ...base,
1197
+ status: generated.status,
1198
+ attempts: generated.attempts,
1199
+ finishedAt: this.now(),
1200
+ route,
1201
+ ...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
1202
+ ...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {},
1203
+ ...generated.error ? { error: generated.error } : {}
1204
+ });
1205
+ return finish({
1206
+ ...base,
1207
+ status: "success",
1208
+ attempts: generated.attempts,
1209
+ finishedAt: this.now(),
1210
+ route,
1211
+ ...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
1212
+ ...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {}
1213
+ }, generated.text);
1214
+ } finally {
1215
+ release();
1216
+ }
1217
+ }
1218
+ record(receipt) {
1219
+ const task = this.pending.then(async () => {
1220
+ const index = this.receipts.findIndex((row) => row.id === receipt.id);
1221
+ const list = [...this.receipts];
1222
+ if (index >= 0) list[index] = receipt;
1223
+ else list.push(receipt);
1224
+ const next = boundedReceipts(list);
1225
+ try {
1226
+ await this.options.store.saveReceipts(next);
1227
+ this.receipts = next;
1228
+ } catch {
1229
+ this.storageFailed = true;
1230
+ this.receipts = next;
1231
+ }
1232
+ });
1233
+ this.pending = task.then(() => {}, () => {});
1234
+ return task;
1235
+ }
1236
+ };
1237
+ //#endregion
1238
+ //#region src/index.ts
1239
+ const name = "@klarkxy/dsh-ai-services";
1240
+ const inject = [
1241
+ "llm",
1242
+ "storageDomain",
1243
+ "connection",
1244
+ "webServer",
1245
+ "agents",
1246
+ "sessionProjections",
1247
+ "agentDefaultModel"
1248
+ ];
1249
+ function installForegroundPriority(ctx, service) {
1250
+ return ctx.on.call(ctx, "llm/stream", (options, next) => {
1251
+ if (!isAgentLoopRequest(options)) return next();
1252
+ return (async function* () {
1253
+ service.noteAgent(options.provider, 1);
1254
+ try {
1255
+ yield* next();
1256
+ } finally {
1257
+ service.noteAgent(options.provider, -1);
1258
+ }
1259
+ })();
1260
+ }, {
1261
+ global: true,
1262
+ prepend: true
1263
+ });
1264
+ }
1265
+ async function apply(ctx) {
1266
+ const host = ctx;
1267
+ const domain = await ctx.storageDomain.open(aiServicesDomain);
1268
+ const policyTable = domain.table("policy");
1269
+ const receiptTable = domain.table("receipts");
1270
+ const stored = policyTable.get(POLICY_KEY);
1271
+ const { imports = [], ...initialPolicy } = stored ?? {};
1272
+ const service = new AiServicesRuntime({
1273
+ llm: ctx.llm,
1274
+ initialPolicy: stored ? initialPolicy : void 0,
1275
+ initialImports: imports,
1276
+ initialReceipts: receiptTable.get("log")?.items,
1277
+ store: {
1278
+ savePolicy: (policy, imports) => policyTable.put(POLICY_KEY, {
1279
+ ...policy,
1280
+ imports
1281
+ }),
1282
+ saveReceipts: (items) => receiptTable.put("log", { items: boundedReceipts(items) })
1283
+ },
1284
+ sessionModels: sessionModelsFromHost(() => ctx),
1285
+ defaultModel: () => ctx.get("agentDefaultModel")?.currentSelection(),
1286
+ modelCenterAvailable: () => Boolean(ctx.get("modelCenter"))
1287
+ });
1288
+ ctx.effect(() => async () => {
1289
+ await service.dispose();
1290
+ await domain.close();
1291
+ }, "ai-services.dispose");
1292
+ ctx.effect(() => installForegroundPriority(ctx, service), "ai-services.foreground");
1293
+ ctx.provide("aiServices", service);
1294
+ ctx.effect(() => registerHostRpc(host, AI_RPC_CHANNEL, (endpoint, payload, signal) => handleAiRpc(service, endpoint, payload, signal)), "ai-services.rpc");
1295
+ }
1296
+ //#endregion
1297
+ export { AI_RPC_CHANNEL, AiServicesRuntime, CHAT_EVENTS_SLOT, MODEL_SETTINGS_SLOT, apply, inject, name, registerHostRpc };
1298
+
1299
+ //# sourceMappingURL=index.js.map