lumiverse-spindle-types 0.6.5 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumiverse-spindle-types",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "types": "./src/index.ts",
5
5
  "keywords": [
6
6
  "lumiverse",
package/src/api.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SpindleManifest } from "./manifest";
2
+ import type { SpindleHostDescriptorV1 } from "./host";
2
3
  import type { CouncilMemberContext } from "./council";
3
4
  import type {
4
5
  ChatLinkAttachDTO,
@@ -16,13 +17,21 @@ export type LlmMessagePartDTO =
16
17
  | { type: "text"; text: string; cache_control?: Record<string, unknown> }
17
18
  | { type: "image"; data: string; mime_type: string; cache_control?: Record<string, unknown> }
18
19
  | { type: "audio"; data: string; mime_type: string; cache_control?: Record<string, unknown> }
19
- | { type: "tool_use"; id: string; name: string; input: Record<string, unknown>; cache_control?: Record<string, unknown> }
20
+ | { type: "tool_use"; id: string; name: string; input: Record<string, unknown>; cache_control?: Record<string, unknown>; thought_signature?: string }
20
21
  | { type: "tool_result"; tool_use_id: string; content: string; is_error?: boolean; cache_control?: Record<string, unknown> };
21
22
 
23
+ export interface LlmThinkingBlockDTO {
24
+ type: "thinking" | "redacted_thinking";
25
+ thinking?: string;
26
+ signature?: string;
27
+ data?: string;
28
+ }
29
+
22
30
  export interface LlmMessageDTO {
23
31
  role: "system" | "user" | "assistant";
24
32
  content: string | LlmMessagePartDTO[];
25
33
  name?: string;
34
+ cache_control?: Record<string, unknown>;
26
35
  /**
27
36
  * Thinking-mode reasoning content from the previous assistant turn, echoed
28
37
  * back on the next request. Required by DeepSeek's thinking-mode models
@@ -36,6 +45,8 @@ export interface LlmMessageDTO {
36
45
  * only on `role: 'assistant'` messages.
37
46
  */
38
47
  reasoning_content?: string;
48
+ thinking_blocks?: LlmThinkingBlockDTO[];
49
+ reasoning_details?: Record<string, unknown>[];
39
50
  /**
40
51
  * True when this message is a chat-history turn (as opposed to a depth-injected
41
52
  * world-info/preset/author's-note block that was spliced into the chat-history
@@ -57,6 +68,80 @@ export interface LlmMessageDTO {
57
68
  }
58
69
 
59
70
  export type SpindleUserRoleDTO = "operator" | "admin" | "user";
71
+ export type InterceptorGenerationType =
72
+ | "normal"
73
+ | "continue"
74
+ | "regenerate"
75
+ | "swipe"
76
+ | "impersonate"
77
+ | "quiet";
78
+
79
+ export type InterceptorMatchScalar = string | number | boolean | null;
80
+
81
+ export interface InterceptorMatchDTO {
82
+ /**
83
+ * Serializable filter domain. Terminal callbacks currently run only for
84
+ * live `normal` and `continue`; every other value is a fail-closed filter.
85
+ */
86
+ generationTypes?: InterceptorGenerationType[];
87
+ /** Terminal callbacks are never invoked for dry runs. */
88
+ isDryRun?: boolean;
89
+ presetField?: {
90
+ path: string[];
91
+ exists?: boolean;
92
+ oneOf?: InterceptorMatchScalar[];
93
+ notIn?: InterceptorMatchScalar[];
94
+ };
95
+ }
96
+
97
+ export interface InterceptorRegistrationMatchOptions {
98
+ match?: InterceptorMatchDTO;
99
+ }
100
+
101
+ export interface InterceptorRegistrationOptions {
102
+ priority?: number;
103
+ match?: InterceptorMatchDTO;
104
+ }
105
+
106
+ /**
107
+ * Host-owned, immutable context for one bound interceptor callback.
108
+ * `signal` is local to the worker invocation and is never serialized.
109
+ */
110
+ export interface InterceptorContextDTO {
111
+ readonly userId: string;
112
+ readonly chatId: string;
113
+ readonly generationId: string;
114
+ readonly generationType: InterceptorGenerationType;
115
+ readonly isDryRun: boolean;
116
+ readonly presetId: string | null;
117
+ /** Deep clone of only this extension's own preset metadata namespace. */
118
+ readonly presetMetadata: unknown;
119
+ readonly personaId: string | null;
120
+ readonly characterId: string | null;
121
+ readonly personaAddonStates: Readonly<Record<string, boolean>>;
122
+ readonly excludeMessageId?: string;
123
+ readonly rejectedSwipe?: string;
124
+ readonly regenFeedback?: string;
125
+ readonly regenFeedbackPosition?: "system" | "user";
126
+ readonly mainDispatch: {
127
+ readonly source: "main";
128
+ readonly descriptor: Readonly<ConnectionDispatchDescriptorDTO> | null;
129
+ readonly connectionDispatchRevision: string | null;
130
+ readonly dispatchKind: "concrete" | "roulette" | null;
131
+ };
132
+ readonly prefillCarrier: BoundPrefillAttachmentDTO;
133
+ readonly interceptorDeadlineAt: number;
134
+ readonly boundWorkDeadlineAt: number;
135
+ /** Worker-local cancellation signal; never serialized over the worker protocol. */
136
+ readonly signal: AbortSignal;
137
+ }
138
+
139
+ export interface DeferredGuidanceDTO {
140
+ id: string;
141
+ content: string;
142
+ role: "system";
143
+ }
144
+
60
145
 
61
146
  /**
62
147
  * Optional metadata returned by an interceptor so Lumiverse can surface
@@ -73,6 +158,13 @@ export interface InterceptorBreakdownEntryDTO {
73
158
  name?: string;
74
159
  }
75
160
 
161
+ /** Privileged response override returned by an interceptor. */
162
+ export interface FinalResponseDTO {
163
+ content: string;
164
+ reasoning?: string;
165
+ fallbackMessageIndex: number;
166
+ }
167
+
76
168
  /**
77
169
  * Return type for interceptor handlers.
78
170
  * Interceptors may return either a plain `LlmMessageDTO[]` (backwards-compatible)
@@ -84,8 +176,19 @@ export interface InterceptorResultDTO {
84
176
  parameters?: Record<string, unknown>;
85
177
  /** Optional prompt-breakdown entries for injected messages. */
86
178
  breakdown?: InterceptorBreakdownEntryDTO[];
179
+ /** Host-owned system guidance retained for each authoritative Main attempt. */
180
+ deferredGuidance?: DeferredGuidanceDTO[];
181
+ /** Optional privileged response replacement. Requires the `final_response` permission. */
182
+ finalResponse?: FinalResponseDTO;
87
183
  }
88
184
 
185
+ export type InterceptorDisposer = () => void;
186
+
187
+ export type InterceptorHandler = (
188
+ messages: LlmMessageDTO[],
189
+ context: InterceptorContextDTO,
190
+ ) => Promise<LlmMessageDTO[] | InterceptorResultDTO>;
191
+
89
192
  export interface MacroDefinitionDTO {
90
193
  name: string;
91
194
  category: string;
@@ -261,6 +364,15 @@ export interface ToolSchemaDTO {
261
364
  parameters: Record<string, unknown>; // JSON Schema
262
365
  }
263
366
 
367
+ export interface ToolDefinitionDTO {
368
+ name: string;
369
+ description: string;
370
+ parameters: Record<string, unknown>;
371
+ strict?: boolean;
372
+ inputExamples?: Array<Record<string, unknown>>;
373
+ cache_control?: Record<string, unknown>;
374
+ }
375
+
264
376
  /** A single function call made by the LLM. */
265
377
  export interface ToolCallDTO {
266
378
  /** Tool name (as given in the schema). */
@@ -269,8 +381,35 @@ export interface ToolCallDTO {
269
381
  args: Record<string, unknown>;
270
382
  /** Provider call ID (e.g. Anthropic `id`, OpenAI `id`). Synthetic UUID for providers that don't supply one (e.g. Google). */
271
383
  call_id: string;
384
+ /** Opaque provider signature preserved for tool-call continuations. */
385
+ thought_signature?: string;
386
+ }
387
+
388
+ export interface GenerationUsageDTO {
389
+ prompt_tokens?: number;
390
+ completion_tokens?: number;
391
+ total_tokens?: number;
392
+ provider_raw?: Record<string, unknown>;
393
+ }
394
+
395
+ export interface GenerationResponseDTO {
396
+ content: string;
397
+ reasoning?: string;
398
+ finish_reason: string;
399
+ tool_calls?: ToolCallDTO[];
400
+ thinking_blocks?: LlmThinkingBlockDTO[];
401
+ reasoning_details?: Record<string, unknown>[];
402
+ usage?: GenerationUsageDTO;
272
403
  }
273
404
 
405
+ export type GenerationDispatchSourceDTO =
406
+ | { source: "main"; expectedConnectionDispatchRevision: string }
407
+ | {
408
+ source: "slot";
409
+ connectionId: string;
410
+ expectedConnectionDispatchRevision: string;
411
+ };
412
+
274
413
  export interface GenerationRequestDTO {
275
414
  type: "raw" | "quiet" | "batch";
276
415
  messages?: LlmMessageDTO[];
@@ -535,6 +674,16 @@ export interface ConnectionProfileDTO {
535
674
  updated_at: number;
536
675
  }
537
676
 
677
+ export interface ConnectionDispatchDescriptorDTO {
678
+ connectionId: string;
679
+ connectionName: string;
680
+ provider: string;
681
+ model: string;
682
+ endpointOrigin: string;
683
+ dispatchKind: "concrete" | "roulette";
684
+ connectionDispatchRevision: string | null;
685
+ }
686
+
538
687
  // ─── Image Generation DTOs ──────────────────────────────────────────────
539
688
 
540
689
  /**
@@ -1753,6 +1902,126 @@ export interface AssembleResultDTO {
1753
1902
  breakdown: AssemblyBreakdownEntryDTO[];
1754
1903
  }
1755
1904
 
1905
+ export interface BoundPrefillAttachmentDTO {
1906
+ /**
1907
+ * Opaque parent-prefill attestation. It may be presented only by the live
1908
+ * worker invocation; it is not itself a reusable attachment capability,
1909
+ * message index, or carrier content.
1910
+ */
1911
+ readonly id: string;
1912
+ readonly state: "absent" | "available" | "invalid";
1913
+ }
1914
+
1915
+ export interface BoundAssembleRequestDTO {
1916
+ blocks: PromptBlockDTO[];
1917
+ promptVariableValues?: PromptVariableValuesDTO;
1918
+ dispatch: GenerationDispatchSourceDTO;
1919
+ deadlineAt: number;
1920
+ hookFailureMode?: "degrade" | "reject";
1921
+ macroFailureMode?: "degrade" | "reject";
1922
+ signal?: AbortSignal;
1923
+ }
1924
+
1925
+ export interface BoundAssemblySuccessDTO {
1926
+ messages: LlmMessageDTO[];
1927
+ breakdown: AssemblyBreakdownEntryDTO[];
1928
+ resolved: {
1929
+ source: "main" | "slot";
1930
+ connectionId: string | null;
1931
+ connectionDispatchRevision: string;
1932
+ dispatchKind: "concrete";
1933
+ };
1934
+ }
1935
+
1936
+ export type BoundAssemblyFailureDTO =
1937
+ | {
1938
+ kind: "hook";
1939
+ code: "ASSEMBLY_HOOK_FAILED";
1940
+ phase: "context" | "world_info" | "macro";
1941
+ reason: "error" | "timeout";
1942
+ message: string;
1943
+ }
1944
+ | {
1945
+ kind: "macro";
1946
+ code: "ASSEMBLY_MACRO_FAILED";
1947
+ reason: "definition" | "parse" | "recursion" | "budget" | "evaluation";
1948
+ message: string;
1949
+ }
1950
+ | {
1951
+ kind: "retrieval_snapshot";
1952
+ code: "ASSEMBLY_RETRIEVAL_SNAPSHOT_UNAVAILABLE";
1953
+ reason: "missing" | "expired" | "unavailable" | "oversize";
1954
+ message: string;
1955
+ }
1956
+ | {
1957
+ kind: "abort";
1958
+ code: "ASSEMBLY_ABORTED";
1959
+ name: "AbortError";
1960
+ message: string;
1961
+ }
1962
+ | {
1963
+ kind: "precondition" | "security" | "internal";
1964
+ code: string;
1965
+ message: string;
1966
+ };
1967
+
1968
+ export type BoundAssemblyOutcomeDTO =
1969
+ | { ok: true; result: BoundAssemblySuccessDTO }
1970
+ | { ok: false; error: BoundAssemblyFailureDTO };
1971
+
1972
+ export interface QuietTrackedRequestDTO {
1973
+ messages: LlmMessageDTO[];
1974
+ dispatch: GenerationDispatchSourceDTO;
1975
+ /**
1976
+ * When supplied, WorkerHost—not extension code—attaches the authenticated
1977
+ * parent assistant carrier as the final continuation carrier for this dispatch.
1978
+ */
1979
+ continuation?: {
1980
+ parentPrefill: BoundPrefillAttachmentDTO;
1981
+ mode: "append-parent-carrier-last";
1982
+ };
1983
+ parameters?: Record<string, unknown>;
1984
+ reasoning?: Record<string, unknown>;
1985
+ tools?: ToolDefinitionDTO[];
1986
+ deadlineAt: number;
1987
+ signal?: AbortSignal;
1988
+ }
1989
+
1990
+ export interface QuietDispatchReceiptDTO {
1991
+ providerInvoked: boolean;
1992
+ terminalResponse: boolean;
1993
+ source: "main" | "slot";
1994
+ connectionId: string | null;
1995
+ connectionDispatchRevision: string;
1996
+ usage?: GenerationUsageDTO;
1997
+ }
1998
+
1999
+ export type QuietTrackedResultDTO =
2000
+ | { ok: true; response: GenerationResponseDTO; receipt: QuietDispatchReceiptDTO }
2001
+ | {
2002
+ ok: false;
2003
+ phase: "preflight";
2004
+ providerInvoked: false;
2005
+ receipt: null;
2006
+ error: {
2007
+ kind: "precondition" | "security";
2008
+ code: string;
2009
+ name: string;
2010
+ message: string;
2011
+ };
2012
+ }
2013
+ | {
2014
+ ok: false;
2015
+ phase: "resolved";
2016
+ receipt: QuietDispatchReceiptDTO;
2017
+ error: {
2018
+ kind: "precondition" | "provider" | "abort" | "security" | "internal";
2019
+ code: string;
2020
+ name: string;
2021
+ message: string;
2022
+ };
2023
+ };
2024
+
1756
2025
  // ─── Chat Memory DTOs ──────────────────────────────────────────────────
1757
2026
 
1758
2027
  export interface ChatMemoryChunkDTO {
@@ -2641,13 +2910,22 @@ export type WorkerToHost =
2641
2910
  | { type: "register_macro"; definition: MacroDefinitionDTO }
2642
2911
  | { type: "unregister_macro"; name: string }
2643
2912
  | { type: "update_macro_value"; name: string; value: string }
2644
- | { type: "register_interceptor"; priority?: number }
2913
+ | {
2914
+ type: "register_interceptor";
2915
+ registrationId: string;
2916
+ priority?: number;
2917
+ match?: InterceptorMatchDTO;
2918
+ }
2919
+ | { type: "unregister_interceptor"; registrationId: string }
2645
2920
  | {
2646
2921
  type: "intercept_result";
2647
2922
  requestId: string;
2923
+ registrationId: string;
2648
2924
  messages: LlmMessageDTO[];
2649
2925
  parameters?: Record<string, unknown>;
2650
2926
  breakdown?: InterceptorBreakdownEntryDTO[];
2927
+ deferredGuidance?: DeferredGuidanceDTO[];
2928
+ finalResponse?: FinalResponseDTO;
2651
2929
  }
2652
2930
  | {
2653
2931
  type: "assemble_prompt";
@@ -2655,6 +2933,33 @@ export type WorkerToHost =
2655
2933
  input: Omit<AssembleRequestDTO, "signal">;
2656
2934
  userId?: string;
2657
2935
  }
2936
+ /**
2937
+ * Assemble through the active bound interceptor generation context. The
2938
+ * worker-local signal is omitted from the structured-clone payload.
2939
+ */
2940
+ | {
2941
+ type: "generate_assemble";
2942
+ requestId: string;
2943
+ input: Omit<BoundAssembleRequestDTO, "signal">;
2944
+ }
2945
+ /**
2946
+ * Dispatch tracked quiet generation through the active bound context.
2947
+ * The worker-local signal is omitted from the structured-clone payload.
2948
+ */
2949
+ | {
2950
+ type: "generate_quiet_tracked";
2951
+ requestId: string;
2952
+ input: Omit<QuietTrackedRequestDTO, "signal">;
2953
+ }
2954
+ /**
2955
+ * Inspect an owned concrete slot through the active bound interceptor
2956
+ * context. The host derives user scope from the authenticated callback.
2957
+ */
2958
+ | {
2959
+ type: "connections_resolve_dispatch";
2960
+ requestId: string;
2961
+ connectionId: string;
2962
+ }
2658
2963
  | { type: "register_tool"; tool: ToolRegistrationDTO }
2659
2964
  | { type: "unregister_tool"; name: string }
2660
2965
  | { type: "request_generation"; requestId: string; input: GenerationRequestDTO }
@@ -3205,7 +3510,7 @@ export type WorkerToHost =
3205
3510
  // ─── Host → Worker messages ──────────────────────────────────────────────
3206
3511
 
3207
3512
  export type HostToWorker =
3208
- | { type: "init"; manifest: SpindleManifest; storagePath: string }
3513
+ | { type: "init"; manifest: SpindleManifest; storagePath: string; host: SpindleHostDescriptorV1 }
3209
3514
  | { type: "event"; event: string; payload: unknown; userId?: string }
3210
3515
  | {
3211
3516
  type: "rpc_pool_request";
@@ -3218,8 +3523,15 @@ export type HostToWorker =
3218
3523
  | {
3219
3524
  type: "intercept_request";
3220
3525
  requestId: string;
3526
+ registrationId: string;
3221
3527
  messages: LlmMessageDTO[];
3222
- context: unknown;
3528
+ context: Omit<InterceptorContextDTO, "signal">;
3529
+ }
3530
+ | {
3531
+ type: "intercept_abort";
3532
+ requestId: string;
3533
+ registrationId: string;
3534
+ reason?: string;
3223
3535
  }
3224
3536
  | {
3225
3537
  type: "context_handler_request";
package/src/dom.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { PromptBlockDTO, PromptVariableValuesDTO, RequestInitDTO } from "./api";
2
2
  import type { SpindleComponentsHelper } from "./components";
3
+ import type { SpindleHostDescriptorV1, SpindleHostLocaleAPI } from "./host";
3
4
 
4
5
  /** A chat-message DOM element paired with its stable message id. */
5
6
  export interface SpindleMessageElement {
@@ -859,6 +860,10 @@ export interface SpindleDisplayResolverRegistry {
859
860
 
860
861
  /** Context object provided to frontend extension modules */
861
862
  export interface SpindleFrontendContext {
863
+ /** Immutable host compatibility descriptor for this extension runtime. */
864
+ readonly host: SpindleHostDescriptorV1;
865
+ /** Synchronous host locale access with removable live-change subscriptions. */
866
+ readonly locale: SpindleHostLocaleAPI;
862
867
  dom: SpindleDOMHelper;
863
868
  events: {
864
869
  on(event: string, handler: (payload: unknown) => void): () => void;
@@ -923,6 +928,8 @@ export interface SpindleFrontendContext {
923
928
  showConfirm(options: SpindleConfirmOptions): Promise<SpindleConfirmResult>;
924
929
  /** Request a tab move to a specific drawer location. */
925
930
  requestTabLocation(tabId: string, location: SpindleTabLocation): void;
931
+ /** Read the current drawer location for a built-in or extension tab. */
932
+ getTabLocation(tabId: string): SpindleTabLocation;
926
933
  /** Get the display title of a built-in drawer tab by its id. */
927
934
  getBuiltInTabTitle(tabId: string): string | undefined;
928
935
  /** Get the root HTMLElement of a built-in drawer tab by its id, or undefined if not mounted. */
@@ -1045,8 +1052,13 @@ export interface SpindleFrontendContext {
1045
1052
  manifest: import("./manifest").SpindleManifest;
1046
1053
  }
1047
1054
 
1055
+ /** Cleanup returned by a frontend extension setup hook. */
1056
+ export type SpindleFrontendTeardown = () => void | Promise<void>;
1057
+
1048
1058
  /** What a frontend extension module must export */
1049
1059
  export interface SpindleFrontendModule {
1050
- setup(ctx: SpindleFrontendContext): void | (() => void);
1051
- teardown?(): void;
1060
+ setup(
1061
+ ctx: SpindleFrontendContext,
1062
+ ): void | SpindleFrontendTeardown | Promise<void | SpindleFrontendTeardown>;
1063
+ teardown?(): void | Promise<void>;
1052
1064
  }
package/src/host.ts ADDED
@@ -0,0 +1,32 @@
1
+ export const SPINDLE_HOST_CAPABILITIES = Object.freeze({
2
+ "preset-extension-data-v1": 1,
3
+ "preset-editor-v1": 1,
4
+ "loom-block-editor-v1": 1,
5
+ "generation-assembly-v1": 1,
6
+ "interceptor-context-v1": 1,
7
+ "interceptor-final-response-v1": 1,
8
+ }) as Readonly<Record<string, number>>;
9
+ /** Structured error code returned when host compatibility validation fails. */
10
+ export const SPINDLE_COMPATIBILITY_ERROR_CODE = "SPINDLE_COMPATIBILITY_ERROR" as const;
11
+
12
+ /**
13
+ * Immutable host/runtime compatibility information exposed to extensions.
14
+ *
15
+ * Hosts may add capability keys over time. Extensions should only depend on
16
+ * the capability names they understand and the descriptor version they support.
17
+ */
18
+ export interface SpindleHostDescriptorV1 {
19
+ readonly descriptorVersion: 1;
20
+ readonly lumiverseVersion: string;
21
+ readonly capabilities: Readonly<Record<string, number>>;
22
+ readonly extensionInstallationId: string;
23
+ }
24
+
25
+ /** Locale identifiers supplied by the host's active UI locale. */
26
+ export type SpindleHostLocale = "en" | "zh" | "zh-TW" | "ja" | "fr" | "it";
27
+
28
+ /** Synchronous host locale lookup with a removable live-change subscription. */
29
+ export interface SpindleHostLocaleAPI {
30
+ get(): SpindleHostLocale;
31
+ subscribe(listener: (locale: SpindleHostLocale) => void): () => void;
32
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,16 @@ export {
8
8
  validateIdentifier,
9
9
  } from "./manifest";
10
10
 
11
+ export type {
12
+ SpindleHostDescriptorV1,
13
+ SpindleHostLocale,
14
+ SpindleHostLocaleAPI,
15
+ } from "./host";
16
+ export {
17
+ SPINDLE_HOST_CAPABILITIES,
18
+ SPINDLE_COMPATIBILITY_ERROR_CODE,
19
+ } from "./host";
20
+
11
21
  export type { SpindlePermission as SpindlePermissionType } from "./permissions";
12
22
  export { ALL_PERMISSIONS, isValidPermission } from "./permissions";
13
23
 
@@ -18,8 +28,19 @@ export { SpindleEvent, CoreEventType } from "./events";
18
28
 
19
29
  export type {
20
30
  LlmMessageDTO,
31
+ LlmThinkingBlockDTO,
21
32
  SpindleUserRoleDTO,
33
+ InterceptorGenerationType,
34
+ InterceptorMatchScalar,
35
+ InterceptorMatchDTO,
36
+ InterceptorRegistrationMatchOptions,
37
+ InterceptorRegistrationOptions,
38
+ InterceptorContextDTO,
39
+ InterceptorDisposer,
40
+ InterceptorHandler,
41
+ DeferredGuidanceDTO,
22
42
  InterceptorBreakdownEntryDTO,
43
+ FinalResponseDTO,
23
44
  InterceptorResultDTO,
24
45
  MacroDefinitionDTO,
25
46
  MacroInvocationContextDTO,
@@ -28,6 +49,11 @@ export type {
28
49
  ToolRegistrationDTO,
29
50
  ToolSchemaDTO,
30
51
  ToolCallDTO,
52
+ ToolDefinitionDTO,
53
+ GenerationUsageDTO,
54
+ GenerationResponseDTO,
55
+ GenerationDispatchSourceDTO,
56
+ ConnectionDispatchDescriptorDTO,
31
57
  GenerationRequestDTO,
32
58
  GenerationReasoningOverrideDTO,
33
59
  ReasoningEffortDTO,
@@ -112,6 +138,14 @@ export type {
112
138
  DryRunResultDTO,
113
139
  AssembleRequestDTO,
114
140
  AssembleResultDTO,
141
+ BoundPrefillAttachmentDTO,
142
+ BoundAssembleRequestDTO,
143
+ BoundAssemblySuccessDTO,
144
+ BoundAssemblyFailureDTO,
145
+ BoundAssemblyOutcomeDTO,
146
+ QuietTrackedRequestDTO,
147
+ QuietDispatchReceiptDTO,
148
+ QuietTrackedResultDTO,
115
149
  AssemblyBreakdownEntryDTO,
116
150
  ActivationStatsDTO,
117
151
  MemoryStatsDTO,
@@ -216,6 +250,7 @@ export type {
216
250
  PermissionRequestOptions,
217
251
  SpindleFrontendContext,
218
252
  SpindleFrontendModule,
253
+ SpindleFrontendTeardown,
219
254
  SpindleDrawerTabOptions,
220
255
  SpindleDrawerTabHandle,
221
256
  SpindleCharacterEditorTabOptions,
@@ -381,6 +416,8 @@ export type {
381
416
 
382
417
  export type {
383
418
  SpindleAPI,
419
+ SpindleGenerateAPI,
420
+ SpindleConnectionsAPI,
384
421
  SpindlePromptRegex,
385
422
  FrontendProcessHandle,
386
423
  BackendProcessHandle,
@@ -26,6 +26,8 @@
26
26
  * web search settings (never the API key).
27
27
  * - "unsafe_eval" — opt a sandboxed widget frame into CSP 'unsafe-eval'
28
28
  * (eval / new Function) for runtime-compiling frameworks.
29
+ * - "final_response" — replace the authoritative assistant response from
30
+ * an interceptor (privileged; never auto-granted)
29
31
  */
30
32
  export type SpindlePermission =
31
33
  | "generation"
@@ -54,7 +56,8 @@ export type SpindlePermission =
54
56
  | "generation_parameters"
55
57
  | "macro_interceptor"
56
58
  | "web_search"
57
- | "unsafe_eval";
59
+ | "unsafe_eval"
60
+ | "final_response";
58
61
 
59
62
  export const ALL_PERMISSIONS: readonly SpindlePermission[] = [
60
63
  "generation",
@@ -84,6 +87,7 @@ export const ALL_PERMISSIONS: readonly SpindlePermission[] = [
84
87
  "macro_interceptor",
85
88
  "web_search",
86
89
  "unsafe_eval",
90
+ "final_response",
87
91
  ] as const;
88
92
 
89
93
  export function isValidPermission(p: string): p is SpindlePermission {
@@ -1,11 +1,15 @@
1
1
  import type { SpindleManifest } from "./manifest";
2
+ import type { SpindleHostDescriptorV1 } from "./host";
2
3
  import type {
3
4
  CouncilMemberContext,
4
5
  CouncilSettings,
5
6
  } from "./council";
6
7
  import type {
7
8
  LlmMessageDTO,
8
- InterceptorResultDTO,
9
+ InterceptorDisposer,
10
+ InterceptorHandler,
11
+ InterceptorRegistrationMatchOptions,
12
+ InterceptorRegistrationOptions,
9
13
  MacroDefinitionDTO,
10
14
  MacroResolveOptionsDTO,
11
15
  MacroResolveResultDTO,
@@ -14,6 +18,7 @@ import type {
14
18
  ChatAppendMessageOptionsDTO,
15
19
  RequestInitDTO,
16
20
  ConnectionProfileDTO,
21
+ ConnectionDispatchDescriptorDTO,
17
22
  PermissionDeniedDetail,
18
23
  PermissionChangedDetail,
19
24
  CharacterDTO,
@@ -58,6 +63,10 @@ import type {
58
63
  DryRunResultDTO,
59
64
  AssembleRequestDTO,
60
65
  AssembleResultDTO,
66
+ BoundAssembleRequestDTO,
67
+ BoundAssemblyOutcomeDTO,
68
+ QuietTrackedRequestDTO,
69
+ QuietTrackedResultDTO,
61
70
  ChatMemoryResultDTO,
62
71
  ImageGenRequestDTO,
63
72
  ImageGenResultDTO,
@@ -235,8 +244,20 @@ export interface SpindlePromptRegex {
235
244
  setOwnedChats(chatIds: string[]): void;
236
245
  }
237
246
 
247
+ export type SpindleGenerateAPI = SpindleAPI["generate"];
248
+
249
+ export interface SpindleConnectionsAPI {
250
+ /**
251
+ * Resolve an owned concrete slot's current safe dispatch descriptor inside
252
+ * an active interceptor callback. Requires the `generation` permission.
253
+ */
254
+ resolveDispatch(connectionId: string): Promise<ConnectionDispatchDescriptorDTO | null>;
255
+ }
256
+
238
257
  /** The global `spindle` object available in backend extension workers */
239
258
  export interface SpindleAPI {
259
+ /** Immutable host compatibility descriptor for this extension runtime. */
260
+ readonly host: SpindleHostDescriptorV1;
240
261
  /**
241
262
  * Subscribe to permission grant/revoke changes for this extension only.
242
263
  * This is delivered directly by the worker host and does not subscribe to
@@ -314,12 +335,14 @@ export interface SpindleAPI {
314
335
  * Without it, returned parameters are silently stripped.
315
336
  */
316
337
  registerInterceptor(
317
- handler: (
318
- messages: LlmMessageDTO[],
319
- context: unknown
320
- ) => Promise<LlmMessageDTO[] | InterceptorResultDTO>,
321
- priority?: number
322
- ): void;
338
+ handler: InterceptorHandler,
339
+ priority?: number,
340
+ options?: InterceptorRegistrationMatchOptions,
341
+ ): InterceptorDisposer;
342
+ registerInterceptor(
343
+ handler: InterceptorHandler,
344
+ options?: InterceptorRegistrationOptions,
345
+ ): InterceptorDisposer;
323
346
 
324
347
  /**
325
348
  * Assemble an arbitrary Loom block graph against a chat without calling an
@@ -378,6 +401,18 @@ export interface SpindleAPI {
378
401
  * ```
379
402
  */
380
403
  generate: {
404
+ /**
405
+ * Assemble against the active callback's frozen parent retrieval snapshot
406
+ * without provider work or fresh retrieval effects. Requires the
407
+ * `generation` permission and an active interceptor callback.
408
+ */
409
+ assemble(input: BoundAssembleRequestDTO): Promise<BoundAssemblyOutcomeDTO>;
410
+ /**
411
+ * Dispatch a receipt-bearing quiet call through the active callback's
412
+ * revision-bound Main or explicit-slot source. Requires the `generation`
413
+ * permission and an active interceptor callback.
414
+ */
415
+ quietTracked(input: QuietTrackedRequestDTO): Promise<QuietTrackedResultDTO>;
381
416
  raw(input: GenerationRequestDTO): Promise<unknown>;
382
417
  quiet(input: GenerationRequestDTO): Promise<unknown>;
383
418
  batch(input: GenerationRequestDTO): Promise<unknown>;
@@ -716,6 +751,11 @@ export interface SpindleAPI {
716
751
  * Returns null if the connection doesn't exist or isn't accessible.
717
752
  */
718
753
  get(connectionId: string, userId?: string): Promise<ConnectionProfileDTO | null>;
754
+ /**
755
+ * Resolve an owned concrete slot's safe current dispatch descriptor inside
756
+ * an active interceptor callback. Requires the `generation` permission.
757
+ */
758
+ resolveDispatch(connectionId: string): Promise<ConnectionDispatchDescriptorDTO | null>;
719
759
  };
720
760
 
721
761
  /** Server-side token counting helpers (free tier). */
@@ -0,0 +1,254 @@
1
+ import type {
2
+ BoundAssembleRequestDTO,
3
+ BoundAssemblyFailureDTO,
4
+ BoundAssemblyOutcomeDTO,
5
+ BoundAssemblySuccessDTO,
6
+ BoundPrefillAttachmentDTO,
7
+ ConnectionDispatchDescriptorDTO,
8
+ GenerationDispatchSourceDTO,
9
+ GenerationResponseDTO,
10
+ GenerationUsageDTO,
11
+ LlmMessageDTO,
12
+ QuietDispatchReceiptDTO,
13
+ QuietTrackedRequestDTO,
14
+ QuietTrackedResultDTO,
15
+ SpindleAPI,
16
+ SpindleConnectionsAPI,
17
+ SpindleGenerateAPI,
18
+ ToolDefinitionDTO,
19
+ } from "lumiverse-spindle-types";
20
+
21
+ const mainDispatch: GenerationDispatchSourceDTO = {
22
+ source: "main",
23
+ expectedConnectionDispatchRevision: "main-revision",
24
+ };
25
+
26
+ const slotDispatch: GenerationDispatchSourceDTO = {
27
+ source: "slot",
28
+ connectionId: "connection-id",
29
+ expectedConnectionDispatchRevision: "slot-revision",
30
+ };
31
+
32
+ const messages: LlmMessageDTO[] = [
33
+ {
34
+ role: "assistant",
35
+ content: [
36
+ {
37
+ type: "tool_use",
38
+ id: "call-1",
39
+ name: "lookup",
40
+ input: { query: "safe" },
41
+ thought_signature: "provider-signature",
42
+ },
43
+ ],
44
+ cache_control: { ttl: "request" },
45
+ reasoning_content: "reasoning carrier",
46
+ thinking_blocks: [
47
+ { type: "thinking", thinking: "private provider reasoning", signature: "opaque" },
48
+ ],
49
+ reasoning_details: [{ type: "summary_text", text: "provider detail" }],
50
+ },
51
+ ];
52
+
53
+ const promptBlocks = [] as BoundAssembleRequestDTO["blocks"];
54
+ const boundRequest: BoundAssembleRequestDTO = {
55
+ blocks: promptBlocks,
56
+ promptVariableValues: {},
57
+ dispatch: mainDispatch,
58
+ deadlineAt: Date.now() + 1_000,
59
+ hookFailureMode: "degrade",
60
+ macroFailureMode: "reject",
61
+ };
62
+
63
+ const boundSuccess: BoundAssemblySuccessDTO = {
64
+ messages,
65
+ breakdown: [],
66
+ resolved: {
67
+ source: "main",
68
+ connectionId: null,
69
+ connectionDispatchRevision: "main-revision",
70
+ dispatchKind: "concrete",
71
+ },
72
+ };
73
+
74
+ const boundFailures: BoundAssemblyFailureDTO[] = [
75
+ {
76
+ kind: "hook",
77
+ code: "ASSEMBLY_HOOK_FAILED",
78
+ phase: "context",
79
+ reason: "timeout",
80
+ message: "context hook timed out",
81
+ },
82
+ {
83
+ kind: "macro",
84
+ code: "ASSEMBLY_MACRO_FAILED",
85
+ reason: "evaluation",
86
+ message: "macro failed",
87
+ },
88
+ {
89
+ kind: "retrieval_snapshot",
90
+ code: "ASSEMBLY_RETRIEVAL_SNAPSHOT_UNAVAILABLE",
91
+ reason: "missing",
92
+ message: "snapshot unavailable",
93
+ },
94
+ { kind: "abort", code: "ASSEMBLY_ABORTED", name: "AbortError", message: "aborted" },
95
+ { kind: "precondition", code: "ASSEMBLY_PRECONDITION_FAILED", message: "stale dispatch" },
96
+ { kind: "security", code: "ASSEMBLY_SECURITY_FAILED", message: "invalid source" },
97
+ { kind: "internal", code: "ASSEMBLY_INTERNAL_FAILED", message: "host error" },
98
+ ];
99
+
100
+ const boundOutcomes: BoundAssemblyOutcomeDTO[] = [
101
+ { ok: true, result: boundSuccess },
102
+ ...boundFailures.map((error) => ({ ok: false as const, error })),
103
+ ];
104
+
105
+ function inspectBoundOutcome(outcome: BoundAssemblyOutcomeDTO): string {
106
+ if (outcome.ok) {
107
+ return outcome.result.resolved.connectionDispatchRevision;
108
+ }
109
+ switch (outcome.error.kind) {
110
+ case "hook":
111
+ return outcome.error.phase;
112
+ case "macro":
113
+ return outcome.error.reason;
114
+ case "retrieval_snapshot":
115
+ return outcome.error.reason;
116
+ case "abort":
117
+ return outcome.error.name;
118
+ case "precondition":
119
+ case "security":
120
+ case "internal":
121
+ return outcome.error.code;
122
+ }
123
+ }
124
+
125
+ const tool: ToolDefinitionDTO = {
126
+ name: "lookup",
127
+ description: "Look up a safe value",
128
+ parameters: { type: "object", properties: {} },
129
+ strict: true,
130
+ inputExamples: [{ query: "safe" }],
131
+ cache_control: { ttl: "request" },
132
+ };
133
+
134
+ const continuation: BoundPrefillAttachmentDTO = {
135
+ id: "parent-attestation",
136
+ state: "available",
137
+ };
138
+
139
+ const quietRequest: QuietTrackedRequestDTO = {
140
+ messages,
141
+ dispatch: slotDispatch,
142
+ continuation: {
143
+ parentPrefill: continuation,
144
+ mode: "append-parent-carrier-last",
145
+ },
146
+ parameters: { temperature: 0.2 },
147
+ reasoning: { effort: "low" },
148
+ tools: [tool],
149
+ deadlineAt: Date.now() + 1_000,
150
+ };
151
+
152
+ const usage: GenerationUsageDTO = {
153
+ prompt_tokens: 10,
154
+ completion_tokens: 5,
155
+ total_tokens: 15,
156
+ provider_raw: { cached: 2 },
157
+ };
158
+
159
+ const response: GenerationResponseDTO = {
160
+ content: "safe response",
161
+ reasoning: "safe reasoning carrier",
162
+ finish_reason: "stop",
163
+ tool_calls: [
164
+ { name: "lookup", args: { query: "safe" }, call_id: "call-1", thought_signature: "opaque" },
165
+ ],
166
+ thinking_blocks: [{ type: "redacted_thinking", data: "opaque" }],
167
+ reasoning_details: [{ type: "summary_text", text: "provider detail" }],
168
+ usage,
169
+ };
170
+
171
+ const receipt: QuietDispatchReceiptDTO = {
172
+ providerInvoked: true,
173
+ terminalResponse: true,
174
+ source: "slot",
175
+ connectionId: "connection-id",
176
+ connectionDispatchRevision: "slot-revision",
177
+ usage,
178
+ };
179
+
180
+ const quietResults: QuietTrackedResultDTO[] = [
181
+ { ok: true, response, receipt },
182
+ {
183
+ ok: false,
184
+ phase: "preflight",
185
+ providerInvoked: false,
186
+ receipt: null,
187
+ error: { kind: "precondition", code: "BOUND_INVOCATION_REQUIRED", name: "PreconditionError", message: "not bound" },
188
+ },
189
+ {
190
+ ok: false,
191
+ phase: "resolved",
192
+ receipt,
193
+ error: { kind: "provider", code: "PROVIDER_ACCESS_FAILED", name: "ProviderError", message: "provider unavailable" },
194
+ },
195
+ ];
196
+
197
+ function inspectQuietResult(result: QuietTrackedResultDTO): string {
198
+ if (result.ok) return result.response.finish_reason;
199
+ if (result.phase === "preflight") return result.error.kind;
200
+ return `${result.error.kind}:${result.receipt.connectionDispatchRevision}`;
201
+ }
202
+
203
+ const descriptor: ConnectionDispatchDescriptorDTO = {
204
+ connectionId: "connection-id",
205
+ connectionName: "Safe connection",
206
+ provider: "provider",
207
+ model: "model",
208
+ endpointOrigin: "https://provider.example",
209
+ dispatchKind: "concrete",
210
+ connectionDispatchRevision: "slot-revision",
211
+ };
212
+
213
+ const generateApi = {
214
+ assemble: async () => ({ ok: true as const, result: boundSuccess }),
215
+ quietTracked: async () => ({ ok: true as const, response, receipt }),
216
+ } satisfies Pick<SpindleGenerateAPI, "assemble" | "quietTracked">;
217
+
218
+ const connectionsApi: SpindleConnectionsAPI = {
219
+ resolveDispatch: async () => descriptor,
220
+ };
221
+
222
+ declare const spindle: SpindleAPI;
223
+ const generateSurface: SpindleGenerateAPI = spindle.generate;
224
+ const connectionsSurface: SpindleConnectionsAPI = spindle.connections;
225
+
226
+ // These accesses intentionally fail if private runtime protocol material leaks
227
+ // into the public DTOs.
228
+ // @ts-expect-error invocation tokens are host-private
229
+ void ({} as BoundAssembleRequestDTO).invocationToken;
230
+ // @ts-expect-error snapshots are host-private
231
+ void ({} as QuietTrackedRequestDTO).parentGenerationSnapshot;
232
+ // @ts-expect-error leases are host-private
233
+ void ({} as QuietDispatchReceiptDTO).terminalFinalizationLease;
234
+ // @ts-expect-error API keys are not safe dispatch descriptor fields
235
+ void ({} as ConnectionDispatchDescriptorDTO).apiKey;
236
+ const invalidMainDispatch: GenerationDispatchSourceDTO = {
237
+ source: "main",
238
+ expectedConnectionDispatchRevision: "revision",
239
+ // @ts-expect-error a main source cannot carry an explicit slot id
240
+ connectionId: "must-not-be-public-on-main",
241
+ };
242
+ // @ts-expect-error attachment attestations are immutable
243
+ continuation.id = "mutated";
244
+
245
+ void inspectBoundOutcome;
246
+ void inspectQuietResult;
247
+ void generateApi;
248
+ void connectionsApi;
249
+ void generateSurface;
250
+ void connectionsSurface;
251
+ void boundRequest;
252
+ void quietRequest;
253
+ void boundOutcomes;
254
+ void quietResults;
@@ -0,0 +1,24 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ SPINDLE_COMPATIBILITY_ERROR_CODE,
4
+ SPINDLE_HOST_CAPABILITIES,
5
+ } from "../src/host";
6
+ import { ALL_PERMISSIONS, isValidPermission } from "../src/permissions";
7
+
8
+ test("final-response permission is valid and publicly declared", () => {
9
+ expect(ALL_PERMISSIONS).toContain("final_response");
10
+ expect(isValidPermission("final_response")).toBe(true);
11
+ });
12
+
13
+ test("host compatibility constants are canonical and immutable", () => {
14
+ expect(SPINDLE_COMPATIBILITY_ERROR_CODE).toBe("SPINDLE_COMPATIBILITY_ERROR");
15
+ expect(SPINDLE_HOST_CAPABILITIES).toEqual({
16
+ "preset-extension-data-v1": 1,
17
+ "preset-editor-v1": 1,
18
+ "loom-block-editor-v1": 1,
19
+ "generation-assembly-v1": 1,
20
+ "interceptor-context-v1": 1,
21
+ "interceptor-final-response-v1": 1,
22
+ });
23
+ expect(Object.isFrozen(SPINDLE_HOST_CAPABILITIES)).toBe(true);
24
+ });
@@ -0,0 +1,137 @@
1
+ import type {
2
+ FinalResponseDTO,
3
+ HostToWorker,
4
+ InterceptorBreakdownEntryDTO,
5
+ InterceptorResultDTO,
6
+ SpindleAPI,
7
+ SpindleFrontendContext,
8
+ SpindleHostDescriptorV1,
9
+ SpindleHostLocale,
10
+ SpindleHostLocaleAPI,
11
+ SpindleManifest,
12
+ SpindlePermission,
13
+ SpindleTabLocation,
14
+ WorkerToHost,
15
+ } from "lumiverse-spindle-types";
16
+ import {
17
+ ALL_PERMISSIONS,
18
+ SPINDLE_COMPATIBILITY_ERROR_CODE,
19
+ SPINDLE_HOST_CAPABILITIES,
20
+ isValidPermission,
21
+ } from "lumiverse-spindle-types";
22
+
23
+ const host: SpindleHostDescriptorV1 = {
24
+ descriptorVersion: 1,
25
+ lumiverseVersion: "1.2.3",
26
+ capabilities: SPINDLE_HOST_CAPABILITIES,
27
+ extensionInstallationId: "550e8400-e29b-41d4-a716-446655440000",
28
+ };
29
+
30
+ const localeApi: SpindleHostLocaleAPI = {
31
+ get: () => "en",
32
+ subscribe: (listener) => {
33
+ listener("fr");
34
+ return () => undefined;
35
+ },
36
+ };
37
+
38
+ const locale: SpindleHostLocale = localeApi.get();
39
+ const permission: SpindlePermission = "final_response";
40
+ const allPermissions: readonly SpindlePermission[] = ALL_PERMISSIONS;
41
+ if (!isValidPermission(permission) || !allPermissions.includes(permission)) {
42
+ throw new Error("final_response must be a valid public permission");
43
+ }
44
+
45
+ const finalResponse: FinalResponseDTO = {
46
+ content: "An extension-authored final answer",
47
+ reasoning: "The selected branch completed successfully.",
48
+ fallbackMessageIndex: 0,
49
+ };
50
+ const breakdown: InterceptorBreakdownEntryDTO = { messageIndex: 0, name: "Additional context" };
51
+
52
+ const result: InterceptorResultDTO = {
53
+ messages: [{ role: "system", content: "Additional context" }],
54
+ breakdown: [breakdown],
55
+ finalResponse,
56
+ };
57
+ const wireResult: WorkerToHost = {
58
+ type: "intercept_result",
59
+ requestId: "request-id",
60
+ registrationId: "registration-id",
61
+ messages: result.messages,
62
+ breakdown: [breakdown],
63
+ finalResponse,
64
+ };
65
+
66
+ const init: HostToWorker = {
67
+ type: "init",
68
+ manifest: {
69
+ version: "1.0.0",
70
+ name: "Legacy extension",
71
+ identifier: "legacy_extension",
72
+ author: "Extension author",
73
+ github: "https://github.com/example/legacy-extension",
74
+ homepage: "https://example.com/legacy-extension",
75
+ permissions: [],
76
+ },
77
+ storagePath: "/extensions/legacy_extension",
78
+ host,
79
+ };
80
+
81
+ // The compatibility field is intentionally optional for legacy manifests.
82
+ const legacyManifest: SpindleManifest = {
83
+ version: "1.0.0",
84
+ name: "Legacy extension",
85
+ identifier: "legacy_extension",
86
+ author: "Extension author",
87
+ github: "https://github.com/example/legacy-extension",
88
+ homepage: "https://example.com/legacy-extension",
89
+ permissions: [],
90
+ };
91
+ // @ts-expect-error host descriptor is required on the public init message
92
+ const legacyInitWithoutHost: HostToWorker = {
93
+ type: "init",
94
+ manifest: legacyManifest,
95
+ storagePath: "/extensions/legacy_extension",
96
+ };
97
+ void locale;
98
+ void result;
99
+ void legacyManifest;
100
+
101
+ declare const spindle: SpindleAPI;
102
+ const backendHost: SpindleHostDescriptorV1 = spindle.host;
103
+ // @ts-expect-error the host descriptor is exposed as a readonly API property
104
+ spindle.host = host;
105
+ // @ts-expect-error nested capability records are readonly
106
+ spindle.host.capabilities["interceptor-final-response-v1"] = 2;
107
+ // @ts-expect-error descriptor scalar fields are readonly
108
+ spindle.host.lumiverseVersion = "2.0.0";
109
+ void backendHost;
110
+
111
+ declare const ctx: SpindleFrontendContext;
112
+ const frontendHost: SpindleHostDescriptorV1 = ctx.host;
113
+ const activeLocale: SpindleHostLocale = ctx.locale.get();
114
+ const tabLocation: SpindleTabLocation = ctx.ui.getTabLocation("profile");
115
+ const unsubscribe = ctx.locale.subscribe((nextLocale) => {
116
+ const checked: SpindleHostLocale = nextLocale;
117
+ void checked;
118
+ });
119
+ // @ts-expect-error the frontend host descriptor is exposed as a readonly API property
120
+ ctx.host = host;
121
+ // @ts-expect-error the frontend locale API is exposed as a readonly API property
122
+ ctx.locale = localeApi;
123
+ void frontendHost;
124
+ void activeLocale;
125
+ void unsubscribe;
126
+ void tabLocation;
127
+ const compatibilityCode: "SPINDLE_COMPATIBILITY_ERROR" = SPINDLE_COMPATIBILITY_ERROR_CODE;
128
+ void compatibilityCode;
129
+
130
+ const asyncFrontendModule = {
131
+ async setup(_ctx: SpindleFrontendContext) {
132
+ return async () => undefined;
133
+ },
134
+ async teardown() {},
135
+ };
136
+ const typedAsyncFrontendModule: import("lumiverse-spindle-types").SpindleFrontendModule = asyncFrontendModule;
137
+ void typedAsyncFrontendModule;
@@ -0,0 +1,196 @@
1
+ import type {
2
+ BoundAssembleRequestDTO,
3
+ BoundPrefillAttachmentDTO,
4
+ ConnectionDispatchDescriptorDTO,
5
+ DeferredGuidanceDTO,
6
+ HostToWorker,
7
+ InterceptorContextDTO,
8
+ InterceptorGenerationType,
9
+ InterceptorHandler,
10
+ InterceptorMatchDTO,
11
+ InterceptorRegistrationMatchOptions,
12
+ InterceptorRegistrationOptions,
13
+ InterceptorResultDTO,
14
+ InterceptorDisposer,
15
+ InterceptorBreakdownEntryDTO,
16
+ LlmMessageDTO,
17
+ QuietTrackedRequestDTO,
18
+ SpindleAPI,
19
+ WorkerToHost,
20
+ } from "lumiverse-spindle-types";
21
+
22
+ const generationType: InterceptorGenerationType = "continue";
23
+ const match: InterceptorMatchDTO = {
24
+ generationTypes: ["normal", generationType],
25
+ isDryRun: false,
26
+ presetField: {
27
+ path: ["activeMode"],
28
+ exists: true,
29
+ notIn: ["single", 0, false, null],
30
+ oneOf: ["sequential", "parallel"],
31
+ },
32
+ };
33
+ const matchOptions: InterceptorRegistrationMatchOptions = { match };
34
+ const options: InterceptorRegistrationOptions = { priority: 900, match };
35
+
36
+ const descriptor: ConnectionDispatchDescriptorDTO = {
37
+ connectionId: "connection-id",
38
+ connectionName: "Main",
39
+ provider: "provider",
40
+ model: "model",
41
+ endpointOrigin: "https://provider.example",
42
+ dispatchKind: "concrete",
43
+ connectionDispatchRevision: "revision-1",
44
+ };
45
+ const prefillCarrier: BoundPrefillAttachmentDTO = {
46
+ id: "prefill-attestation",
47
+ state: "available",
48
+ };
49
+ const context: InterceptorContextDTO = {
50
+ userId: "user-id",
51
+ chatId: "chat-id",
52
+ generationId: "generation-id",
53
+ generationType,
54
+ isDryRun: false,
55
+ presetId: "preset-id",
56
+ presetMetadata: { activeMode: "parallel" },
57
+ personaId: null,
58
+ characterId: "character-id",
59
+ personaAddonStates: { example: true },
60
+ excludeMessageId: "message-id",
61
+ rejectedSwipe: "rejected-swipe",
62
+ regenFeedback: "try again",
63
+ regenFeedbackPosition: "user",
64
+ mainDispatch: {
65
+ source: "main",
66
+ descriptor,
67
+ connectionDispatchRevision: "revision-1",
68
+ dispatchKind: "concrete",
69
+ },
70
+ prefillCarrier,
71
+ interceptorDeadlineAt: Date.now() + 1_000,
72
+ boundWorkDeadlineAt: Date.now() + 900,
73
+ signal: new AbortController().signal,
74
+ };
75
+
76
+ const guidance: DeferredGuidanceDTO = {
77
+ id: "7c6d2b1a-5f44-4e90-8a31-2d7b9c4e6f80",
78
+ content: "Keep the response concise.",
79
+ role: "system",
80
+ };
81
+ const breakdown: InterceptorBreakdownEntryDTO = { messageIndex: 0, name: "injected" };
82
+ const messages: LlmMessageDTO[] = [{ role: "user", content: "hello" }];
83
+
84
+ const callback: InterceptorHandler = async (input, callbackContext) => {
85
+ const userId: string = callbackContext.userId;
86
+ const generationId: string = callbackContext.generationId;
87
+ const dispatchKind: "concrete" | "roulette" | null = callbackContext.mainDispatch.dispatchKind;
88
+ const signal: AbortSignal = callbackContext.signal;
89
+ const metadata: unknown = callbackContext.presetMetadata;
90
+ void userId;
91
+ void generationId;
92
+ void dispatchKind;
93
+ void signal;
94
+ void metadata;
95
+ const boundAssembly = await spindle.generate.assemble({
96
+ blocks: [],
97
+ dispatch: {
98
+ source: "main",
99
+ expectedConnectionDispatchRevision: "revision-1",
100
+ },
101
+ deadlineAt: Date.now() + 900,
102
+ signal,
103
+ });
104
+ const boundQuiet = await spindle.generate.quietTracked({
105
+ messages: input,
106
+ dispatch: {
107
+ source: "main",
108
+ expectedConnectionDispatchRevision: "revision-1",
109
+ },
110
+ deadlineAt: Date.now() + 900,
111
+ signal,
112
+ });
113
+ void boundAssembly;
114
+ void boundQuiet;
115
+ // @ts-expect-error callback context is readonly
116
+ callbackContext.userId = "must-not-compile";
117
+ // @ts-expect-error nested dispatch context is readonly
118
+ callbackContext.mainDispatch.dispatchKind = "roulette";
119
+ return {
120
+ messages: input,
121
+ breakdown: [breakdown],
122
+ deferredGuidance: [guidance],
123
+ } satisfies InterceptorResultDTO;
124
+ };
125
+
126
+ const legacyArrayCallback: InterceptorHandler = async (input) => input;
127
+ const legacyResultCallback: InterceptorHandler = async (input) => ({ messages: input });
128
+
129
+ declare const spindle: SpindleAPI;
130
+ const priorityDisposer: InterceptorDisposer = spindle.registerInterceptor(callback, 900, matchOptions);
131
+ const optionsDisposer: InterceptorDisposer = spindle.registerInterceptor(callback, options);
132
+ const handlerOnlyDisposer: InterceptorDisposer = spindle.registerInterceptor(legacyArrayCallback);
133
+ const legacyPriorityDisposer: InterceptorDisposer = spindle.registerInterceptor(legacyResultCallback, 100);
134
+ priorityDisposer();
135
+ optionsDisposer();
136
+ handlerOnlyDisposer();
137
+ legacyPriorityDisposer();
138
+
139
+ const { signal: _signal, ...wireContext } = context;
140
+ const wireIntercept: HostToWorker = {
141
+ type: "intercept_request",
142
+ requestId: "request-id",
143
+ registrationId: "registration-id",
144
+ messages,
145
+ context: wireContext,
146
+ };
147
+ const abortIntercept: HostToWorker = {
148
+ type: "intercept_abort",
149
+ requestId: "request-id",
150
+ registrationId: "registration-id",
151
+ };
152
+ const register: WorkerToHost = {
153
+ type: "register_interceptor",
154
+ registrationId: "registration-id",
155
+ priority: 900,
156
+ match,
157
+ };
158
+ const unregister: WorkerToHost = {
159
+ type: "unregister_interceptor",
160
+ registrationId: "registration-id",
161
+ };
162
+ const result: WorkerToHost = {
163
+ type: "intercept_result",
164
+ requestId: "request-id",
165
+ registrationId: "registration-id",
166
+ messages,
167
+ deferredGuidance: [guidance],
168
+ };
169
+ const assembleBound: WorkerToHost = {
170
+ type: "generate_assemble",
171
+ requestId: "assemble-id",
172
+ input: {
173
+ blocks: [] as BoundAssembleRequestDTO["blocks"],
174
+ dispatch: { source: "main", expectedConnectionDispatchRevision: "revision-1" },
175
+ deadlineAt: Date.now() + 500,
176
+ },
177
+ };
178
+ const quietTracked: WorkerToHost = {
179
+ type: "generate_quiet_tracked",
180
+ requestId: "quiet-id",
181
+ input: {
182
+ messages,
183
+ dispatch: { source: "main", expectedConnectionDispatchRevision: "revision-1" },
184
+ deadlineAt: Date.now() + 500,
185
+ } satisfies Omit<QuietTrackedRequestDTO, "signal">,
186
+ };
187
+
188
+ void abortIntercept;
189
+ void assembleBound;
190
+ void options;
191
+ void priorityDisposer;
192
+ void quietTracked;
193
+ void register;
194
+ void result;
195
+ void unregister;
196
+ void wireIntercept;
@@ -5,5 +5,5 @@
5
5
  "outDir": "./.consumer-dist",
6
6
  "declaration": false
7
7
  },
8
- "include": ["test/loom-block-editor-consumer.ts"]
8
+ "include": ["test/loom-block-editor-consumer.ts", "test/bound-generation-consumer.ts", "test/interceptor-consumer.ts", "test/host-runtime-contract-consumer.ts"]
9
9
  }